github.com/octohelm/cuemod@v0.9.4/internal/cmd/go/internals/imports/build.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 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. 18 19 package imports 20 21 import ( 22 "bytes" 23 "errors" 24 "fmt" 25 "github.com/octohelm/cuemod/internal/cmd/go/internals/cfg" 26 "go/build/constraint" 27 "strings" 28 "unicode" 29 ) 30 31 var ( 32 bSlashSlash = []byte("//") 33 bStarSlash = []byte("*/") 34 bSlashStar = []byte("/*") 35 bPlusBuild = []byte("+build") 36 37 goBuildComment = []byte("//go:build") 38 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 } 50 51 // ShouldBuild reports whether it is okay to use this file, 52 // The rule is that in the file's leading run of // comments 53 // and blank lines, which must be followed by a blank line 54 // (to avoid including a Go package clause doc comment), 55 // lines beginning with '// +build' are taken as build directives. 56 // 57 // The file is accepted only if each such line lists something 58 // matching the file. For example: 59 // 60 // // +build windows linux 61 // 62 // marks the file as applicable only on Windows and Linux. 63 // 64 // If tags["*"] is true, then ShouldBuild will consider every 65 // build tag except "ignore" to be both true and false for 66 // the purpose of satisfying build tags, in order to estimate 67 // (conservatively) whether a file could ever possibly be used 68 // in any build. 69 func ShouldBuild(content []byte, tags map[string]bool) bool { 70 // Identify leading run of // comments and blank lines, 71 // which must be followed by a blank line. 72 // Also identify any //go:build comments. 73 content, goBuild, _, err := parseFileHeader(content) 74 if err != nil { 75 return false 76 } 77 78 // If //go:build line is present, it controls. 79 // Otherwise fall back to +build processing. 80 var shouldBuild bool 81 switch { 82 case goBuild != nil: 83 x, err := constraint.Parse(string(goBuild)) 84 if err != nil { 85 return false 86 } 87 shouldBuild = eval(x, tags, true) 88 89 default: 90 shouldBuild = true 91 p := content 92 for len(p) > 0 { 93 line := p 94 if i := bytes.IndexByte(line, '\n'); i >= 0 { 95 line, p = line[:i], p[i+1:] 96 } else { 97 p = p[len(p):] 98 } 99 line = bytes.TrimSpace(line) 100 if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) { 101 continue 102 } 103 text := string(line) 104 if !constraint.IsPlusBuild(text) { 105 continue 106 } 107 if x, err := constraint.Parse(text); err == nil { 108 if !eval(x, tags, true) { 109 shouldBuild = false 110 } 111 } 112 } 113 } 114 115 return shouldBuild 116 } 117 118 func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) { 119 end := 0 120 p := content 121 ended := false // found non-blank, non-// line, so stopped accepting // +build lines 122 inSlashStar := false // in /* */ comment 123 124 Lines: 125 for len(p) > 0 { 126 line := p 127 if i := bytes.IndexByte(line, '\n'); i >= 0 { 128 line, p = line[:i], p[i+1:] 129 } else { 130 p = p[len(p):] 131 } 132 line = bytes.TrimSpace(line) 133 if len(line) == 0 && !ended { // Blank line 134 // Remember position of most recent blank line. 135 // When we find the first non-blank, non-// line, 136 // this "end" position marks the latest file position 137 // where a // +build line can appear. 138 // (It must appear _before_ a blank line before the non-blank, non-// line. 139 // Yes, that's confusing, which is part of why we moved to //go:build lines.) 140 // Note that ended==false here means that inSlashStar==false, 141 // since seeing a /* would have set ended==true. 142 end = len(content) - len(p) 143 continue Lines 144 } 145 if !bytes.HasPrefix(line, bSlashSlash) { // Not comment line 146 ended = true 147 } 148 149 if !inSlashStar && isGoBuildComment(line) { 150 if goBuild != nil { 151 return nil, nil, false, errMultipleGoBuild 152 } 153 goBuild = line 154 } 155 156 Comments: 157 for len(line) > 0 { 158 if inSlashStar { 159 if i := bytes.Index(line, bStarSlash); i >= 0 { 160 inSlashStar = false 161 line = bytes.TrimSpace(line[i+len(bStarSlash):]) 162 continue Comments 163 } 164 continue Lines 165 } 166 if bytes.HasPrefix(line, bSlashSlash) { 167 continue Lines 168 } 169 if bytes.HasPrefix(line, bSlashStar) { 170 inSlashStar = true 171 line = bytes.TrimSpace(line[len(bSlashStar):]) 172 continue Comments 173 } 174 // Found non-comment text. 175 break Lines 176 } 177 } 178 179 return content[:end], goBuild, sawBinaryOnly, nil 180 } 181 182 // matchTag reports whether the tag name is valid and tags[name] is true. 183 // As a special case, if tags["*"] is true and name is not empty or ignore, 184 // then matchTag will return prefer instead of the actual answer, 185 // which allows the caller to pretend in that case that most tags are 186 // both true and false. 187 func matchTag(name string, tags map[string]bool, prefer bool) bool { 188 // Tags must be letters, digits, underscores or dots. 189 // Unlike in Go identifiers, all digits are fine (e.g., "386"). 190 for _, c := range name { 191 if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { 192 return false 193 } 194 } 195 196 if tags["*"] && name != "" && name != "ignore" { 197 // Special case for gathering all possible imports: 198 // if we put * in the tags map then all tags 199 // except "ignore" are considered both present and not 200 // (so we return true no matter how 'want' is set). 201 return prefer 202 } 203 204 if tags[name] { 205 return true 206 } 207 208 switch name { 209 case "linux": 210 return tags["android"] 211 case "solaris": 212 return tags["illumos"] 213 case "darwin": 214 return tags["ios"] 215 case "unix": 216 return unixOS[cfg.BuildContext.GOOS] 217 default: 218 return false 219 } 220 } 221 222 // eval is like 223 // 224 // x.Eval(func(tag string) bool { return matchTag(tag, tags) }) 225 // 226 // except that it implements the special case for tags["*"] meaning 227 // all tags are both true and false at the same time. 228 func eval(x constraint.Expr, tags map[string]bool, prefer bool) bool { 229 switch x := x.(type) { 230 case *constraint.TagExpr: 231 return matchTag(x.Tag, tags, prefer) 232 case *constraint.NotExpr: 233 return !eval(x.X, tags, !prefer) 234 case *constraint.AndExpr: 235 return eval(x.X, tags, prefer) && eval(x.Y, tags, prefer) 236 case *constraint.OrExpr: 237 return eval(x.X, tags, prefer) || eval(x.Y, tags, prefer) 238 } 239 panic(fmt.Sprintf("unexpected constraint expression %T", x)) 240 } 241 242 // Eval is like 243 // 244 // x.Eval(func(tag string) bool { return matchTag(tag, tags) }) 245 // 246 // except that it implements the special case for tags["*"] meaning 247 // all tags are both true and false at the same time. 248 func Eval(x constraint.Expr, tags map[string]bool, prefer bool) bool { 249 return eval(x, tags, prefer) 250 } 251 252 // MatchFile returns false if the name contains a $GOOS or $GOARCH 253 // suffix which does not match the current system. 254 // The recognized name formats are: 255 // 256 // name_$(GOOS).* 257 // name_$(GOARCH).* 258 // name_$(GOOS)_$(GOARCH).* 259 // name_$(GOOS)_test.* 260 // name_$(GOARCH)_test.* 261 // name_$(GOOS)_$(GOARCH)_test.* 262 // 263 // Exceptions: 264 // 265 // if GOOS=android, then files with GOOS=linux are also matched. 266 // if GOOS=illumos, then files with GOOS=solaris are also matched. 267 // if GOOS=ios, then files with GOOS=darwin are also matched. 268 // 269 // If tags["*"] is true, then MatchFile will consider all possible 270 // GOOS and GOARCH to be available and will consequently 271 // always return true. 272 func MatchFile(name string, tags map[string]bool) bool { 273 if tags["*"] { 274 return true 275 } 276 if dot := strings.Index(name, "."); dot != -1 { 277 name = name[:dot] 278 } 279 280 // Before Go 1.4, a file called "linux.go" would be equivalent to having a 281 // build tag "linux" in that file. For Go 1.4 and beyond, we require this 282 // auto-tagging to apply only to files with a non-empty prefix, so 283 // "foo_linux.go" is tagged but "linux.go" is not. This allows new operating 284 // systems, such as android, to arrive without breaking existing code with 285 // innocuous source code in "android.go". The easiest fix: cut everything 286 // in the name before the initial _. 287 i := strings.Index(name, "_") 288 if i < 0 { 289 return true 290 } 291 name = name[i:] // ignore everything before first _ 292 293 l := strings.Split(name, "_") 294 if n := len(l); n > 0 && l[n-1] == "test" { 295 l = l[:n-1] 296 } 297 n := len(l) 298 if n >= 2 && KnownOS[l[n-2]] && KnownArch[l[n-1]] { 299 return matchTag(l[n-2], tags, true) && matchTag(l[n-1], tags, true) 300 } 301 if n >= 1 && KnownOS[l[n-1]] { 302 return matchTag(l[n-1], tags, true) 303 } 304 if n >= 1 && KnownArch[l[n-1]] { 305 return matchTag(l[n-1], tags, true) 306 } 307 return true 308 } 309 310 var KnownOS = map[string]bool{ 311 "aix": true, 312 "android": true, 313 "darwin": true, 314 "dragonfly": true, 315 "freebsd": true, 316 "hurd": true, 317 "illumos": true, 318 "ios": true, 319 "js": true, 320 "linux": true, 321 "nacl": true, // legacy; don't remove 322 "netbsd": true, 323 "openbsd": true, 324 "plan9": true, 325 "solaris": true, 326 "wasip1": true, 327 "windows": true, 328 "zos": true, 329 } 330 331 // unixOS is the set of GOOS values matched by the "unix" build tag. 332 // This is not used for filename matching. 333 // This is the same list as in go/build/syslist.go and cmd/dist/build.go. 334 var unixOS = map[string]bool{ 335 "aix": true, 336 "android": true, 337 "darwin": true, 338 "dragonfly": true, 339 "freebsd": true, 340 "hurd": true, 341 "illumos": true, 342 "ios": true, 343 "linux": true, 344 "netbsd": true, 345 "openbsd": true, 346 "solaris": true, 347 } 348 349 var KnownArch = map[string]bool{ 350 "386": true, 351 "amd64": true, 352 "amd64p32": true, // legacy; don't remove 353 "arm": true, 354 "armbe": true, 355 "arm64": true, 356 "arm64be": true, 357 "ppc64": true, 358 "ppc64le": true, 359 "mips": true, 360 "mipsle": true, 361 "mips64": true, 362 "mips64le": true, 363 "mips64p32": true, 364 "mips64p32le": true, 365 "loong64": true, 366 "ppc": true, 367 "riscv": true, 368 "riscv64": true, 369 "s390": true, 370 "s390x": true, 371 "sparc": true, 372 "sparc64": true, 373 "wasm": true, 374 }