Skip to content

Commit ed475fe

Browse files
committed
cmd/go: update for //go:build lines
cmd/go has its own //go:build evaluator, which is needed for patterns like 'all'. The code is a modified copy of some unexported routines from the go/build package. Update it by copying those again and re-modifying them. The modifications are primarily the new func eval and also ignoring errors. This CL will need to be backported to Go 1.17, or else Go 1.17 will break when faced with certain //go:build-only repos during 'go list all' or 'go mod tidy'. For #41184. Fixes #49198. Change-Id: Ie0fe3caa8d49004935ecd76d7977f767fe50e317 Reviewed-on: https://go-review.googlesource.com/c/go/+/359355 Trust: Russ Cox <[email protected]> Run-TryBot: Russ Cox <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 2bc8ed8 commit ed475fe

File tree

2 files changed

+192
-65
lines changed

2 files changed

+192
-65
lines changed

src/cmd/go/internal/imports/build.go

+151-65
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,51 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
// Copied from Go distribution src/go/build/build.go, syslist.go
5+
// Copied from Go distribution src/go/build/build.go, syslist.go.
6+
// That package does not export the ability to process raw file data,
7+
// although we could fake it with an appropriate build.Context
8+
// and a lot of unwrapping.
9+
// More importantly, that package does not implement the tags["*"]
10+
// special case, in which both tag and !tag are considered to be true
11+
// for essentially all tags (except "ignore").
12+
//
13+
// If we added this API to go/build directly, we wouldn't need this
14+
// file anymore, but this API is not terribly general-purpose and we
15+
// don't really want to commit to any public form of it, nor do we
16+
// want to move the core parts of go/build into a top-level internal package.
17+
// These details change very infrequently, so the copy is fine.
618

719
package imports
820

921
import (
1022
"bytes"
23+
"errors"
24+
"fmt"
25+
"go/build/constraint"
1126
"strings"
1227
"unicode"
1328
)
1429

15-
var slashslash = []byte("//")
30+
var (
31+
bSlashSlash = []byte("//")
32+
bStarSlash = []byte("*/")
33+
bSlashStar = []byte("/*")
34+
bPlusBuild = []byte("+build")
35+
36+
goBuildComment = []byte("//go:build")
37+
38+
errGoBuildWithoutBuild = errors.New("//go:build comment without // +build comment")
39+
errMultipleGoBuild = errors.New("multiple //go:build comments")
40+
)
41+
42+
func isGoBuildComment(line []byte) bool {
43+
if !bytes.HasPrefix(line, goBuildComment) {
44+
return false
45+
}
46+
line = bytes.TrimSpace(line)
47+
rest := line[len(goBuildComment):]
48+
return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest)
49+
}
1650

