Use go:build directives in generated files.

Build constraints are now inferred from go:build directives rather than +build
directives. +build directives are still emitted in generated files as required
in Go 1.16 and earlier.

Note that go/build/constraint was added in Go 1.16, so gVisor now requires Go
1.16.

PiperOrigin-RevId: 387240779
This commit is contained in:
Jamie Liu
2021-07-27 18:14:05 -07:00
committed by gVisor bot
parent 9a96e00f0f
commit 964fb3ca76
11 changed files with 361 additions and 127 deletions
+18
View File
@@ -0,0 +1,18 @@
load("//tools:defs.bzl", "go_library", "go_test")
package(licenses = ["notice"])
go_library(
name = "constraintutil",
srcs = ["constraintutil.go"],
marshal = False,
stateify = False,
visibility = ["//tools:__subpackages__"],
)
go_test(
name = "constraintutil_test",
size = "small",
srcs = ["constraintutil_test.go"],
library = ":constraintutil",
)
+169
View File
@@ -0,0 +1,169 @@
// Copyright 2021 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package constraintutil provides utilities for working with Go build
// constraints.
package constraintutil
import (
"bufio"
"bytes"
"fmt"
"go/build/constraint"
"io"
"os"
"strings"
)
// FromReader extracts the build constraint from the Go source or assembly file
// whose contents are read by r.
func FromReader(r io.Reader) (constraint.Expr, error) {
// See go/build.parseFileHeader() for the "official" logic that this is
// derived from.
const (
slashStar = "/*"
starSlash = "*/"
gobuildPrefix = "//go:build"
)
s := bufio.NewScanner(r)
var (
inSlashStar = false // between /* and */
haveGobuild = false
e constraint.Expr
)
Lines:
for s.Scan() {
line := bytes.TrimSpace(s.Bytes())
if !inSlashStar && constraint.IsGoBuild(string(line)) {
if haveGobuild {
return nil, fmt.Errorf("multiple go:build directives")
}
haveGobuild = true
var err error
e, err = constraint.Parse(string(line))
if err != nil {
return nil, err
}
}
ThisLine:
for len(line) > 0 {
if inSlashStar {
if i := bytes.Index(line, []byte(starSlash)); i >= 0 {
inSlashStar = false
line = bytes.TrimSpace(line[i+len(starSlash):])
continue ThisLine
}
continue Lines
}
if bytes.HasPrefix(line, []byte("//")) {
continue Lines
}
// Note that if /* appears in the line, but not at the beginning,
// then the line is still non-empty, so skipping this and
// terminating below is correct.
if bytes.HasPrefix(line, []byte(slashStar)) {
inSlashStar = true
line = bytes.TrimSpace(line[len(slashStar):])
continue ThisLine
}
// A non-empty non-comment line terminates scanning for go:build.
break Lines
}
}
return e, s.Err()
}
// FromString extracts the build constraint from the Go source or assembly file
// containing the given data. If no build constraint applies to the file, it
// returns nil.
func FromString(str string) (constraint.Expr, error) {
return FromReader(strings.NewReader(str))
}
// FromFile extracts the build constraint from the Go source or assembly file
// at the given path. If no build constraint applies to the file, it returns
// nil.
func FromFile(path string) (constraint.Expr, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return FromReader(f)
}
// Combine returns a constraint.Expr that evaluates to true iff all expressions
// in es evaluate to true. If es is empty, Combine returns nil.
//
// Preconditions: All constraint.Exprs in es are non-nil.
func Combine(es []constraint.Expr) constraint.Expr {
switch len(es) {
case 0:
return nil
case 1:
return es[0]
default:
a := &constraint.AndExpr{es[0], es[1]}
for i := 2; i < len(es); i++ {
a = &constraint.AndExpr{a, es[i]}
}
return a
}
}
// CombineFromFiles returns a build constraint expression that evaluates to
// true iff the build constraints from all of the given Go source or assembly
// files evaluate to true. If no build constraints apply to any of the given
// files, it returns nil.
func CombineFromFiles(paths []string) (constraint.Expr, error) {
var es []constraint.Expr
for _, path := range paths {
e, err := FromFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read build constraints from %q: %v", path, err)
}
if e != nil {
es = append(es, e)
}
}
return Combine(es), nil
}
// Lines returns a string containing build constraint directives for the given
// constraint.Expr, including two trailing newlines, as appropriate for a Go
// source or assembly file. At least a go:build directive will be emitted; if
// the constraint is expressible using +build directives as well, then +build
// directives will also be emitted.
//
// If e is nil, Lines returns the empty string.
func Lines(e constraint.Expr) string {
if e == nil {
return ""
}
var b strings.Builder
b.WriteString("//go:build ")
b.WriteString(e.String())
b.WriteByte('\n')
if pblines, err := constraint.PlusBuildLines(e); err == nil {
for _, line := range pblines {
b.WriteString(line)
b.WriteByte('\n')
}
}
b.WriteByte('\n')
return b.String()
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright 2021 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package constraintutil
import (
"go/build/constraint"
"testing"
)
func TestFileParsing(t *testing.T) {
for _, test := range []struct {
name string
data string
expr string
}{
{
name: "Empty",
},
{
name: "NoConstraint",
data: "// copyright header\n\npackage main",
},
{
name: "ConstraintOnFirstLine",
data: "//go:build amd64\n#include \"textflag.h\"",
expr: "amd64",
},
{
name: "ConstraintAfterSlashSlashComment",
data: "// copyright header\n\n//go:build linux\n\npackage newlib",
expr: "linux",
},
{
name: "ConstraintAfterSlashStarComment",
data: "/*\ncopyright header\n*/\n\n//go:build !race\n\npackage oldlib",
expr: "!race",
},
{
name: "ConstraintInSlashSlashComment",
data: "// blah blah //go:build windows",
},
{
name: "ConstraintInSlashStarComment",
data: "/*\n//go:build windows\n*/",
},
{
name: "ConstraintAfterPackageClause",
data: "package oops\n//go:build race",
},
{
name: "ConstraintAfterCppInclude",
data: "#include \"textflag.h\"\n//go:build arm64",
},
} {
t.Run(test.name, func(t *testing.T) {
e, err := FromString(test.data)
if err != nil {
t.Fatalf("FromString(%q) failed: %v", test.data, err)
}
if e == nil {
if len(test.expr) != 0 {
t.Errorf("FromString(%q): got no constraint, wanted %q", test.data, test.expr)
}
} else {
got := e.String()
if len(test.expr) == 0 {
t.Errorf("FromString(%q): got %q, wanted no constraint", test.data, got)
} else if got != test.expr {
t.Errorf("FromString(%q): got %q, wanted %q", test.data, got, test.expr)
}
}
})
}
}
func TestCombine(t *testing.T) {
for _, test := range []struct {
name string
in []string
out string
}{
{
name: "0",
},
{
name: "1",
in: []string{"amd64 || arm64"},
out: "amd64 || arm64",
},
{
name: "2",
in: []string{"amd64", "amd64 && linux"},
out: "amd64 && amd64 && linux",
},
{
name: "3",
in: []string{"amd64", "amd64 || arm64", "amd64 || riscv64"},
out: "amd64 && (amd64 || arm64) && (amd64 || riscv64)",
},
} {
t.Run(test.name, func(t *testing.T) {
inexprs := make([]constraint.Expr, 0, len(test.in))
for _, estr := range test.in {
line := "//go:build " + estr
e, err := constraint.Parse(line)
if err != nil {
t.Fatalf("constraint.Parse(%q) failed: %v", line, err)
}
inexprs = append(inexprs, e)
}
outexpr := Combine(inexprs)
if outexpr == nil {
if len(test.out) != 0 {
t.Errorf("Combine(%v): got no constraint, wanted %q", test.in, test.out)
}
} else {
got := outexpr.String()
if len(test.out) == 0 {
t.Errorf("Combine(%v): got %q, wanted no constraint", test.in, got)
} else if got != test.out {
t.Errorf("Combine(%v): got %q, wanted %q", test.in, got, test.out)
}
}
})
}
}
+1 -1
View File
@@ -7,6 +7,6 @@ go_binary(
srcs = ["main.go"],
visibility = ["//:sandbox"],
deps = [
"//tools/tags",
"//tools/constraintutil",
],
)
+8 -5
View File
@@ -25,9 +25,8 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"gvisor.dev/gvisor/tools/tags"
"gvisor.dev/gvisor/tools/constraintutil"
)
var (
@@ -131,6 +130,12 @@ func main() {
}
f.Decls = newDecls
// Infer build constraints for the output file.
bcexpr, err := constraintutil.CombineFromFiles(flag.Args())
if err != nil {
fatalf("Failed to read build constraints: %v\n", err)
}
// Write the output file.
var buf bytes.Buffer
if err := format.Node(&buf, fset, f); err != nil {
@@ -141,9 +146,7 @@ func main() {
fatalf("opening output: %v\n", err)
}
defer outf.Close()
if t := tags.Aggregate(flag.Args()); len(t) > 0 {
fmt.Fprintf(outf, "%s\n\n", strings.Join(t.Lines(), "\n"))
}
outf.WriteString(constraintutil.Lines(bcexpr))
if _, err := outf.Write(buf.Bytes()); err != nil {
fatalf("write: %v\n", err)
}
+1 -1
View File
@@ -18,5 +18,5 @@ go_library(
visibility = [
"//:sandbox",
],
deps = ["//tools/tags"],
deps = ["//tools/constraintutil"],
)
+18 -15
View File
@@ -25,7 +25,7 @@ import (
"sort"
"strings"
"gvisor.dev/gvisor/tools/tags"
"gvisor.dev/gvisor/tools/constraintutil"
)
// List of identifiers we use in generated code that may conflict with a
@@ -123,16 +123,18 @@ func (g *Generator) writeHeader() error {
var b sourceBuffer
b.emit("// Automatically generated marshal implementation. See tools/go_marshal.\n\n")
// Emit build tags.
b.emit("// If there are issues with build tag aggregation, see\n")
b.emit("// tools/go_marshal/gomarshal/generator.go:writeHeader(). The build tags here\n")
b.emit("// come from the input set of files used to generate this file. This input set\n")
b.emit("// is filtered based on pre-defined file suffixes related to build tags, see \n")
b.emit("// tools/defs.bzl:calculate_sets().\n\n")
if t := tags.Aggregate(g.inputs); len(t) > 0 {
b.emit(strings.Join(t.Lines(), "\n"))
b.emit("\n\n")
bcexpr, err := constraintutil.CombineFromFiles(g.inputs)
if err != nil {
return err
}
if bcexpr != nil {
// Emit build constraints.
b.emit("// If there are issues with build constraint aggregation, see\n")
b.emit("// tools/go_marshal/gomarshal/generator.go:writeHeader(). The constraints here\n")
b.emit("// come from the input set of files used to generate this file. This input set\n")
b.emit("// is filtered based on pre-defined file suffixes related to build constraints,\n")
b.emit("// see tools/defs.bzl:calculate_sets().\n\n")
b.emit(constraintutil.Lines(bcexpr))
}
// Package header.
@@ -553,11 +555,12 @@ func (g *Generator) writeTests(ts []*testGenerator) error {
b.reset()
b.emit("// Automatically generated marshal tests. See tools/go_marshal.\n\n")
// Emit build tags.
if t := tags.Aggregate(g.inputs); len(t) > 0 {
b.emit(strings.Join(t.Lines(), "\n"))
b.emit("\n\n")
// Emit build constraints.
bcexpr, err := constraintutil.CombineFromFiles(g.inputs)
if err != nil {
return err
}
b.emit(constraintutil.Lines(bcexpr))
b.emit("package %s\n\n", g.pkg)
if err := b.write(g.outputTest); err != nil {
+1 -1
View File
@@ -6,7 +6,7 @@ go_binary(
name = "stateify",
srcs = ["main.go"],
visibility = ["//:sandbox"],
deps = ["//tools/tags"],
deps = ["//tools/constraintutil"],
)
bzl_library(
+7 -4
View File
@@ -28,7 +28,7 @@ import (
"strings"
"sync"
"gvisor.dev/gvisor/tools/tags"
"gvisor.dev/gvisor/tools/constraintutil"
)
var (
@@ -214,10 +214,13 @@ func main() {
// Automated warning.
fmt.Fprint(outputFile, "// automatically generated by stateify.\n\n")
// Emit build tags.
if t := tags.Aggregate(flag.Args()); len(t) > 0 {
fmt.Fprintf(outputFile, "%s\n\n", strings.Join(t.Lines(), "\n"))
// Emit build constraints.
bcexpr, err := constraintutil.CombineFromFiles(flag.Args())
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to infer build constraints: %v", err)
os.Exit(1)
}
outputFile.WriteString(constraintutil.Lines(bcexpr))
// Emit the package name.
_, pkg := filepath.Split(*fullPkg)
-11
View File
@@ -1,11 +0,0 @@
load("//tools:defs.bzl", "go_library")
package(licenses = ["notice"])
go_library(
name = "tags",
srcs = ["tags.go"],
marshal = False,
stateify = False,
visibility = ["//tools:__subpackages__"],
)
-89
View File
@@ -1,89 +0,0 @@
// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package tags is a utility for parsing build tags.
package tags
import (
"fmt"
"io/ioutil"
"strings"
)
// OrSet is a set of tags on a single line.
//
// Note that tags may include ",", and we don't distinguish this case in the
// logic below. Ideally, this constraints can be split into separate top-level
// build tags in order to resolve any issues.
type OrSet []string
// Line returns the line for this or.
func (or OrSet) Line() string {
return fmt.Sprintf("// +build %s", strings.Join([]string(or), " "))
}
// AndSet is the set of all OrSets.
type AndSet []OrSet
// Lines returns the lines to be printed.
func (and AndSet) Lines() (ls []string) {
for _, or := range and {
ls = append(ls, or.Line())
}
return
}
// Join joins this AndSet with another.
func (and AndSet) Join(other AndSet) AndSet {
return append(and, other...)
}
// Tags returns the unique set of +build tags.
//
// Derived form the runtime's canBuild.
func Tags(file string) (tags AndSet) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil
}
// Check file contents for // +build lines.
for _, p := range strings.Split(string(data), "\n") {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if !strings.HasPrefix(p, "//") {
break
}
if !strings.Contains(p, "+build") {
continue
}
fields := strings.Fields(p[2:])
if len(fields) < 1 || fields[0] != "+build" {
continue
}
tags = append(tags, OrSet(fields[1:]))
}
return tags
}
// Aggregate aggregates all tags from a set of files.
//
// Note that these may be in conflict, in which case the build will fail.
func Aggregate(files []string) (tags AndSet) {
for _, file := range files {
tags = tags.Join(Tags(file))
}
return tags
}