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
719package imports
820
921import (
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//
3670func 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
0 commit comments