1751
// ShouldBuild reports whether it is okay to use this file,
1852
// The rule is that in the file's leading run of // comments
@@ -34,10 +68,61 @@ var slashslash = []byte("//")
3468
// in any build.
3569
//
3670
func ShouldBuild(content []byte, tags map[string]bool) bool {
37-
// Pass 1. Identify leading run of // comments and blank lines,
71+
// Identify leading run of // comments and blank lines,
3872
// which must be followed by a blank line.
73+
// Also identify any //go:build comments.
74+
content, goBuild, _, err := parseFileHeader(content)
75+
if err != nil {
76+
return false
77+
}
78+
79+
// If //go:build line is present, it controls.
80+
// Otherwise fall back to +build processing.
81+
var shouldBuild bool
82+
switch {
83+
case goBuild != nil:
84+
x, err := constraint.Parse(string(goBuild))
85+
if err != nil {
86+
return false
87+
}
88+
shouldBuild = eval(x, tags, true)
89+
90+
default:
91+
shouldBuild = true
92+
p := content
93+
for len(p) > 0 {
94+
line := p
95+
if i := bytes.IndexByte(line, '\n'); i >= 0 {
96+
line, p = line[:i], p[i+1:]
97+
} else {
98+
p = p[len(p):]
99+
}
100+
line = bytes.TrimSpace(line)
101+
if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) {
102+
continue
103+
}
104+
text := string(line)
105+
if !constraint.IsPlusBuild(text) {
106+
continue
107+
}
108+
if x, err := constraint.Parse(text); err == nil {
109+
if !eval(x, tags, true) {
110+
shouldBuild = false
111+
}
112+
}
113+
}
114+
}
115+
116+
return shouldBuild
117+
}
118+
119+
func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) {
39120
end := 0
40121
p := content
122+
ended := false // found non-blank, non-// line, so stopped accepting // +build lines
123+
inSlashStar := false // in /* */ comment
124+
125+
Lines:
41126
for len(p) > 0 {
42127
line := p
43128
if i := bytes.IndexByte(line, '\n'); i >= 0 {
@@ -46,78 +131,61 @@ func ShouldBuild(content []byte, tags map[string]bool) bool {
46131
p = p[len(p):]
47132
}
48133
line = bytes.TrimSpace(line)
49-
if len(line) == 0 { // Blank line
134+
if len(line) == 0 && !ended { // Blank line
135+
// Remember position of most recent blank line.
136+
// When we find the first non-blank, non-// line,
137+
// this "end" position marks the latest file position
138+
// where a // +build line can appear.
139+
// (It must appear _before_ a blank line before the non-blank, non-// line.
140+
// Yes, that's confusing, which is part of why we moved to //go:build lines.)
141+
// Note that ended==false here means that inSlashStar==false,
142+
// since seeing a /* would have set ended==true.
50143
end = len(content) - len(p)
51-
continue
144+
continue Lines
52145
}
53-
if !bytes.HasPrefix(line, slashslash) { // Not comment line
54-
break
146+
if !bytes.HasPrefix(line, bSlashSlash) { // Not comment line
147+
ended = true
55148
}
56-
}
57-
content = content[:end]
58149

59-
// Pass 2. Process each line in the run.
60-
p = content
61-
allok := true
62-
for len(p) > 0 {
63-
line := p
64-
if i := bytes.IndexByte(line, '\n'); i >= 0 {
65-
line, p = line[:i], p[i+1:]
66-
} else {
67-
p = p[len(p):]
68-
}
69-
line = bytes.TrimSpace(line)
70-
if !bytes.HasPrefix(line, slashslash) {
71-
continue
150+
if !inSlashStar && isGoBuildComment(line) {
151+
if goBuild != nil {
152+
return nil, nil, false, errMultipleGoBuild
153+
}
154+
goBuild = line
72155
}
73-
line = bytes.TrimSpace(line[len(slashslash):])
74-
if len(line) > 0 && line[0] == '+' {
75-
// Looks like a comment +line.
76-
f := strings.Fields(string(line))
77-
if f[0] == "+build" {
78-
ok := false
79-
for _, tok := range f[1:] {
80-
if matchTags(tok, tags) {
81-
ok = true
82-
}
83-
}
84-
if !ok {
85-
allok = false
156+
157+
Comments:
158+
for len(line) > 0 {
159+
if inSlashStar {
160+
if i := bytes.Index(line, bStarSlash); i >= 0 {
161+
inSlashStar = false
162+
line = bytes.TrimSpace(line[i+len(bStarSlash):])
163+
continue Comments
86164
}
165+
continue Lines
87166
}
167+
if bytes.HasPrefix(line, bSlashSlash) {
168+
continue Lines
169+
}
170+
if bytes.HasPrefix(line, bSlashStar) {
171+
inSlashStar = true
172+
line = bytes.TrimSpace(line[len(bSlashStar):])
173+
continue Comments
174+
}
175+
// Found non-comment text.
176+
break Lines
88177
}
89178
}
90179

91-
return allok
92-
}
93-
94-
// matchTags reports whether the name is one of:
95-
//
96-
// tag (if tags[tag] is true)
97-
// !tag (if tags[tag] is false)
98-
// a comma-separated list of any of these
99-
//
100-
func matchTags(name string, tags map[string]bool) bool {
101-
if name == "" {
102-
return false
103-
}
104-
if i := strings.Index(name, ","); i >= 0 {
105-
// comma-separated list
106-
ok1 := matchTags(name[:i], tags)
107-
ok2 := matchTags(name[i+1:], tags)
108-
return ok1 && ok2
109-
}
110-
if strings.HasPrefix(name, "!!") { // bad syntax, reject always
111-
return false
112-
}
113-
if strings.HasPrefix(name, "!") { // negation
114-
return len(name) > 1 && matchTag(name[1:], tags, false)
115-
}
116-
return matchTag(name, tags, true)
180+
return content[:end], goBuild, sawBinaryOnly, nil
117181
}
118182

119-
// matchTag reports whether the tag name is valid and satisfied by tags[name]==want.
120-
func matchTag(name string, tags map[string]bool, want bool) bool {
183+
// matchTag reports whether the tag name is valid and tags[name] is true.
184+
// As a special case, if tags["*"] is true and name is not empty or ignore,
185+
// then matchTag will return prefer instead of the actual answer,
186+
// which allows the caller to pretend in that case that most tags are
187+
// both true and false.
188+
func matchTag(name string, tags map[string]bool, prefer bool) bool {
121189
// Tags must be letters, digits, underscores or dots.
122190
// Unlike in Go identifiers, all digits are fine (e.g., "386").
123191
for _, c := range name {
@@ -131,7 +199,7 @@ func matchTag(name string, tags map[string]bool, want bool) bool {
131199
// if we put * in the tags map then all tags
132200
// except "ignore" are considered both present and not
133201
// (so we return true no matter how 'want' is set).
134-
return true
202+
return prefer
135203
}
136204

137205
have := tags[name]
@@ -144,7 +212,25 @@ func matchTag(name string, tags map[string]bool, want bool) bool {
144212
if name == "darwin" {
145213
have = have || tags["ios"]
146214
}
147-
return have == want
215+
return have
216+
}
217+
218+
// eval is like
219+
// x.Eval(func(tag string) bool { return matchTag(tag, tags) })
220+
// except that it implements the special case for tags["*"] meaning
221+
// all tags are both true and false at the same time.
222+
func eval(x constraint.Expr, tags map[string]bool, prefer bool) bool {
223+
switch x := x.(type) {
224+
case *constraint.TagExpr:
225+
return matchTag(x.Tag, tags, prefer)
226+
case *constraint.NotExpr:
227+
return !eval(x.X, tags, !prefer)
228+
case *constraint.AndExpr:
229+
return eval(x.X, tags, prefer) && eval(x.Y, tags, prefer)
230+
case *constraint.OrExpr:
231+
return eval(x.X, tags, prefer) || eval(x.Y, tags, prefer)
232+
}
233+
panic(fmt.Sprintf("unexpected constraint expression %T", x))
148234
}
149235

150236
// MatchFile returns false if the name contains a $GOOS or $GOARCH
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# go list all should work with GOOS=linux because all packages build on Linux
2+
env GOOS=linux
3+
go list all
4+
5+
# go list all should work with GOOS=darwin, but it used to fail because
6+
# in the absence of //go:build support, p looked like it needed q
7+
# (p_test.go was not properly excluded), and q was Linux-only.
8+
#
9+
# Also testing with r and s that +build lines keep working.
10+
env GOOS=darwin
11+
go list all
12+
13+
-- go.mod --
14+
go 1.17
15+
module m
16+
17+
-- p/p.go --
18+
package p
19+
20+
-- p/p_test.go --
21+
//go:build linux
22+
23+
package p
24+
25+
import "m/q"
26+
27+
-- q/q_linux.go --
28+
package q
29+
30+
-- r/r.go --
31+
package r
32+
33+
-- r/r_test.go --
34+
// +build linux
35+
36+
package r
37+
38+
import "m/s"
39+
40+
-- s/s_linux.go --
41+
package s

0 commit comments

Comments
 (0)