From a4b4db4cdeefb7b4ea5adb09073dd123846b3588 Mon Sep 17 00:00:00 2001 From: "Alan D. Cabrera" Date: Mon, 22 Mar 2021 18:44:08 +0000 Subject: cmd/go2go: add ability to specify build tags in go2go You can specify build tags, so that imports properly work during translation, by using the `-tags` option, e.g. `go tool go2go run -tags=appengine x.go2`. The `-tags` option is available for all the `go2go` sub-commands. Change-Id: Ib60e7542b10c6a561b61db23d35592b2bc7f63cd GitHub-Last-Rev: 954cccfae819aaac1cb78b97905e2a97dfdcefa7 GitHub-Pull-Request: golang/go#45147 Reviewed-on: https://go-review.googlesource.com/c/go/+/303275 Run-TryBot: Ian Lance Taylor TryBot-Result: Go Bot Reviewed-by: Ian Lance Taylor Trust: Emmanuel Odeke --- src/cmd/go2go/doc.go | 9 ++- src/cmd/go2go/go2go_test.go | 63 +++++++++++++++++ src/cmd/go2go/main.go | 53 +++++++++++++-- src/go/go2go/importer.go | 161 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 277 insertions(+), 9 deletions(-) diff --git a/src/cmd/go2go/doc.go b/src/cmd/go2go/doc.go index c78fd9d6a4..059b91b0bb 100644 --- a/src/cmd/go2go/doc.go +++ b/src/cmd/go2go/doc.go @@ -7,7 +7,7 @@ // // Usage: // -// go2go [options] [arguments] +// go2go [options] [go2go flags] [arguments] // // Commands: // @@ -25,6 +25,13 @@ // imports will be looked up in the usual way. If an import includes // .go2 files, they will be translated into .go files. // +// The go2go flags are shared by the build, run, test, and translate commands: +// +// -tags tag,list +// a comma-separated list of build tags to consider satisfied during the +// build. For more information about build tags, see the description of +// build constraints in the documentation for the go/build package. +// // There is a sample GO2PATH in cmd/go2go/testdata/go2path. It provides // several packages that serve as examples of using generics, and may // be useful in experimenting with your own generic code. diff --git a/src/cmd/go2go/go2go_test.go b/src/cmd/go2go/go2go_test.go index 296fa3edb7..4d78c271ba 100644 --- a/src/cmd/go2go/go2go_test.go +++ b/src/cmd/go2go/go2go_test.go @@ -321,3 +321,66 @@ func TestTransitiveGo1(t *testing.T) { t.Fatalf(`error running "go2go build": %v`, err) } } + +func TestBuildWithTags(t *testing.T) { + t.Parallel() + buildGo2go(t) + + gopath := t.TempDir() + testFiles{ + { + "a/a.go2", + `package a; func ident[T any](v T) T { return v }; func F1(s string) string { return ident(s) }`, + }, + { + "b/b_appengine.go", + `// +build appengine + +package b; import "a"; func F2(s string) string { return a.F1(s) + " App Engine!" }`, + }, + { + "b/b.go", + `// +build !appengine + +package b; import "a"; func F2(s string) string { return a.F1(s) + " World!" }`, + }, + { + "c/c.go2", + `package main; import ("fmt"; "b"); func main() { fmt.Println(b.F2("Hello")) }`, + }, + }.create(t, gopath) + + t.Log("go2go build") + cmd := exec.Command(testGo2go, "build", "-tags=appengine", "c") + cmd.Dir = gopath + cmd.Env = append(os.Environ(), + "GO2PATH="+gopath, + ) + out, err := cmd.CombinedOutput() + if len(out) > 0 { + t.Logf("%s", out) + } + if err != nil { + t.Fatalf(`error running "go2go build": %v`, err) + } + + cmdName := "./c" + if runtime.GOOS == "windows" { + cmdName += ".exe" + } + cmd = exec.Command(cmdName) + cmd.Dir = gopath + out, err = cmd.CombinedOutput() + t.Log("./c") + if len(out) > 0 { + t.Logf("%s", out) + } + if err != nil { + t.Fatalf("error running c: %v", err) + } + got := strings.Split(strings.TrimSpace(string(out)), "\n")[0] + want := "Hello App Engine!" + if !reflect.DeepEqual(got, want) { + t.Errorf("c output %v, want %v", got, want) + } +} diff --git a/src/cmd/go2go/main.go b/src/cmd/go2go/main.go index a3543613ab..d778b4502f 100644 --- a/src/cmd/go2go/main.go +++ b/src/cmd/go2go/main.go @@ -26,6 +26,26 @@ var cmds = map[string]bool{ "translate": true, } +// tagsFlag is the implementation of the -tags flag. +type tagsFlag []string + +var buildTags tagsFlag + +func (v *tagsFlag) Set(s string) error { + // Split on commas, ignore empty strings. + *v = []string{} + for _, s := range strings.Split(s, ",") { + if s != "" { + *v = append(*v, s) + } + } + return nil +} + +func (v *tagsFlag) String() string { + return strings.Join(*v, ",") +} + func main() { flag.Usage = usage flag.Parse() @@ -38,6 +58,13 @@ func main() { if !cmds[args[0]] { usage() } + cmd := args[0] + + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Var((*tagsFlag)(&buildTags), "tags", "tag,list") + fs.Parse(args[1:]) + + args = fs.Args() importerTmpdir, err := ioutil.TempDir("", "go2go") if err != nil { @@ -47,30 +74,42 @@ func main() { importer := go2go.NewImporter(importerTmpdir) + if len(buildTags) > 0 { + importer.SetTags(buildTags) + } + var rundir string - if args[0] == "run" { - tmpdir := copyToTmpdir(args[1:]) + if cmd == "run" { + tmpdir := copyToTmpdir(args) defer os.RemoveAll(tmpdir) translate(importer, tmpdir) nargs := []string{"run"} - for _, arg := range args[1:] { + for _, arg := range args { base := filepath.Base(arg) f := strings.TrimSuffix(base, ".go2") + ".go" nargs = append(nargs, f) } args = nargs rundir = tmpdir - } else if args[0] == "translate" && isGo2Files(args[1:]...) { - for _, arg := range args[1:] { + } else if cmd == "translate" && isGo2Files(args...) { + for _, arg := range args { translateFile(importer, arg) } } else { - for _, dir := range expandPackages(args[1:]) { + for _, dir := range expandPackages(args) { translate(importer, dir) } } - if args[0] != "translate" { + if cmd != "translate" { + if len(buildTags) > 0 { + args = append([]string{ + fmt.Sprintf("-tags=%s", strings.Join(buildTags, ",")), + }, args...) + } + + args = append([]string{cmd}, args...) + cmd := exec.Command(gotool, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/src/go/go2go/importer.go b/src/go/go2go/importer.go index 6bd2e8b602..c73dc7ba82 100644 --- a/src/go/go2go/importer.go +++ b/src/go/go2go/importer.go @@ -5,6 +5,7 @@ package go2go import ( + "bytes" "fmt" "go/ast" "go/build" @@ -17,10 +18,12 @@ import ( "log" "os" "os/exec" + "path" "path/filepath" "runtime" "sort" "strings" + "unicode" ) // Importer implements the types.ImporterFrom interface. @@ -58,6 +61,9 @@ type Importer struct { // since it doesn't deal with import information, // but Importer is a useful common location to store the data. instantiations map[*types.Package]*instantiations + + // build tags + tags map[string]bool } var _ types.ImporterFrom = &Importer{} @@ -81,6 +87,13 @@ func NewImporter(tmpdir string) *Importer { idToFunc: make(map[types.Object]*ast.FuncDecl), idToTypeSpec: make(map[types.Object]*ast.TypeSpec), instantiations: make(map[*types.Package]*instantiations), + tags: make(map[string]bool), + } +} + +func (imp *Importer) SetTags(tags []string) { + for _, tag := range tags { + imp.tags[tag] = true } } @@ -217,7 +230,11 @@ func (imp *Importer) importGo1Package(importPath, dir string, mode types.ImportM fset := token.NewFileSet() filter := func(fi os.FileInfo) bool { - return !strings.HasSuffix(fi.Name(), "_test.go") + name := fi.Name() + if strings.HasSuffix(name, "_test.go") { + return false + } + return imp.shouldInclude(path.Join(pdir, name)) } pkgs, err := parser.ParseDir(fset, pdir, filter, 0) if err != nil { @@ -412,3 +429,145 @@ func (imp *Importer) gatherTransitiveImports(path string, m map[string]bool) []s sort.Strings(r) return r } + +var slashslash = []byte("//") + +// shouldInclude reports whether it is okay to use this file, +// The rule is that in the file's leading run of // comments +// and blank lines, which must be followed by a blank line +// (to avoid including a Go package clause doc comment), +// lines beginning with '// +build' are taken as build directives. +// +// The file is accepted only if each such line lists something +// matching the file. For example: +// +// // +build windows linux +// +// marks the file as applicable only on Windows and Linux. +// +// If tags["*"] is true, then ShouldBuild will consider every +// build tag except "ignore" to be both true and false for +// the purpose of satisfying build tags, in order to estimate +// (conservatively) whether a file could ever possibly be used +// in any build. +// +// This code was copied from the go command internals. +func (imp *Importer) shouldInclude(path string) bool { + content, err := os.ReadFile(path) + if err != nil { + return false + } + + // Pass 1. Identify leading run of // comments and blank lines, + // which must be followed by a blank line. + end := 0 + p := content + for len(p) > 0 { + line := p + if i := bytes.IndexByte(line, '\n'); i >= 0 { + line, p = line[:i], p[i+1:] + } else { + p = p[len(p):] + } + + line = bytes.TrimSpace(line) + if len(line) == 0 { // Blank line + end = len(content) - len(p) + continue + } + if !bytes.HasPrefix(line, slashslash) { // Not comment line + break + } + } + content = content[:end] + + // Pass 2. Process each line in the run. + p = content + allok := true + for len(p) > 0 { + line := p + if i := bytes.IndexByte(line, '\n'); i >= 0 { + line, p = line[:i], p[i+1:] + } else { + p = p[len(p):] + } + line = bytes.TrimSpace(line) + if !bytes.HasPrefix(line, slashslash) { + continue + } + line = bytes.TrimSpace(line[len(slashslash):]) + if len(line) > 0 && line[0] == '+' { + // Looks like a comment +line. + f := strings.Fields(string(line)) + if f[0] == "+build" { + ok := false + for _, tok := range f[1:] { + if matchTags(tok, imp.tags) { + ok = true + } + } + if !ok { + allok = false + } + } + } + } + + return allok +} + +// matchTags reports whether the name is one of: +// +// tag (if tags[tag] is true) +// !tag (if tags[tag] is false) +// a comma-separated list of any of these +// +func matchTags(name string, tags map[string]bool) bool { + if name == "" { + return false + } + if i := strings.Index(name, ","); i >= 0 { + // comma-separated list + ok1 := matchTags(name[:i], tags) + ok2 := matchTags(name[i+1:], tags) + return ok1 && ok2 + } + if strings.HasPrefix(name, "!!") { // bad syntax, reject always + return false + } + if strings.HasPrefix(name, "!") { // negation + return len(name) > 1 && matchTag(name[1:], tags, false) + } + return matchTag(name, tags, true) +} + +// matchTag reports whether the tag name is valid and satisfied by tags[name]==want. +func matchTag(name string, tags map[string]bool, want bool) bool { + // Tags must be letters, digits, underscores or dots. + // Unlike in Go identifiers, all digits are fine (e.g., "386"). + for _, c := range name { + if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { + return false + } + } + + if tags["*"] && name != "" && name != "ignore" { + // Special case for gathering all possible imports: + // if we put * in the tags map then all tags + // except "ignore" are considered both present and not + // (so we return true no matter how 'want' is set). + return true + } + + have := tags[name] + if name == "linux" { + have = have || tags["android"] + } + if name == "solaris" { + have = have || tags["illumos"] + } + if name == "darwin" { + have = have || tags["ios"] + } + return have == want +} -- cgit v1.2.3-54-g00ecf