2
2
// Use of this source code is governed by a BSD-style
3
3
// license that can be found in the LICENSE file.
4
4
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.
6
18
7
19
package imports
8
20
9
21
import (
10
22
"bytes"
23
+ "errors"
24
+ "fmt"
25
+ "go/build/constraint"
11
26
"strings"
12
27
"unicode"
13
28
)
14
29
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
+ }
16
50
17
51
// ShouldBuild reports whether it is okay to use this file,
18
52
// The rule is that in the file's leading run of // comments
@@ -34,10 +68,61 @@ var slashslash = []byte("//")
34
68
// in any build.
35
69
//
36
70
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,
38
72
// 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 ) {
39
120
end := 0
40
121
p := content
122
+ ended := false // found non-blank, non-// line, so stopped accepting // +build lines
123
+ inSlashStar := false // in /* */ comment
124
+
125
+ Lines:
41
126
for len (p ) > 0 {
42
127
line := p
43
128
if i := bytes .IndexByte (line , '\n' ); i >= 0 {
@@ -46,78 +131,61 @@ func ShouldBuild(content []byte, tags map[string]bool) bool {
46
131
p = p [len (p ):]
47
132
}
48
133
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.
50
143
end = len (content ) - len (p )
51
- continue
144
+ continue Lines
52
145
}
53
- if ! bytes .HasPrefix (line , slashslash ) { // Not comment line
54
- break
146
+ if ! bytes .HasPrefix (line , bSlashSlash ) { // Not comment line
147
+ ended = true
55
148
}
56
- }
57
- content = content [:end ]
58
149
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
72
155
}
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
86
164
}
165
+ continue Lines
87
166
}
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
88
177
}
89
178
}
90
179
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
117
181
}
118
182
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 {
121
189
// Tags must be letters, digits, underscores or dots.
122
190
// Unlike in Go identifiers, all digits are fine (e.g., "386").
123
191
for _ , c := range name {
@@ -131,7 +199,7 @@ func matchTag(name string, tags map[string]bool, want bool) bool {
131
199
// if we put * in the tags map then all tags
132
200
// except "ignore" are considered both present and not
133
201
// (so we return true no matter how 'want' is set).
134
- return true
202
+ return prefer
135
203
}
136
204
137
205
have := tags [name ]
@@ -144,7 +212,25 @@ func matchTag(name string, tags map[string]bool, want bool) bool {
144
212
if name == "darwin" {
145
213
have = have || tags ["ios" ]
146
214
}
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 ))
148
234
}
149
235
150
236
// MatchFile returns false if the name contains a $GOOS or $GOARCH
0 commit comments