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  	slashSlash = []byte("//")
   186  	slashStar  = []byte("/*")
   187  	starSlash  = []byte("*/")
   188  )
   189  
   190  var dummyPkg build.Package
   191  
   192  // fileInfo records information learned about a file included in a build.
   193  type fileInfo struct {
   194  	name       string // full name including dir
   195  	header     []byte
   196  	fset       *token.FileSet
   197  	parsed     *ast.File
   198  	parseErr   error
   199  	imports    []fileImport
   200  	embeds     []fileEmbed
   201  	directives []build.Directive
   202  
   203  	// Additional fields added to go/build's fileinfo for the purposes of the modindex package.
   204  	binaryOnly           bool
   205  	goBuildConstraint    string
   206  	plusBuildConstraints []string
   207  }
   208  
   209  type fileImport struct {
   210  	path string
   211  	pos  token.Pos
   212  	doc  *ast.CommentGroup
   213  }
   214  
   215  type fileEmbed struct {
   216  	pattern string
   217  	pos     token.Position
   218  }
   219  
   220  var errNonSource = errors.New("non source file")
   221  
   222  // getFileInfo extracts the information needed from each go file for the module
   223  // index.
   224  //
   225  // If Name denotes a Go program, matchFile reads until the end of the
   226  // Imports and returns that section of the file in the FileInfo's Header field,
   227  // even though it only considers text until the first non-comment
   228  // for +build lines.
   229  //
   230  // getFileInfo will return errNonSource if the file is not a source or object
   231  // file and shouldn't even be added to IgnoredFiles.
   232  func getFileInfo(dir, name string, fset *token.FileSet) (*fileInfo, error) {
   233  	if strings.HasPrefix(name, "_") ||
   234  		strings.HasPrefix(name, ".") {
   235  		return nil, nil
   236  	}
   237  
   238  	i := strings.LastIndex(name, ".")
   239  	if i < 0 {
   240  		i = len(name)
   241  	}
   242  	ext := name[i:]
   243  
   244  	if ext != ".go" && fileListForExt(&dummyPkg, ext) == nil {
   245  		// skip
   246  		return nil, errNonSource
   247  	}
   248  
   249  	info := &fileInfo{name: filepath.Join(dir, name), fset: fset}
   250  	if ext == ".syso" {
   251  		// binary, no reading
   252  		return info, nil
   253  	}
   254  
   255  	f, err := fsys.Open(info.name)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	// TODO(matloob) should we decide whether to ignore binary only here or earlier
   261  	// when we create the index file?
   262  	var ignoreBinaryOnly bool
   263  	if strings.HasSuffix(name, ".go") {
   264  		err = readGoInfo(f, info)
   265  		if strings.HasSuffix(name, "_test.go") {
   266  			ignoreBinaryOnly = true // ignore //go:binary-only-package comments in _test.go files
   267  		}
   268  	} else {
   269  		info.header, err = readComments(f)
   270  	}
   271  	f.Close()
   272  	if err != nil {
   273  		return nil, fmt.Errorf("read %s: %v", info.name, err)
   274  	}
   275  
   276  	// Look for +build comments to accept or reject the file.
   277  	info.goBuildConstraint, info.plusBuildConstraints, info.binaryOnly, err = getConstraints(info.header)
   278  	if err != nil {
   279  		return nil, fmt.Errorf("%s: %v", name, err)
   280  	}
   281  
   282  	if ignoreBinaryOnly && info.binaryOnly {
   283  		info.binaryOnly = false // override info.binaryOnly
   284  	}
   285  
   286  	return info, nil
   287  }
   288  
   289  func cleanDecls(m map[string][]token.Position) ([]string, map[string][]token.Position) {
   290  	all := make([]string, 0, len(m))
   291  	for path := range m {
   292  		all = append(all, path)
   293  	}
   294  	sort.Strings(all)
   295  	return all, m
   296  }
   297  
   298  var (
   299  	bSlashSlash = []byte(slashSlash)
   300  	bStarSlash  = []byte(starSlash)
   301  	bSlashStar  = []byte(slashStar)
   302  	bPlusBuild  = []byte("+build")
   303  
   304  	goBuildComment = []byte("//go:build")
   305  
   306  	errMultipleGoBuild = errors.New("multiple //go:build comments")
   307  )
   308  
   309  func isGoBuildComment(line []byte) bool {
   310  	if !bytes.HasPrefix(line, goBuildComment) {
   311  		return false
   312  	}
   313  	line = bytes.TrimSpace(line)
   314  	rest := line[len(goBuildComment):]
   315  	return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest)
   316  }
   317  
   318  // Special comment denoting a binary-only package.
   319  // See https://golang.org/design/2775-binary-only-packages
   320  // for more about the design of binary-only packages.
   321  var binaryOnlyComment = []byte("//go:binary-only-package")
   322  
   323  func getConstraints(content []byte) (goBuild string, plusBuild []string, binaryOnly bool, err error) {
   324  	// Identify leading run of // comments and blank lines,
   325  	// which must be followed by a blank line.
   326  	// Also identify any //go:build comments.
   327  	content, goBuildBytes, sawBinaryOnly, err := parseFileHeader(content)
   328  	if err != nil {
   329  		return "", nil, false, err
   330  	}
   331  
   332  	// If //go:build line is present, it controls, so no need to look for +build .
   333  	// Otherwise, get plusBuild constraints.
   334  	if goBuildBytes == nil {
   335  		p := content
   336  		for len(p) > 0 {
   337  			line := p
   338  			if i := bytes.IndexByte(line, '\n'); i >= 0 {
   339  				line, p = line[:i], p[i+1:]
   340  			} else {
   341  				p = p[len(p):]
   342  			}
   343  			line = bytes.TrimSpace(line)
   344  			if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) {
   345  				continue
   346  			}
   347  			text := string(line)
   348  			if !constraint.IsPlusBuild(text) {
   349  				continue
   350  			}
   351  			plusBuild = append(plusBuild, text)
   352  		}
   353  	}
   354  
   355  	return string(goBuildBytes), plusBuild, sawBinaryOnly, nil
   356  }
   357  
   358  func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) {
   359  	end := 0
   360  	p := content
   361  	ended := false       // found non-blank, non-// line, so stopped accepting // +build lines
   362  	inSlashStar := false // in /* */ comment
   363  
   364  Lines:
   365  	for len(p) > 0 {
   366  		line := p
   367  		if i := bytes.IndexByte(line, '\n'); i >= 0 {
   368  			line, p = line[:i], p[i+1:]
   369  		} else {
   370  			p = p[len(p):]
   371  		}
   372  		line = bytes.TrimSpace(line)
   373  		if len(line) == 0 && !ended { // Blank line
   374  			// Remember position of most recent blank line.
   375  			// When we find the first non-blank, non-// line,
   376  			// this "end" position marks the latest file position
   377  			// where a // +build line can appear.
   378  			// (It must appear _before_ a blank line before the non-blank, non-// line.
   379  			// Yes, that's confusing, which is part of why we moved to //go:build lines.)
   380  			// Note that ended==false here means that inSlashStar==false,
   381  			// since seeing a /* would have set ended==true.
   382  			end = len(content) - len(p)
   383  			continue Lines
   384  		}
   385  		if !bytes.HasPrefix(line, slashSlash) { // Not comment line
   386  			ended = true
   387  		}
   388  
   389  		if !inSlashStar && isGoBuildComment(line) {
   390  			if goBuild != nil {
   391  				return nil, nil, false, errMultipleGoBuild
   392  			}
   393  			goBuild = line
   394  		}
   395  		if !inSlashStar && bytes.Equal(line, binaryOnlyComment) {
   396  			sawBinaryOnly = true
   397  		}
   398  
   399  	Comments:
   400  		for len(line) > 0 {
   401  			if inSlashStar {
   402  				if i := bytes.Index(line, starSlash); i >= 0 {
   403  					inSlashStar = false
   404  					line = bytes.TrimSpace(line[i+len(starSlash):])
   405  					continue Comments
   406  				}
   407  				continue Lines
   408  			}
   409  			if bytes.HasPrefix(line, bSlashSlash) {
   410  				continue Lines
   411  			}
   412  			if bytes.HasPrefix(line, bSlashStar) {
   413  				inSlashStar = true
   414  				line = bytes.TrimSpace(line[len(bSlashStar):])
   415  				continue Comments
   416  			}
   417  			// Found non-comment text.
   418  			break Lines
   419  		}
   420  	}
   421  
   422  	return content[:end], goBuild, sawBinaryOnly, nil
   423  }
   424  
   425  // saveCgo saves the information from the #cgo lines in the import "C" comment.
   426  // These lines set CFLAGS, CPPFLAGS, CXXFLAGS and LDFLAGS and pkg-config directives
   427  // that affect the way cgo's C code is built.
   428  func (ctxt *Context) saveCgo(filename string, di *build.Package, text string) error {
   429  	for _, line := range strings.Split(text, "\n") {
   430  		orig := line
   431  
   432  		// Line is
   433  		//	#cgo [GOOS/GOARCH...] LDFLAGS: stuff
   434  		//
   435  		line = strings.TrimSpace(line)
   436  		if len(line) < 5 || line[:4] != "#cgo" || (line[4] != ' ' && line[4] != '\t') {
   437  			continue
   438  		}
   439  
   440  		// #cgo (nocallback|noescape) <function name>
   441  		if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") {
   442  			continue
   443  		}
   444  
   445  		// Split at colon.
   446  		line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
   447  		if !ok {
   448  			return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
   449  		}
   450  
   451  		// Parse GOOS/GOARCH stuff.
   452  		f := strings.Fields(line)
   453  		if len(f) < 1 {
   454  			return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
   455  		}
   456  
   457  		cond, verb := f[:len(f)-1], f[len(f)-1]
   458  		if len(cond) > 0 {
   459  			ok := false
   460  			for _, c := range cond {
   461  				if ctxt.matchAuto(c, nil) {
   462  					ok = true
   463  					break
   464  				}
   465  			}
   466  			if !ok {
   467  				continue
   468  			}
   469  		}
   470  
   471  		args, err := splitQuoted(argstr)
   472  		if err != nil {
   473  			return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
   474  		}
   475  		for i, arg := range args {
   476  			if arg, ok = expandSrcDir(arg, di.Dir); !ok {
   477  				return fmt.Errorf("%s: malformed #cgo argument: %s", filename, arg)
   478  			}
   479  			args[i] = arg
   480  		}
   481  
   482  		switch verb {
   483  		case "CFLAGS", "CPPFLAGS", "CXXFLAGS", "FFLAGS", "LDFLAGS":
   484  			// Change relative paths to absolute.
   485  			ctxt.makePathsAbsolute(args, di.Dir)
   486  		}
   487  
   488  		switch verb {
   489  		case "CFLAGS":
   490  			di.CgoCFLAGS = append(di.CgoCFLAGS, args...)
   491  		case "CPPFLAGS":
   492  			di.CgoCPPFLAGS = append(di.CgoCPPFLAGS, args...)
   493  		case "CXXFLAGS":
   494  			di.CgoCXXFLAGS = append(di.CgoCXXFLAGS, args...)
   495  		case "FFLAGS":
   496  			di.CgoFFLAGS = append(di.CgoFFLAGS, args...)
   497  		case "LDFLAGS":
   498  			di.CgoLDFLAGS = append(di.CgoLDFLAGS, args...)
   499  		case "pkg-config":
   500  			di.CgoPkgConfig = append(di.CgoPkgConfig, args...)
   501  		default:
   502  			return fmt.Errorf("%s: invalid #cgo verb: %s", filename, orig)
   503  		}
   504  	}
   505  	return nil
   506  }
   507  
   508  // expandSrcDir expands any occurrence of ${SRCDIR}, making sure
   509  // the result is safe for the shell.
   510  func expandSrcDir(str string, srcdir string) (string, bool) {
   511  	// "\" delimited paths cause safeCgoName to fail
   512  	// so convert native paths with a different delimiter
   513  	// to "/" before starting (eg: on windows).
   514  	srcdir = filepath.ToSlash(srcdir)
   515  
   516  	chunks := strings.Split(str, "${SRCDIR}")
   517  	if len(chunks) < 2 {
   518  		return str, safeCgoName(str)
   519  	}
   520  	ok := true
   521  	for _, chunk := range chunks {
   522  		ok = ok && (chunk == "" || safeCgoName(chunk))
   523  	}
   524  	ok = ok && (srcdir == "" || safeCgoName(srcdir))
   525  	res := strings.Join(chunks, srcdir)
   526  	return res, ok && res != ""
   527  }
   528  
   529  // makePathsAbsolute looks for compiler options that take paths and
   530  // makes them absolute. We do this because through the 1.8 release we
   531  // ran the compiler in the package directory, so any relative -I or -L
   532  // options would be relative to that directory. In 1.9 we changed to
   533  // running the compiler in the build directory, to get consistent
   534  // build results (issue #19964). To keep builds working, we change any
   535  // relative -I or -L options to be absolute.
   536  //
   537  // Using filepath.IsAbs and filepath.Join here means the results will be
   538  // different on different systems, but that's OK: -I and -L options are
   539  // inherently system-dependent.
   540  func (ctxt *Context) makePathsAbsolute(args []string, srcDir string) {
   541  	nextPath := false
   542  	for i, arg := range args {
   543  		if nextPath {
   544  			if !filepath.IsAbs(arg) {
   545  				args[i] = filepath.Join(srcDir, arg)
   546  			}
   547  			nextPath = false
   548  		} else if strings.HasPrefix(arg, "-I") || strings.HasPrefix(arg, "-L") {
   549  			if len(arg) == 2 {
   550  				nextPath = true
   551  			} else {
   552  				if !filepath.IsAbs(arg[2:]) {
   553  					args[i] = arg[:2] + filepath.Join(srcDir, arg[2:])
   554  				}
   555  			}
   556  		}
   557  	}
   558  }
   559  
   560  // NOTE: $ is not safe for the shell, but it is allowed here because of linker options like -Wl,$ORIGIN.
   561  // We never pass these arguments to a shell (just to programs we construct argv for), so this should be okay.
   562  // See golang.org/issue/6038.
   563  // The @ is for OS X. See golang.org/issue/13720.
   564  // The % is for Jenkins. See golang.org/issue/16959.
   565  // The ! is because module paths may use them. See golang.org/issue/26716.
   566  // The ~ and ^ are for sr.ht. See golang.org/issue/32260.
   567  const safeString = "+-.,/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz:$@%! ~^"
   568  
   569  func safeCgoName(s string) bool {
   570  	if s == "" {
   571  		return false
   572  	}
   573  	for i := 0; i < len(s); i++ {
   574  		if c := s[i]; c < utf8.RuneSelf && strings.IndexByte(safeString, c) < 0 {
   575  			return false
   576  		}
   577  	}
   578  	return true
   579  }
   580  
   581  // splitQuoted splits the string s around each instance of one or more consecutive
   582  // white space characters while taking into account quotes and escaping, and
   583  // returns an array of substrings of s or an empty list if s contains only white space.
   584  // Single quotes and double quotes are recognized to prevent splitting within the
   585  // quoted region, and are removed from the resulting substrings. If a quote in s
   586  // isn't closed err will be set and r will have the unclosed argument as the
   587  // last element. The backslash is used for escaping.
   588  //
   589  // For example, the following string:
   590  //
   591  //	a b:"c d" 'e''f'  "g\""
   592  //
   593  // Would be parsed as:
   594  //
   595  //	[]string{"a", "b:c d", "ef", `g"`}
   596  func splitQuoted(s string) (r []string, err error) {
   597  	var args []string
   598  	arg := make([]rune, len(s))
   599  	escaped := false
   600  	quoted := false
   601  	quote := '\x00'
   602  	i := 0
   603  	for _, rune := range s {
   604  		switch {
   605  		case escaped:
   606  			escaped = false
   607  		case rune == '\\':
   608  			escaped = true
   609  			continue
   610  		case quote != '\x00':
   611  			if rune == quote {
   612  				quote = '\x00'
   613  				continue
   614  			}
   615  		case rune == '"' || rune == '\'':
   616  			quoted = true
   617  			quote = rune
   618  			continue
   619  		case unicode.IsSpace(rune):
   620  			if quoted || i > 0 {
   621  				quoted = false
   622  				args = append(args, string(arg[:i]))
   623  				i = 0
   624  			}
   625  			continue
   626  		}
   627  		arg[i] = rune
   628  		i++
   629  	}
   630  	if quoted || i > 0 {
   631  		args = append(args, string(arg[:i]))
   632  	}
   633  	if quote != 0 {
   634  		err = errors.New("unclosed quote")
   635  	} else if escaped {
   636  		err = errors.New("unfinished escaping")
   637  	}
   638  	return args, err
   639  }
   640  
   641  // matchAuto interprets text as either a +build or //go:build expression (whichever works),
   642  // reporting whether the expression matches the build context.
   643  //
   644  // matchAuto is only used for testing of tag evaluation
   645  // and in #cgo lines, which accept either syntax.
   646  func (ctxt *Context) matchAuto(text string, allTags map[string]bool) bool {
   647  	if strings.ContainsAny(text, "&|()") {
   648  		text = "//go:build " + text
   649  	} else {
   650  		text = "// +build " + text
   651  	}
   652  	x, err := constraint.Parse(text)
   653  	if err != nil {
   654  		return false
   655  	}
   656  	return ctxt.eval(x, allTags)
   657  }
   658  
   659  func (ctxt *Context) eval(x constraint.Expr, allTags map[string]bool) bool {
   660  	return x.Eval(func(tag string) bool { return ctxt.matchTag(tag, allTags) })
   661  }
   662  
   663  // matchTag reports whether the name is one of:
   664  //
   665  //	cgo (if cgo is enabled)
   666  //	$GOOS
   667  //	$GOARCH
   668  //	boringcrypto
   669  //	ctxt.Compiler
   670  //	linux (if GOOS == android)
   671  //	solaris (if GOOS == illumos)
   672  //	tag (if tag is listed in ctxt.BuildTags or ctxt.ReleaseTags)
   673  //
   674  // It records all consulted tags in allTags.
   675  func (ctxt *Context) matchTag(name string, allTags map[string]bool) bool {
   676  	if allTags != nil {
   677  		allTags[name] = true
   678  	}
   679  
   680  	// special tags
   681  	if ctxt.CgoEnabled && name == "cgo" {
   682  		return true
   683  	}
   684  	if name == ctxt.GOOS || name == ctxt.GOARCH || name == ctxt.Compiler {
   685  		return true
   686  	}
   687  	if ctxt.GOOS == "android" && name == "linux" {
   688  		return true
   689  	}
   690  	if ctxt.GOOS == "illumos" && name == "solaris" {
   691  		return true
   692  	}
   693  	if ctxt.GOOS == "ios" && name == "darwin" {
   694  		return true
   695  	}
   696  	if name == "unix" && syslist.UnixOS[ctxt.GOOS] {
   697  		return true
   698  	}
   699  	if name == "boringcrypto" {
   700  		name = "goexperiment.boringcrypto" // boringcrypto is an old name for goexperiment.boringcrypto
   701  	}
   702  
   703  	// other tags
   704  	return slices.Contains(ctxt.BuildTags, name) || slices.Contains(ctxt.ToolTags, name) ||
   705  		slices.Contains(ctxt.ReleaseTags, name)
   706  }
   707  
   708  // goodOSArchFile returns false if the name contains a $GOOS or $GOARCH
   709  // suffix which does not match the current system.
   710  // The recognized name formats are:
   711  //
   712  //	name_$(GOOS).*
   713  //	name_$(GOARCH).*
   714  //	name_$(GOOS)_$(GOARCH).*
   715  //	name_$(GOOS)_test.*
   716  //	name_$(GOARCH)_test.*
   717  //	name_$(GOOS)_$(GOARCH)_test.*
   718  //
   719  // Exceptions:
   720  // if GOOS=android, then files with GOOS=linux are also matched.
   721  // if GOOS=illumos, then files with GOOS=solaris are also matched.
   722  // if GOOS=ios, then files with GOOS=darwin are also matched.
   723  func (ctxt *Context) goodOSArchFile(name string, allTags map[string]bool) bool {
   724  	name, _, _ = strings.Cut(name, ".")
   725  
   726  	// Before Go 1.4, a file called "linux.go" would be equivalent to having a
   727  	// build tag "linux" in that file. For Go 1.4 and beyond, we require this
   728  	// auto-tagging to apply only to files with a non-empty prefix, so
   729  	// "foo_linux.go" is tagged but "linux.go" is not. This allows new operating
   730  	// systems, such as android, to arrive without breaking existing code with
   731  	// innocuous source code in "android.go". The easiest fix: cut everything
   732  	// in the name before the initial _.
   733  	i := strings.Index(name, "_")
   734  	if i < 0 {
   735  		return true
   736  	}
   737  	name = name[i:] // ignore everything before first _
   738  
   739  	l := strings.Split(name, "_")
   740  	if n := len(l); n > 0 && l[n-1] == "test" {
   741  		l = l[:n-1]
   742  	}
   743  	n := len(l)
   744  	if n >= 2 && syslist.KnownOS[l[n-2]] && syslist.KnownArch[l[n-1]] {
   745  		if allTags != nil {
   746  			// In case we short-circuit on l[n-1].
   747  			allTags[l[n-2]] = true
   748  		}
   749  		return ctxt.matchTag(l[n-1], allTags) && ctxt.matchTag(l[n-2], allTags)
   750  	}
   751  	if n >= 1 && (syslist.KnownOS[l[n-1]] || syslist.KnownArch[l[n-1]]) {
   752  		return ctxt.matchTag(l[n-1], allTags)
   753  	}
   754  	return true
   755  }
   756  

View as plain text