Skip to content

Commit

Permalink
feature: add support for custom build tags
Browse files Browse the repository at this point in the history
  • Loading branch information
mvertes authored and traefiker committed Oct 11, 2019
1 parent 2765478 commit 0b4dcbf
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 15 deletions.
3 changes: 3 additions & 0 deletions _test/ct/ct1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package ct

func init() { println("hello from ct1") }
5 changes: 5 additions & 0 deletions _test/ct/ct2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// +build !dummy

package ct

func init() { println("hello from ct2") }
5 changes: 5 additions & 0 deletions _test/ct/ct3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// +build dummy

package ct

func init() { println("hello from ct3") }
15 changes: 15 additions & 0 deletions _test/tag0.go
Original file line number Diff line number Diff line change
@@ -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
9 changes: 7 additions & 2 deletions cmd/yaegi/yaegi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:")
Expand All @@ -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)

Expand Down
9 changes: 7 additions & 2 deletions interp/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
33 changes: 27 additions & 6 deletions interp/build.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package interp

import (
"go/ast"
"go/build"
"go/parser"
"path"
Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -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) {
Expand All @@ -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:]
Expand All @@ -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 {
Expand All @@ -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, ".")
Expand All @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions interp/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
Expand Down Expand Up @@ -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)
}
})
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions interp/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion interp/interp_consistent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion interp/src.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit 0b4dcbf

Please sign in to comment.