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

View as plain text