blob: ff6bea6777dce89c81385f1e38182e43de12f821 [file] [log] [blame]
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Copied from Go distribution src/go/build/build.go, syslist.go.
// That package does not export the ability to process raw file data,
// although we could fake it with an appropriate build.Context
// and a lot of unwrapping.
// More importantly, that package does not implement the tags["*"]
// special case, in which both tag and !tag are considered to be true
// for essentially all tags (except "ignore").
//
// If we added this API to go/build directly, we wouldn't need this
// file anymore, but this API is not terribly general-purpose and we
// don't really want to commit to any public form of it, nor do we
// want to move the core parts of go/build into a top-level internal package.
// These details change very infrequently, so the copy is fine.
package imports
import (
"bytes"
"errors"
"fmt"
"go/build/constraint"
"strings"
"unicode"
)
var (
bSlashSlash = []byte("//")
bStarSlash = []byte("*/")
bSlashStar = []byte("/*")
bPlusBuild = []byte("+build")
goBuildComment = []byte("//go:build")
errGoBuildWithoutBuild = errors.New("//go:build comment without // +build comment")
errMultipleGoBuild = errors.New("multiple //go:build comments")
)
func isGoBuildComment(line []byte) bool {
if !bytes.HasPrefix(line, goBuildComment) {
return false
}
line = bytes.TrimSpace(line)
rest := line[len(goBuildComment):]
return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest)
}
// ShouldBuild reports whether it is okay to use this file,
// The rule is that in the file's leading run of // comments
// and blank lines, which must be followed by a blank line
// (to avoid including a Go package clause doc comment),
// lines beginning with '// +build' are taken as build directives.
//
// The file is accepted only if each such line lists something
// matching the file. For example:
//
// // +build windows linux
//
// marks the file as applicable only on Windows and Linux.
//
// If tags["*"] is true, then ShouldBuild will consider every
// build tag except "ignore" to be both true and false for
// the purpose of satisfying build tags, in order to estimate
// (conservatively) whether a file could ever possibly be used
// in any build.
//
func ShouldBuild(content []byte, tags map[string]bool) bool {
// Identify leading run of // comments and blank lines,
// which must be followed by a blank line.
// Also identify any //go:build comments.
content, goBuild, _, err := parseFileHeader(content)
if err != nil {
return false
}
// If //go:build line is present, it controls.
// Otherwise fall back to +build processing.
var shouldBuild bool
switch {
case goBuild != nil:
x, err := constraint.Parse(string(goBuild))
if err != nil {
return false
}
shouldBuild = eval(x, tags, true)
default:
shouldBuild = true
p := content
for len(p) > 0 {
line := p
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line, p = line[:i], p[i+1:]
} else {
p = p[len(p):]
}
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) {
continue
}
text := string(line)
if !constraint.IsPlusBuild(text) {
continue
}
if x, err := constraint.Parse(text); err == nil {
if !eval(x, tags, true) {
shouldBuild = false
}
}
}
}
return shouldBuild
}
func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) {
end := 0
p := content
ended := false // found non-blank, non-// line, so stopped accepting // +build lines
inSlashStar := false // in /* */ comment
Lines:
for len(p) > 0 {
line := p
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line, p = line[:i], p[i+1:]
} else {
p = p[len(p):]
}
line = bytes.TrimSpace(line)
if len(line) == 0 && !ended { // Blank line
// Remember position of most recent blank line.
// When we find the first non-blank, non-// line,
// this "end" position marks the latest file position
// where a // +build line can appear.
// (It must appear _before_ a blank line before the non-blank, non-// line.
// Yes, that's confusing, which is part of why we moved to //go:build lines.)
// Note that ended==false here means that inSlashStar==false,
// since seeing a /* would have set ended==true.
end = len(content) - len(p)
continue Lines
}
if !bytes.HasPrefix(line, bSlashSlash) { // Not comment line
ended = true
}
if !inSlashStar && isGoBuildComment(line) {
if goBuild != nil {
return nil, nil, false, errMultipleGoBuild
}
goBuild = line
}
Comments:
for len(line) > 0 {
if inSlashStar {
if i := bytes.Index(line, bStarSlash); i >= 0 {
inSlashStar = false
line = bytes.TrimSpace(line[i+len(bStarSlash):])
continue Comments
}
continue Lines
}
if bytes.HasPrefix(line, bSlashSlash) {
continue Lines
}
if bytes.HasPrefix(line, bSlashStar) {
inSlashStar = true
line = bytes.TrimSpace(line[len(bSlashStar):])
continue Comments
}
// Found non-comment text.
break Lines
}
}
return content[:end], goBuild, sawBinaryOnly, nil
}
// matchTag reports whether the tag name is valid and tags[name] is true.
// As a special case, if tags["*"] is true and name is not empty or ignore,
// then matchTag will return prefer instead of the actual answer,
// which allows the caller to pretend in that case that most tags are
// both true and false.
func matchTag(name string, tags map[string]bool, prefer bool) bool {
// Tags must be letters, digits, underscores or dots.
// Unlike in Go identifiers, all digits are fine (e.g., "386").
for _, c := range name {
if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
return false
}
}
if tags["*"] && name != "" && name != "ignore" {
// Special case for gathering all possible imports:
// if we put * in the tags map then all tags
// except "ignore" are considered both present and not
// (so we return true no matter how 'want' is set).
return prefer
}
have := tags[name]
if name == "linux" {
have = have || tags["android"]
}
if name == "solaris" {
have = have || tags["illumos"]
}
if name == "darwin" {
have = have || tags["ios"]
}
return have
}
// eval is like
// x.Eval(func(tag string) bool { return matchTag(tag, tags) })
// except that it implements the special case for tags["*"] meaning
// all tags are both true and false at the same time.
func eval(x constraint.Expr, tags map[string]bool, prefer bool) bool {
switch x := x.(type) {
case *constraint.TagExpr:
return matchTag(x.Tag, tags, prefer)
case *constraint.NotExpr:
return !eval(x.X, tags, !prefer)
case *constraint.AndExpr:
return eval(x.X, tags, prefer) && eval(x.Y, tags, prefer)
case *constraint.OrExpr:
return eval(x.X, tags, prefer) || eval(x.Y, tags, prefer)
}
panic(fmt.Sprintf("unexpected constraint expression %T", x))
}
// MatchFile returns false if the name contains a $GOOS or $GOARCH
// suffix which does not match the current system.
// The recognized name formats are:
//
// name_$(GOOS).*
// name_$(GOARCH).*
// name_$(GOOS)_$(GOARCH).*
// name_$(GOOS)_test.*
// name_$(GOARCH)_test.*
// name_$(GOOS)_$(GOARCH)_test.*
//
// Exceptions:
// if GOOS=android, then files with GOOS=linux are also matched.
// if GOOS=illumos, then files with GOOS=solaris are also matched.
// if GOOS=ios, then files with GOOS=darwin are also matched.
//
// If tags["*"] is true, then MatchFile will consider all possible
// GOOS and GOARCH to be available and will consequently
// always return true.
func MatchFile(name string, tags map[string]bool) bool {
if tags["*"] {
return true
}
if dot := strings.Index(name, "."); dot != -1 {
name = name[:dot]
}
// Before Go 1.4, a file called "linux.go" would be equivalent to having a
// build tag "linux" in that file. For Go 1.4 and beyond, we require this
// auto-tagging to apply only to files with a non-empty prefix, so
// "foo_linux.go" is tagged but "linux.go" is not. This allows new operating
// systems, such as android, to arrive without breaking existing code with
// innocuous source code in "android.go". The easiest fix: cut everything
// in the name before the initial _.
i := strings.Index(name, "_")
if i < 0 {
return true
}
name = name[i:] // ignore everything before first _
l := strings.Split(name, "_")
if n := len(l); n > 0 && l[n-1] == "test" {
l = l[:n-1]
}
n := len(l)
if n >= 2 && KnownOS[l[n-2]] && KnownArch[l[n-1]] {
return matchTag(l[n-2], tags, true) && matchTag(l[n-1], tags, true)
}
if n >= 1 && KnownOS[l[n-1]] {
return matchTag(l[n-1], tags, true)
}
if n >= 1 && KnownArch[l[n-1]] {
return matchTag(l[n-1], tags, true)
}
return true
}
var KnownOS = map[string]bool{
"aix": true,
"android": true,
"darwin": true,
"dragonfly": true,
"freebsd": true,
"hurd": true,
"illumos": true,
"ios": true,
"js": true,
"linux": true,
"nacl": true, // legacy; don't remove
"netbsd": true,
"openbsd": true,
"plan9": true,
"solaris": true,
"windows": true,
"zos": true,
}
var KnownArch = map[string]bool{
"386": true,
"amd64": true,
"amd64p32": true, // legacy; don't remove
"arm": true,
"armbe": true,
"arm64": true,
"arm64be": true,
"ppc64": true,
"ppc64le": true,
"mips": true,
"mipsle": true,
"mips64": true,
"mips64le": true,
"mips64p32": true,
"mips64p32le": true,
"ppc": true,
"riscv": true,
"riscv64": true,
"s390": true,
"s390x": true,
"sparc": true,
"sparc64": true,
"wasm": true,
}