Skip to content

Commit 089f461

Browse files
committed
cue/load: add support for build tags
Fixes #511 Change-Id: I012286cffe357ab7d835ef35e0f5e2ece00b9b89 Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7064 Reviewed-by: CUE cueckoo <[email protected]> Reviewed-by: Marcel van Lohuizen <[email protected]>
1 parent 1e8906a commit 089f461

File tree

10 files changed

+211
-13
lines changed

10 files changed

+211
-13
lines changed

cmd/cue/cmd/help.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,32 @@ $ cue export -e name -o=text:foo
262262

263263
var injectHelp = &cobra.Command{
264264
Use: "injection",
265-
Short: "inject values from the command line",
266-
Long: `Many of the cue commands allow injecting values
267-
from the command line using the --inject/-t flag.
265+
Short: "inject files or values into specific fields for a build",
266+
Long: `Many of the cue commands allow injecting values or
267+
selecting files from the command line using the --inject/-t flag.
268+
269+
270+
Injecting files
271+
272+
A "build" attribute defines a boolean expression that causes a file
273+
to only be included in a build if its expression evaluates to true.
274+
There may only be a single @if attribute per file and it must
275+
appear before a package clause.
276+
277+
The expression is a subset of CUE consisting only of identifiers
278+
and the operators &&, ||, !, where identifiers refer to tags
279+
defined by the user on the command line.
280+
281+
For example, the following file will only be included in a build
282+
if the user includes the flag "-t prod" on the command line.
283+
284+
// File prod.cue
285+
@if(prod)
286+
287+
package foo
288+
289+
290+
Injecting values
268291
269292
The injection mechanism allows values to be injected into fields
270293
that are marked with a "tag" attribute. For any field of the form

cue/load/config.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ type Config struct {
163163
// build and to inject values into the AST.
164164
//
165165
//
166+
// File selection
167+
//
168+
// Files with an attribute of the form @if(expr) before a package clause
169+
// are conditionally included if expr resolves to true, where expr refers to
170+
// boolean values in Tags.
171+
//
172+
// It is an error for a file to have more than one @if attribute or to
173+
// have a @if attribute without or after a package clause.
174+
//
175+
//
166176
// Value injection
167177
//
168178
// The Tags values are also used to inject values into fields with a
@@ -490,7 +500,10 @@ func (c Config) complete() (cfg *Config, err error) {
490500
}
491501
}
492502

493-
c.loader = &loader{cfg: &c}
503+
c.loader = &loader{
504+
cfg: &c,
505+
buildTags: make(map[string]bool),
506+
}
494507

495508
// TODO: also make this work if run from outside the module?
496509
switch {

cue/load/import.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,15 @@ func (fp *fileProcessor) add(pos token.Pos, root string, file *build.File, mode
375375
return false // don't mark as added
376376
}
377377

378+
if include, err := shouldBuildFile(pf, fp); !include {
379+
if err != nil {
380+
fp.err = errors.Append(fp.err, err)
381+
}
382+
p.IgnoredCUEFiles = append(p.InvalidCUEFiles, fullPath)
383+
p.IgnoredFiles = append(p.InvalidFiles, file)
384+
return false
385+
}
386+
378387
if pkg != "" && pkg != "_" {
379388
if p.PkgName == "" {
380389
p.PkgName = pkg

cue/load/loader.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func Instances(args []string, c *Config) []*build.Instance {
8686

8787
// TODO(api): have API call that returns an error which is the aggregate
8888
// of all build errors. Certain errors, like these, hold across builds.
89-
if err := injectTags(c.Tags, l.tags); err != nil {
89+
if err := injectTags(c.Tags, l); err != nil {
9090
for _, p := range a {
9191
p.ReportError(err)
9292
}
@@ -110,9 +110,10 @@ const (
110110
)
111111

112112
type loader struct {
113-
cfg *Config
114-
stk importStack
115-
tags []tag // tags found in files
113+
cfg *Config
114+
stk importStack
115+
tags []tag // tags found in files
116+
buildTags map[string]bool
116117
}
117118

118119
func (l *loader) abs(filename string) string {

cue/load/loader_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,47 @@ module: example.org/test
253253
root: $CWD/testdata
254254
dir: $CWD/testdata/toolonly
255255
display:./toolonly`,
256+
}, {
257+
cfg: &Config{
258+
Dir: testdataDir,
259+
Tags: []string{"prod"},
260+
},
261+
args: args("./tags"),
262+
want: `
263+
path: example.org/test/tags
264+
module: example.org/test
265+
root: $CWD/testdata
266+
dir: $CWD/testdata/tags
267+
display:./tags
268+
files:
269+
$CWD/testdata/tags/prod.cue`,
270+
}, {
271+
cfg: &Config{
272+
Dir: testdataDir,
273+
Tags: []string{"prod", "foo=bar"},
274+
},
275+
args: args("./tags"),
276+
want: `
277+
path: example.org/test/tags
278+
module: example.org/test
279+
root: $CWD/testdata
280+
dir: $CWD/testdata/tags
281+
display:./tags
282+
files:
283+
$CWD/testdata/tags/prod.cue`,
284+
}, {
285+
cfg: &Config{
286+
Dir: testdataDir,
287+
Tags: []string{"prod"},
288+
},
289+
args: args("./tagsbad"),
290+
want: `
291+
err: multiple @if attributes (and 2 more errors)
292+
path: example.org/test/tagsbad
293+
module: example.org/test
294+
root: $CWD/testdata
295+
dir: $CWD/testdata/tagsbad
296+
display:./tagsbad`,
256297
}}
257298
for i, tc := range testCases {
258299
t.Run(strconv.Itoa(i)+"/"+strings.Join(tc.args, ":"), func(t *testing.T) {

cue/load/tags.go

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"cuelang.org/go/cue/ast"
2222
"cuelang.org/go/cue/build"
2323
"cuelang.org/go/cue/errors"
24+
"cuelang.org/go/cue/parser"
2425
"cuelang.org/go/cue/token"
2526
"cuelang.org/go/internal"
2627
"cuelang.org/go/internal/cli"
@@ -138,13 +139,13 @@ func findTags(b *build.Instance) (tags []tag, errs errors.Error) {
138139
return tags, errs
139140
}
140141

141-
func injectTags(tags []string, a []tag) errors.Error {
142+
func injectTags(tags []string, l *loader) errors.Error {
142143
// Parses command line args
143144
for _, s := range tags {
144145
p := strings.Index(s, "=")
145-
found := false
146+
found := l.buildTags[s]
146147
if p > 0 { // key-value
147-
for _, t := range a {
148+
for _, t := range l.tags {
148149
if t.key == s[:p] {
149150
found = true
150151
if err := t.inject(s[p+1:]); err != nil {
@@ -156,7 +157,7 @@ func injectTags(tags []string, a []tag) errors.Error {
156157
return errors.Newf(token.NoPos, "no tag for %q", s[:p])
157158
}
158159
} else { // shorthand
159-
for _, t := range a {
160+
for _, t := range l.tags {
160161
for _, sh := range t.shorthands {
161162
if sh == s {
162163
found = true
@@ -167,9 +168,104 @@ func injectTags(tags []string, a []tag) errors.Error {
167168
}
168169
}
169170
if !found {
170-
return errors.Newf(token.NoPos, "no shorthand for %q", s)
171+
return errors.Newf(token.NoPos, "tag %q not used in any file", s)
171172
}
172173
}
173174
}
174175
return nil
175176
}
177+
178+
func shouldBuildFile(f *ast.File, fp *fileProcessor) (bool, errors.Error) {
179+
tags := fp.c.Tags
180+
181+
a, errs := getBuildAttr(f)
182+
if errs != nil {
183+
return false, errs
184+
}
185+
if a == nil {
186+
return true, nil
187+
}
188+
189+
_, body := a.Split()
190+
191+
expr, err := parser.ParseExpr("", body)
192+
if err != nil {
193+
return false, errors.Promote(err, "")
194+
}
195+
196+
tagMap := map[string]bool{}
197+
for _, t := range tags {
198+
tagMap[t] = !strings.ContainsRune(t, '=')
199+
}
200+
201+
c := checker{tags: tagMap, loader: fp.c.loader}
202+
include := c.shouldInclude(expr)
203+
if c.err != nil {
204+
return false, c.err
205+
}
206+
return include, nil
207+
}
208+
209+
func getBuildAttr(f *ast.File) (*ast.Attribute, errors.Error) {
210+
var a *ast.Attribute
211+
for _, d := range f.Decls {
212+
switch x := d.(type) {
213+
case *ast.Attribute:
214+
key, _ := x.Split()
215+
if key != "if" {
216+
continue
217+
}
218+
if a != nil {
219+
err := errors.Newf(d.Pos(), "multiple @if attributes")
220+
err = errors.Append(err,
221+
errors.Newf(a.Pos(), "previous declaration here"))
222+
return nil, err
223+
}
224+
a = x
225+
226+
case *ast.Package:
227+
break
228+
}
229+
}
230+
return a, nil
231+
}
232+
233+
type checker struct {
234+
loader *loader
235+
tags map[string]bool
236+
err errors.Error
237+
}
238+
239+
func (c *checker) shouldInclude(expr ast.Expr) bool {
240+
switch x := expr.(type) {
241+
case *ast.Ident:
242+
c.loader.buildTags[x.Name] = true
243+
return c.tags[x.Name]
244+
245+
case *ast.BinaryExpr:
246+
switch x.Op {
247+
case token.LAND:
248+
return c.shouldInclude(x.X) && c.shouldInclude(x.Y)
249+
250+
case token.LOR:
251+
return c.shouldInclude(x.X) || c.shouldInclude(x.Y)
252+
253+
default:
254+
c.err = errors.Append(c.err, errors.Newf(token.NoPos,
255+
"invalid operator %v", x.Op))
256+
return false
257+
}
258+
259+
case *ast.UnaryExpr:
260+
if x.Op != token.NOT {
261+
c.err = errors.Append(c.err, errors.Newf(token.NoPos,
262+
"invalid operator %v", x.Op))
263+
}
264+
return !c.shouldInclude(x.X)
265+
266+
default:
267+
c.err = errors.Append(c.err, errors.Newf(token.NoPos,
268+
"invalid type %T in build attribute", expr))
269+
return false
270+
}
271+
}

cue/load/testdata/tags/prod.cue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@if(prod)
2+
3+
package tags
4+
5+
foo: string @tag(foo)

cue/load/testdata/tags/stage.cue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@if(stage)
2+
3+
package tags

cue/load/testdata/tagsbad/prod.cue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@if(foo)
2+
@if(bar)
3+
4+
package tagsbad

cue/load/testdata/tagsbad/stage.cue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package tagsbad
2+
3+
@if(prod)

0 commit comments

Comments
 (0)