// Copyright 2017 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 work import ( "bytes" "fmt" "os" "os/exec" "strings" "cmd/go/internal/base" "cmd/go/internal/cache" "cmd/go/internal/cfg" "cmd/go/internal/fsys" "cmd/go/internal/str" "cmd/internal/buildid" "cmd/internal/quoted" ) // Build IDs // // Go packages and binaries are stamped with build IDs that record both // the action ID, which is a hash of the inputs to the action that produced // the packages or binary, and the content ID, which is a hash of the action // output, namely the archive or binary itself. The hash is the same one // used by the build artifact cache (see cmd/go/internal/cache), but // truncated when stored in packages and binaries, as the full length is not // needed and is a bit unwieldy. The precise form is // // actionID/[.../]contentID // // where the actionID and contentID are prepared by buildid.HashToString below. // and are found by looking for the first or last slash. // Usually the buildID is simply actionID/contentID, but see below for an // exception. // // The build ID serves two primary purposes. // // 1. The action ID half allows installed packages and binaries to serve as // one-element cache entries. If we intend to build math.a with a given // set of inputs summarized in the action ID, and the installed math.a already // has that action ID, we can reuse the installed math.a instead of rebuilding it. // // 2. The content ID half allows the easy preparation of action IDs for steps // that consume a particular package or binary. The content hash of every // input file for a given action must be included in the action ID hash. // Storing the content ID in the build ID lets us read it from the file with // minimal I/O, instead of reading and hashing the entire file. // This is especially effective since packages and binaries are typically // the largest inputs to an action. // // Separating action ID from content ID is important for reproducible builds. // The compiler is compiled with itself. If an output were represented by its // own action ID (instead of content ID) when computing the action ID of // the next step in the build process, then the compiler could never have its // own input action ID as its output action ID (short of a miraculous hash collision). // Instead we use the content IDs to compute the next action ID, and because // the content IDs converge, so too do the action IDs and therefore the // build IDs and the overall compiler binary. See cmd/dist's cmdbootstrap // for the actual convergence sequence. // // The “one-element cache” purpose is a bit more complex for installed // binaries. For a binary, like cmd/gofmt, there are two steps: compile // cmd/gofmt/*.go into main.a, and then link main.a into the gofmt binary. // We do not install gofmt's main.a, only the gofmt binary. Being able to // decide that the gofmt binary is up-to-date means computing the action ID // for the final link of the gofmt binary and comparing it against the // already-installed gofmt binary. But computing the action ID for the link // means knowing the content ID of main.a, which we did not keep. // To sidestep this problem, each binary actually stores an expanded build ID: // // actionID(binary)/actionID(main.a)/contentID(main.a)/contentID(binary) // // (Note that this can be viewed equivalently as: // // actionID(binary)/buildID(main.a)/contentID(binary) // // Storing the buildID(main.a) in the middle lets the computations that care // about the prefix or suffix halves ignore the middle and preserves the // original build ID as a contiguous string.) // // During the build, when it's time to build main.a, the gofmt binary has the // information needed to decide whether the eventual link would produce // the same binary: if the action ID for main.a's inputs matches and then // the action ID for the link step matches when assuming the given main.a // content ID, then the binary as a whole is up-to-date and need not be rebuilt. // // This is all a bit complex and may be simplified once we can rely on the // main cache, but at least at the start we will be using the content-based // staleness determination without a cache beyond the usual installed // package and binary locations. const buildIDSeparator = "/" // actionID returns the action ID half of a build ID. func actionID(buildID string) string { i := strings.Index(buildID, buildIDSeparator) if i < 0 { return buildID } return buildID[:i] } // contentID returns the content ID half of a build ID. func contentID(buildID string) string { return buildID[strings.LastIndex(buildID, buildIDSeparator)+1:] } // toolID returns the unique ID to use for the current copy of the // named tool (asm, compile, cover, link). // // It is important that if the tool changes (for example a compiler bug is fixed // and the compiler reinstalled), toolID returns a different string, so that old // package archives look stale and are rebuilt (with the fixed compiler). // This suggests using a content hash of the tool binary, as stored in the build ID. // // Unfortunately, we can't just open the tool binary, because the tool might be // invoked via a wrapper program specified by -toolexec and we don't know // what the wrapper program does. In particular, we want "-toolexec toolstash" // to continue working: it does no good if "-toolexec toolstash" is executing a // stashed copy of the compiler but the go command is acting as if it will run // the standard copy of the compiler. The solution is to ask the tool binary to tell // us its own build ID using the "-V=full" flag now supported by all tools. // Then we know we're getting the build ID of the compiler that will actually run // during the build. (How does the compiler binary know its own content hash? // We store it there using updateBuildID after the standard link step.) // // A final twist is that we'd prefer to have reproducible builds for release toolchains. // It should be possible to cross-compile for Windows from either Linux or Mac // or Windows itself and produce the same binaries, bit for bit. If the tool ID, // which influences the action ID half of the build ID, is based on the content ID, // then the Linux compiler binary and Mac compiler binary will have different tool IDs // and therefore produce executables with different action IDs. // To avoid this problem, for releases we use the release version string instead // of the compiler binary's content hash. This assumes that all compilers built // on all different systems are semantically equivalent, which is of course only true // modulo bugs. (Producing the exact same executables also requires that the different // build setups agree on details like $GOROOT and file name paths, but at least the // tool IDs do not make it impossible.) func (b *Builder) toolID(name string) string { b.id.Lock() id := b.toolIDCache[name] b.id.Unlock() if id != "" { return id } path := base.Tool(name) desc := "go tool " + name // Special case: undocumented -vettool overrides usual vet, // for testing vet or supplying an alternative analysis tool. if name == "vet" && VetTool != "" { path = VetTool desc = VetTool } cmdline := str.StringList(cfg.BuildToolexec, path, "-V=full") cmd := exec.Command(cmdline[0], cmdline[1:]...) var stdout, stderr strings.Builder cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { if stderr.Len() > 0 { os.Stderr.WriteString(stderr.String()) } base.Fatalf("go: error obtaining buildID for %s: %v", desc, err) } line := stdout.String() f := strings.Fields(line) if len(f) < 3 || f[0] != name && path != VetTool || f[1] != "version" || f[2] == "devel" && !strings.HasPrefix(f[len(f)-1], "buildID=") { base.Fatalf("go: parsing buildID from %s -V=full: unexpected output:\n\t%s", desc, line) } if f[2] == "devel" { // On the development branch, use the content ID part of the build ID. id = contentID(f[len(f)-1]) } else { // For a release, the output is like: "compile version go1.9.1 X:framepointer". // Use the whole line. id = strings.TrimSpace(line) } b.id.Lock() b.toolIDCache[name] = id b.id.Unlock() return id } // gccToolID returns the unique ID to use for a tool that is invoked // by the GCC driver. This is used particularly for gccgo, but this can also // be used for gcc, g++, gfortran, etc.; those tools all use the GCC // driver under different names. The approach used here should also // work for sufficiently new versions of clang. Unlike toolID, the // name argument is the program to run. The language argument is the // type of input file as passed to the GCC driver's -x option. // // For these tools we have no -V=full option to dump the build ID, // but we can run the tool with -v -### to reliably get the compiler proper // and hash that. That will work in the presence of -toolexec. // // In order to get reproducible builds for released compilers, we // detect a released compiler by the absence of "experimental" in the // --version output, and in that case we just use the version string. // // gccToolID also returns the underlying executable for the compiler. // The caller assumes that stat of the exe can be used, combined with the id, // to detect changes in the underlying compiler. The returned exe can be empty, // which means to rely only on the id. func (b *Builder) gccToolID(name, language string) (id, exe string, err error) { key := name + "." + language b.id.Lock() id = b.toolIDCache[key] exe = b.toolIDCache[key+".exe"] b.id.Unlock() if id != "" { return id, exe, nil } // Invoke the driver with -### to see the subcommands and the // version strings. Use -x to set the language. Pretend to // compile an empty file on standard input. cmdline := str.StringList(cfg.BuildToolexec, name, "-###", "-x", language, "-c", "-") cmd := exec.Command(cmdline[0], cmdline[1:]...) // Force untranslated output so that we see the string "version". cmd.Env = append(os.Environ(), "LC_ALL=C") out, err := cmd.CombinedOutput() if err != nil { return "", "", fmt.Errorf("%s: %v; output: %q", name, err, out) } version := "" lines := strings.Split(string(out), "\n") for _, line := range lines { fields := strings.Fields(line) for i, field := range fields { if strings.HasSuffix(field, ":") { // Avoid parsing fields of lines like "Configured with: …", which may // contain arbitrary substrings. break } if field == "version" && i < len(fields)-1 { // Check that the next field is plausibly a version number. // We require only that it begins with an ASCII digit, // since we don't know what version numbering schemes a given // C compiler may use. (Clang and GCC mostly seem to follow the scheme X.Y.Z, // but in https://go.dev/issue/64619 we saw "8.3 [DragonFly]", and who knows // what other C compilers like "zig cc" might report?) next := fields[i+1] if len(next) > 0 && next[0] >= '0' && next[0] <= '9' { version = line break } } } if version != "" { break } } if version == "" { return "", "", fmt.Errorf("%s: can not find version number in %q", name, out) } if !strings.Contains(version, "experimental") { // This is a release. Use this line as the tool ID. id = version } else { // This is a development version. The first line with // a leading space is the compiler proper. compiler := "" for _, line := range lines { if strings.HasPrefix(line, " ") && !strings.HasPrefix(line, " (in-process)") { compiler = line break } } if compiler == "" { return "", "", fmt.Errorf("%s: can not find compilation command in %q", name, out) } fields, _ := quoted.Split(compiler) if len(fields) == 0 { return "", "", fmt.Errorf("%s: compilation command confusion %q", name, out) } exe = fields[0] if !strings.ContainsAny(exe, `/\`) { if lp, err := cfg.LookPath(exe); err == nil { exe = lp } } id, err = buildid.ReadFile(exe) if err != nil { return "", "", err } // If we can't find a build ID, use a hash. if id == "" { id = b.fileHash(exe) } } b.id.Lock() b.toolIDCache[key] = id b.toolIDCache[key+".exe"] = exe b.id.Unlock() return id, exe, nil } // Check if assembler used by gccgo is GNU as. func assemblerIsGas() bool { cmd := exec.Command(BuildToolchain.compiler(), "-print-prog-name=as") assembler, err := cmd.Output() if err == nil { cmd := exec.Command(strings.TrimSpace(string(assembler)), "--version") out, err := cmd.Output() return err == nil && strings.Contains(string(out), "GNU") } else { return false } } // gccgoBuildIDFile creates an assembler file that records the // action's build ID in an SHF_EXCLUDE section for ELF files or // in a CSECT in XCOFF files. func (b *Builder) gccgoBuildIDFile(a *Action) (string, error) { sfile := a.Objdir + "_buildid.s" var buf bytes.Buffer if cfg.Goos == "aix" { fmt.Fprintf(&buf, "\t.csect .go.buildid[XO]\n") } else if (cfg.Goos != "solaris" && cfg.Goos != "illumos") || assemblerIsGas() { fmt.Fprintf(&buf, "\t"+`.section .go.buildid,"e"`+"\n") } else if cfg.Goarch == "sparc" || cfg.Goarch == "sparc64" { fmt.Fprintf(&buf, "\t"+`.section ".go.buildid",#exclude`+"\n") } else { // cfg.Goarch == "386" || cfg.Goarch == "amd64" fmt.Fprintf(&buf, "\t"+`.section .go.buildid,#exclude`+"\n") } fmt.Fprintf(&buf, "\t.byte ") for i := 0; i < len(a.buildID); i++ { if i > 0 { if i%8 == 0 { fmt.Fprintf(&buf, "\n\t.byte ") } else { fmt.Fprintf(&buf, ",") } } fmt.Fprintf(&buf, "%#02x", a.buildID[i]) } fmt.Fprintf(&buf, "\n") if cfg.Goos != "solaris" && cfg.Goos != "illumos" && cfg.Goos != "aix" { secType := "@progbits" if cfg.Goarch == "arm" { secType = "%progbits" } fmt.Fprintf(&buf, "\t"+`.section .note.GNU-stack,"",%s`+"\n", secType) fmt.Fprintf(&buf, "\t"+`.section .note.GNU-split-stack,"",%s`+"\n", secType) } if err := b.Shell(a).writeFile(sfile, buf.Bytes()); err != nil { return "", err } return sfile, nil } // buildID returns the build ID found in the given file. // If no build ID is found, buildID returns the content hash of the file. func (b *Builder) buildID(file string) string { b.id.Lock() id := b.buildIDCache[file] b.id.Unlock() if id != "" { return id } id, err := buildid.ReadFile(file) if err != nil { id = b.fileHash(file) } b.id.Lock() b.buildIDCache[file] = id b.id.Unlock() return id } // fileHash returns the content hash of the named file. func (b *Builder) fileHash(file string) string { file, _ = fsys.OverlayPath(file) sum, err := cache.FileHash(file) if err != nil { return "" } return buildid.HashToString(sum) } // useCache tries to satisfy the action a, which has action ID actionHash, // by using a cached result from an earlier build. At the moment, the only // cached result is the installed package or binary at target. // If useCache decides that the cache can be used, it sets a.buildID // and a.built for use by parent actions and then returns true. // Otherwise it sets a.buildID to a temporary build ID for use in the build // and returns false. When useCache returns false the expectation is that // the caller will build the target and then call updateBuildID to finish the // build ID computation. // When useCache returns false, it may have initiated buffering of output // during a's work. The caller should defer b.flushOutput(a), to make sure // that flushOutput is eventually called regardless of whether the action // succeeds. The flushOutput call must happen after updateBuildID. func (b *Builder) useCache(a *Action, actionHash cache.ActionID, target string, printOutput bool) bool { // The second half of the build ID here is a placeholder for the content hash. // It's important that the overall buildID be unlikely verging on impossible // to appear in the output by chance, but that should be taken care of by // the actionID half; if it also appeared in the input that would be like an // engineered 120-bit partial SHA256 collision. a.actionID = actionHash actionID := buildid.HashToString(actionHash) if a.json != nil { a.json.ActionID = actionID } contentID := actionID // temporary placeholder, likely unique a.buildID = actionID + buildIDSeparator + contentID // Executable binaries also record the main build ID in the middle. // See "Build IDs" comment above. if a.Mode == "link" { mainpkg := a.Deps[0] a.buildID = actionID + buildIDSeparator + mainpkg.buildID + buildIDSeparator + contentID } // If user requested -a, we force a rebuild, so don't use the cache. if cfg.BuildA { if p := a.Package; p != nil && !p.Stale { p.Stale = true p.StaleReason = "build -a flag in use" } // Begin saving output for later writing to cache. a.output = []byte{} return false } c := cache.Default() if target != "" { buildID, _ := buildid.ReadFile(target) if strings.HasPrefix(buildID, actionID+buildIDSeparator) { a.buildID = buildID if a.json != nil { a.json.BuildID = a.buildID } a.built = target // Poison a.Target to catch uses later in the build. a.Target = "DO NOT USE - " + a.Mode return true } // Special case for building a main package: if the only thing we // want the package for is to link a binary, and the binary is // already up-to-date, then to avoid a rebuild, report the package // as up-to-date as well. See "Build IDs" comment above. // TODO(rsc): Rewrite this code to use a TryCache func on the link action. if !b.NeedExport && a.Mode == "build" && len(a.triggers) == 1 && a.triggers[0].Mode == "link" { if id := strings.Split(buildID, buildIDSeparator); len(id) == 4 && id[1] == actionID { // Temporarily assume a.buildID is the package build ID // stored in the installed binary, and see if that makes // the upcoming link action ID a match. If so, report that // we built the package, safe in the knowledge that the // link step will not ask us for the actual package file. // Note that (*Builder).LinkAction arranged that all of // a.triggers[0]'s dependencies other than a are also // dependencies of a, so that we can be sure that, // other than a.buildID, b.linkActionID is only accessing // build IDs of completed actions. oldBuildID := a.buildID a.buildID = id[1] + buildIDSeparator + id[2] linkID := buildid.HashToString(b.linkActionID(a.triggers[0])) if id[0] == linkID { // Best effort attempt to display output from the compile and link steps. // If it doesn't work, it doesn't work: reusing the cached binary is more // important than reprinting diagnostic information. if printOutput { showStdout(b, c, a, "stdout") // compile output showStdout(b, c, a, "link-stdout") // link output } // Poison a.Target to catch uses later in the build. a.Target = "DO NOT USE - main build pseudo-cache Target" a.built = "DO NOT USE - main build pseudo-cache built" if a.json != nil { a.json.BuildID = a.buildID } return true } // Otherwise restore old build ID for main build. a.buildID = oldBuildID } } } // Special case for linking a test binary: if the only thing we // want the binary for is to run the test, and the test result is cached, // then to avoid the link step, report the link as up-to-date. // We avoid the nested build ID problem in the previous special case // by recording the test results in the cache under the action ID half. if len(a.triggers) == 1 && a.triggers[0].TryCache != nil && a.triggers[0].TryCache(b, a.triggers[0]) { // Best effort attempt to display output from the compile and link steps. // If it doesn't work, it doesn't work: reusing the test result is more // important than reprinting diagnostic information. if printOutput { showStdout(b, c, a.Deps[0], "stdout") // compile output showStdout(b, c, a.Deps[0], "link-stdout") // link output } // Poison a.Target to catch uses later in the build. a.Target = "DO NOT USE - pseudo-cache Target" a.built = "DO NOT USE - pseudo-cache built" return true } // Check to see if the action output is cached. if file, _, err := cache.GetFile(c, actionHash); err == nil { if buildID, err := buildid.ReadFile(file); err == nil { if printOutput { showStdout(b, c, a, "stdout") } a.built = file a.Target = "DO NOT USE - using cache" a.buildID = buildID if a.json != nil { a.json.BuildID = a.buildID } if p := a.Package; p != nil && target != "" { p.Stale = true // Clearer than explaining that something else is stale. p.StaleReason = "not installed but available in build cache" } return true } } // If we've reached this point, we can't use the cache for the action. if p := a.Package; p != nil && !p.Stale { p.Stale = true p.StaleReason = "build ID mismatch" if b.IsCmdList { // Since we may end up printing StaleReason, include more detail. for _, p1 := range p.Internal.Imports { if p1.Stale && p1.StaleReason != "" { if strings.HasPrefix(p1.StaleReason, "stale dependency: ") { p.StaleReason = p1.StaleReason break } if strings.HasPrefix(p.StaleReason, "build ID mismatch") { p.StaleReason = "stale dependency: " + p1.ImportPath } } } } } // Begin saving output for later writing to cache. a.output = []byte{} return false } func showStdout(b *Builder, c cache.Cache, a *Action, key string) error { actionID := a.actionID stdout, stdoutEntry, err := cache.GetBytes(c, cache.Subkey(actionID, key)) if err != nil { return err } if len(stdout) > 0 { sh := b.Shell(a) if cfg.BuildX || cfg.BuildN { sh.ShowCmd("", "%s # internal", joinUnambiguously(str.StringList("cat", c.OutputFile(stdoutEntry.OutputID)))) } if !cfg.BuildN { sh.Print(string(stdout)) } } return nil } // flushOutput flushes the output being queued in a. func (b *Builder) flushOutput(a *Action) { b.Shell(a).Print(string(a.output)) a.output = nil } // updateBuildID updates the build ID in the target written by action a. // It requires that useCache was called for action a and returned false, // and that the build was then carried out and given the temporary // a.buildID to record as the build ID in the resulting package or binary. // updateBuildID computes the final content ID and updates the build IDs // in the binary. // // Keep in sync with src/cmd/buildid/buildid.go func (b *Builder) updateBuildID(a *Action, target string, rewrite bool) error { sh := b.Shell(a) if cfg.BuildX || cfg.BuildN { if rewrite { sh.ShowCmd("", "%s # internal", joinUnambiguously(str.StringList(base.Tool("buildid"), "-w", target))) } if cfg.BuildN { return nil } } c := cache.Default() // Cache output from compile/link, even if we don't do the rest. switch a.Mode { case "build": cache.PutBytes(c, cache.Subkey(a.actionID, "stdout"), a.output) case "link": // Even though we don't cache the binary, cache the linker text output. // We might notice that an installed binary is up-to-date but still // want to pretend to have run the linker. // Store it under the main package's action ID // to make it easier to find when that's all we have. for _, a1 := range a.Deps { if p1 := a1.Package; p1 != nil && p1.Name == "main" { cache.PutBytes(c, cache.Subkey(a1.actionID, "link-stdout"), a.output) break } } } // Find occurrences of old ID and compute new content-based ID. r, err := os.Open(target) if err != nil { return err } matches, hash, err := buildid.FindAndHash(r, a.buildID, 0) r.Close() if err != nil { return err } newID := a.buildID[:strings.LastIndex(a.buildID, buildIDSeparator)] + buildIDSeparator + buildid.HashToString(hash) if len(newID) != len(a.buildID) { return fmt.Errorf("internal error: build ID length mismatch %q vs %q", a.buildID, newID) } // Replace with new content-based ID. a.buildID = newID if a.json != nil { a.json.BuildID = a.buildID } if len(matches) == 0 { // Assume the user specified -buildid= to override what we were going to choose. return nil } if rewrite { w, err := os.OpenFile(target, os.O_RDWR, 0) if err != nil { return err } err = buildid.Rewrite(w, matches, newID) if err != nil { w.Close() return err } if err := w.Close(); err != nil { return err } } // Cache package builds, but not binaries (link steps). // The expectation is that binaries are not reused // nearly as often as individual packages, and they're // much larger, so the cache-footprint-to-utility ratio // of binaries is much lower for binaries. // Not caching the link step also makes sure that repeated "go run" at least // always rerun the linker, so that they don't get too fast. // (We don't want people thinking go is a scripting language.) // Note also that if we start caching binaries, then we will // copy the binaries out of the cache to run them, and then // that will mean the go process is itself writing a binary // and then executing it, so we will need to defend against // ETXTBSY problems as discussed in exec.go and golang.org/issue/22220. if a.Mode == "build" { r, err := os.Open(target) if err == nil { if a.output == nil { panic("internal error: a.output not set") } outputID, _, err := c.Put(a.actionID, r) r.Close() if err == nil && cfg.BuildX { sh.ShowCmd("", "%s # internal", joinUnambiguously(str.StringList("cp", target, c.OutputFile(outputID)))) } if b.NeedExport { if err != nil { return err } a.Package.Export = c.OutputFile(outputID) a.Package.BuildID = a.buildID } } } return nil }