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

View as plain text