From 0b4dcbf7bb4f7a2341e67df470fb557d7c4d85c9 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 11 Oct 2019 16:02:05 +0200 Subject: [PATCH] feature: add support for custom build tags --- _test/ct/ct1.go | 3 +++ _test/ct/ct2.go | 5 +++++ _test/ct/ct3.go | 5 +++++ _test/tag0.go | 15 +++++++++++++++ cmd/yaegi/yaegi.go | 9 +++++++-- interp/ast.go | 9 +++++++-- interp/build.go | 33 ++++++++++++++++++++++++++------ interp/build_test.go | 6 +++--- interp/doc.go | 25 ++++++++++++++++++++++++ interp/interp_consistent_test.go | 2 +- interp/src.go | 2 +- 11 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 _test/ct/ct1.go create mode 100644 _test/ct/ct2.go create mode 100644 _test/ct/ct3.go create mode 100644 _test/tag0.go diff --git a/_test/ct/ct1.go b/_test/ct/ct1.go new file mode 100644 index 00000000..e1afac52 --- /dev/null +++ b/_test/ct/ct1.go @@ -0,0 +1,3 @@ +package ct + +func init() { println("hello from ct1") } diff --git a/_test/ct/ct2.go b/_test/ct/ct2.go new file mode 100644 index 00000000..c290630f --- /dev/null +++ b/_test/ct/ct2.go @@ -0,0 +1,5 @@ +// +build !dummy + +package ct + +func init() { println("hello from ct2") } diff --git a/_test/ct/ct3.go b/_test/ct/ct3.go new file mode 100644 index 00000000..25584223 --- /dev/null +++ b/_test/ct/ct3.go @@ -0,0 +1,5 @@ +// +build dummy + +package ct + +func init() { println("hello from ct3") } diff --git a/_test/tag0.go b/_test/tag0.go new file mode 100644 index 00000000..e986668a --- /dev/null +++ b/_test/tag0.go @@ -0,0 +1,15 @@ +// The following comment line has the same effect as 'go run -tags=dummy' +//yaegi:tags dummy + +package main + +import _ "github.com/containous/yaegi/_test/ct" + +func main() { + println("bye") +} + +// Output: +// hello from ct1 +// hello from ct3 +// bye diff --git a/cmd/yaegi/yaegi.go b/cmd/yaegi/yaegi.go index 2b52b11f..e0ddcfe4 100644 --- a/cmd/yaegi/yaegi.go +++ b/cmd/yaegi/yaegi.go @@ -18,7 +18,10 @@ at global level in an implicit main package. Options: -i - start an interactive REPL after file execution + start an interactive REPL after file execution. + -tags tag,list + a comma-separated list of build tags to consider satisfied during + the interpretation. Debugging support (may be removed at any time): YAEGI_AST_DOT=1 @@ -43,7 +46,9 @@ import ( func main() { var interactive bool + var tags string flag.BoolVar(&interactive, "i", false, "start an interactive REPL") + flag.StringVar(&tags, "tags", "", "set a list of build tags") flag.Usage = func() { fmt.Println("Usage:", os.Args[0], "[options] [script] [args]") fmt.Println("Options:") @@ -53,7 +58,7 @@ func main() { args := flag.Args() log.SetFlags(log.Lshortfile) - i := interp.New(interp.Options{GoPath: build.Default.GOPATH}) + i := interp.New(interp.Options{GoPath: build.Default.GOPATH, BuildTags: strings.Split(tags, ",")}) i.Use(stdlib.Symbols) i.Use(interp.Symbols) diff --git a/interp/ast.go b/interp/ast.go index 15d72709..dd070607 100644 --- a/interp/ast.go +++ b/interp/ast.go @@ -320,6 +320,7 @@ func (interp *Interpreter) firstToken(src string) token.Token { func (interp *Interpreter) ast(src, name string) (string, *node, error) { inRepl := name == "" var inFunc bool + var mode parser.Mode // Allow incremental parsing of declarations or statements, by inserting // them in a pseudo file package or function. Those statements or @@ -334,17 +335,21 @@ func (interp *Interpreter) ast(src, name string) (string, *node, error) { inFunc = true src = "package main; func main() {" + src + "}" } + // Parse comments in REPL mode, to allow tag setting + mode |= parser.ParseComments } - if ok, err := interp.buildOk(interp.context, name, src); !ok || err != nil { + if ok, err := interp.buildOk(&interp.context, name, src); !ok || err != nil { return "", nil, err // skip source not matching build constraints } - f, err := parser.ParseFile(interp.fset, name, src, 0) + f, err := parser.ParseFile(interp.fset, name, src, mode) if err != nil { return "", nil, err } + setYaegiTags(&interp.context, f.Comments) + var root *node var anc astNode var st nodestack diff --git a/interp/build.go b/interp/build.go index 11730d78..4384e69b 100644 --- a/interp/build.go +++ b/interp/build.go @@ -1,6 +1,7 @@ package interp import ( + "go/ast" "go/build" "go/parser" "path" @@ -11,7 +12,7 @@ import ( // buildOk returns true if a file or script matches build constraints // as specified in https://golang.org/pkg/go/build/#hdr-Build_Constraints. // An error from parser is returned as well. -func (interp *Interpreter) buildOk(ctx build.Context, name, src string) (bool, error) { +func (interp *Interpreter) buildOk(ctx *build.Context, name, src string) (bool, error) { // Extract comments before the first clause f, err := parser.ParseFile(interp.fset, name, src, parser.PackageClauseOnly|parser.ParseComments) if err != nil { @@ -25,12 +26,13 @@ func (interp *Interpreter) buildOk(ctx build.Context, name, src string) (bool, e } } } + setYaegiTags(ctx, f.Comments) return true, nil } // buildLineOk returns true if line is not a build constraint or // if build constraint is satisfied -func buildLineOk(ctx build.Context, line string) (ok bool) { +func buildLineOk(ctx *build.Context, line string) (ok bool) { if len(line) < 7 || line[:7] != "+build " { return true } @@ -45,7 +47,7 @@ func buildLineOk(ctx build.Context, line string) (ok bool) { } // buildOptionOk return true if all comma separated tags match, false otherwise -func buildOptionOk(ctx build.Context, tag string) bool { +func buildOptionOk(ctx *build.Context, tag string) bool { // in option, evaluate the AND of individual tags for _, t := range strings.Split(tag, ",") { if !buildTagOk(ctx, t) { @@ -57,7 +59,7 @@ func buildOptionOk(ctx build.Context, tag string) bool { // buildTagOk returns true if a build tag matches, false otherwise // if first character is !, result is negated -func buildTagOk(ctx build.Context, s string) (r bool) { +func buildTagOk(ctx *build.Context, s string) (r bool) { not := s[0] == '!' if not { s = s[1:] @@ -82,6 +84,25 @@ func buildTagOk(ctx build.Context, s string) (r bool) { return } +// setYaegiTags scans a comment group for "yaegi:tags tag1 tag2 ..." lines +// and adds the corresponding tags to the interpreter build tags. +func setYaegiTags(ctx *build.Context, comments []*ast.CommentGroup) { + for _, g := range comments { + for _, line := range strings.Split(strings.TrimSpace(g.Text()), "\n") { + if len(line) < 11 || line[:11] != "yaegi:tags " { + continue + } + + tags := strings.Split(strings.TrimSpace(line[10:]), " ") + for _, tag := range tags { + if !contains(ctx.BuildTags, tag) { + ctx.BuildTags = append(ctx.BuildTags, tag) + } + } + } + } +} + func contains(tags []string, tag string) bool { for _, t := range tags { if t == tag { @@ -92,7 +113,7 @@ func contains(tags []string, tag string) bool { } // goMinorVersion returns the go minor version number -func goMinorVersion(ctx build.Context) int { +func goMinorVersion(ctx *build.Context) int { current := ctx.ReleaseTags[len(ctx.ReleaseTags)-1] v := strings.Split(current, ".") @@ -108,7 +129,7 @@ func goMinorVersion(ctx build.Context) int { } // skipFile returns true if file should be skipped -func skipFile(ctx build.Context, p string) bool { +func skipFile(ctx *build.Context, p string) bool { if !strings.HasSuffix(p, ".go") { return true } diff --git a/interp/build_test.go b/interp/build_test.go index ac63bee1..2388f835 100644 --- a/interp/build_test.go +++ b/interp/build_test.go @@ -44,7 +44,7 @@ func TestBuildTag(t *testing.T) { test := test src := test.src + "\npackage x" t.Run(test.src, func(t *testing.T) { - if r, _ := i.buildOk(ctx, "", src); r != test.res { + if r, _ := i.buildOk(&ctx, "", src); r != test.res { t.Errorf("got %v, want %v", r, test.res) } }) @@ -74,7 +74,7 @@ func TestBuildFile(t *testing.T) { for _, test := range tests { test := test t.Run(test.src, func(t *testing.T) { - if r := skipFile(ctx, test.src); r != test.res { + if r := skipFile(&ctx, test.src); r != test.res { t.Errorf("got %v, want %v", r, test.res) } }) @@ -106,7 +106,7 @@ func Test_goMinorVersion(t *testing.T) { for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { - minor := goMinorVersion(test.context) + minor := goMinorVersion(&test.context) if minor != test.expected { t.Errorf("got %v, want %v", minor, test.expected) diff --git a/interp/doc.go b/interp/doc.go index 0d2e79e4..421c3593 100644 --- a/interp/doc.go +++ b/interp/doc.go @@ -4,6 +4,31 @@ Package interp provides a complete Go interpreter For the Go language itself, refer to the official Go specification https://golang.org/ref/spec. +Custom build tags + +Custom build tags allow to control which files in imported source +packages are interpreted, in the same way as the "-tags" option of the +"go build" command. Setting a custom build tag spans globally for all +future imports of the session. + +A build tag is a line comment that begins + + // yaegi:tags + +that lists the build constraints to be satisfied by the further +imports of source packages. + +For example the following custom build tag + + // yaegi:tags noasm + +Will ensure that an import of a package will exclude files containing + + // +build !noasm + +And include files containing + + // +build noasm */ package interp diff --git a/interp/interp_consistent_test.go b/interp/interp_consistent_test.go index 9a681c85..2e69ff5d 100644 --- a/interp/interp_consistent_test.go +++ b/interp/interp_consistent_test.go @@ -104,7 +104,7 @@ func TestInterpConsistencyBuild(t *testing.T) { bin := filepath.Join(dir, strings.TrimSuffix(file.Name(), ".go")) - cmdBuild := exec.Command("go", "build", "-o", bin, filePath) + cmdBuild := exec.Command("go", "build", "-tags=dummy", "-o", bin, filePath) outBuild, err := cmdBuild.CombinedOutput() if err != nil { t.Log(string(outBuild)) diff --git a/interp/src.go b/interp/src.go index bda81e8c..055edb48 100644 --- a/interp/src.go +++ b/interp/src.go @@ -49,7 +49,7 @@ func (interp *Interpreter) importSrc(rPath, path, alias string) error { // Parse source files for _, file := range files { name := file.Name() - if skipFile(interp.context, name) { + if skipFile(&interp.context, name) { continue }