github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/build/cache/cache.go (about) 1 // Package cache solves one of the hardest computer science problems in 2 // application to GopherJS compiler outputs. 3 package cache 4 5 import ( 6 "crypto/sha256" 7 "fmt" 8 "go/build" 9 "os" 10 "path" 11 "path/filepath" 12 13 "github.com/gopherjs/gopherjs/compiler" 14 log "github.com/sirupsen/logrus" 15 ) 16 17 // cacheRoot is the base path for GopherJS's own build cache. 18 // 19 // It serves a similar function to the Go build cache, but is a lot more 20 // simplistic and therefore not compatible with Go. We use this cache directory 21 // to store build artifacts for packages loaded from a module, for which PkgObj 22 // provided by go/build points inside the module source tree, which can cause 23 // inconvenience with version control, etc. 24 var cacheRoot = func() string { 25 path, err := os.UserCacheDir() 26 if err == nil { 27 return filepath.Join(path, "gopherjs", "build_cache") 28 } 29 30 return filepath.Join(build.Default.GOPATH, "pkg", "gopherjs_build_cache") 31 }() 32 33 // cachedPath returns a location inside the build cache for a given set of key 34 // strings. The set of keys must uniquely identify cacheable object. Prefer 35 // using more specific functions to ensure key consistency. 36 func cachedPath(keys ...string) string { 37 key := path.Join(keys...) 38 if key == "" { 39 panic("CachedPath() must not be used with an empty string") 40 } 41 sum := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) 42 return filepath.Join(cacheRoot, sum[0:2], sum) 43 } 44 45 // Clear the cache. This will remove *all* cached artifacts from *all* build 46 // configurations. 47 func Clear() error { 48 return os.RemoveAll(cacheRoot) 49 } 50 51 // BuildCache manages build artifacts that are cached for incremental builds. 52 // 53 // Cache is designed to be non-durable: any store and load errors are swallowed 54 // and simply lead to a cache miss. The caller must be able to handle cache 55 // misses. Nil pointer to BuildCache is valid and simply disables caching. 56 // 57 // BuildCache struct fields represent build parameters which change invalidates 58 // the cache. For example, any artifacts that were cached for a minified build 59 // must not be reused for a non-minified build. GopherJS version change also 60 // invalidates the cache. It is callers responsibility to ensure that artifacts 61 // passed the StoreArchive function were generated with the same build 62 // parameters as the cache is configured. 63 // 64 // There is no upper limit for the total cache size. It can be cleared 65 // programmatically via the Clear() function, or the user can just delete the 66 // directory if it grows too big. 67 // 68 // TODO(nevkontakte): changes in the input sources or dependencies doesn't 69 // currently invalidate the cache. This is handled at the higher level by 70 // checking cached archive timestamp against loaded package modification time. 71 // 72 // TODO(nevkontakte): this cache could benefit from checksum integrity checks. 73 type BuildCache struct { 74 GOOS string 75 GOARCH string 76 GOROOT string 77 GOPATH string 78 BuildTags []string 79 Minify bool 80 // When building for tests, import path of the package being tested. The 81 // package under test is built with *_test.go sources included, and since it 82 // may be imported by other packages in the binary we can't reuse the "normal" 83 // cache. 84 TestedPackage string 85 } 86 87 func (bc BuildCache) String() string { 88 return fmt.Sprintf("%#v", bc) 89 } 90 91 // StoreArchive compiled archive in the cache. Any error inside this method 92 // will cause the cache not to be persisted. 93 func (bc *BuildCache) StoreArchive(a *compiler.Archive) { 94 if bc == nil { 95 return // Caching is disabled. 96 } 97 path := cachedPath(bc.archiveKey(a.ImportPath)) 98 if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { 99 log.Warningf("Failed to create build cache directory: %v", err) 100 return 101 } 102 // Write the archive in a temporary file first to avoid concurrency errors. 103 f, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)) 104 if err != nil { 105 log.Warningf("Failed to temporary build cache file: %v", err) 106 return 107 } 108 defer f.Close() 109 if err := compiler.WriteArchive(a, f); err != nil { 110 log.Warningf("Failed to write build cache archive %q: %v", a, err) 111 // Make sure we don't leave a half-written archive behind. 112 os.Remove(f.Name()) 113 return 114 } 115 f.Close() 116 // Rename fully written file into its permanent name. 117 if err := os.Rename(f.Name(), path); err != nil { 118 log.Warningf("Failed to rename build cache archive to %q: %v", path, err) 119 } 120 log.Infof("Successfully stored build archive %q as %q.", a, path) 121 } 122 123 // LoadArchive returns a previously cached archive of the given package or nil 124 // if it wasn't previously stored. 125 // 126 // The returned archive would have been built with the same configuration as 127 // the build cache was. 128 func (bc *BuildCache) LoadArchive(importPath string) *compiler.Archive { 129 if bc == nil { 130 return nil // Caching is disabled. 131 } 132 path := cachedPath(bc.archiveKey(importPath)) 133 f, err := os.Open(path) 134 if err != nil { 135 if os.IsNotExist(err) { 136 log.Infof("No cached package archive for %q.", importPath) 137 } else { 138 log.Warningf("Failed to open cached package archive for %q: %v", importPath, err) 139 } 140 return nil // Cache miss. 141 } 142 defer f.Close() 143 a, err := compiler.ReadArchive(importPath, f) 144 if err != nil { 145 log.Warningf("Failed to read cached package archive for %q: %v", importPath, err) 146 return nil // Invalid/corrupted archive, cache miss. 147 } 148 log.Infof("Found cached package archive for %q, built at %v.", importPath, a.BuildTime) 149 return a 150 } 151 152 // commonKey returns a part of the cache key common for all artifacts generated 153 // under a given BuildCache configuration. 154 func (bc *BuildCache) commonKey() string { 155 return fmt.Sprintf("%#v + %v", *bc, compiler.Version) 156 } 157 158 // archiveKey returns a full cache key for a package's compiled archive. 159 func (bc *BuildCache) archiveKey(importPath string) string { 160 return path.Join("archive", bc.commonKey(), importPath) 161 }