// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package build import ( "bytes" "errors" "fmt" "go/ast" "go/build/constraint" "go/doc" "go/token" "internal/buildcfg" "internal/godebug" "internal/goroot" "internal/goversion" "internal/platform" "io" "io/fs" "os" "os/exec" pathpkg "path" "path/filepath" "runtime" "slices" "strconv" "strings" "unicode" "unicode/utf8" _ "unsafe" // for linkname ) // A Context specifies the supporting context for a build. type Context struct { GOARCH string // target architecture GOOS string // target operating system GOROOT string // Go root GOPATH string // Go paths // Dir is the caller's working directory, or the empty string to use // the current directory of the running process. In module mode, this is used // to locate the main module. // // If Dir is non-empty, directories passed to Import and ImportDir must // be absolute. Dir string CgoEnabled bool // whether cgo files are included UseAllFiles bool // use files regardless of go:build lines, file names Compiler string // compiler to assume when computing target paths // The build, tool, and release tags specify build constraints // that should be considered satisfied when processing go:build lines. // Clients creating a new context may customize BuildTags, which // defaults to empty, but it is usually an error to customize ToolTags or ReleaseTags. // ToolTags defaults to build tags appropriate to the current Go toolchain configuration. // ReleaseTags defaults to the list of Go releases the current release is compatible with. // BuildTags is not set for the Default build Context. // In addition to the BuildTags, ToolTags, and ReleaseTags, build constraints // consider the values of GOARCH and GOOS as satisfied tags. // The last element in ReleaseTags is assumed to be the current release. BuildTags []string ToolTags []string ReleaseTags []string // The install suffix specifies a suffix to use in the name of the installation // directory. By default it is empty, but custom builds that need to keep // their outputs separate can set InstallSuffix to do so. For example, when // using the race detector, the go command uses InstallSuffix = "race", so // that on a Linux/386 system, packages are written to a directory named // "linux_386_race" instead of the usual "linux_386". InstallSuffix string // By default, Import uses the operating system's file system calls // to read directories and files. To read from other sources, // callers can set the following functions. They all have default // behaviors that use the local file system, so clients need only set // the functions whose behaviors they wish to change. // JoinPath joins the sequence of path fragments into a single path. // If JoinPath is nil, Import uses filepath.Join. JoinPath func(elem ...string) string // SplitPathList splits the path list into a slice of individual paths. // If SplitPathList is nil, Import uses filepath.SplitList. SplitPathList func(list string) []string // IsAbsPath reports whether path is an absolute path. // If IsAbsPath is nil, Import uses filepath.IsAbs. IsAbsPath func(path string) bool // IsDir reports whether the path names a directory. // If IsDir is nil, Import calls os.Stat and uses the result's IsDir method. IsDir func(path string) bool // HasSubdir reports whether dir is lexically a subdirectory of // root, perhaps multiple levels below. It does not try to check // whether dir exists. // If so, HasSubdir sets rel to a slash-separated path that // can be joined to root to produce a path equivalent to dir. // If HasSubdir is nil, Import uses an implementation built on // filepath.EvalSymlinks. HasSubdir func(root, dir string) (rel string, ok bool) // ReadDir returns a slice of fs.FileInfo, sorted by Name, // describing the content of the named directory. // If ReadDir is nil, Import uses os.ReadDir. ReadDir func(dir string) ([]fs.FileInfo, error) // OpenFile opens a file (not a directory) for reading. // If OpenFile is nil, Import uses os.Open. OpenFile func(path string) (io.ReadCloser, error) } // joinPath calls ctxt.JoinPath (if not nil) or else filepath.Join. func (ctxt *Context) joinPath(elem ...string) string { if f := ctxt.JoinPath; f != nil { return f(elem...) } return filepath.Join(elem...) } // splitPathList calls ctxt.SplitPathList (if not nil) or else filepath.SplitList. func (ctxt *Context) splitPathList(s string) []string { if f := ctxt.SplitPathList; f != nil { return f(s) } return filepath.SplitList(s) } // isAbsPath calls ctxt.IsAbsPath (if not nil) or else filepath.IsAbs. func (ctxt *Context) isAbsPath(path string) bool { if f := ctxt.IsAbsPath; f != nil { return f(path) } return filepath.IsAbs(path) } // isDir calls ctxt.IsDir (if not nil) or else uses os.Stat. func (ctxt *Context) isDir(path string) bool { if f := ctxt.IsDir; f != nil { return f(path) } fi, err := os.Stat(path) return err == nil && fi.IsDir() } // hasSubdir calls ctxt.HasSubdir (if not nil) or else uses // the local file system to answer the question. func (ctxt *Context) hasSubdir(root, dir string) (rel string, ok bool) { if f := ctxt.HasSubdir; f != nil { return f(root, dir) } // Try using paths we received. if rel, ok = hasSubdir(root, dir); ok { return } // Try expanding symlinks and comparing // expanded against unexpanded and // expanded against expanded. rootSym, _ := filepath.EvalSymlinks(root) dirSym, _ := filepath.EvalSymlinks(dir) if rel, ok = hasSubdir(rootSym, dir); ok { return } if rel, ok = hasSubdir(root, dirSym); ok { return } return hasSubdir(rootSym, dirSym) } // hasSubdir reports if dir is within root by performing lexical analysis only. func hasSubdir(root, dir string) (rel string, ok bool) { const sep = string(filepath.Separator) root = filepath.Clean(root) if !strings.HasSuffix(root, sep) { root += sep } dir = filepath.Clean(dir) after, found := strings.CutPrefix(dir, root) if !found { return "", false } return filepath.ToSlash(after), true } // readDir calls ctxt.ReadDir (if not nil) or else os.ReadDir. func (ctxt *Context) readDir(path string) ([]fs.DirEntry, error) { // TODO: add a fs.DirEntry version of Context.ReadDir if f := ctxt.ReadDir; f != nil { fis, err := f(path) if err != nil { return nil, err } des := make([]fs.DirEntry, len(fis)) for i, fi := range fis { des[i] = fs.FileInfoToDirEntry(fi) } return des, nil } return os.ReadDir(path) } // openFile calls ctxt.OpenFile (if not nil) or else os.Open. func (ctxt *Context) openFile(path string) (io.ReadCloser, error) { if fn := ctxt.OpenFile; fn != nil { return fn(path) } f, err := os.Open(path) if err != nil { return nil, err // nil interface } return f, nil } // isFile determines whether path is a file by trying to open it. // It reuses openFile instead of adding another function to the // list in Context. func (ctxt *Context) isFile(path string) bool { f, err := ctxt.openFile(path) if err != nil { return false } f.Close() return true } // gopath returns the list of Go path directories. func (ctxt *Context) gopath() []string { var all []string for _, p := range ctxt.splitPathList(ctxt.GOPATH) { if p == "" || p == ctxt.GOROOT { // Empty paths are uninteresting. // If the path is the GOROOT, ignore it. // People sometimes set GOPATH=$GOROOT. // Do not get confused by this common mistake. continue } if strings.HasPrefix(p, "~") { // Path segments starting with ~ on Unix are almost always // users who have incorrectly quoted ~ while setting GOPATH, // preventing it from expanding to $HOME. // The situation is made more confusing by the fact that // bash allows quoted ~ in $PATH (most shells do not). // Do not get confused by this, and do not try to use the path. // It does not exist, and printing errors about it confuses // those users even more, because they think "sure ~ exists!". // The go command diagnoses this situation and prints a // useful error. // On Windows, ~ is used in short names, such as c:\progra~1 // for c:\program files. continue } all = append(all, p) } return all } // SrcDirs returns a list of package source root directories. // It draws from the current Go root and Go path but omits directories // that do not exist. func (ctxt *Context) SrcDirs() []string { var all []string if ctxt.GOROOT != "" && ctxt.Compiler != "gccgo" { dir := ctxt.joinPath(ctxt.GOROOT, "src") if ctxt.isDir(dir) { all = append(all, dir) } } for _, p := range ctxt.gopath() { dir := ctxt.joinPath(p, "src") if ctxt.isDir(dir) { all = append(all, dir) } } return all } // Default is the default Context for builds. // It uses the GOARCH, GOOS, GOROOT, and GOPATH environment variables // if set, or else the compiled code's GOARCH, GOOS, and GOROOT. var Default Context = defaultContext() // Keep consistent with cmd/go/internal/cfg.defaultGOPATH. func defaultGOPATH() string { env := "HOME" if runtime.GOOS == "windows" { env = "USERPROFILE" } else if runtime.GOOS == "plan9" { env = "home" } if home := os.Getenv(env); home != "" { def := filepath.Join(home, "go") if filepath.Clean(def) == filepath.Clean(runtime.GOROOT()) { // Don't set the default GOPATH to GOROOT, // as that will trigger warnings from the go tool. return "" } return def } return "" } // defaultToolTags should be an internal detail, // but widely used packages access it using linkname. // Notable members of the hall of shame include: // - github.com/gopherjs/gopherjs // // Do not remove or change the type signature. // See go.dev/issue/67401. // //go:linkname defaultToolTags var defaultToolTags []string // defaultReleaseTags should be an internal detail, // but widely used packages access it using linkname. // Notable members of the hall of shame include: // - github.com/gopherjs/gopherjs // // Do not remove or change the type signature. // See go.dev/issue/67401. // //go:linkname defaultReleaseTags var defaultReleaseTags []string func defaultContext() Context { var c Context c.GOARCH = buildcfg.GOARCH c.GOOS = buildcfg.GOOS if goroot := runtime.GOROOT(); goroot != "" { c.GOROOT = filepath.Clean(goroot) } c.GOPATH = envOr("GOPATH", defaultGOPATH()) c.Compiler = runtime.Compiler c.ToolTags = append(c.ToolTags, buildcfg.ToolTags...) defaultToolTags = append([]string{}, c.ToolTags...) // our own private copy // Each major Go release in the Go 1.x series adds a new // "go1.x" release tag. That is, the go1.x tag is present in // all releases >= Go 1.x. Code that requires Go 1.x or later // should say "go:build go1.x", and code that should only be // built before Go 1.x (perhaps it is the stub to use in that // case) should say "go:build !go1.x". // The last element in ReleaseTags is the current release. for i := 1; i <= goversion.Version; i++ { c.ReleaseTags = append(c.ReleaseTags, "go1."+strconv.Itoa(i)) } defaultReleaseTags = append([]string{}, c.ReleaseTags...) // our own private copy env := os.Getenv("CGO_ENABLED") if env == "" { env = defaultCGO_ENABLED } switch env { case "1": c.CgoEnabled = true case "0": c.CgoEnabled = false default: // cgo must be explicitly enabled for cross compilation builds if runtime.GOARCH == c.GOARCH && runtime.GOOS == c.GOOS { c.CgoEnabled = platform.CgoSupported(c.GOOS, c.GOARCH) break } c.CgoEnabled = false } return c } func envOr(name, def string) string { s := os.Getenv(name) if s == "" { return def } return s } // An ImportMode controls the behavior of the Import method. type ImportMode uint const ( // If FindOnly is set, Import stops after locating the directory // that should contain the sources for a package. It does not // read any files in the directory. FindOnly ImportMode = 1 << iota // If AllowBinary is set, Import can be satisfied by a compiled // package object without corresponding sources. // // Deprecated: // The supported way to create a compiled-only package is to // write source code containing a //go:binary-only-package comment at // the top of the file. Such a package will be recognized // regardless of this flag setting (because it has source code) // and will have BinaryOnly set to true in the returned Package. AllowBinary // If ImportComment is set, parse import comments on package statements. // Import returns an error if it finds a comment it cannot understand // or finds conflicting comments in multiple source files. // See golang.org/s/go14customimport for more information. ImportComment // By default, Import searches vendor directories // that apply in the given source directory before searching // the GOROOT and GOPATH roots. // If an Import finds and returns a package using a vendor // directory, the resulting ImportPath is the complete path // to the package, including the path elements leading up // to and including "vendor". // For example, if Import("y", "x/subdir", 0) finds // "x/vendor/y", the returned package's ImportPath is "x/vendor/y", // not plain "y". // See golang.org/s/go15vendor for more information. // // Setting IgnoreVendor ignores vendor directories. // // In contrast to the package's ImportPath, // the returned package's Imports, TestImports, and XTestImports // are always the exact import paths from the source files: // Import makes no attempt to resolve or check those paths. IgnoreVendor ) // A Package describes the Go package found in a directory. type Package struct { Dir string // directory containing package sources Name string // package name ImportComment string // path in import comment on package statement Doc string // documentation synopsis ImportPath string // import path of package ("" if unknown) Root string // root of Go tree where this package lives SrcRoot string // package source root directory ("" if unknown) PkgRoot string // package install root directory ("" if unknown) PkgTargetRoot string // architecture dependent install root directory ("" if unknown) BinDir string // command install directory ("" if unknown) Goroot bool // package found in Go root PkgObj string // installed .a file AllTags []string // tags that can influence file selection in this directory ConflictDir string // this directory shadows Dir in $GOPATH BinaryOnly bool // cannot be rebuilt from source (has //go:binary-only-package comment) // Source files GoFiles []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles) CgoFiles []string // .go source files that import "C" IgnoredGoFiles []string // .go source files ignored for this build (including ignored _test.go files) InvalidGoFiles []string // .go source files with detected problems (parse error, wrong package name, and so on) IgnoredOtherFiles []string // non-.go source files ignored for this build CFiles []string // .c source files CXXFiles []string // .cc, .cpp and .cxx source files MFiles []string // .m (Objective-C) source files HFiles []string // .h, .hh, .hpp and .hxx source files FFiles []string // .f, .F, .for and .f90 Fortran source files SFiles []string // .s source files SwigFiles []string // .swig files SwigCXXFiles []string // .swigcxx files SysoFiles []string // .syso system object files to add to archive // Cgo directives CgoCFLAGS []string // Cgo CFLAGS directives CgoCPPFLAGS []string // Cgo CPPFLAGS directives CgoCXXFLAGS []string // Cgo CXXFLAGS directives CgoFFLAGS []string // Cgo FFLAGS directives CgoLDFLAGS []string // Cgo LDFLAGS directives CgoPkgConfig []string // Cgo pkg-config directives // Test information TestGoFiles []string // _test.go files in package XTestGoFiles []string // _test.go files outside package // Go directive comments (//go:zzz...) found in source files. Directives []Directive TestDirectives []Directive XTestDirectives []Directive // Dependency information Imports []string // import paths from GoFiles, CgoFiles ImportPos map[string][]token.Position // line information for Imports TestImports []string // import paths from TestGoFiles TestImportPos map[string][]token.Position // line information for TestImports XTestImports []string // import paths from XTestGoFiles XTestImportPos map[string][]token.Position // line information for XTestImports // //go:embed patterns found in Go source files // For example, if a source file says // //go:embed a* b.c // then the list will contain those two strings as separate entries. // (See package embed for more details about //go:embed.) EmbedPatterns []string // patterns from GoFiles, CgoFiles EmbedPatternPos map[string][]token.Position // line information for EmbedPatterns TestEmbedPatterns []string // patterns from TestGoFiles TestEmbedPatternPos map[string][]token.Position // line information for TestEmbedPatterns XTestEmbedPatterns []string // patterns from XTestGoFiles XTestEmbedPatternPos map[string][]token.Position // line information for XTestEmbedPatternPos } // A Directive is a Go directive comment (//go:zzz...) found in a source file. type Directive struct { Text string // full line comment including leading slashes Pos token.Position // position of comment } // IsCommand reports whether the package is considered a // command to be installed (not just a library). // Packages named "main" are treated as commands. func (p *Package) IsCommand() bool { return p.Name == "main" } // ImportDir is like [Import] but processes the Go package found in // the named directory. func (ctxt *Context) ImportDir(dir string, mode ImportMode) (*Package, error) { return ctxt.Import(".", dir, mode) } // NoGoError is the error used by [Import] to describe a directory // containing no buildable Go source files. (It may still contain // test files, files hidden by build tags, and so on.) type NoGoError struct { Dir string } func (e *NoGoError) Error() string { return "no buildable Go source files in " + e.Dir } // MultiplePackageError describes a directory containing // multiple buildable Go source files for multiple packages. type MultiplePackageError struct { Dir string // directory containing files Packages []string // package names found Files []string // corresponding files: Files[i] declares package Packages[i] } func (e *MultiplePackageError) Error() string { // Error string limited to two entries for compatibility. 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) } func nameExt(name string) string { i := strings.LastIndex(name, ".") if i < 0 { return "" } return name[i:] } var installgoroot = godebug.New("installgoroot") // Import returns details about the Go package named by the import path, // interpreting local import paths relative to the srcDir directory. // If the path is a local import path naming a package that can be imported // using a standard import path, the returned package will set p.ImportPath // to that path. // // In the directory containing the package, .go, .c, .h, and .s files are // considered part of the package except for: // // - .go files in package documentation // - files starting with _ or . (likely editor temporary files) // - files with build constraints not satisfied by the context // // If an error occurs, Import returns a non-nil error and a non-nil // *[Package] containing partial information. func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Package, error) { p := &Package{ ImportPath: path, } if path == "" { return p, fmt.Errorf("import %q: invalid import path", path) } var pkgtargetroot string var pkga string var pkgerr error suffix := "" if ctxt.InstallSuffix != "" { suffix = "_" + ctxt.InstallSuffix } switch ctxt.Compiler { case "gccgo": pkgtargetroot = "pkg/gccgo_" + ctxt.GOOS + "_" + ctxt.GOARCH + suffix case "gc": pkgtargetroot = "pkg/" + ctxt.GOOS + "_" + ctxt.GOARCH + suffix default: // Save error for end of function. pkgerr = fmt.Errorf("import %q: unknown compiler %q", path, ctxt.Compiler) } setPkga := func() { switch ctxt.Compiler { case "gccgo": dir, elem := pathpkg.Split(p.ImportPath) pkga = pkgtargetroot + "/" + dir + "lib" + elem + ".a" case "gc": pkga = pkgtargetroot + "/" + p.ImportPath + ".a" } } setPkga() binaryOnly := false if IsLocalImport(path) { pkga = "" // local imports have no installed path if srcDir == "" { return p, fmt.Errorf("import %q: import relative to unknown directory", path) } if !ctxt.isAbsPath(path) { p.Dir = ctxt.joinPath(srcDir, path) } // p.Dir directory may or may not exist. Gather partial information first, check if it exists later. // Determine canonical import path, if any. // Exclude results where the import path would include /testdata/. inTestdata := func(sub string) bool { return strings.Contains(sub, "/testdata/") || strings.HasSuffix(sub, "/testdata") || strings.HasPrefix(sub, "testdata/") || sub == "testdata" } if ctxt.GOROOT != "" { root := ctxt.joinPath(ctxt.GOROOT, "src") if sub, ok := ctxt.hasSubdir(root, p.Dir); ok && !inTestdata(sub) { p.Goroot = true p.ImportPath = sub p.Root = ctxt.GOROOT setPkga() // p.ImportPath changed goto Found } } all := ctxt.gopath() for i, root := range all { rootsrc := ctxt.joinPath(root, "src") if sub, ok := ctxt.hasSubdir(rootsrc, p.Dir); ok && !inTestdata(sub) { // We found a potential import path for dir, // but check that using it wouldn't find something // else first. if ctxt.GOROOT != "" && ctxt.Compiler != "gccgo" { if dir := ctxt.joinPath(ctxt.GOROOT, "src", sub); ctxt.isDir(dir) { p.ConflictDir = dir goto Found } } for _, earlyRoot := range all[:i] { if dir := ctxt.joinPath(earlyRoot, "src", sub); ctxt.isDir(dir) { p.ConflictDir = dir goto Found } } // sub would not name some other directory instead of this one. // Record it. p.ImportPath = sub p.Root = root setPkga() // p.ImportPath changed goto Found } } // It's okay that we didn't find a root containing dir. // Keep going with the information we have. } else { if strings.HasPrefix(path, "/") { return p, fmt.Errorf("import %q: cannot import absolute path", path) } if err := ctxt.importGo(p, path, srcDir, mode); err == nil { goto Found } else if err != errNoModules { return p, err } gopath := ctxt.gopath() // needed twice below; avoid computing many times // tried records the location of unsuccessful package lookups var tried struct { vendor []string goroot string gopath []string } // Vendor directories get first chance to satisfy import. if mode&IgnoreVendor == 0 && srcDir != "" { searchVendor := func(root string, isGoroot bool) bool { sub, ok := ctxt.hasSubdir(root, srcDir) if !ok || !strings.HasPrefix(sub, "src/") || strings.Contains(sub, "/testdata/") { return false } for { vendor := ctxt.joinPath(root, sub, "vendor") if ctxt.isDir(vendor) { dir := ctxt.joinPath(vendor, path) if ctxt.isDir(dir) && hasGoFiles(ctxt, dir) { p.Dir = dir p.ImportPath = strings.TrimPrefix(pathpkg.Join(sub, "vendor", path), "src/") p.Goroot = isGoroot p.Root = root setPkga() // p.ImportPath changed return true } tried.vendor = append(tried.vendor, dir) } i := strings.LastIndex(sub, "/") if i < 0 { break } sub = sub[:i] } return false } if ctxt.Compiler != "gccgo" && ctxt.GOROOT != "" && searchVendor(ctxt.GOROOT, true) { goto Found } for _, root := range gopath { if searchVendor(root, false) { goto Found } } } // Determine directory from import path. if ctxt.GOROOT != "" { // If the package path starts with "vendor/", only search GOROOT before // GOPATH if the importer is also within GOROOT. That way, if the user has // vendored in a package that is subsequently included in the standard // distribution, they'll continue to pick up their own vendored copy. gorootFirst := srcDir == "" || !strings.HasPrefix(path, "vendor/") if !gorootFirst { _, gorootFirst = ctxt.hasSubdir(ctxt.GOROOT, srcDir) } if gorootFirst { dir := ctxt.joinPath(ctxt.GOROOT, "src", path) if ctxt.Compiler != "gccgo" { isDir := ctxt.isDir(dir) binaryOnly = !isDir && mode&AllowBinary != 0 && pkga != "" && ctxt.isFile(ctxt.joinPath(ctxt.GOROOT, pkga)) if isDir || binaryOnly { p.Dir = dir p.Goroot = true p.Root = ctxt.GOROOT goto Found } } tried.goroot = dir } if ctxt.Compiler == "gccgo" && goroot.IsStandardPackage(ctxt.GOROOT, ctxt.Compiler, path) { // TODO(bcmills): Setting p.Dir here is misleading, because gccgo // doesn't actually load its standard-library packages from this // directory. See if we can leave it unset. p.Dir = ctxt.joinPath(ctxt.GOROOT, "src", path) p.Goroot = true p.Root = ctxt.GOROOT goto Found } } for _, root := range gopath { dir := ctxt.joinPath(root, "src", path) isDir := ctxt.isDir(dir) binaryOnly = !isDir && mode&AllowBinary != 0 && pkga != "" && ctxt.isFile(ctxt.joinPath(root, pkga)) if isDir || binaryOnly { p.Dir = dir p.Root = root goto Found } tried.gopath = append(tried.gopath, dir) } // If we tried GOPATH first due to a "vendor/" prefix, fall back to GOPATH. // That way, the user can still get useful results from 'go list' for // standard-vendored paths passed on the command line. if ctxt.GOROOT != "" && tried.goroot == "" { dir := ctxt.joinPath(ctxt.GOROOT, "src", path) if ctxt.Compiler != "gccgo" { isDir := ctxt.isDir(dir) binaryOnly = !isDir && mode&AllowBinary != 0 && pkga != "" && ctxt.isFile(ctxt.joinPath(ctxt.GOROOT, pkga)) if isDir || binaryOnly { p.Dir = dir p.Goroot = true p.Root = ctxt.GOROOT goto Found } } tried.goroot = dir } // package was not found var paths []string format := "\t%s (vendor tree)" for _, dir := range tried.vendor { paths = append(paths, fmt.Sprintf(format, dir)) format = "\t%s" } if tried.goroot != "" { paths = append(paths, fmt.Sprintf("\t%s (from $GOROOT)", tried.goroot)) } else { paths = append(paths, "\t($GOROOT not set)") } format = "\t%s (from $GOPATH)" for _, dir := range tried.gopath { paths = append(paths, fmt.Sprintf(format, dir)) format = "\t%s" } if len(tried.gopath) == 0 { paths = append(paths, "\t($GOPATH not set. For more details see: 'go help gopath')") } return p, fmt.Errorf("cannot find package %q in any of:\n%s", path, strings.Join(paths, "\n")) } Found: if p.Root != "" { p.SrcRoot = ctxt.joinPath(p.Root, "src") p.PkgRoot = ctxt.joinPath(p.Root, "pkg") p.BinDir = ctxt.joinPath(p.Root, "bin") if pkga != "" { // Always set PkgTargetRoot. It might be used when building in shared // mode. p.PkgTargetRoot = ctxt.joinPath(p.Root, pkgtargetroot) // Set the install target if applicable. if !p.Goroot || (installgoroot.Value() == "all" && p.ImportPath != "unsafe" && p.ImportPath != "builtin") { if p.Goroot { installgoroot.IncNonDefault() } p.PkgObj = ctxt.joinPath(p.Root, pkga) } } } // If it's a local import path, by the time we get here, we still haven't checked // that p.Dir directory exists. This is the right time to do that check. // We can't do it earlier, because we want to gather partial information for the // non-nil *Package returned when an error occurs. // We need to do this before we return early on FindOnly flag. if IsLocalImport(path) && !ctxt.isDir(p.Dir) { if ctxt.Compiler == "gccgo" && p.Goroot { // gccgo has no sources for GOROOT packages. return p, nil } // package was not found return p, fmt.Errorf("cannot find package %q in:\n\t%s", p.ImportPath, p.Dir) } if mode&FindOnly != 0 { return p, pkgerr } if binaryOnly && (mode&AllowBinary) != 0 { return p, pkgerr } if ctxt.Compiler == "gccgo" && p.Goroot { // gccgo has no sources for GOROOT packages. return p, nil } dirs, err := ctxt.readDir(p.Dir) if err != nil { return p, err } var badGoError error badGoFiles := make(map[string]bool) badGoFile := func(name string, err error) { if badGoError == nil { badGoError = err } if !badGoFiles[name] { p.InvalidGoFiles = append(p.InvalidGoFiles, name) badGoFiles[name] = true } } var Sfiles []string // files with ".S"(capital S)/.sx(capital s equivalent for case insensitive filesystems) var firstFile, firstCommentFile string embedPos := make(map[string][]token.Position) testEmbedPos := make(map[string][]token.Position) xTestEmbedPos := make(map[string][]token.Position) importPos := make(map[string][]token.Position) testImportPos := make(map[string][]token.Position) xTestImportPos := make(map[string][]token.Position) allTags := make(map[string]bool) fset := token.NewFileSet() for _, d := range dirs { if d.IsDir() { continue } if d.Type() == fs.ModeSymlink { if ctxt.isDir(ctxt.joinPath(p.Dir, d.Name())) { // Symlinks to directories are not source files. continue } } name := d.Name() ext := nameExt(name) info, err := ctxt.matchFile(p.Dir, name, allTags, &p.BinaryOnly, fset) if err != nil && strings.HasSuffix(name, ".go") { badGoFile(name, err) continue } if info == nil { if strings.HasPrefix(name, "_") || strings.HasPrefix(name, ".") { // not due to build constraints - don't report } else if ext == ".go" { p.IgnoredGoFiles = append(p.IgnoredGoFiles, name) } else if fileListForExt(p, ext) != nil { p.IgnoredOtherFiles = append(p.IgnoredOtherFiles, name) } continue } // Going to save the file. For non-Go files, can stop here. switch ext { case ".go": // keep going case ".S", ".sx": // special case for cgo, handled at end Sfiles = append(Sfiles, name) continue default: if list := fileListForExt(p, ext); list != nil { *list = append(*list, name) } continue } data, filename := info.header, info.name if info.parseErr != nil { badGoFile(name, info.parseErr) // Fall through: we might still have a partial AST in info.parsed, // and we want to list files with parse errors anyway. } var pkg string if info.parsed != nil { pkg = info.parsed.Name.Name if pkg == "documentation" { p.IgnoredGoFiles = append(p.IgnoredGoFiles, name) continue } } isTest := strings.HasSuffix(name, "_test.go") isXTest := false if isTest && strings.HasSuffix(pkg, "_test") && p.Name != pkg { isXTest = true pkg = pkg[:len(pkg)-len("_test")] } if p.Name == "" { p.Name = pkg firstFile = name } else if pkg != p.Name { // TODO(#45999): The choice of p.Name is arbitrary based on file iteration // order. Instead of resolving p.Name arbitrarily, we should clear out the // existing name and mark the existing files as also invalid. badGoFile(name, &MultiplePackageError{ Dir: p.Dir, Packages: []string{p.Name, pkg}, Files: []string{firstFile, name}, }) } // Grab the first package comment as docs, provided it is not from a test file. if info.parsed != nil && info.parsed.Doc != nil && p.Doc == "" && !isTest && !isXTest { p.Doc = doc.Synopsis(info.parsed.Doc.Text()) } if mode&ImportComment != 0 { qcom, line := findImportComment(data) if line != 0 { com, err := strconv.Unquote(qcom) if err != nil { badGoFile(name, fmt.Errorf("%s:%d: cannot parse import comment", filename, line)) } else if p.ImportComment == "" { p.ImportComment = com firstCommentFile = name } else if p.ImportComment != com { badGoFile(name, fmt.Errorf("found import comments %q (%s) and %q (%s) in %s", p.ImportComment, firstCommentFile, com, name, p.Dir)) } } } // Record imports and information about cgo. isCgo := false for _, imp := range info.imports { if imp.path == "C" { if isTest { badGoFile(name, fmt.Errorf("use of cgo in test %s not supported", filename)) continue } isCgo = true if imp.doc != nil { if err := ctxt.saveCgo(filename, p, imp.doc); err != nil { badGoFile(name, err) } } } } var fileList *[]string var importMap, embedMap map[string][]token.Position var directives *[]Directive switch { case isCgo: allTags["cgo"] = true if ctxt.CgoEnabled { fileList = &p.CgoFiles importMap = importPos embedMap = embedPos directives = &p.Directives } else { // Ignore imports and embeds from cgo files if cgo is disabled. fileList = &p.IgnoredGoFiles } case isXTest: fileList = &p.XTestGoFiles importMap = xTestImportPos embedMap = xTestEmbedPos directives = &p.XTestDirectives case isTest: fileList = &p.TestGoFiles importMap = testImportPos embedMap = testEmbedPos directives = &p.TestDirectives default: fileList = &p.GoFiles importMap = importPos embedMap = embedPos directives = &p.Directives } *fileList = append(*fileList, name) if importMap != nil { for _, imp := range info.imports { importMap[imp.path] = append(importMap[imp.path], fset.Position(imp.pos)) } } if embedMap != nil { for _, emb := range info.embeds { embedMap[emb.pattern] = append(embedMap[emb.pattern], emb.pos) } } if directives != nil { *directives = append(*directives, info.directives...) } } for tag := range allTags { p.AllTags = append(p.AllTags, tag) } slices.Sort(p.AllTags) p.EmbedPatterns, p.EmbedPatternPos = cleanDecls(embedPos) p.TestEmbedPatterns, p.TestEmbedPatternPos = cleanDecls(testEmbedPos) p.XTestEmbedPatterns, p.XTestEmbedPatternPos = cleanDecls(xTestEmbedPos) p.Imports, p.ImportPos = cleanDecls(importPos) p.TestImports, p.TestImportPos = cleanDecls(testImportPos) p.XTestImports, p.XTestImportPos = cleanDecls(xTestImportPos) // add the .S/.sx files only if we are using cgo // (which means gcc will compile them). // The standard assemblers expect .s files. if len(p.CgoFiles) > 0 { p.SFiles = append(p.SFiles, Sfiles...) slices.Sort(p.SFiles) } else { p.IgnoredOtherFiles = append(p.IgnoredOtherFiles, Sfiles...) slices.Sort(p.IgnoredOtherFiles) } if badGoError != nil { return p, badGoError } if len(p.GoFiles)+len(p.CgoFiles)+len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 { return p, &NoGoError{p.Dir} } return p, pkgerr } func fileListForExt(p *Package, ext string) *[]string { switch ext { case ".c": return &p.CFiles case ".cc", ".cpp", ".cxx": return &p.CXXFiles case ".m": return &p.MFiles case ".h", ".hh", ".hpp", ".hxx": return &p.HFiles case ".f", ".F", ".for", ".f90": return &p.FFiles case ".s", ".S", ".sx": return &p.SFiles case ".swig": return &p.SwigFiles case ".swigcxx": return &p.SwigCXXFiles case ".syso": return &p.SysoFiles } return nil } func uniq(list []string) []string { if list == nil { return nil } out := make([]string, len(list)) copy(out, list) slices.Sort(out) uniq := out[:0] for _, x := range out { if len(uniq) == 0 || uniq[len(uniq)-1] != x { uniq = append(uniq, x) } } return uniq } var errNoModules = errors.New("not using modules") // importGo checks whether it can use the go command to find the directory for path. // If using the go command is not appropriate, importGo returns errNoModules. // Otherwise, importGo tries using the go command and reports whether that succeeded. // Using the go command lets build.Import and build.Context.Import find code // in Go modules. In the long term we want tools to use go/packages (currently golang.org/x/tools/go/packages), // which will also use the go command. // Invoking the go command here is not very efficient in that it computes information // about the requested package and all dependencies and then only reports about the requested package. // Then we reinvoke it for every dependency. But this is still better than not working at all. // See golang.org/issue/26504. func (ctxt *Context) importGo(p *Package, path, srcDir string, mode ImportMode) error { // To invoke the go command, // we must not being doing special things like AllowBinary or IgnoreVendor, // and all the file system callbacks must be nil (we're meant to use the local file system). if mode&AllowBinary != 0 || mode&IgnoreVendor != 0 || ctxt.JoinPath != nil || ctxt.SplitPathList != nil || ctxt.IsAbsPath != nil || ctxt.IsDir != nil || ctxt.HasSubdir != nil || ctxt.ReadDir != nil || ctxt.OpenFile != nil || !equal(ctxt.ToolTags, defaultToolTags) || !equal(ctxt.ReleaseTags, defaultReleaseTags) { return errNoModules } // If ctxt.GOROOT is not set, we don't know which go command to invoke, // and even if we did we might return packages in GOROOT that we wouldn't otherwise find // (because we don't know to search in 'go env GOROOT' otherwise). if ctxt.GOROOT == "" { return errNoModules } // Predict whether module aware mode is enabled by checking the value of // GO111MODULE and looking for a go.mod file in the source directory or // one of its parents. Running 'go env GOMOD' in the source directory would // give a canonical answer, but we'd prefer not to execute another command. go111Module := os.Getenv("GO111MODULE") switch go111Module { case "off": return errNoModules default: // "", "on", "auto", anything else // Maybe use modules. } if srcDir != "" { var absSrcDir string if filepath.IsAbs(srcDir) { absSrcDir = srcDir } else if ctxt.Dir != "" { return fmt.Errorf("go/build: Dir is non-empty, so relative srcDir is not allowed: %v", srcDir) } else { // Find the absolute source directory. hasSubdir does not handle // relative paths (and can't because the callbacks don't support this). var err error absSrcDir, err = filepath.Abs(srcDir) if err != nil { return errNoModules } } // If the source directory is in GOROOT, then the in-process code works fine // and we should keep using it. Moreover, the 'go list' approach below doesn't // take standard-library vendoring into account and will fail. if _, ok := ctxt.hasSubdir(filepath.Join(ctxt.GOROOT, "src"), absSrcDir); ok { return errNoModules } } // For efficiency, if path is a standard library package, let the usual lookup code handle it. if dir := ctxt.joinPath(ctxt.GOROOT, "src", path); ctxt.isDir(dir) { return errNoModules } // If GO111MODULE=auto, look to see if there is a go.mod. // Since go1.13, it doesn't matter if we're inside GOPATH. if go111Module == "auto" { var ( parent string err error ) if ctxt.Dir == "" { parent, err = os.Getwd() if err != nil { // A nonexistent working directory can't be in a module. return errNoModules } } else { parent, err = filepath.Abs(ctxt.Dir) if err != nil { // If the caller passed a bogus Dir explicitly, that's materially // different from not having modules enabled. return err } } for { if f, err := ctxt.openFile(ctxt.joinPath(parent, "go.mod")); err == nil { buf := make([]byte, 100) _, err := f.Read(buf) f.Close() if err == nil || err == io.EOF { // go.mod exists and is readable (is a file, not a directory). break } } d := filepath.Dir(parent) if len(d) >= len(parent) { return errNoModules // reached top of file system, no go.mod } parent = d } } goCmd := filepath.Join(ctxt.GOROOT, "bin", "go") cmd := exec.Command(goCmd, "list", "-e", "-compiler="+ctxt.Compiler, "-tags="+strings.Join(ctxt.BuildTags, ","), "-installsuffix="+ctxt.InstallSuffix, "-f={{.Dir}}\n{{.ImportPath}}\n{{.Root}}\n{{.Goroot}}\n{{if .Error}}{{.Error}}{{end}}\n", "--", path) if ctxt.Dir != "" { cmd.Dir = ctxt.Dir } var stdout, stderr strings.Builder cmd.Stdout = &stdout cmd.Stderr = &stderr cgo := "0" if ctxt.CgoEnabled { cgo = "1" } cmd.Env = append(cmd.Environ(), "GOOS="+ctxt.GOOS, "GOARCH="+ctxt.GOARCH, "GOROOT="+ctxt.GOROOT, "GOPATH="+ctxt.GOPATH, "CGO_ENABLED="+cgo, ) if err := cmd.Run(); err != nil { return fmt.Errorf("go/build: go list %s: %v\n%s\n", path, err, stderr.String()) } f := strings.SplitN(stdout.String(), "\n", 5) if len(f) != 5 { return fmt.Errorf("go/build: importGo %s: unexpected output:\n%s\n", path, stdout.String()) } dir := f[0] errStr := strings.TrimSpace(f[4]) if errStr != "" && dir == "" { // If 'go list' could not locate the package (dir is empty), // return the same error that 'go list' reported. return errors.New(errStr) } // If 'go list' did locate the package, ignore the error. // It was probably related to loading source files, and we'll // encounter it ourselves shortly if the FindOnly flag isn't set. p.Dir = dir p.ImportPath = f[1] p.Root = f[2] p.Goroot = f[3] == "true" return nil } func equal(x, y []string) bool { if len(x) != len(y) { return false } for i, xi := range x { if xi != y[i] { return false } } return true } // hasGoFiles reports whether dir contains any files with names ending in .go. // For a vendor check we must exclude directories that contain no .go files. // Otherwise it is not possible to vendor just a/b/c and still import the // non-vendored a/b. See golang.org/issue/13832. func hasGoFiles(ctxt *Context, dir string) bool { ents, _ := ctxt.readDir(dir) for _, ent := range ents { if !ent.IsDir() && strings.HasSuffix(ent.Name(), ".go") { return true } } return false } func findImportComment(data []byte) (s string, line int) { // expect keyword package word, data := parseWord(data) if string(word) != "package" { return "", 0 } // expect package name _, data = parseWord(data) // now ready for import comment, a // or /* */ comment // beginning and ending on the current line. for len(data) > 0 && (data[0] == ' ' || data[0] == '\t' || data[0] == '\r') { data = data[1:] } var comment []byte switch { case bytes.HasPrefix(data, slashSlash): comment, _, _ = bytes.Cut(data[2:], newline) case bytes.HasPrefix(data, slashStar): var ok bool comment, _, ok = bytes.Cut(data[2:], starSlash) if !ok { // malformed comment return "", 0 } if bytes.Contains(comment, newline) { return "", 0 } } comment = bytes.TrimSpace(comment) // split comment into `import`, `"pkg"` word, arg := parseWord(comment) if string(word) != "import" { return "", 0 } line = 1 + bytes.Count(data[:cap(data)-cap(arg)], newline) return strings.TrimSpace(string(arg)), line } var ( slashSlash = []byte("//") slashStar = []byte("/*") starSlash = []byte("*/") newline = []byte("\n") ) // skipSpaceOrComment returns data with any leading spaces or comments removed. func skipSpaceOrComment(data []byte) []byte { for len(data) > 0 { switch data[0] { case ' ', '\t', '\r', '\n': data = data[1:] continue case '/': if bytes.HasPrefix(data, slashSlash) { i := bytes.Index(data, newline) if i < 0 { return nil } data = data[i+1:] continue } if bytes.HasPrefix(data, slashStar) { data = data[2:] i := bytes.Index(data, starSlash) if i < 0 { return nil } data = data[i+2:] continue } } break } return data } // parseWord skips any leading spaces or comments in data // and then parses the beginning of data as an identifier or keyword, // returning that word and what remains after the word. func parseWord(data []byte) (word, rest []byte) { data = skipSpaceOrComment(data) // Parse past leading word characters. rest = data for { r, size := utf8.DecodeRune(rest) if unicode.IsLetter(r) || '0' <= r && r <= '9' || r == '_' { rest = rest[size:] continue } break } word = data[:len(data)-len(rest)] if len(word) == 0 { return nil, nil } return word, rest } // MatchFile reports whether the file with the given name in the given directory // matches the context and would be included in a [Package] created by [ImportDir] // of that directory. // // MatchFile considers the name of the file and may use ctxt.OpenFile to // read some or all of the file's content. func (ctxt *Context) MatchFile(dir, name string) (match bool, err error) { info, err := ctxt.matchFile(dir, name, nil, nil, nil) return info != nil, err } var dummyPkg Package // fileInfo records information learned about a file included in a build. type fileInfo struct { name string // full name including dir header []byte fset *token.FileSet parsed *ast.File parseErr error imports []fileImport embeds []fileEmbed directives []Directive } type fileImport struct { path string pos token.Pos doc *ast.CommentGroup } type fileEmbed struct { pattern string pos token.Position } // matchFile determines whether the file with the given name in the given directory // should be included in the package being constructed. // If the file should be included, matchFile returns a non-nil *fileInfo (and a nil error). // Non-nil errors are reserved for unexpected problems. // // If name denotes a Go program, matchFile reads until the end of the // imports and returns that section of the file in the fileInfo's header field, // even though it only considers text until the first non-comment // for go:build lines. // // If allTags is non-nil, matchFile records any encountered build tag // by setting allTags[tag] = true. func (ctxt *Context) matchFile(dir, name string, allTags map[string]bool, binaryOnly *bool, fset *token.FileSet) (*fileInfo, error) { if strings.HasPrefix(name, "_") || strings.HasPrefix(name, ".") { return nil, nil } i := strings.LastIndex(name, ".") if i < 0 { i = len(name) } ext := name[i:] if ext != ".go" && fileListForExt(&dummyPkg, ext) == nil { // skip return nil, nil } if !ctxt.goodOSArchFile(name, allTags) && !ctxt.UseAllFiles { return nil, nil } info := &fileInfo{name: ctxt.joinPath(dir, name), fset: fset} if ext == ".syso" { // binary, no reading return info, nil } f, err := ctxt.openFile(info.name) if err != nil { return nil, err } if strings.HasSuffix(name, ".go") { err = readGoInfo(f, info) if strings.HasSuffix(name, "_test.go") { binaryOnly = nil // ignore //go:binary-only-package comments in _test.go files } } else { binaryOnly = nil // ignore //go:binary-only-package comments in non-Go sources info.header, err = readComments(f) } f.Close() if err != nil { return info, fmt.Errorf("read %s: %v", info.name, err) } // Look for go:build comments to accept or reject the file. ok, sawBinaryOnly, err := ctxt.shouldBuild(info.header, allTags) if err != nil { return nil, fmt.Errorf("%s: %v", name, err) } if !ok && !ctxt.UseAllFiles { return nil, nil } if binaryOnly != nil && sawBinaryOnly { *binaryOnly = true } return info, nil } func cleanDecls(m map[string][]token.Position) ([]string, map[string][]token.Position) { all := make([]string, 0, len(m)) for path := range m { all = append(all, path) } slices.Sort(all) return all, m } // Import is shorthand for Default.Import. func Import(path, srcDir string, mode ImportMode) (*Package, error) { return Default.Import(path, srcDir, mode) } // ImportDir is shorthand for Default.ImportDir. func ImportDir(dir string, mode ImportMode) (*Package, error) { return Default.ImportDir(dir, mode) } var ( plusBuild = []byte("+build") goBuildComment = []byte("//go:build") errMultipleGoBuild = errors.New("multiple //go:build comments") ) func isGoBuildComment(line []byte) bool { if !bytes.HasPrefix(line, goBuildComment) { return false } line = bytes.TrimSpace(line) rest := line[len(goBuildComment):] return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest) } // Special comment denoting a binary-only package. // See https://golang.org/design/2775-binary-only-packages // for more about the design of binary-only packages. var binaryOnlyComment = []byte("//go:binary-only-package") // shouldBuild reports whether it is okay to use this file, // The rule is that in the file's leading run of // comments // and blank lines, which must be followed by a blank line // (to avoid including a Go package clause doc comment), // lines beginning with '//go:build' are taken as build directives. // // The file is accepted only if each such line lists something // matching the file. For example: // // //go:build windows linux // // marks the file as applicable only on Windows and Linux. // // For each build tag it consults, shouldBuild sets allTags[tag] = true. // // shouldBuild reports whether the file should be built // and whether a //go:binary-only-package comment was found. func (ctxt *Context) shouldBuild(content []byte, allTags map[string]bool) (shouldBuild, binaryOnly bool, err error) { // Identify leading run of // comments and blank lines, // which must be followed by a blank line. // Also identify any //go:build comments. content, goBuild, sawBinaryOnly, err := parseFileHeader(content) if err != nil { return false, false, err } // If //go:build line is present, it controls. // Otherwise fall back to +build processing. switch { case goBuild != nil: x, err := constraint.Parse(string(goBuild)) if err != nil { return false, false, fmt.Errorf("parsing //go:build line: %v", err) } shouldBuild = ctxt.eval(x, allTags) default: shouldBuild = true p := content for len(p) > 0 { line := p if i := bytes.IndexByte(line, '\n'); i >= 0 { line, p = line[:i], p[i+1:] } else { p = p[len(p):] } line = bytes.TrimSpace(line) if !bytes.HasPrefix(line, slashSlash) || !bytes.Contains(line, plusBuild) { continue } text := string(line) if !constraint.IsPlusBuild(text) { continue } if x, err := constraint.Parse(text); err == nil { if !ctxt.eval(x, allTags) { shouldBuild = false } } } } return shouldBuild, sawBinaryOnly, nil } // parseFileHeader should be an internal detail, // but widely used packages access it using linkname. // Notable members of the hall of shame include: // - github.com/bazelbuild/bazel-gazelle // // Do not remove or change the type signature. // See go.dev/issue/67401. // //go:linkname parseFileHeader func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) { end := 0 p := content ended := false // found non-blank, non-// line, so stopped accepting //go:build lines inSlashStar := false // in /* */ comment Lines: for len(p) > 0 { line := p if i := bytes.IndexByte(line, '\n'); i >= 0 { line, p = line[:i], p[i+1:] } else { p = p[len(p):] } line = bytes.TrimSpace(line) if len(line) == 0 && !ended { // Blank line // Remember position of most recent blank line. // When we find the first non-blank, non-// line, // this "end" position marks the latest file position // where a //go:build line can appear. // (It must appear _before_ a blank line before the non-blank, non-// line. // Yes, that's confusing, which is part of why we moved to //go:build lines.) // Note that ended==false here means that inSlashStar==false, // since seeing a /* would have set ended==true. end = len(content) - len(p) continue Lines } if !bytes.HasPrefix(line, slashSlash) { // Not comment line ended = true } if !inSlashStar && isGoBuildComment(line) { if goBuild != nil { return nil, nil, false, errMultipleGoBuild } goBuild = line } if !inSlashStar && bytes.Equal(line, binaryOnlyComment) { sawBinaryOnly = true } Comments: for len(line) > 0 { if inSlashStar { if i := bytes.Index(line, starSlash); i >= 0 { inSlashStar = false line = bytes.TrimSpace(line[i+len(starSlash):]) continue Comments } continue Lines } if bytes.HasPrefix(line, slashSlash) { continue Lines } if bytes.HasPrefix(line, slashStar) { inSlashStar = true line = bytes.TrimSpace(line[len(slashStar):]) continue Comments } // Found non-comment text. break Lines } } return content[:end], goBuild, sawBinaryOnly, nil } // saveCgo saves the information from the #cgo lines in the import "C" comment. // These lines set CFLAGS, CPPFLAGS, CXXFLAGS and LDFLAGS and pkg-config directives // that affect the way cgo's C code is built. func (ctxt *Context) saveCgo(filename string, di *Package, cg *ast.CommentGroup) error { text := cg.Text() for _, line := range strings.Split(text, "\n") { orig := line // Line is // #cgo [GOOS/GOARCH...] LDFLAGS: stuff // line = strings.TrimSpace(line) if len(line) < 5 || line[:4] != "#cgo" || (line[4] != ' ' && line[4] != '\t') { continue } // #cgo (nocallback|noescape) if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") { continue } // Split at colon. line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":") if !ok { return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig) } // Parse GOOS/GOARCH stuff. f := strings.Fields(line) if len(f) < 1 { return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig) } cond, verb := f[:len(f)-1], f[len(f)-1] if len(cond) > 0 { ok := false for _, c := range cond { if ctxt.matchAuto(c, nil) { ok = true break } } if !ok { continue } } args, err := splitQuoted(argstr) if err != nil { return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig) } for i, arg := range args { if arg, ok = expandSrcDir(arg, di.Dir); !ok { return fmt.Errorf("%s: malformed #cgo argument: %s", filename, arg) } args[i] = arg } switch verb { case "CFLAGS", "CPPFLAGS", "CXXFLAGS", "FFLAGS", "LDFLAGS": // Change relative paths to absolute. ctxt.makePathsAbsolute(args, di.Dir) } switch verb { case "CFLAGS": di.CgoCFLAGS = append(di.CgoCFLAGS, args...) case "CPPFLAGS": di.CgoCPPFLAGS = append(di.CgoCPPFLAGS, args...) case "CXXFLAGS": di.CgoCXXFLAGS = append(di.CgoCXXFLAGS, args...) case "FFLAGS": di.CgoFFLAGS = append(di.CgoFFLAGS, args...) case "LDFLAGS": di.CgoLDFLAGS = append(di.CgoLDFLAGS, args...) case "pkg-config": di.CgoPkgConfig = append(di.CgoPkgConfig, args...) default: return fmt.Errorf("%s: invalid #cgo verb: %s", filename, orig) } } return nil } // expandSrcDir expands any occurrence of ${SRCDIR}, making sure // the result is safe for the shell. func expandSrcDir(str string, srcdir string) (string, bool) { // "\" delimited paths cause safeCgoName to fail // so convert native paths with a different delimiter // to "/" before starting (eg: on windows). srcdir = filepath.ToSlash(srcdir) chunks := strings.Split(str, "${SRCDIR}") if len(chunks) < 2 { return str, safeCgoName(str) } ok := true for _, chunk := range chunks { ok = ok && (chunk == "" || safeCgoName(chunk)) } ok = ok && (srcdir == "" || safeCgoName(srcdir)) res := strings.Join(chunks, srcdir) return res, ok && res != "" } // makePathsAbsolute looks for compiler options that take paths and // makes them absolute. We do this because through the 1.8 release we // ran the compiler in the package directory, so any relative -I or -L // options would be relative to that directory. In 1.9 we changed to // running the compiler in the build directory, to get consistent // build results (issue #19964). To keep builds working, we change any // relative -I or -L options to be absolute. // // Using filepath.IsAbs and filepath.Join here means the results will be // different on different systems, but that's OK: -I and -L options are // inherently system-dependent. func (ctxt *Context) makePathsAbsolute(args []string, srcDir string) { nextPath := false for i, arg := range args { if nextPath { if !filepath.IsAbs(arg) { args[i] = filepath.Join(srcDir, arg) } nextPath = false } else if strings.HasPrefix(arg, "-I") || strings.HasPrefix(arg, "-L") { if len(arg) == 2 { nextPath = true } else { if !filepath.IsAbs(arg[2:]) { args[i] = arg[:2] + filepath.Join(srcDir, arg[2:]) } } } } } // NOTE: $ is not safe for the shell, but it is allowed here because of linker options like -Wl,$ORIGIN. // We never pass these arguments to a shell (just to programs we construct argv for), so this should be okay. // See golang.org/issue/6038. // The @ is for OS X. See golang.org/issue/13720. // The % is for Jenkins. See golang.org/issue/16959. // The ! is because module paths may use them. See golang.org/issue/26716. // The ~ and ^ are for sr.ht. See golang.org/issue/32260. const safeString = "+-.,/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz:$@%! ~^" func safeCgoName(s string) bool { if s == "" { return false } for i := 0; i < len(s); i++ { if c := s[i]; c < utf8.RuneSelf && strings.IndexByte(safeString, c) < 0 { return false } } return true } // splitQuoted splits the string s around each instance of one or more consecutive // white space characters while taking into account quotes and escaping, and // returns an array of substrings of s or an empty list if s contains only white space. // Single quotes and double quotes are recognized to prevent splitting within the // quoted region, and are removed from the resulting substrings. If a quote in s // isn't closed err will be set and r will have the unclosed argument as the // last element. The backslash is used for escaping. // // For example, the following string: // // a b:"c d" 'e''f' "g\"" // // Would be parsed as: // // []string{"a", "b:c d", "ef", `g"`} func splitQuoted(s string) (r []string, err error) { var args []string arg := make([]rune, len(s)) escaped := false quoted := false quote := '\x00' i := 0 for _, rune := range s { switch { case escaped: escaped = false case rune == '\\': escaped = true continue case quote != '\x00': if rune == quote { quote = '\x00' continue } case rune == '"' || rune == '\'': quoted = true quote = rune continue case unicode.IsSpace(rune): if quoted || i > 0 { quoted = false args = append(args, string(arg[:i])) i = 0 } continue } arg[i] = rune i++ } if quoted || i > 0 { args = append(args, string(arg[:i])) } if quote != 0 { err = errors.New("unclosed quote") } else if escaped { err = errors.New("unfinished escaping") } return args, err } // matchAuto interprets text as either a +build or //go:build expression (whichever works), // reporting whether the expression matches the build context. // // matchAuto is only used for testing of tag evaluation // and in #cgo lines, which accept either syntax. func (ctxt *Context) matchAuto(text string, allTags map[string]bool) bool { if strings.ContainsAny(text, "&|()") { text = "//go:build " + text } else { text = "// +build " + text } x, err := constraint.Parse(text) if err != nil { return false } return ctxt.eval(x, allTags) } func (ctxt *Context) eval(x constraint.Expr, allTags map[string]bool) bool { return x.Eval(func(tag string) bool { return ctxt.matchTag(tag, allTags) }) } // matchTag reports whether the name is one of: // // cgo (if cgo is enabled) // $GOOS // $GOARCH // ctxt.Compiler // linux (if GOOS = android) // solaris (if GOOS = illumos) // darwin (if GOOS = ios) // unix (if this is a Unix GOOS) // boringcrypto (if GOEXPERIMENT=boringcrypto is enabled) // tag (if tag is listed in ctxt.BuildTags, ctxt.ToolTags, or ctxt.ReleaseTags) // // It records all consulted tags in allTags. func (ctxt *Context) matchTag(name string, allTags map[string]bool) bool { if allTags != nil { allTags[name] = true } // special tags if ctxt.CgoEnabled && name == "cgo" { return true } if name == ctxt.GOOS || name == ctxt.GOARCH || name == ctxt.Compiler { return true } if ctxt.GOOS == "android" && name == "linux" { return true } if ctxt.GOOS == "illumos" && name == "solaris" { return true } if ctxt.GOOS == "ios" && name == "darwin" { return true } if name == "unix" && unixOS[ctxt.GOOS] { return true } if name == "boringcrypto" { name = "goexperiment.boringcrypto" // boringcrypto is an old name for goexperiment.boringcrypto } // other tags for _, tag := range ctxt.BuildTags { if tag == name { return true } } for _, tag := range ctxt.ToolTags { if tag == name { return true } } for _, tag := range ctxt.ReleaseTags { if tag == name { return true } } return false } // goodOSArchFile returns false if the name contains a $GOOS or $GOARCH // suffix which does not match the current system. // The recognized name formats are: // // name_$(GOOS).* // name_$(GOARCH).* // name_$(GOOS)_$(GOARCH).* // name_$(GOOS)_test.* // name_$(GOARCH)_test.* // name_$(GOOS)_$(GOARCH)_test.* // // Exceptions: // if GOOS=android, then files with GOOS=linux are also matched. // if GOOS=illumos, then files with GOOS=solaris are also matched. // if GOOS=ios, then files with GOOS=darwin are also matched. func (ctxt *Context) goodOSArchFile(name string, allTags map[string]bool) bool { name, _, _ = strings.Cut(name, ".") // Before Go 1.4, a file called "linux.go" would be equivalent to having a // build tag "linux" in that file. For Go 1.4 and beyond, we require this // auto-tagging to apply only to files with a non-empty prefix, so // "foo_linux.go" is tagged but "linux.go" is not. This allows new operating // systems, such as android, to arrive without breaking existing code with // innocuous source code in "android.go". The easiest fix: cut everything // in the name before the initial _. i := strings.Index(name, "_") if i < 0 { return true } name = name[i:] // ignore everything before first _ l := strings.Split(name, "_") if n := len(l); n > 0 && l[n-1] == "test" { l = l[:n-1] } n := len(l) if n >= 2 && knownOS[l[n-2]] && knownArch[l[n-1]] { if allTags != nil { // In case we short-circuit on l[n-1]. allTags[l[n-2]] = true } return ctxt.matchTag(l[n-1], allTags) && ctxt.matchTag(l[n-2], allTags) } if n >= 1 && (knownOS[l[n-1]] || knownArch[l[n-1]]) { return ctxt.matchTag(l[n-1], allTags) } return true } // ToolDir is the directory containing build tools. var ToolDir = getToolDir() // IsLocalImport reports whether the import path is // a local import path, like ".", "..", "./foo", or "../foo". func IsLocalImport(path string) bool { return path == "." || path == ".." || strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") } // ArchChar returns "?" and an error. // In earlier versions of Go, the returned string was used to derive // the compiler and linker tool names, the default object file suffix, // and the default linker output name. As of Go 1.5, those strings // no longer vary by architecture; they are compile, link, .o, and a.out, respectively. func ArchChar(goarch string) (string, error) { return "?", errors.New("architecture letter no longer used") }