internal/pkg: compute identifier from deps
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m31s
Test / Hakurei (push) Successful in 3m34s
Test / ShareFS (push) Successful in 3m40s
Test / Hpkg (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m50s
Test / Sandbox (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m46s
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m31s
Test / Hakurei (push) Successful in 3m34s
Test / ShareFS (push) Successful in 3m40s
Test / Hpkg (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m50s
Test / Sandbox (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m46s
This provides infrastructure for computing a deterministic identifier based on current artifact kind, opaque parameters data, and optional dependency kind and identifiers. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -53,6 +53,9 @@ func (c *Cache) NewHTTPGet(hc *http.Client, url string, checksum Checksum) (File
|
||||
return c.NewHTTP(hc, req, checksum), nil
|
||||
}
|
||||
|
||||
// Kind returns the hardcoded [Kind] constant.
|
||||
func (a *httpArtifact) Kind() Kind { return KindHTTP }
|
||||
|
||||
// ID returns the caller-supplied hash of the response body.
|
||||
func (a *httpArtifact) ID() ID { return a.id }
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
@@ -39,6 +42,11 @@ func MustDecode(s string) (checksum Checksum) {
|
||||
// deterministically but might not currently be available in memory or on the
|
||||
// filesystem.
|
||||
type Artifact interface {
|
||||
// Kind returns the [Kind] of artifact. This is usually unique to the
|
||||
// concrete type but two functionally identical implementations of
|
||||
// [Artifact] is allowed to return the same [Kind] value.
|
||||
Kind() Kind
|
||||
|
||||
// ID returns a globally unique identifier referring to the current
|
||||
// [Artifact]. This value must be known ahead of time and guaranteed to be
|
||||
// unique without having obtained the full contents of the [Artifact].
|
||||
@@ -71,6 +79,39 @@ type File interface {
|
||||
Artifact
|
||||
}
|
||||
|
||||
// Kind corresponds to the concrete type of [Artifact] and is used to create
|
||||
// identifier for an [Artifact] with dependencies.
|
||||
type Kind uint64
|
||||
|
||||
const (
|
||||
// KindHTTP is the kind of [Artifact] returned by [Cache.NewHTTP].
|
||||
KindHTTP Kind = iota
|
||||
KindTar
|
||||
)
|
||||
|
||||
// Ident returns a deterministic identifier for the supplied params and
|
||||
// dependencies. The caller is responsible for ensuring params uniquely and
|
||||
// deterministically describes the current [Artifact].
|
||||
func (k Kind) Ident(params []byte, deps ...Artifact) ID {
|
||||
type extIdent [len(ID{}) + wordSize]byte
|
||||
identifiers := make([]extIdent, len(deps))
|
||||
for i, a := range deps {
|
||||
id := a.ID()
|
||||
copy(identifiers[i][wordSize:], id[:])
|
||||
binary.LittleEndian.PutUint64(identifiers[i][:], uint64(a.Kind()))
|
||||
}
|
||||
slices.SortFunc(identifiers, func(a, b extIdent) int { return bytes.Compare(a[:], b[:]) })
|
||||
slices.Compact(identifiers)
|
||||
|
||||
h := sha512.New384()
|
||||
h.Write(binary.LittleEndian.AppendUint64(nil, uint64(k)))
|
||||
h.Write(params)
|
||||
for _, e := range identifiers {
|
||||
h.Write(e[:])
|
||||
}
|
||||
return ID(h.Sum(nil))
|
||||
}
|
||||
|
||||
const (
|
||||
// dirIdentifier is the directory name appended to Cache.base for storing
|
||||
// artifacts named after their [ID].
|
||||
|
||||
@@ -18,6 +18,46 @@ import (
|
||||
"hakurei.app/internal/pkg"
|
||||
)
|
||||
|
||||
// A stubArtifact implements [Artifact] with hardcoded kind and identifier.
|
||||
type stubArtifact struct {
|
||||
kind pkg.Kind
|
||||
id pkg.ID
|
||||
}
|
||||
|
||||
func (a stubArtifact) Kind() pkg.Kind { return a.kind }
|
||||
func (a stubArtifact) ID() pkg.ID { return a.id }
|
||||
func (a stubArtifact) Hash() (pkg.Checksum, error) { panic("unreachable") }
|
||||
func (a stubArtifact) Pathname() (*check.Absolute, error) { panic("unreachable") }
|
||||
|
||||
func TestIdent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
kind pkg.Kind
|
||||
params []byte
|
||||
deps []pkg.Artifact
|
||||
want pkg.ID
|
||||
}{
|
||||
{"tar", pkg.KindTar, []byte{
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
}, []pkg.Artifact{
|
||||
stubArtifact{pkg.KindHTTP, pkg.ID{}},
|
||||
}, pkg.MustDecode("HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY")},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.kind.Ident(tc.params, tc.deps...); got != tc.want {
|
||||
t.Errorf("Ident: %s, want %s",
|
||||
base64.URLEncoding.EncodeToString(got[:]),
|
||||
base64.URLEncoding.EncodeToString(tc.want[:]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cacheTestCase is a test case passed to checkWithCache where a new instance
|
||||
// of [pkg.Cache] is prepared for the test case, and is validated and removed
|
||||
// on test completion.
|
||||
|
||||
Reference in New Issue
Block a user