Skip to content

Commit 3ff868f

Browse files
committed
cmd/go: cache executables built for go run
This change implements executable caching. It always caches the outputs of link steps used by go run. To do so we need to make a few changes: The first is that we want to cache binaries in a slightly different location than we cache other outputs. The reason for doing so is so that the name of the file could be the name of the program built. Instead of placing the files in $GOCACHE/<two digit prefix>/<hash>-d, we place them in $GOCACHE/<two digit prefix>/<hash>-d/<executable name>. This is done by adding a new function called PutExecutable that works differently from Put in two ways: first, it causes the binaries to written 0777 rather than 0666 so they can be executed. Second, PutExecutable also writes its outputs to a new location in a directory with the output id based name, with the file named based on p.Internal.ExeName or otherwise the base name of the package (plus the .exe suffix on Windows). The next changes are for writing and reading binaries from the cache. In cmd/go/internal/work.updateBuildID, which updates build ids to the content based id and then writes outputs to the cache, we first make the change to always write the content based id into a binary. This is because we won't be throwing the binaries away after running them. Then, if the action is a link action, and we enabled excutable caching for the action, we write the output to the binary cache. When reading binaries, in the useCache function, we switch to using the binary cache, and we also print the cached link outputs (which are stored using the build action's action id). Finally, we change go run to execute the built output from the cache. The support for caching tools defined in a module that are run by go tool will also use this functionality. Fixes #69290 For #48429 Change-Id: Ic5f1d3b29d8e9786fd0d564460e3a5f53e951f41 Reviewed-on: https://go-review.googlesource.com/c/go/+/613095 Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 28f4e14 commit 3ff868f

File tree

10 files changed

+170
-78
lines changed

10 files changed

+170
-78
lines changed

src/cmd/go/internal/base/base.go

+26-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"slices"
1818
"strings"
1919
"sync"
20+
"time"
2021

2122
"cmd/go/internal/cfg"
2223
"cmd/go/internal/str"
@@ -206,18 +207,34 @@ func Run(cmdargs ...any) {
206207
}
207208
}
208209

