forked from uhthomas/rules_cue
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(gazelle): A bazel-gazelle language extension for rules_cue
gazelle/cue contains a go_library which can be linked into a gazelle_binary to automate generation of bazel BUILD files for .cue files. When a .cue file declares a package and the package name matches the directory in which it lives, it becomes part of a cue_library(name="cue_default_library", ...) target. When a .cue file does not declare a package, it is turned into a cue_binary named after the file's base name, e.g. foo.cue becomes cue_binary(name="foo").
Showing
6 changed files
with
486 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
load("@bazel_gazelle//:def.bzl", "DEFAULT_LANGUAGES", "gazelle_binary", "gazelle") | ||
|
||
gazelle_binary( | ||
name = "gazelle_binary", | ||
languages = DEFAULT_LANGUAGES + ["@com_github_tnarg_rules_cue//gazelle/cue:go_default_library"], | ||
msan = "off", | ||
pure = "off", | ||
race = "off", | ||
static = "off", | ||
visibility = ["//visibility:public"], | ||
) | ||
|
||
# gazelle:prefix github.com/tnarg/rules_cue | ||
gazelle( | ||
name = "gazelle", | ||
gazelle = "//:gazelle_binary", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_library") | ||
|
||
go_library( | ||
name = "go_default_library", | ||
srcs = [ | ||
"config.go", | ||
"cue.go", | ||
"generate.go", | ||
"resolve.go", | ||
], | ||
importpath = "github.com/tnarg/rules_cue/gazelle/cue", | ||
visibility = ["//visibility:public"], | ||
deps = [ | ||
"@bazel_gazelle//config:go_default_library", | ||
"@bazel_gazelle//label:go_default_library", | ||
"@bazel_gazelle//language:go_default_library", | ||
"@bazel_gazelle//repo:go_default_library", | ||
"@bazel_gazelle//resolve:go_default_library", | ||
"@bazel_gazelle//rule:go_default_library", | ||
"@com_github_iancoleman_strcase//:go_default_library", | ||
"@org_cuelang_go//cue/ast:go_default_library", | ||
"@org_cuelang_go//cue/parser:go_default_library", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package cuelang | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"go/build" | ||
"log" | ||
"path" | ||
"strings" | ||
|
||
"github.com/bazelbuild/bazel-gazelle/config" | ||
"github.com/bazelbuild/bazel-gazelle/rule" | ||
) | ||
|
||
type cueConfig struct { | ||
// prefix is a prefix of an import path, used to generate importpath | ||
// attributes. Set with -go_prefix or # gazelle:prefix. | ||
prefix string | ||
prefixRel string | ||
} | ||
|
||
// KnownDirectives returns a list of directive keys that this | ||
// Configurer can interpret. Gazelle prints errors for directives that | ||
// are not recoginized by any Configurer. | ||
func (s *cueLang) KnownDirectives() []string { | ||
return []string{"prefix"} | ||
} | ||
|
||
// RegisterFlags registers command-line flags used by the | ||
// extension. This method is called once with the root configuration | ||
// when Gazelle starts. RegisterFlags may set an initial values in | ||
// Config.Exts. When flags are set, they should modify these values. | ||
func (s *cueLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) { | ||
c.Exts[cueName] = &cueConfig{} | ||
} | ||
|
||
// CheckFlags validates the configuration after command line flags are | ||
// parsed. This is called once with the root configuration when | ||
// Gazelle starts. CheckFlags may set default values in flags or make | ||
// implied changes. | ||
func (s *cueLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { | ||
return nil | ||
} | ||
|
||
// Configure modifies the configuration using directives and other | ||
// information extracted from a build file. Configure is called in | ||
// each directory. | ||
// | ||
// c is the configuration for the current directory. It starts out as | ||
// a copy of the configuration for the parent directory. | ||
// | ||
// rel is the slash-separated relative path from the repository root | ||
// to the current directory. It is "" for the root directory itself. | ||
// | ||
// f is the build file for the current directory or nil if there is no | ||
// existing build file. | ||
func (s *cueLang) Configure(c *config.Config, rel string, f *rule.File) { | ||
var conf *cueConfig | ||
if raw, ok := c.Exts[cueName]; !ok { | ||
conf = &cueConfig{} | ||
} else { | ||
tmp := *(raw.(*cueConfig)) | ||
conf = &tmp | ||
} | ||
c.Exts[cueName] = conf | ||
|
||
// We follow the same pattern as the go language to allow | ||
// vendoring of cue repositories. | ||
if path.Base(rel) == "vendor" { | ||
conf.prefix = "" | ||
conf.prefixRel = rel | ||
} | ||
|
||
if f != nil { | ||
for _, d := range f.Directives { | ||
switch d.Key { | ||
case "prefix": | ||
if err := checkPrefix(d.Value); err != nil { | ||
log.Print(err) | ||
return | ||
} | ||
conf.prefix = d.Value | ||
conf.prefixRel = rel | ||
return | ||
} | ||
} | ||
} | ||
} | ||
|
||
// checkPrefix checks that a string may be used as a prefix. We forbid local | ||
// (relative) imports and those beginning with "/". We allow the empty string, | ||
// but generated rules must not have an empty importpath. | ||
func checkPrefix(prefix string) error { | ||
if strings.HasPrefix(prefix, "/") || build.IsLocalImport(prefix) { | ||
return fmt.Errorf("invalid prefix: %q", prefix) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package cuelang | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/bazelbuild/bazel-gazelle/config" | ||
"github.com/bazelbuild/bazel-gazelle/language" | ||
"github.com/bazelbuild/bazel-gazelle/rule" | ||
) | ||
|
||
const ( | ||
cueName = "cue" | ||
) | ||
|
||
var _ = fmt.Printf | ||
|
||
type cueLang struct{} | ||
|
||
// NewLanguage returns an instace of the Gazelle plugin for rules_cue. | ||
func NewLanguage() language.Language { | ||
return &cueLang{} | ||
} | ||
|
||
func (cl *cueLang) Name() string { return cueName } | ||
|
||
// Kinds returns a map of maps rule names (kinds) and information on | ||
// how to match and merge attributes that may be found in rules of | ||
// those kinds. All kinds of rules generated for this language may be | ||
// found here. | ||
func (cl *cueLang) Kinds() map[string]rule.KindInfo { | ||
return map[string]rule.KindInfo{ | ||
"cue_library": { | ||
MatchAttrs: []string{"importpath"}, | ||
NonEmptyAttrs: map[string]bool{ | ||
"deps": true, | ||
"srcs": true, | ||
}, | ||
MergeableAttrs: map[string]bool{ | ||
"srcs": true, | ||
"importpath": true, | ||
}, | ||
ResolveAttrs: map[string]bool{"deps": true}, | ||
}, | ||
"cue_binary": { | ||
MatchAny: true, | ||
NonEmptyAttrs: map[string]bool{ | ||
"deps": true, | ||
"src": true, | ||
}, | ||
MergeableAttrs: map[string]bool{ | ||
"escape": true, | ||
"output_format": true, | ||
"src": true, | ||
}, | ||
ResolveAttrs: map[string]bool{"deps": true}, | ||
}, | ||
} | ||
} | ||
|
||
// Loads returns .bzl files and symbols they define. Every rule | ||
// generated by GenerateRules, now or in the past, should be loadable | ||
// from one of these files. | ||
func (cl *cueLang) Loads() []rule.LoadInfo { | ||
return []rule.LoadInfo{ | ||
{ | ||
Name: "@com_github_tnarg_rules_cue//cue:cue.bzl", | ||
Symbols: []string{"cue_binary", "cue_library"}, | ||
}, | ||
} | ||
} | ||
|
||
// Fix repairs deprecated usage of language-specific rules in f. This | ||
// is called before the file is indexed. Unless c.ShouldFix is true, | ||
// fixes that delete or rename rules should not be performed. | ||
func (s *cueLang) Fix(c *config.Config, f *rule.File) { | ||
// Currently a noop | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package cuelang | ||
|
||
import ( | ||
"log" | ||
"path" | ||
"path/filepath" | ||
"sort" | ||
"strings" | ||
|
||
"cuelang.org/go/cue/ast" | ||
"cuelang.org/go/cue/parser" | ||
"github.com/bazelbuild/bazel-gazelle/config" | ||
"github.com/bazelbuild/bazel-gazelle/language" | ||
"github.com/bazelbuild/bazel-gazelle/rule" | ||
"github.com/iancoleman/strcase" | ||
) | ||
|
||
const ( | ||
cueDefaultLibrary = "cue_default_library" | ||
) | ||
|
||
// GenerateRules extracts build metadata from source files in a | ||
// directory. GenerateRules is called in each directory where an | ||
// update is requested in depth-first post-order. | ||
// | ||
// args contains the arguments for GenerateRules. This is passed as a | ||
// struct to avoid breaking implementations in the future when new | ||
// fields are added. | ||
// | ||
// empty is a list of empty rules that may be deleted after merge. | ||
// | ||
// gen is a list of generated rules that may be updated or added. | ||
// | ||
// Any non-fatal errors this function encounters should be logged | ||
// using log.Print. | ||
func (cl *cueLang) GenerateRules(args language.GenerateArgs) language.GenerateResult { | ||
cueFiles := make(map[string]*ast.File) | ||
for _, f := range append(args.RegularFiles, args.GenFiles...) { | ||
// Only generate Cue entries for cue files (.cue) | ||
if !strings.HasSuffix(f, ".cue") { | ||
continue | ||
} | ||
|
||
pth := filepath.Join(args.Dir, f) | ||
cueFile, err := parser.ParseFile(pth, nil) | ||
if err != nil { | ||
log.Printf("parsing cue file: path=%q, err=%+v", pth, err) | ||
continue | ||
} | ||
cueFiles[f] = cueFile | ||
} | ||
|
||
expectedPkgName := path.Base(args.Rel) | ||
|
||
// categorize cue files into binary and library sources | ||
// cue_libary names are based on cue package name. | ||
var cueLibSrcs []string | ||
cueBinSrc := make(map[string]string) | ||
cueImports := make(map[string][]string) | ||
|
||
for fname, cueFile := range cueFiles { | ||
var target string | ||
pkg := cueFile.PackageName() | ||
if pkg == "" { | ||
target = binName(fname) | ||
cueBinSrc[target] = fname | ||
} else if pkg == expectedPkgName { | ||
target = cueDefaultLibrary | ||
cueLibSrcs = append(cueLibSrcs, fname) | ||
} else { | ||
log.Printf("ignoring cue file: path=%q, pkg=%q", fname, pkg) | ||
} | ||
for _, imprt := range cueFile.Imports { | ||
imprt := strings.Trim(imprt.Path.Value, "\"") | ||
cueImports[target] = append(cueImports[target], imprt) | ||
} | ||
} | ||
|
||
var res language.GenerateResult | ||
if cueLibSrcs != nil { | ||
rule := rule.NewRule("cue_library", cueDefaultLibrary) | ||
rule.SetAttr("srcs", cueLibSrcs) | ||
rule.SetAttr("visibility", []string{"//visibility:public"}) | ||
rule.SetAttr("importpath", computeImportPath(args)) | ||
imprts := cueImports[cueDefaultLibrary] | ||
sort.Strings(imprts) | ||
rule.SetPrivateAttr(config.GazelleImportsKey, imprts) | ||
res.Gen = append(res.Gen, rule) | ||
} | ||
|
||
for tgt, src := range cueBinSrc { | ||
rule := rule.NewRule("cue_binary", tgt) | ||
rule.SetAttr("src", src) | ||
rule.SetAttr("visibility", []string{"//visibility:public"}) | ||
imprts := cueImports[tgt] | ||
sort.Strings(imprts) | ||
rule.SetPrivateAttr(config.GazelleImportsKey, imprts) | ||
res.Gen = append(res.Gen, rule) | ||
} | ||
|
||
res.Imports = make([]interface{}, len(res.Gen)) | ||
for i, r := range res.Gen { | ||
res.Imports[i] = r.PrivateAttr(config.GazelleImportsKey) | ||
} | ||
|
||
res.Empty = generateEmpty(args.File, cueLibSrcs, cueBinSrc) | ||
|
||
return res | ||
} | ||
|
||
func computeImportPath(args language.GenerateArgs) string { | ||
c := args.Config | ||
var conf *cueConfig | ||
if raw, ok := c.Exts[cueName]; ok { | ||
conf = raw.(*cueConfig) | ||
} else { | ||
conf = &cueConfig{} | ||
} | ||
|
||
suffix, err := filepath.Rel(conf.prefixRel, args.Rel) | ||
if err != nil { | ||
log.Printf("Failed to compute importpath: rel=%q, prefixRel=%q, err=%+v", args.Rel, conf.prefixRel, err) | ||
return args.Rel | ||
} | ||
if suffix == "." { | ||
return conf.prefix | ||
} | ||
|
||
return filepath.Join(conf.prefix, suffix) | ||
} | ||
|
||
func binName(basename string) string { | ||
parts := strings.Split(basename, ".") | ||
return strcase.ToSnake(strings.Join(parts[:len(parts)-1], "_")) | ||
} | ||
|
||
func generateEmpty(f *rule.File, cueLibSrcs []string, cueBinSrc map[string]string) []*rule.Rule { | ||
if f == nil { | ||
return nil | ||
} | ||
var empty []*rule.Rule | ||
for _, r := range f.Rules { | ||
switch r.Kind() { | ||
case "cue_library": | ||
if r.Name() == cueDefaultLibrary && cueLibSrcs == nil { | ||
empty = append(empty, rule.NewRule("cue_library", r.Name())) | ||
} | ||
case "cue_binary": | ||
if _, ok := cueBinSrc[r.Name()]; !ok { | ||
empty = append(empty, rule.NewRule("cue_binary", r.Name())) | ||
} | ||
default: | ||
// ignore | ||
} | ||
} | ||
return empty | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package cuelang | ||
|
||
import ( | ||
"log" | ||
"sort" | ||
|
||
"github.com/bazelbuild/bazel-gazelle/config" | ||
"github.com/bazelbuild/bazel-gazelle/label" | ||
"github.com/bazelbuild/bazel-gazelle/repo" | ||
"github.com/bazelbuild/bazel-gazelle/resolve" | ||
"github.com/bazelbuild/bazel-gazelle/rule" | ||
) | ||
|
||
// Imports returns a list of ImportSpecs that can be used to import | ||
// the rule r. This is used to populate RuleIndex. | ||
// | ||
// If nil is returned, the rule will not be indexed. If any non-nil | ||
// slice is returned, including an empty slice, the rule will be | ||
// indexed. | ||
func (cl *cueLang) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { | ||
switch r.Kind() { | ||
case "cue_library": | ||
return []resolve.ImportSpec{ | ||
resolve.ImportSpec{ | ||
Lang: cueName, | ||
Imp: r.AttrString("importpath"), | ||
}, | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Embeds returns a list of labels of rules that the given rule | ||
// embeds. If a rule is embedded by another importable rule of the | ||
// same language, only the embedding rule will be indexed. The | ||
// embedding rule will inherit the imports of the embedded rule. | ||
func (cl *cueLang) Embeds(r *rule.Rule, from label.Label) []label.Label { | ||
// Cue doesn't have a concept of embedding as far as I know. | ||
return nil | ||
} | ||
|
||
// Resolve translates imported libraries for a given rule into Bazel | ||
// dependencies. A list of imported libraries is typically stored in a | ||
// private attribute of the rule when it's generated (this interface | ||
// doesn't dictate how that is stored or represented). Resolve | ||
// generates a "deps" attribute (or the appropriate language-specific | ||
// equivalent) for each import according to language-specific rules | ||
// and heuristics. | ||
func (cl *cueLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, importsRaw interface{}, from label.Label) { | ||
if importsRaw == nil { | ||
return | ||
} | ||
imports := importsRaw.([]string) | ||
r.DelAttr("deps") | ||
depSet := make(map[string]bool) | ||
for _, imp := range imports { | ||
if _, ok := stdlib[imp]; ok { | ||
continue | ||
} | ||
|
||
res := ix.FindRulesByImport( | ||
resolve.ImportSpec{ | ||
Lang: cueName, | ||
Imp: imp, | ||
}, cueName) | ||
if res == nil { | ||
log.Print("import error: from=%q, import=%q", from.String(), imp) | ||
} | ||
for _, entry := range res { | ||
l := entry.Label.Rel(from.Repo, from.Pkg) | ||
depSet[l.String()] = true | ||
} | ||
} | ||
if len(depSet) > 0 { | ||
deps := make([]string, 0, len(depSet)) | ||
for dep := range depSet { | ||
deps = append(deps, dep) | ||
} | ||
sort.Strings(deps) | ||
r.SetAttr("deps", deps) | ||
} | ||
} | ||
|
||
var stdlib = map[string]bool{ | ||
"crypto/md5": true, | ||
"crypto/sha1": true, | ||
"crypto/sha256": true, | ||
"crypto/sha512": true, | ||
"encoding/base64": true, | ||
"encoding/csv": true, | ||
"encoding/hex": true, | ||
"encoding/json": true, | ||
"encoding/yaml": true, | ||
"html": true, | ||
"list": true, | ||
"math": true, | ||
"math/bits": true, | ||
"net": true, | ||
"path": true, | ||
"regexp": true, | ||
"strconv": true, | ||
"strings": true, | ||
"struct": true, | ||
"text/tabwriter": true, | ||
"text/template": true, | ||
"time": true, | ||
"tool": true, | ||
"tool/cli": true, | ||
"tool/exec": true, | ||
"tool/file": true, | ||
"tool/http": true, | ||
"tool/os": true, | ||
} |