internal/pkg: record cache variant on-disk
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m47s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m16s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Hakurei (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m25s
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m47s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m16s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Hakurei (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m25s
This makes custom artifacts much less error-prone to use. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user