mirror of
https://github.com/imjasonh/terraform-playground
synced 2026-07-12 01:19:39 +00:00
- sbom: normalize component names (PEP 503) for canonical pkg:pypi PURLs and stable ordering - wheel: reject any member with a '..' path segment (catches escapes that resolve back under the prefix but out of site-packages) - cache/wheelhouse: replace file atomically across platforms (os.Rename fails on existing dest on Windows) - project: include requirements.lock in the no-lock-found error - tests for each Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
// Package cache is a content-addressed, on-disk store for built OCI layers.
|
|
//
|
|
// Building a layer from a wheel means reading every member and gzip-compressing
|
|
// the tar — pure CPU that is wasted on every rebuild for dependencies that have
|
|
// not changed. Keyed by the wheel's sha256 (plus the install layout), the cache
|
|
// lets an unchanged dependency reuse its previously compressed blob verbatim,
|
|
// so a rebuild does no decompression/recompression for those layers.
|
|
package cache
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
|
)
|
|
|
|
// Cache stores compressed layer blobs under a directory.
|
|
type Cache struct {
|
|
dir string
|
|
}
|
|
|
|
// New returns a cache rooted at dir, creating it if necessary.
|
|
func New(dir string) (*Cache, error) {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("cache: create %q: %w", dir, err)
|
|
}
|
|
return &Cache{dir: dir}, nil
|
|
}
|
|
|
|
func (c *Cache) path(key string) string {
|
|
sum := sha256.Sum256([]byte(key))
|
|
return filepath.Join(c.dir, hex.EncodeToString(sum[:])+".tar.gz")
|
|
}
|
|
|
|
// Get returns the cached layer for key, or (nil, false) on a miss.
|
|
func (c *Cache) Get(key string) (v1.Layer, bool) {
|
|
p := c.path(key)
|
|
if _, err := os.Stat(p); err != nil {
|
|
return nil, false
|
|
}
|
|
l, err := tarball.LayerFromFile(p)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return l, true
|
|
}
|
|
|
|
// metaPath returns the on-disk path for a small text value keyed by key.
|
|
func (c *Cache) metaPath(key string) string {
|
|
sum := sha256.Sum256([]byte(key))
|
|
return filepath.Join(c.dir, "meta", hex.EncodeToString(sum[:])+".txt")
|
|
}
|
|
|
|
// GetText returns a small cached text value for key (e.g. a detected base
|
|
// interpreter version), or ("", false) on a miss.
|
|
func (c *Cache) GetText(key string) (string, bool) {
|
|
data, err := os.ReadFile(c.metaPath(key))
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return string(data), true
|
|
}
|
|
|
|
// PutText stores a small text value for key.
|
|
func (c *Cache) PutText(key, val string) error {
|
|
p := c.metaPath(key)
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(p), ".tmp-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpName := tmp.Name()
|
|
defer func() { _ = os.Remove(tmpName) }()
|
|
if _, err := tmp.WriteString(val); err != nil {
|
|
_ = tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return replaceFile(tmpName, p)
|
|
}
|
|
|
|
// Put stores the layer's compressed blob under key. Writes are atomic (temp
|
|
// file + rename) so concurrent builders and interrupted writes are safe.
|
|
func (c *Cache) Put(key string, l v1.Layer) error {
|
|
rc, err := l.Compressed()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = rc.Close() }()
|
|
|
|
tmp, err := os.CreateTemp(c.dir, ".tmp-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpName := tmp.Name()
|
|
defer func() { _ = os.Remove(tmpName) }()
|
|
|
|
if _, err := io.Copy(tmp, rc); err != nil {
|
|
_ = tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return replaceFile(tmpName, c.path(key))
|
|
}
|
|
|
|
// replaceFile atomically moves src onto dst. os.Rename replaces an existing
|
|
// dst on POSIX but fails on Windows, so retry after removing dst there. The
|
|
// store is content-addressed (or overwrites an equivalent value), so removing
|
|
// an existing dst is safe.
|
|
func replaceFile(src, dst string) error {
|
|
if err := os.Rename(src, dst); err != nil {
|
|
if _, statErr := os.Stat(dst); statErr == nil {
|
|
_ = os.Remove(dst)
|
|
return os.Rename(src, dst)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|