Add ability to specify build tags in go2go

This commit is contained in:
Alan D. Cabrera 2021-03-20 17:15:51 -07:00
parent 8f19f8a9e1
commit 954cccfae8
4 changed files with 277 additions and 9 deletions

View File

@ -7,7 +7,7 @@
//
// Usage:
//
// go2go [options] <command> [arguments]
// go2go [options] <command> [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.

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}