aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pkg
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-04-21 11:35:16 +0900
committerOphestra <cat@gensokyo.uk>2026-04-23 00:53:21 +0900
commitceb4d260871119e73688ee99a844b79c94fa5fe6 (patch)
treeb8b409f71f6f53c1ec30e5b15097d1f7a3def4f7 /internal/pkg
parent852f3a9b3dfcb41ebd8f6587e232f2eb4708a44f (diff)
internal/pkg: record cache variant on-disk
This makes custom artifacts much less error-prone to use. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/pkg')
-rw-r--r--internal/pkg/ir.go6
-rw-r--r--internal/pkg/pkg.go150
-rw-r--r--internal/pkg/pkg_test.go260
3 files changed, 407 insertions, 9 deletions
diff --git a/internal/pkg/ir.go b/internal/pkg/ir.go
index 41e75845..c93abc9e 100644
--- a/internal/pkg/ir.go
+++ b/internal/pkg/ir.go
@@ -432,6 +432,12 @@ func (e InvalidKindError) Error() string {
// register is not safe for concurrent use. register must not be called after
// the first instance of [Cache] has been opened.
func register(k Kind, f IRReadFunc) {
+ openMu.Lock()
+ defer openMu.Unlock()
+
+ if opened {
+ panic("attempting to register after open")
+ }
if _, ok := irArtifact[k]; ok {
panic("attempting to register " + strconv.Itoa(int(k)) + " twice")
}
diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go
index e94cf9c4..767c35a7 100644
--- a/internal/pkg/pkg.go
+++ b/internal/pkg/pkg.go
@@ -18,6 +18,7 @@ import (
"path/filepath"
"runtime"
"slices"
+ "strconv"
"strings"
"sync"
"sync/atomic"
@@ -70,6 +71,64 @@ func MustDecode(s string) (checksum Checksum) {
return
}
+var (
+ // extension is a string uniquely identifying a set of custom [Artifact]
+ // implementations registered by calling [Register].
+ extension string
+
+ // openMu synchronises access to global state for initialisation.
+ openMu sync.Mutex
+ // opened is false if [Open] was never called.
+ opened bool
+)
+
+// Extension returns a string uniquely identifying the currently registered set
+// of custom [Artifact], or the zero value if none was registered.
+func Extension() string { return extension }
+
+// ValidExtension returns whether s is valid for use in a call to SetExtension.
+func ValidExtension(s string) bool {
+ if l := len(s); l == 0 || l > 128 {
+ return false
+ }
+ for _, v := range s {
+ if v < 'a' || v > 'z' {
+ return false
+ }
+ }
+ return true
+}
+
+// ErrInvalidExtension is returned for a variant identification string for which
+// [ValidExtension] returns false.
+var ErrInvalidExtension = errors.New("invalid extension variant identification string")
+
+// SetExtension sets the extension variant identification string. SetExtension
+// must be called before [Open] if custom [Artifact] implementations had been
+// recorded by calling [Register].
+//
+// The variant identification string must be between 1 and 128 bytes long and
+// consists of only bytes between 'a' and 'z'.
+//
+// SetExtension is not safe for concurrent use. SetExtension is called at most
+// once and must not be called after the first instance of Cache has been opened.
+func SetExtension(s string) {
+ openMu.Lock()
+ defer openMu.Unlock()
+
+ if opened {
+ panic("attempting to set extension after open")
+ }
+ if extension != "" {
+ panic("attempting to set extension twice")
+ }
+ if !ValidExtension(s) {
+ panic(ErrInvalidExtension)
+ }
+ extension = s
+ statusHeader = makeStatusHeader(s)
+}
+
// common holds elements and receives methods shared between different contexts.
type common struct {
// Context specific to this [Artifact]. The toplevel context in [Cache] must
@@ -102,19 +161,27 @@ type TContext struct {
common
}
-// statusHeader is the header written to all status files in dirStatus.
-var statusHeader = func() string {
+// makeStatusHeader creates the header written to every status file. This should
+// not be called directly, its result is stored in statusHeader and will not
+// change after the first [Cache] is opened.
+func makeStatusHeader(extension string) string {
s := programName
if v := info.Version(); v != info.FallbackVersion {
s += " " + v
}
+ if extension != "" {
+ s += " with " + extension + " extensions"
+ }
s += " (" + runtime.GOARCH + ")"
if name, err := os.Hostname(); err == nil {
s += " on " + name
}
s += "\n\n"
return s
-}()
+}
+
+// statusHeader is the header written to all status files in dirStatus.
+var statusHeader = makeStatusHeader("")
// prepareStatus initialises the status file once.
func (t *TContext) prepareStatus() error {
@@ -427,6 +494,9 @@ const (
// KindFile is the kind of [Artifact] returned by [NewFile].
KindFile
+ // _kindEnd is the total number of kinds and does not denote a kind.
+ _kindEnd
+
// KindCustomOffset is the first [Kind] value reserved for implementations
// not from this package.
KindCustomOffset = 1 << 31
@@ -441,6 +511,9 @@ const (
// fileLock is the file name appended to Cache.base for guaranteeing
// exclusive access to the cache directory.
fileLock = "lock"
+ // fileVariant is the file name appended to Cache.base holding the variant
+ // identification string set by a prior call to [SetExtension].
+ fileVariant = "variant"
// dirIdentifier is the directory name appended to Cache.base for storing
// artifacts named after their [ID].
@@ -540,6 +613,10 @@ const (
// impurity due to [KindExecNet] being [KnownChecksum]. This flag exists
// to support kernels without Landlock LSM enabled.
CHostAbstract
+
+ // CPromoteVariant allows [pkg.Open] to promote an unextended on-disk cache
+ // to the current extension variant. This is a one-way operation.
+ CPromoteVariant
)
// toplevel holds [context.WithCancel] over caller-supplied context, where all
@@ -1930,6 +2007,20 @@ func (c *Cache) Close() {
c.unlock()
}
+// UnsupportedVariantError describes an on-disk cache with an extension variant
+// identification string that differs from the value returned by [Extension].
+type UnsupportedVariantError string
+
+func (e UnsupportedVariantError) Error() string {
+ return "unsupported variant " + strconv.Quote(string(e))
+}
+
+var (
+ // ErrWouldPromote is returned by [Open] if the [CPromoteVariant] bit is not
+ // set and the on-disk cache requires variant promotion.
+ ErrWouldPromote = errors.New("operation would promote unextended cache")
+)
+
// Open returns the address of a newly opened instance of [Cache].
//
// Concurrent cures of a [FloodArtifact] dependency graph is limited to the
@@ -1961,6 +2052,14 @@ func open(
base *check.Absolute,
lock bool,
) (*Cache, error) {
+ openMu.Lock()
+ defer openMu.Unlock()
+ opened = true
+
+ if extension == "" && len(irArtifact) != int(_kindEnd) {
+ panic("attempting to open cache with incomplete variant setup")
+ }
+
if cures < 1 {
cures = runtime.NumCPU()
}
@@ -1974,8 +2073,10 @@ func open(
dirStatus,
dirWork,
} {
- if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&
- !errors.Is(err, os.ErrExist) {
+ if err := os.MkdirAll(
+ base.Append(name).String(),
+ 0700,
+ ); err != nil && !errors.Is(err, os.ErrExist) {
return nil, err
}
}
@@ -2013,6 +2114,45 @@ func open(
c.unlock = func() {}
}
+ variantPath := base.Append(fileVariant).String()
+ if p, err := os.ReadFile(variantPath); err != nil {
+ if !errors.Is(err, os.ErrNotExist) {
+ c.unlock()
+ return nil, err
+ }
+ // nonexistence implies newly created cache, or a cache predating
+ // variant identification strings, in which case it is silently promoted
+ if err = os.WriteFile(
+ variantPath,
+ []byte(extension),
+ 0400,
+ ); err != nil {
+ c.unlock()
+ return nil, err
+ }
+ } else if s := string(p); s == "" {
+ if extension != "" {
+ if flags&CPromoteVariant == 0 {
+ c.unlock()
+ return nil, ErrWouldPromote
+ }
+ if err = os.WriteFile(
+ variantPath,
+ []byte(extension),
+ 0400,
+ ); err != nil {
+ c.unlock()
+ return nil, err
+ }
+ }
+ } else if !ValidExtension(s) {
+ c.unlock()
+ return nil, ErrInvalidExtension
+ } else if s != extension {
+ c.unlock()
+ return nil, UnsupportedVariantError(s)
+ }
+
return &c, nil
}
diff --git a/internal/pkg/pkg_test.go b/internal/pkg/pkg_test.go
index f3011902..f0145e47 100644
--- a/internal/pkg/pkg_test.go
+++ b/internal/pkg/pkg_test.go
@@ -41,6 +41,25 @@ func unsafeOpen(
lock bool,
) (*pkg.Cache, error)
+var (
+ // extension is a string uniquely identifying a set of custom [Artifact]
+ // implementations registered by calling [Register].
+ //
+ //go:linkname extension hakurei.app/internal/pkg.extension
+ extension string
+
+ // opened is false if [Open] was never called.
+ //
+ //go:linkname opened hakurei.app/internal/pkg.opened
+ opened bool
+
+ // irArtifact refers to artifact IR interpretation functions and must not be
+ // written to directly.
+ //
+ //go:linkname irArtifact hakurei.app/internal/pkg.irArtifact
+ irArtifact map[pkg.Kind]pkg.IRReadFunc
+)
+
// newRContext returns the address of a new [pkg.RContext] unsafely created for
// the specified [testing.TB].
func newRContext(tb testing.TB, c *pkg.Cache) *pkg.RContext {
@@ -342,9 +361,20 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
restoreTemp = true
}
- // destroy lock file to avoid changing cache checksums
- if err := os.Remove(base.Append("lock").String()); err != nil {
- t.Fatal(err)
+ // destroy lock and variant file to avoid changing cache checksums
+ for _, s := range []string{
+ "lock",
+ "variant",
+ } {
+ pathname := base.Append(s)
+ if p, err := os.ReadFile(pathname.String()); err != nil {
+ t.Fatal(err)
+ } else if len(p) != 0 {
+ t.Fatalf("file %q: %q", s, string(p))
+ }
+ if err := os.Remove(pathname.String()); err != nil {
+ t.Fatal(err)
+ }
}
// destroy non-deterministic status files
@@ -1101,6 +1131,10 @@ func TestErrors(t *testing.T) {
Want: pkg.IRKindIdent,
Ancillary: 0xcafe,
}, "got invalid kind 48879 IR value (0xcafe) instead of ident"},
+
+ {"UnsupportedVariantError", pkg.UnsupportedVariantError(
+ "rosa",
+ ), `unsupported variant "rosa"`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -1309,6 +1343,8 @@ func (a earlyFailureF) Cure(*pkg.FContext) error {
}
func TestDependencyCureErrorEarly(t *testing.T) {
+ t.Parallel()
+
checkWithCache(t, []cacheTestCase{
{"early", 0, nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) {
_, _, err := c.Cure(earlyFailureF(8))
@@ -1319,7 +1355,7 @@ func TestDependencyCureErrorEarly(t *testing.T) {
})
}
-func TestNew(t *testing.T) {
+func TestOpen(t *testing.T) {
t.Parallel()
t.Run("nonexistent", func(t *testing.T) {
@@ -1367,3 +1403,219 @@ func TestNew(t *testing.T) {
}
})
}
+
+func TestExtensionRegister(t *testing.T) {
+ extensionOld := extension
+ openedOld := opened
+ t.Cleanup(func() { extension = extensionOld; opened = openedOld })
+ extension = ""
+ opened = false
+
+ t.Run("set", func(t *testing.T) {
+ t.Cleanup(func() { extension = "" })
+
+ const want = "rosa"
+ pkg.SetExtension(want)
+ if got := pkg.Extension(); got != want {
+ t.Fatalf("Extension: %q, want %q", got, want)
+ }
+ })
+
+ t.Run("twice", func(t *testing.T) {
+ t.Cleanup(func() { extension = "" })
+
+ defer func() {
+ const wantPanic = "attempting to set extension twice"
+ if r := recover(); r != wantPanic {
+ t.Errorf("panic: %#v, want %q", r, wantPanic)
+ }
+ }()
+ pkg.SetExtension("rosa")
+ pkg.SetExtension("rosa")
+ })
+
+ t.Run("invalid", func(t *testing.T) {
+ defer func() {
+ var wantPanic = pkg.ErrInvalidExtension
+ if r := recover(); r != wantPanic {
+ t.Errorf("panic: %#v, want %#v", r, wantPanic)
+ }
+ }()
+ pkg.SetExtension(" ")
+ })
+
+ t.Run("opened", func(t *testing.T) {
+ t.Cleanup(func() { opened = false })
+
+ if _, err := pkg.Open(
+ t.Context(),
+ message.New(log.Default()),
+ 0, 0, 0,
+ check.MustAbs(container.Nonexistent),
+ ); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("Open: error = %v", err)
+ }
+
+ t.Run("variant", func(t *testing.T) {
+ defer func() {
+ const wantPanic = "attempting to set extension after open"
+ if r := recover(); r != wantPanic {
+ t.Errorf("panic: %#v, want %q", r, wantPanic)
+ }
+ }()
+ pkg.SetExtension("rosa")
+ })
+
+ t.Run("register", func(t *testing.T) {
+ defer func() {
+ const wantPanic = "attempting to register after open"
+ if r := recover(); r != wantPanic {
+ t.Errorf("panic: %#v, want %q", r, wantPanic)
+ }
+ }()
+ pkg.Register(pkg.KindCustomOffset, nil)
+ })
+ })
+
+ t.Run("incomplete", func(t *testing.T) {
+ t.Cleanup(func() { delete(irArtifact, pkg.KindCustomOffset) })
+
+ defer func() {
+ const wantPanic = "attempting to open cache with incomplete variant setup"
+ if r := recover(); r != wantPanic {
+ t.Errorf("panic: %#v, want %q", r, wantPanic)
+ }
+ }()
+ pkg.Register(pkg.KindCustomOffset, nil)
+
+ t.Cleanup(func() { opened = false })
+ _, _ = pkg.Open(nil, nil, 0, 0, 0, nil)
+ panic("unreachable")
+ })
+
+ t.Run("create", func(t *testing.T) {
+ t.Cleanup(func() { extension = "" })
+ const want = "rosa"
+ pkg.SetExtension(want)
+
+ base := check.MustAbs(t.TempDir())
+ t.Cleanup(func() { opened = false })
+ if c, err := pkg.Open(
+ t.Context(), nil,
+ 0, 0, 0,
+ base,
+ ); err != nil {
+ t.Fatal(err)
+ } else {
+ c.Close()
+ }
+
+ if got, err := os.ReadFile(base.Append("variant").String()); err != nil {
+ t.Fatal(err)
+ } else if string(got) != want {
+ t.Fatalf("variant: %q", string(got))
+ }
+ })
+
+ t.Run("access", func(t *testing.T) {
+ base := check.MustAbs(t.TempDir())
+ t.Cleanup(func() { opened = false })
+
+ if err := os.WriteFile(base.Append("variant").String(), nil, 0); err != nil {
+ t.Fatal(err)
+ }
+
+ wantErr := &os.PathError{
+ Op: "open",
+ Path: base.Append("variant").String(),
+ Err: syscall.EACCES,
+ }
+ if _, err := pkg.Open(
+ t.Context(), nil,
+ 0, 0, 0,
+ base,
+ ); !reflect.DeepEqual(err, wantErr) {
+ t.Fatalf("Open: error = %v, want %v", err, wantErr)
+ }
+ })
+
+ t.Run("promote", func(t *testing.T) {
+ t.Cleanup(func() { extension = "" })
+ const want = "rosa"
+ pkg.SetExtension(want)
+
+ base := check.MustAbs(t.TempDir())
+ t.Cleanup(func() { opened = false })
+
+ variantPath := base.Append("variant")
+ if err := os.WriteFile(variantPath.String(), nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := pkg.Open(
+ t.Context(), nil,
+ 0, 0, 0,
+ base,
+ ); !reflect.DeepEqual(err, pkg.ErrWouldPromote) {
+ t.Fatalf("Open: error = %v", err)
+ }
+
+ if p, err := os.ReadFile(variantPath.String()); err != nil {
+ t.Fatal(err)
+ } else if len(p) != 0 {
+ t.Fatalf("variant: %q", string(p))
+ }
+
+ if c, err := pkg.Open(
+ t.Context(), nil,
+ pkg.CPromoteVariant, 0, 0,
+ base,
+ ); err != nil {
+ t.Fatalf("Open: error = %v", err)
+ } else {
+ c.Close()
+ }
+
+ if p, err := os.ReadFile(variantPath.String()); err != nil {
+ t.Fatal(err)
+ } else if string(p) != want {
+ t.Fatalf("variant: %q, want %q", string(p), want)
+ }
+ })
+
+ t.Run("open invalid", func(t *testing.T) {
+ base := check.MustAbs(t.TempDir())
+ t.Cleanup(func() { opened = false })
+
+ variantPath := base.Append("variant")
+ if err := os.WriteFile(variantPath.String(), make([]byte, 129), 0400); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := pkg.Open(
+ t.Context(), nil,
+ 0, 0, 0,
+ base,
+ ); !reflect.DeepEqual(err, pkg.ErrInvalidExtension) {
+ t.Fatalf("Open: error = %v", err)
+ }
+ })
+
+ t.Run("unsupported", func(t *testing.T) {
+ base := check.MustAbs(t.TempDir())
+ t.Cleanup(func() { opened = false })
+
+ variantPath := base.Append("variant")
+ if err := os.WriteFile(variantPath.String(), []byte("rosa"), 0400); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := pkg.Open(
+ t.Context(), nil,
+ 0, 0, 0,
+ base,
+ ); !reflect.DeepEqual(err, pkg.UnsupportedVariantError("rosa")) {
+ t.Fatalf("Open: error = %v", err)
+ }
+ })
+}