Source file src/cmd/go/internal/modindex/build.go

     1  // Copyright 2011 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  // This file is a lightly modified copy go/build/build.go with unused parts
     6  // removed.
     7  
     8  package modindex
     9  
    10  import (
    11  	"bytes"
    12  	"cmd/go/internal/fsys"
    13  	"errors"
    14  	"fmt"
    15  	"go/ast"
    16  	"go/build"
    17  	"go/build/constraint"
    18  	"go/token"
    19  	"internal/syslist"
    20  	"io"
    21  	"io/fs"
    22  	"path/filepath"
    23  	"slices"
    24  	"sort"
    25  	"strings"
    26  	"unicode"
    27  	"unicode/utf8"
    28  )
    29  
    30  // A Context specifies the supporting context for a build.
    31  type Context struct {
    32  	GOARCH string // target architecture
    33  	GOOS   string // target operating system
    34  	GOROOT string // Go root
    35  	GOPATH string // Go paths
    36  
    37  	// Dir is the caller's working directory, or the empty string to use
    38  	// the current directory of the running process. In module mode, this is used
    39  	// to locate the main module.
    40  	//
    41  	// If Dir is non-empty, directories passed to Import and ImportDir must
    42  	// be absolute.
    43  	Dir string
    44  
    45  	CgoEnabled  bool   // whether cgo files are included
    46  	UseAllFiles bool   // use files regardless of //go:build lines, file names
    47  	Compiler    string // compiler to assume when computing target paths
    48  
    49  	// The build, tool, and release tags specify build constraints
    50  	// that should be considered satisfied when processing +build lines.
    51  	// Clients creating a new context may customize BuildTags, which
    52  	// defaults to empty, but it is usually an error to customize ToolTags or ReleaseTags.
    53  	// ToolTags defaults to build tags appropriate to the current Go toolchain configuration.
    54  	// ReleaseTags defaults to the list of Go releases the current release is compatible with.
    55  	// BuildTags is not set for the Default build Context.
    56  	// In addition to the BuildTags, ToolTags, and ReleaseTags, build constraints
    57  	// consider the values of GOARCH and GOOS as satisfied tags.
    58  	// The last element in ReleaseTags is assumed to be the current release.
    59  	BuildTags   []string
    60  	ToolTags    []string
    61  	ReleaseTags []string
    62  
    63  	// The install suffix specifies a suffix to use in the name of the installation
    64  	// directory. By default it is empty, but custom builds that need to keep
    65  	// their outputs separate can set InstallSuffix to do so. For example, when
    66  	// using the race detector, the go command uses InstallSuffix = "race", so
    67  	// that on a Linux/386 system, packages are written to a directory named
    68  	// "linux_386_race" instead of the usual "linux_386".
    69  	InstallSuffix string
    70  
    71  	// By default, Import uses the operating system's file system calls
    72  	// to read directories and files. To read from other sources,
    73  	// callers can set the following functions. They all have default
    74  	// behaviors that use the local file system, so clients need only set
    75  	// the functions whose behaviors they wish to change.
    76  
    77  	// JoinPath joins the sequence of path fragments into a single path.
    78  	// If JoinPath is nil, Import uses filepath.Join.
    79  	JoinPath func(elem ...string) string
    80  
    81  	// SplitPathList splits the path list into a slice of individual paths.
    82  	// If SplitPathList is nil, Import uses filepath.SplitList.
    83  	SplitPathList func(list string) []string
    84  
    85  	// IsAbsPath reports whether path is an absolute path.
    86  	// If IsAbsPath is nil, Import uses filepath.IsAbs.
    87  	IsAbsPath func(path string) bool
    88  
    89  	// IsDir reports whether the path names a directory.
    90  	// If IsDir is nil, Import calls os.Stat and uses the result's IsDir method.
    91  	IsDir func(path string) bool
    92  
    93  	// HasSubdir reports whether dir is lexically a subdirectory of
    94  	// root, perhaps multiple levels below. It does not try to check
    95  	// whether dir exists.
    96  	// If so, HasSubdir sets rel to a slash-separated path that
    97  	// can be joined to root to produce a path equivalent to dir.
    98  	// If HasSubdir is nil, Import uses an implementation built on
    99  	// filepath.EvalSymlinks.
   100  	HasSubdir func(root, dir string) (rel string, ok bool)
   101  
   102  	// ReadDir returns a slice of fs.FileInfo, sorted by Name,
   103  	// describing the content of the named directory.
   104  	// If ReadDir is nil, Import uses ioutil.ReadDir.
   105  	ReadDir func(dir string) ([]fs.FileInfo, error)
   106  
   107  	// OpenFile opens a file (not a directory) for reading.
   108  	// If OpenFile is nil, Import uses os.Open.
   109  	OpenFile func(path string) (io.ReadCloser, error)
   110  }
   111  
   112  // joinPath calls ctxt.JoinPath (if not nil) or else filepath.Join.
   113  func (ctxt *Context) joinPath(elem ...string) string {
   114  	if f := ctxt.JoinPath; f != nil {
   115  		return f(elem...)
   116  	}
   117  	return filepath.Join(elem...)
   118  }
   119  
   120  // isDir reports whether path is a directory.
   121  func isDir(path string) bool {
   122  	fi, err := fsys.Stat(path)
   123  	return err == nil && fi.IsDir()
   124  }
   125  
   126  var defaultToolTags, defaultReleaseTags []string
   127  
   128  // NoGoError is the error used by Import to describe a directory
   129  // containing no buildable Go source files. (It may still contain
   130  // test files, files hidden by build tags, and so on.)
   131  type NoGoError struct {
   132  	Dir string
   133  }
   134  
   135  func (e *NoGoError) Error() string {
   136  	return "no buildable Go source files in " + e.Dir
   137  }
   138  
   139  // MultiplePackageError describes a directory containing
   140  // multiple buildable Go source files for multiple packages.
   141  type MultiplePackageError struct {
   142  	Dir      string   // directory containing files
   143  	Packages []string // package names found
   144  	Files    []string // corresponding files: Files[i] declares package Packages[i]
   145  }
   146  
   147  func (e *MultiplePackageError) Error() string {
   148  	// Error string limited to two entries for compatibility.
   149  	return fmt.Sprintf("found packages %s (%s) and %s (%s) in %s", e.Packages[0], e.Files[0], e.Packages[1], e.Files[1], e.Dir)
   150  }
   151  
   152  func nameExt(name string) string {
   153  	i := strings.LastIndex(name, ".")
   154  	if i < 0 {
   155  		return ""
   156  	}
   157  	return name[i:]
   158  }
   159  
   160  func fileListForExt(p *build.Package, ext string) *[]string {
   161  	switch ext {
   162  	case ".c":
   163  		return &p.CFiles
   164  	case ".cc", ".cpp", ".cxx":
   165  		return &p.CXXFiles
   166  	case ".m":
   167  		return &p.MFiles
   168  	case ".h", ".hh", ".hpp", ".hxx":
   169  		return &p.HFiles
   170  	case ".f", ".F", ".for", ".f90":
   171  		return &p.FFiles
   172  	case ".s", ".S", ".sx":
   173  		return &p.SFiles
   174  	case ".swig":
   175  		return &p.SwigFiles
   176  	case ".swigcxx":
   177  		return &p.SwigCXXFiles
   178  	case ".syso":
   179  		return &p.SysoFiles
   180  	}
   181  	return nil
   182  }
   183  
   184  var (
   185  	bSlashSlash = []byte("//")
   186  	bSlashStar  = []byte("/*")
   187  	bStarSlash  = []byte("*/")
   188  	bPlusBuild  = []byte("+build")
   189  )
   190  
   191  var dummyPkg build.Package
   192  
   193  // fileInfo records information learned about a file included in a build.
   194  type fileInfo struct {
   195  	name       string // full name including dir
   196  	header     []byte
   197  	fset       *token.FileSet
   198  	parsed     *ast.File
   199  	parseErr   error
   200  	imports    []fileImport
   201  	embeds     []fileEmbed
   202  	directives []build.Directive
   203  
   204  	// Additional fields added to go/build's fileinfo for the purposes of the modindex package.
   205  	binaryOnly           bool
   206  	goBuildConstraint    string
   207  	plusBuildConstraints []string
   208  }
   209  
   210  type fileImport struct {
   211  	path string
   212  	pos  token.Pos
   213  	doc  *ast.CommentGroup
   214  }
   215  
   216  type fileEmbed struct {
   217  	pattern string
   218  	pos     token.Position
   219  }
   220  
   221  var errNonSource = errors.New("non source file")
   222  
   223  // getFileInfo extracts the information needed from each go file for the module
   224  // index.
   225  //
   226  // If Name denotes a Go program, matchFile reads until the end of the
   227  // Imports and returns that section of the file in the FileInfo's Header field,
   228  // even though it only considers text until the first non-comment
   229  // for +build lines.
   230  //
   231  // getFileInfo will return errNonSource if the file is not a source or object
   232  // file and shouldn't even be added to IgnoredFiles.
   233  func getFileInfo(dir, name string, fset *token.FileSet) (*fileInfo, error) {
   234  	if strings.HasPrefix(name, "_") ||
   235  		strings.HasPrefix(name, ".") {
   236  		return nil, nil
   237  	}
   238  
   239  	i := strings.LastIndex(name, ".")
   240  	if i < 0 {
   241  		i = len(name)
   242  	}
   243  	ext := name[i:]
   244  
   245  	if ext != ".go" && fileListForExt(&dummyPkg, ext) == nil {
   246  		// skip
   247  		return nil, errNonSource
   248  	}
   249  
   250  	info := &fileInfo{name: filepath.Join(dir, name), fset: fset}
   251  	if ext == ".syso" {
   252  		// binary, no reading
   253  		return info, nil
   254  	}
   255  
   256  	f, err := fsys.Open(info.name)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	// TODO(matloob) should we decide whether to ignore binary only here or earlier
   262  	// when we create the index file?
   263  	var ignoreBinaryOnly bool
   264  	if strings.HasSuffix(name, ".go") {
   265  		err = readGoInfo(f, info)
   266  		if strings.HasSuffix(name, "_test.go") {
   267  			ignoreBinaryOnly = true // ignore //go:binary-only-package comments in _test.go files
   268  		}
   269  	} else {
   270  		info.header, err = readComments(f)
   271  	}
   272  	f.Close()
   273  	if err != nil {
   274  		return nil, fmt.Errorf("read %s: %v", info.name, err)
   275  	}
   276  
   277  	// Look for +build comments to accept or reject the file.
   278  	info.goBuildConstraint, info.plusBuildConstraints, info.binaryOnly, err = getConstraints(info.header)
   279  	if err != nil {
   280  		return nil, fmt.Errorf("%s: %v", name, err)
   281  	}
   282  
   283  	if ignoreBinaryOnly && info.binaryOnly {
   284  		info.binaryOnly = false // override info.binaryOnly
   285  	}
   286  
   287  	return info, nil
   288  }
   289  
   290  func cleanDecls(m map[string][]token.Position) ([]string, map[string][]token.Position) {
   291  	all := make([]string, 0, len(m))
   292  	for path := range m {
   293  		all = append(all, path)
   294  	}
   295  	sort.Strings(all)
   296  	return all, m
   297  }
   298  
   299  var (
   300  	goBuildComment = []byte("//go:build")
   301  
   302  	errMultipleGoBuild = errors.New("multiple //go:build comments")
   303  )
   304  
   305  func isGoBuildComment(line []byte) bool {
   306  	if !bytes.HasPrefix(line, goBuildComment) {
   307  		return false
   308  	}
   309  	line = bytes.TrimSpace(line)
   310  	rest := line[len(goBuildComment):]
   311  	return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest)
   312  }
   313  
   314  // Special comment denoting a binary-only package.
   315  // See https://golang.org/design/2775-binary-only-packages
   316  // for more about the design of binary-only packages.
   317  var binaryOnlyComment = []byte("//go:binary-only-package")
   318  
   319  func getConstraints(content []byte) (goBuild string, plusBuild []string, binaryOnly bool, err error) {
   320  	// Identify leading run of // comments and blank lines,
   321  	// which must be followed by a blank line.
   322  	// Also identify any //go:build comments.
   323  	content, goBuildBytes, sawBinaryOnly, err := parseFileHeader(content)
   324  	if err != nil {
   325  		return "", nil, false, err
   326  	}
   327  
   328  	// If //go:build line is present, it controls, so no need to look for +build .
   329  	// Otherwise, get plusBuild constraints.
   330  	if goBuildBytes == nil {
   331  		p := content
   332  		for len(p) > 0 {
   333  			line := p
   334  			if i := bytes.IndexByte(line, '\n'); i >= 0 {
   335  				line, p = line[:i], p[i+1:]
   336  			} else {
   337  				p = p[len(p):]
   338  			}
   339  			line = bytes.TrimSpace(line)
   340  			if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) {
   341  				continue
   342  			}
   343  			text := string(line)
   344  			if !constraint.IsPlusBuild(text) {
   345  				continue
   346  			}
   347  			plusBuild = append(plusBuild, text)
   348  		}
   349  	}
   350  
   351  	return string(goBuildBytes), plusBuild, sawBinaryOnly, nil
   352  }
   353  
   354  func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) {
   355  	end := 0
   356  	p := content
   357  	ended := false       // found non-blank, non-// line, so stopped accepting // +build lines
   358  	inSlashStar := false // in /* */ comment
   359  
   360  Lines:
   361  	for len(p) > 0 {
   362  		line := p
   363  		if i := bytes.IndexByte(line, '\n'); i >= 0 {
   364  			line, p = line[:i], p[i+1:]
   365  		} else {
   366  			p = p[len(p):]
   367  		}
   368  		line = bytes.TrimSpace(line)
   369  		if len(line) == 0 && !ended { // Blank line
   370  			// Remember position of most recent blank line.
   371  			// When we find the first non-blank, non-// line,
   372  			// this "end" position marks the latest file position
   373  			// where a // +build line can appear.
   374  			// (It must appear _before_ a blank line before the non-blank, non-// line.
   375  			// Yes, that's confusing, which is part of why we moved to //go:build lines.)
   376  			// Note that ended==false here means that inSlashStar==false,
   377  			// since seeing a /* would have set ended==true.
   378  			end = len(content) - len(p)
   379  			continue Lines
   380  		}
   381  		if !bytes.HasPrefix(line, bSlashSlash) { // Not comment line
   382  			ended = true
   383  		}
   384  
   385  		if !inSlashStar && isGoBuildComment(line) {
   386  			if goBuild != nil {
   387  				return nil, nil, false, errMultipleGoBuild
   388  			}
   389  			goBuild = line
   390  		}
   391  		if !inSlashStar && bytes.Equal(line, binaryOnlyComment) {
   392  			sawBinaryOnly = true
   393  		}
   394  
   395  	Comments:
   396  		for len(line) > 0 {
   397  			if inSlashStar {
   398  				if i := bytes.Index(line, bStarSlash); i >= 0 {
   399  					inSlashStar = false
   400  					line = bytes.TrimSpace(line[i+len(bStarSlash):])
   401  					continue Comments
   402  				}
   403  				continue Lines
   404  			}
   405  			if bytes.HasPrefix(line, bSlashSlash) {
   406  				continue Lines
   407  			}
   408  			if bytes.HasPrefix(line, bSlashStar) {
   409  				inSlashStar = true
   410  				line = bytes.TrimSpace(line[len(bSlashStar):])
   411  				continue Comments
   412  			}
   413  			// Found non-comment text.
   414  			break Lines
   415  		}
   416  	}
   417  
   418  	return content[:end], goBuild, sawBinaryOnly, nil
   419  }
   420  
   421  // saveCgo saves the information from the #cgo lines in the import "C" comment.
   422  // These lines set CFLAGS, CPPFLAGS, CXXFLAGS and LDFLAGS and pkg-config directives
   423  // that affect the way cgo's C code is built.
   424  func (ctxt *Context) saveCgo(filename string, di *build.Package, text string) error {
   425  	for line := range strings.SplitSeq(text, "\n") {
   426  		orig := line
   427  
   428  		// Line is
   429  		//	#cgo [GOOS/GOARCH...] LDFLAGS: stuff
   430  		//
   431  		line = strings.TrimSpace(line)
   432  		if len(line) < 5 || line[:4] != "#cgo" || (line[4] != ' ' && line[4] != '\t') {
   433  			continue
   434  		}
   435  
   436  		// #cgo (nocallback|noescape) <function name>
   437  		if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") {
   438  			continue
   439  		}
   440  
   441  		// Split at colon.
   442  		line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
   443  		if !ok {
   444  			return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
   445  		}
   446  
   447  		// Parse GOOS/GOARCH stuff.
   448  		f := strings.Fields(line)
   449  		if len(f) < 1 {
   450  			return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
   451  		}
   452  
   453  		cond, verb := f[:len(f)-1], f[len(f)-1]
   454  		if len(cond) > 0 {
   455  			ok := false
   456  			for _, c := range cond {
   457  				if ctxt.matchAuto(c, nil) {
   458  					ok = true
   459  					break
   460  				}
   461  			}
   462  			if !ok {
   463  				continue
   464  			}
   465  		}
   466  
   467  		args, err := splitQuoted(argstr)
   468  		if err != nil {
   469  			return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
   470  		}
   471  		for i, arg := range args {
   472  			if arg, ok = expandSrcDir(arg, di.Dir); !ok {
   473  				return fmt.Errorf("%s: malformed #cgo argument: %s", filename, arg)
   474  			}
   475  			args[i] = arg
   476  		}
   477  
   478  		switch verb {
   479  		case "CFLAGS", "CPPFLAGS", "CXXFLAGS", "FFLAGS", "LDFLAGS":
   480  			// Change relative paths to absolute.
   481  			ctxt.makePathsAbsolute(args, di.Dir)
   482  		}
   483  
   484  		switch verb {
   485  		case "CFLAGS":
   486  			di.CgoCFLAGS = append(di.CgoCFLAGS, args...)
   487  		case "CPPFLAGS":
   488  			di.CgoCPPFLAGS = append(di.CgoCPPFLAGS, args...)
   489  		case "CXXFLAGS":
   490  			di.CgoCXXFLAGS = append(di.CgoCXXFLAGS, args...)
   491  		case "FFLAGS":
   492  			di.CgoFFLAGS = append(di.CgoFFLAGS, args...)
   493  		case "LDFLAGS":
   494  			di.CgoLDFLAGS = append(di.CgoLDFLAGS, args...)
   495  		case "pkg-config":
   496  			di.CgoPkgConfig = append(di.CgoPkgConfig, args...)
   497  		default:
   498  			return fmt.Errorf("%s: invalid #cgo verb: %s", filename, orig)
   499  		}
   500  	}
   501  	return nil
   502  }
   503  
   504  // expandSrcDir expands any occurrence of ${SRCDIR}, making sure
   505  // the result is safe for the shell.
   506  func expandSrcDir(str string, srcdir string) (string, bool) {
   507  	// "\" delimited paths cause safeCgoName to fail
   508  	// so convert native paths with a different delimiter
   509  	// to "/" before starting (eg: on windows).
   510  	srcdir = filepath.ToSlash(srcdir)
   511  
   512  	chunks := strings.Split(str, "${SRCDIR}")
   513  	if len(chunks) < 2 {
   514  		return str, safeCgoName(str)
   515  	}
   516  	ok := true
   517  	for _, chunk := range chunks {
   518  		ok = ok && (chunk == "" || safeCgoName(chunk))
   519  	}
   520  	ok = ok && (srcdir == "" || safeCgoName(srcdir))
   521  	res := strings.Join(chunks, srcdir)
   522  	return res, ok && res != ""
   523  }
   524  
   525  // makePathsAbsolute looks for compiler options that take paths and
   526  // makes them absolute. We do this because through the 1.8 release we
   527  // ran the compiler in the package directory, so any relative -I or -L
   528  // options would be relative to that directory. In 1.9 we changed to
   529  // running the compiler in the build directory, to get consistent
   530  // build results (issue #19964). To keep builds working, we change any
   531  // relative -I or -L options to be absolute.
   532  //
   533  // Using filepath.IsAbs and filepath.Join here means the results will be
   534  // different on different systems, but that's OK: -I and -L options are
   535  // inherently system-dependent.
   536  func (ctxt *Context) makePathsAbsolute(args []string, srcDir string) {
   537  	nextPath := false
   538  	for i, arg := range args {
   539  		if nextPath {
   540  			if !filepath.IsAbs(arg) {
   541  				args[i] = filepath.Join(srcDir, arg)
   542  			}
   543  			nextPath = false
   544  		} else if strings.HasPrefix(arg, "-I") || strings.HasPrefix(arg, "-L") {
   545  			if len(arg) == 2 {
   546  				nextPath = true
   547  			} else {
   548  				if !filepath.IsAbs(arg[2:]) {
   549  					args[i] = arg[:2] + filepath.Join(srcDir, arg[2:])
   550  				}
   551  			}
   552  		}
   553  	}
   554  }
   555  
   556  // NOTE: $ is not safe for the shell, but it is allowed here because of linker options like -Wl,$ORIGIN.
   557  // We never pass these arguments to a shell (just to programs we construct argv for), so this should be okay.
   558  // See golang.org/issue/6038.
   559  // The @ is for OS X. See golang.org/issue/13720.
   560  // The % is for Jenkins. See golang.org/issue/16959.
   561  // The ! is because module paths may use them. See golang.org/issue/26716.
   562  // The ~ and ^ are for sr.ht. See golang.org/issue/32260.
   563  const safeString = "+-.,/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz:$@%! ~^"
   564  
   565  func safeCgoName(s string) bool {
   566  	if s == "" {
   567  		return false
   568  	}
   569  	for i := 0; i < len(s); i++ {
   570  		if c := s[i]; c < utf8.RuneSelf && strings.IndexByte(safeString, c) < 0 {
   571  			return false
   572  		}
   573  	}
   574  	return true
   575  }
   576  
   577  // splitQuoted splits the string s around each instance of one or more consecutive
   578  // white space characters while taking into account quotes and escaping, and
   579  // returns an array of substrings of s or an empty list if s contains only white space.
   580  // Single quotes and double quotes are recognized to prevent splitting within the
   581  // quoted region, and are removed from the resulting substrings. If a quote in s
   582  // isn't closed err will be set and r will have the unclosed argument as the
   583  // last element. The backslash is used for escaping.
   584  //
   585  // For example, the following string:
   586  //
   587  //	a b:"c d" 'e''f'  "g\""
   588  //
   589  // Would be parsed as:
   590  //
   591  //	[]string{"a", "b:c d", "ef", `g"`}
   592  func splitQuoted(s string) (r []string, err error) {
   593  	var args []string
   594  	arg := make([]rune, len(s))
   595  	escaped := false
   596  	quoted := false
   597  	quote := '\x00'
   598  	i := 0
   599  	for _, rune := range s {
   600  		switch {
   601  		case escaped:
   602  			escaped = false
   603  		case rune == '\\':
   604  			escaped = true
   605  			continue
   606  		case quote != '\x00':
   607  			if rune == quote {
   608  				quote = '\x00'
   609  				continue
   610  			}
   611  		case rune == '"' || rune == '\'':
   612  			quoted = true
   613  			quote = rune
   614  			continue
   615  		case unicode.IsSpace(rune):
   616  			if quoted || i > 0 {
   617  				quoted = false
   618  				args = append(args, string(arg[:i]))
   619  				i = 0
   620  			}
   621  			continue
   622  		}
   623  		arg[i] = rune
   624  		i++
   625  	}
   626  	if quoted || i > 0 {
   627  		args = append(args, string(arg[:i]))
   628  	}
   629  	if quote != 0 {
   630  		err = errors.New("unclosed quote")
   631  	} else if escaped {
   632  		err = errors.New("unfinished escaping")
   633  	}
   634  	return args, err
   635  }
   636  
   637  // matchAuto interprets text as either a +build or //go:build expression (whichever works),
   638  // reporting whether the expression matches the build context.
   639  //
   640  // matchAuto is only used for testing of tag evaluation
   641  // and in #cgo lines, which accept either syntax.
   642  func (ctxt *Context) matchAuto(text string, allTags map[string]bool) bool {
   643  	if strings.ContainsAny(text, "&|()") {
   644  		text = "//go:build " + text
   645  	} else {
   646  		text = "// +build " + text
   647  	}
   648  	x, err := constraint.Parse(text)
   649  	if err != nil {
   650  		return false
   651  	}
   652  	return ctxt.eval(x, allTags)
   653  }
   654  
   655  func (ctxt *Context) eval(x constraint.Expr, allTags map[string]bool) bool {
   656  	return x.Eval(func(tag string) bool { return ctxt.matchTag(tag, allTags) })
   657  }
   658  
   659  // matchTag reports whether the name is one of:
   660  //
   661  //	cgo (if cgo is enabled)
   662  //	$GOOS
   663  //	$GOARCH
   664  //	boringcrypto
   665  //	ctxt.Compiler
   666  //	linux (if GOOS == android)
   667  //	solaris (if GOOS == illumos)
   668  //	tag (if tag is listed in ctxt.BuildTags or ctxt.ReleaseTags)
   669  //
   670  // It records all consulted tags in allTags.
   671  func (ctxt *Context) matchTag(name string, allTags map[string]bool) bool {
   672  	if allTags != nil {
   673  		allTags[name] = true
   674  	}
   675  
   676  	// special tags
   677  	if ctxt.CgoEnabled && name == "cgo" {
   678  		return true
   679  	}
   680  	if name == ctxt.GOOS || name == ctxt.GOARCH || name == ctxt.Compiler {
   681  		return true
   682  	}
   683  	if ctxt.GOOS == "android" && name == "linux" {
   684  		return true
   685  	}
   686  	if ctxt.GOOS == "illumos" && name == "solaris" {
   687  		return true
   688  	}
   689  	if ctxt.GOOS == "ios" && name == "darwin" {
   690  		return true
   691  	}
   692  	if name == "unix" && syslist.UnixOS[ctxt.GOOS] {
   693  		return true
   694  	}
   695  	if name == "boringcrypto" {
   696  		name = "goexperiment.boringcrypto" // boringcrypto is an old name for goexperiment.boringcrypto
   697  	}
   698  
   699  	// other tags
   700  	return slices.Contains(ctxt.BuildTags, name) || slices.Contains(ctxt.ToolTags, name) ||
   701  		slices.Contains(ctxt.ReleaseTags, name)
   702  }
   703  
   704  // goodOSArchFile returns false if the name contains a $GOOS or $GOARCH
   705  // suffix which does not match the current system.
   706  // The recognized name formats are:
   707  //
   708  //	name_$(GOOS).*
   709  //	name_$(GOARCH).*
   710  //	name_$(GOOS)_$(GOARCH).*
   711  //	name_$(GOOS)_test.*
   712  //	name_$(GOARCH)_test.*
   713  //	name_$(GOOS)_$(GOARCH)_test.*
   714  //
   715  // Exceptions:
   716  // if GOOS=android, then files with GOOS=linux are also matched.
   717  // if GOOS=illumos, then files with GOOS=solaris are also matched.
   718  // if GOOS=ios, then files with GOOS=darwin are also matched.
   719  func (ctxt *Context) goodOSArchFile(name string, allTags map[string]bool) bool {
   720  	name, _, _ = strings.Cut(name, ".")
   721  
   722  	// Before Go 1.4, a file called "linux.go" would be equivalent to having a
   723  	// build tag "linux" in that file. For Go 1.4 and beyond, we require this
   724  	// auto-tagging to apply only to files with a non-empty prefix, so
   725  	// "foo_linux.go" is tagged but "linux.go" is not. This allows new operating
   726  	// systems, such as android, to arrive without breaking existing code with
   727  	// innocuous source code in "android.go". The easiest fix: cut everything
   728  	// in the name before the initial _.
   729  	i := strings.Index(name, "_")
   730  	if i < 0 {
   731  		return true
   732  	}
   733  	name = name[i:] // ignore everything before first _
   734  
   735  	l := strings.Split(name, "_")
   736  	if n := len(l); n > 0 && l[n-1] == "test" {
   737  		l = l[:n-1]
   738  	}
   739  	n := len(l)
   740  	if n >= 2 && syslist.KnownOS[l[n-2]] && syslist.KnownArch[l[n-1]] {
   741  		if allTags != nil {
   742  			// In case we short-circuit on l[n-1].
   743  			allTags[l[n-2]] = true
   744  		}
   745  		return ctxt.matchTag(l[n-1], allTags) && ctxt.matchTag(l[n-2], allTags)
   746  	}
   747  	if n >= 1 && (syslist.KnownOS[l[n-1]] || syslist.KnownArch[l[n-1]]) {
   748  		return ctxt.matchTag(l[n-1], allTags)
   749  	}
   750  	return true
   751  }
   752  

View as plain text