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

View as plain text