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  }