209-
// RunStdin is like run but connects Stdin.
210+
// RunStdin is like run but connects Stdin. It retries if it encounters an ETXTBSY.
210211
func RunStdin(cmdline []string) {
211-
cmd := exec.Command(cmdline[0], cmdline[1:]...)
212-
cmd.Stdin = os.Stdin
213-
cmd.Stdout = os.Stdout
214-
cmd.Stderr = os.Stderr
215212
env := slices.Clip(cfg.OrigEnv)
216213
env = AppendPATH(env)
217-
cmd.Env = env
218-
StartSigHandlers()
219-
if err := cmd.Run(); err != nil {
220-
Errorf("%v", err)
214+
for try := range 3 {
215+
cmd := exec.Command(cmdline[0], cmdline[1:]...)
216+
cmd.Stdin = os.Stdin
217+
cmd.Stdout = os.Stdout
218+
cmd.Stderr = os.Stderr
219+
cmd.Env = env
220+
StartSigHandlers()
221+
err := cmd.Run()
222+
if err == nil {
223+
break // success
224+
}
225+
226+
if !IsETXTBSY(err) {
227+
Errorf("%v", err)
228+
break // failure
229+
}
230+
231+
// The error was an ETXTBSY. Sleep and try again. It's possible that
232+
// another go command instance was racing against us to write the executable
233+
// to the executable cache. In that case it may still have the file open, and
234+
// we may get an ETXTBSY. That should resolve once that process closes the file
235+
// so attempt a couple more times. See the discussion in #22220 and also
236+
// (*runTestActor).Act in cmd/go/internal/test, which does something similar.
237+
time.Sleep(100 * time.Millisecond << uint(try))
221238
}
222239
}
223240

src/cmd/go/internal/test/test_nonunix.go renamed to src/cmd/go/internal/base/error_notunix.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
//go:build !unix
66

7-
package test
7+
package base
88

9-
func isETXTBSY(err error) bool {
9+
func IsETXTBSY(err error) bool {
1010
// syscall.ETXTBSY is only meaningful on Unix platforms.
1111
return false
1212
}

src/cmd/go/internal/test/test_unix.go renamed to src/cmd/go/internal/base/error_unix.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
//go:build unix
66

7-
package test
7+
package base
88

99
import (
1010
"errors"
1111
"syscall"
1212
)
1313

14-
func isETXTBSY(err error) bool {
14+
func IsETXTBSY(err error) bool {
1515
return errors.Is(err, syscall.ETXTBSY)
1616
}

src/cmd/go/internal/cache/cache.go

+80-14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"strings"
2121
"time"
2222

23+
"cmd/go/internal/base"
2324
"cmd/go/internal/lockedfile"
2425
"cmd/go/internal/mmap"
2526
)
@@ -101,7 +102,7 @@ func Open(dir string) (*DiskCache, error) {
101102
}
102103
for i := 0; i < 256; i++ {
103104
name := filepath.Join(dir, fmt.Sprintf("%02x", i))
104-
if err := os.MkdirAll(name, 0777); err != nil {
105+
if err := os.MkdirAll(name, 0o777); err != nil {
105106
return nil, err
106107
}
107108
}
@@ -254,7 +255,7 @@ func (c *DiskCache) get(id ActionID) (Entry, error) {
254255
return missing(errors.New("negative timestamp"))
255256
}
256257

257-
c.used(c.fileName(id, "a"))
258+
c.markUsed(c.fileName(id, "a"))
258259

259260
return Entry{buf, size, time.Unix(0, tm)}, nil
260261
}
@@ -313,7 +314,17 @@ func GetMmap(c Cache, id ActionID) ([]byte, Entry, error) {
313314
// OutputFile returns the name of the cache file storing output with the given OutputID.
314315
func (c *DiskCache) OutputFile(out OutputID) string {
315316
file := c.fileName(out, "d")
316-
c.used(file)
317+
isExecutable := c.markUsed(file)
318+
if isExecutable {
319+
entries, err := os.ReadDir(file)
320+
if err != nil {
321+
return fmt.Sprintf("DO NOT USE - missing binary cache entry: %v", err)
322+
}
323+
if len(entries) != 1 {
324+
return "DO NOT USE - invalid binary cache entry"
325+
}
326+
return filepath.Join(file, entries[0].Name())
327+
}
317328
return file
318329
}
319330

@@ -335,7 +346,7 @@ const (
335346
trimLimit = 5 * 24 * time.Hour
336347
)
337348

338-
// used makes a best-effort attempt to update mtime on file,
349+
// markUsed makes a best-effort attempt to update mtime on file,
339350
// so that mtime reflects cache access time.
340351
//
341352
// Because the reflection only needs to be approximate,
@@ -344,12 +355,15 @@ const (
344355
// mtime is more than an hour old. This heuristic eliminates
345356
// nearly all of the mtime updates that would otherwise happen,
346357
// while still keeping the mtimes useful for cache trimming.
347-
func (c *DiskCache) used(file string) {
358+
//
359+
// markUsed reports whether the file is a directory (an executable cache entry).
360+
func (c *DiskCache) markUsed(file string) (isExecutable bool) {
348361
info, err := os.Stat(file)
349362
if err == nil && c.now().Sub(info.ModTime()) < mtimeInterval {
350-
return
363+
return info.IsDir()
351364
}
352365
os.Chtimes(file, c.now(), c.now())
366+
return info.IsDir()
353367
}
354368

355369
func (c *DiskCache) Close() error { return c.Trim() }
@@ -387,7 +401,7 @@ func (c *DiskCache) Trim() error {
387401
// cache will appear older than it is, and we'll trim it again next time.
388402
var b bytes.Buffer
389403
fmt.Fprintf(&b, "%d", now.Unix())
390-
if err := lockedfile.Write(filepath.Join(c.dir, "trim.txt"), &b, 0666); err != nil {
404+
if err := lockedfile.Write(filepath.Join(c.dir, "trim.txt"), &b, 0o666); err != nil {
391405
return err
392406
}
393407

@@ -416,6 +430,10 @@ func (c *DiskCache) trimSubdir(subdir string, cutoff time.Time) {
416430
entry := filepath.Join(subdir, name)
417431
info, err := os.Stat(entry)
418432
if err == nil && info.ModTime().Before(cutoff) {
433+
if info.IsDir() { // executable cache entry
434+
os.RemoveAll(entry)
435+
continue
436+
}
419437
os.Remove(entry)
420438
}
421439
}
@@ -448,7 +466,7 @@ func (c *DiskCache) putIndexEntry(id ActionID, out OutputID, size int64, allowVe
448466

449467
// Copy file to cache directory.
450468
mode := os.O_WRONLY | os.O_CREATE
451-
f, err := os.OpenFile(file, mode, 0666)
469+
f, err := os.OpenFile(file, mode, 0o666)
452470
if err != nil {
453471
return err
454472
}
@@ -491,7 +509,21 @@ func (c *DiskCache) Put(id ActionID, file io.ReadSeeker) (OutputID, int64, error
491509
if isNoVerify {
492510
file = wrapper.ReadSeeker
493511
}
494-
return c.put(id, file, !isNoVerify)
512+
return c.put(id, "", file, !isNoVerify)
513+
}
514+
515+
// PutExecutable is used to store the output as the output for the action ID into a
516+
// file with the given base name, with the executable mode bit set.
517+
// It may read file twice. The content of file must not change between the two passes.
518+
func (c *DiskCache) PutExecutable(id ActionID, name string, file io.ReadSeeker) (OutputID, int64, error) {
519+
if name == "" {
520+
panic("PutExecutable called without a name")
521+
}
522+
wrapper, isNoVerify := file.(noVerifyReadSeeker)
523+
if isNoVerify {
524+
file = wrapper.ReadSeeker
525+
}
526+
return c.put(id, name, file, !isNoVerify)
495527
}
496528

497529
// PutNoVerify is like Put but disables the verify check
@@ -502,7 +534,7 @@ func PutNoVerify(c Cache, id ActionID, file io.ReadSeeker) (OutputID, int64, err
502534
return c.Put(id, noVerifyReadSeeker{file})
503535
}
504536

505-
func (c *DiskCache) put(id ActionID, file io.ReadSeeker, allowVerify bool) (OutputID, int64, error) {
537+
func (c *DiskCache) put(id ActionID, executableName string, file io.ReadSeeker, allowVerify bool) (OutputID, int64, error) {
506538
// Compute output ID.
507539
h := sha256.New()
508540
if _, err := file.Seek(0, 0); err != nil {
@@ -516,7 +548,11 @@ func (c *DiskCache) put(id ActionID, file io.ReadSeeker, allowVerify bool) (Outp
516548
h.Sum(out[:0])
517549

518550
// Copy to cached output file (if not already present).
519-
if err := c.copyFile(file, out, size); err != nil {
551+
fileMode := fs.FileMode(0o666)
552+
if executableName != "" {
553+
fileMode = 0o777
554+
}
555+
if err := c.copyFile(file, executableName, out, size, fileMode); err != nil {
520556
return out, size, err
521557
}
522558

@@ -532,9 +568,33 @@ func PutBytes(c Cache, id ActionID, data []byte) error {
532568

533569
// copyFile copies file into the cache, expecting it to have the given
534570
// output ID and size, if that file is not present already.
535-
func (c *DiskCache) copyFile(file io.ReadSeeker, out OutputID, size int64) error {
536-
name := c.fileName(out, "d")
571+
func (c *DiskCache) copyFile(file io.ReadSeeker, executableName string, out OutputID, size int64, perm os.FileMode) error {
572+
name := c.fileName(out, "d") // TODO(matloob): use a different suffix for the executable cache?
537573
info, err := os.Stat(name)
574+
if executableName != "" {
575+
// This is an executable file. The file at name won't hold the output itself, but will
576+
// be a directory that holds the output, named according to executableName. Check to see
577+
// if the directory already exists, and if it does not, create it. Then reset name
578+
// to the name we want the output written to.
579+
if err != nil {
580+
if !os.IsNotExist(err) {
581+
return err
582+
}
583+
if err := os.Mkdir(name, 0o777); err != nil {
584+
return err
585+
}
586+
if info, err = os.Stat(name); err != nil {
587+
return err
588+
}
589+
}
590+
if !info.IsDir() {
591+
return errors.New("internal error: invalid binary cache entry: not a directory")
592+
}
593+
594+
// directory exists. now set name to the inner file
595+
name = filepath.Join(name, executableName)
596+
info, err = os.Stat(name)
597+
}
538598
if err == nil && info.Size() == size {
539599
// Check hash.
540600
if f, err := os.Open(name); err == nil {
@@ -555,8 +615,14 @@ func (c *DiskCache) copyFile(file io.ReadSeeker, out OutputID, size int64) error
555615
if err == nil && info.Size() > size { // shouldn't happen but fix in case
556616
mode |= os.O_TRUNC
557617
}
558-
f, err := os.OpenFile(name, mode, 0666)
618+
f, err := os.OpenFile(name, mode, perm)
559619
if err != nil {
620+
if base.IsETXTBSY(err) {
621+
// This file is being used by an executable. It must have
622+
// already been written by another go process and then run.
623+
// return without an error.
624+
return nil
625+
}
560626
return err
561627
}
562628
defer f.Close()

src/cmd/go/internal/cache/default.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func initDefaultCache() Cache {
4141
}
4242
base.Fatalf("build cache is disabled by GOCACHE=off, but required as of Go 1.12")
4343
}
44-
if err := os.MkdirAll(dir, 0777); err != nil {
44+
if err := os.MkdirAll(dir, 0o777); err != nil {
4545
base.Fatalf("failed to initialize build cache at %s: %s\n", dir, err)
4646
}
4747
if _, err := os.Stat(filepath.Join(dir, "README")); err != nil {

src/cmd/go/internal/run/run.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ func runRun(ctx context.Context, cmd *base.Command, args []string) {
170170
}
171171

172172
a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
173+
a1.CacheExecutable = true
173174
a := &work.Action{Mode: "go run", Actor: work.ActorFunc(buildRunProgram), Args: cmdArgs, Deps: []*work.Action{a1}}
174175
b.Do(ctx, a)
175176
}
@@ -199,7 +200,7 @@ func shouldUseOutsideModuleMode(args []string) bool {
199200
// buildRunProgram is the action for running a binary that has already
200201
// been compiled. We ignore exit status.
201202
func buildRunProgram(b *work.Builder, ctx context.Context, a *work.Action) error {
202-
cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].Target, a.Args)
203+
cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].BuiltTarget(), a.Args)
203204
if cfg.BuildN || cfg.BuildX {
204205
b.Shell(a).ShowCmd("", "%s", strings.Join(cmdline, " "))
205206
if cfg.BuildN {

src/cmd/go/internal/test/test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1624,7 +1624,7 @@ func (r *runTestActor) Act(b *work.Builder, ctx context.Context, a *work.Action)
16241624
t0 = time.Now()
16251625
err = cmd.Run()
16261626

1627-
if !isETXTBSY(err) {
1627+
if !base.IsETXTBSY(err) {
16281628
// We didn't hit the race in #22315, so there is no reason to retry the
16291629
// command.
16301630
break

src/cmd/go/internal/work/action.go

+2
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ type Action struct {
9292

9393
TryCache func(*Builder, *Action) bool // callback for cache bypass
9494

95+
CacheExecutable bool // Whether to cache executables produced by link steps
96+
9597
// Generated files, directories.
9698
Objdir string // directory for intermediate objects
9799
Target string // goal of the action: the created package or executable

0 commit comments

Comments
 (0)