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  }