From 6d3e013949885395f7f2e77a4cffd984b82d5546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=98=A5?= Date: Fri, 27 Mar 2026 19:30:39 +0900 Subject: [PATCH] perf: content-hash based cache keys for CI compatibility Replace mtime-based cache invalidation with content hashing: - artifact_cache: use SHA-256 of file content instead of ModTime - discovery_cache: use content hash for file matching, add WIRE_DISCOVERY_CACHE_DIR env var - Bump cache versions (artifact v4, discovery v4) This enables wire cache to work correctly in CI environments where file mtimes are not preserved across runs (e.g., S3 cache restore, git checkout). Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/loader/artifact_cache.go | 57 ++++++++++++++++-------------- internal/loader/discovery_cache.go | 43 ++++++++++++++-------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/internal/loader/artifact_cache.go b/internal/loader/artifact_cache.go index e920d5a..ff3f753 100644 --- a/internal/loader/artifact_cache.go +++ b/internal/loader/artifact_cache.go @@ -20,10 +20,10 @@ import ( "encoding/hex" "go/token" "go/types" + "io" "os" "path/filepath" "runtime" - "strconv" "golang.org/x/tools/go/gcexportdata" ) @@ -62,7 +62,7 @@ func loaderArtifactPath(env []string, meta *packageMeta, isLocal bool) (string, func loaderArtifactKey(meta *packageMeta, isLocal bool) (string, error) { sum := sha256.New() - sum.Write([]byte("wire-loader-artifact-v3\n")) + sum.Write([]byte("wire-loader-artifact-v4\n")) sum.Write([]byte(runtime.Version())) sum.Write([]byte{'\n'}) sum.Write([]byte(meta.ImportPath)) @@ -73,26 +73,15 @@ func loaderArtifactKey(meta *packageMeta, isLocal bool) (string, error) { sum.Write([]byte(meta.Export)) sum.Write([]byte{'\n'}) if meta.Export != "" { - info, err := os.Stat(meta.Export) + h, err := hashFileContent(meta.Export) if err != nil { return "", err } - sum.Write([]byte(strconv.FormatInt(info.Size(), 10))) - sum.Write([]byte{'\n'}) - sum.Write([]byte(strconv.FormatInt(info.ModTime().UnixNano(), 10))) + sum.Write([]byte(h)) sum.Write([]byte{'\n'}) } else { - for _, name := range metaFiles(meta) { - info, err := os.Stat(name) - if err != nil { - return "", err - } - sum.Write([]byte(name)) - sum.Write([]byte{'\n'}) - sum.Write([]byte(strconv.FormatInt(info.Size(), 10))) - sum.Write([]byte{'\n'}) - sum.Write([]byte(strconv.FormatInt(info.ModTime().UnixNano(), 10))) - sum.Write([]byte{'\n'}) + if err := hashMetaFiles(sum, metaFiles(meta)); err != nil { + return "", err } } if meta.Error != nil { @@ -101,19 +90,35 @@ func loaderArtifactKey(meta *packageMeta, isLocal bool) (string, error) { } return hex.EncodeToString(sum.Sum(nil)), nil } - for _, name := range metaFiles(meta) { - info, err := os.Stat(name) - if err != nil { - return "", err - } + if err := hashMetaFiles(sum, metaFiles(meta)); err != nil { + return "", err + } + return hex.EncodeToString(sum.Sum(nil)), nil +} + +// hashFileContent returns the hex-encoded SHA-256 of the file content. +func hashFileContent(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + h := sha256.Sum256(data) + return hex.EncodeToString(h[:]), nil +} + +// hashMetaFiles writes content-based hashes for each file into sum. +func hashMetaFiles(sum io.Writer, names []string) error { + for _, name := range names { sum.Write([]byte(name)) sum.Write([]byte{'\n'}) - sum.Write([]byte(strconv.FormatInt(info.Size(), 10))) - sum.Write([]byte{'\n'}) - sum.Write([]byte(strconv.FormatInt(info.ModTime().UnixNano(), 10))) + h, err := hashFileContent(name) + if err != nil { + return err + } + sum.Write([]byte(h)) sum.Write([]byte{'\n'}) } - return hex.EncodeToString(sum.Sum(nil)), nil + return nil } func readLoaderArtifact(path string, fset *token.FileSet, imports map[string]*types.Package, pkgPath string) (*types.Package, error) { diff --git a/internal/loader/discovery_cache.go b/internal/loader/discovery_cache.go index 1151853..6d59a22 100644 --- a/internal/loader/discovery_cache.go +++ b/internal/loader/discovery_cache.go @@ -28,10 +28,11 @@ type discoveryLocalPackage struct { } type discoveryFileMeta struct { - Path string - Size int64 - ModTime int64 - IsDir bool + Path string + Size int64 + ModTime int64 // deprecated: kept for gob compat, not used for matching + ContentHash string // sha256 of file content + IsDir bool } type discoveryDirMeta struct { @@ -44,7 +45,7 @@ type discoveryFileFingerprint struct { Hash string } -const discoveryCacheVersion = 3 +const discoveryCacheVersion = 4 func readDiscoveryCache(req goListRequest) (map[string]*packageMeta, bool) { entry, err := loadDiscoveryCacheEntry(req) @@ -121,10 +122,16 @@ func validateDiscoveryCacheEntry(entry *discoveryCacheEntry) bool { return true } +const discoveryCacheDirEnv = "WIRE_DISCOVERY_CACHE_DIR" + func discoveryCachePath(req goListRequest) (string, error) { - base, err := os.UserCacheDir() - if err != nil { - return "", err + dir := os.Getenv(discoveryCacheDirEnv) + if dir == "" { + base, err := os.UserCacheDir() + if err != nil { + return "", err + } + dir = filepath.Join(base, "wire", "discovery-cache") } sumReq := struct { Version int @@ -147,7 +154,7 @@ func discoveryCachePath(req goListRequest) (string, error) { if err != nil { return "", err } - return filepath.Join(base, "wire", "discovery-cache", key+".gob"), nil + return filepath.Join(dir, key+".gob"), nil } func loadDiscoveryCacheEntry(req goListRequest) (*discoveryCacheEntry, error) { @@ -188,11 +195,19 @@ func statDiscoveryFile(path string) (discoveryFileMeta, bool) { if err != nil { return discoveryFileMeta{}, false } + h := "" + if !info.IsDir() { + var err error + h, err = hashFileContent(path) + if err != nil { + return discoveryFileMeta{}, false + } + } return discoveryFileMeta{ - Path: canonicalLoaderPath(path), - Size: info.Size(), - ModTime: info.ModTime().UnixNano(), - IsDir: info.IsDir(), + Path: canonicalLoaderPath(path), + Size: info.Size(), + ContentHash: h, + IsDir: info.IsDir(), }, true } @@ -201,7 +216,7 @@ func matchesDiscoveryFile(fm discoveryFileMeta) bool { if !ok { return false } - return cur.Size == fm.Size && cur.ModTime == fm.ModTime && cur.IsDir == fm.IsDir + return cur.ContentHash == fm.ContentHash && cur.IsDir == fm.IsDir } func statDiscoveryDir(path string) (discoveryDirMeta, bool) {