aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/rosa/state.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-05-17 13:07:12 +0900
committerOphestra <cat@gensokyo.uk>2026-05-17 15:56:53 +0900
commit30eb0d6a613ad268f6805ccbcdcabf158e7f526f (patch)
tree07e55baa52748fd139e183017eeb43f5f64cee59 /internal/rosa/state.go
parentc2ff9c9fa55ea569a5e70ee90d2eb0b2ee2a0540 (diff)
internal/rosa: key metadata by string
For upcoming azalea integration. The API is quite ugly right now to ease migration. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/rosa/state.go')
-rw-r--r--internal/rosa/state.go307
1 files changed, 307 insertions, 0 deletions
diff --git a/internal/rosa/state.go b/internal/rosa/state.go
new file mode 100644
index 00000000..2ae575b1
--- /dev/null
+++ b/internal/rosa/state.go
@@ -0,0 +1,307 @@
+package rosa
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "runtime"
+ "slices"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "unique"
+
+ "hakurei.app/internal/pkg"
+)
+
+// ArtifactH is a handle of the unique name of a prepared [pkg.Artifact].
+type ArtifactH unique.Handle[string]
+
+// String returns the name of p.
+func (p ArtifactH) String() string {
+ return unique.Handle[string](p).Value()
+}
+
+// MarshalJSON represents [ArtifactH] by its [Artifact.Name].
+func (p ArtifactH) MarshalJSON() ([]byte, error) { return json.Marshal(p.String()) }
+
+// UnmarshalJSON resolves [ArtifactH] by its [Artifact.Name].
+func (p *ArtifactH) UnmarshalJSON(data []byte) error {
+ var name string
+ if err := json.Unmarshal(data, &name); err != nil {
+ return err
+ }
+ *p = ArtifactH(unique.Make(name))
+ return nil
+}
+
+// P represents multiple [ArtifactH].
+type P []ArtifactH
+
+// Artifact is stage-agnostic immutable data with a deterministic resulting
+// [pkg.Artifact]. It can be created natively or through evaluation.
+type Artifact struct {
+ f func(t Toolchain, s *S) (a pkg.Artifact, version string)
+
+ // Unique package name.
+ Name string `json:"name"`
+ // Short user-facing description.
+ Description string `json:"description"`
+ // Project home page.
+ Website string `json:"website,omitempty"`
+
+ // Runtime dependencies.
+ Dependencies P `json:"dependencies"`
+
+ // Project identifier on [Anitya].
+ //
+ // [Anitya]: https://release-monitoring.org/
+ ID int `json:"-"`
+
+ // Whether to exclude from exported functions.
+ Exclude bool `json:"exclude,omitempty"`
+
+ // Optional custom version checking behaviour.
+ latest func(v *Versions) string
+}
+
+// GetLatest returns the latest version described by v.
+func (meta *Artifact) GetLatest(v *Versions) string {
+ if meta.latest != nil {
+ return meta.latest(v)
+ }
+ return v.Latest
+}
+
+// Unversioned denotes an unversioned [Artifact].
+const Unversioned = "\x00"
+
+// UnpopulatedIDError is returned by [Artifact.GetLatest] for an instance of
+// [Artifact] where ID is not populated.
+type UnpopulatedIDError struct{}
+
+func (UnpopulatedIDError) Unwrap() error { return errors.ErrUnsupported }
+func (UnpopulatedIDError) Error() string { return "Anitya ID is not populated" }
+
+// Versions are package versions returned by Anitya.
+type Versions struct {
+ // The latest version for the project, as determined by the version sorting algorithm.
+ Latest string `json:"latest_version"`
+ // List of all versions that aren’t flagged as pre-release.
+ Stable []string `json:"stable_versions"`
+ // List of all versions stored, sorted from newest to oldest.
+ All []string `json:"versions"`
+}
+
+// getStable returns the first Stable version, or Latest if that is unavailable.
+func (v *Versions) getStable() string {
+ if len(v.Stable) == 0 {
+ return v.Latest
+ }
+ return v.Stable[0]
+}
+
+// GetVersions returns versions fetched from Anitya.
+func (meta *Artifact) GetVersions(ctx context.Context) (*Versions, error) {
+ if meta.ID == 0 {
+ return nil, UnpopulatedIDError{}
+ }
+
+ var resp *http.Response
+ if req, err := http.NewRequestWithContext(
+ ctx,
+ http.MethodGet,
+ "https://release-monitoring.org/api/v2/versions/?project_id="+
+ strconv.Itoa(meta.ID),
+ nil,
+ ); err != nil {
+ return nil, err
+ } else {
+ req.Header.Set("User-Agent", "Rosa/1.1")
+ if resp, err = http.DefaultClient.Do(req); err != nil {
+ return nil, err
+ }
+ }
+
+ var v Versions
+ err := json.NewDecoder(resp.Body).Decode(&v)
+ return &v, errors.Join(err, resp.Body.Close())
+}
+
+// A cachedArtifact holds [pkg.Artifact] and its corresponding version string.
+type cachedArtifact struct {
+ a pkg.Artifact
+ v string
+}
+
+const (
+ // OptSkipCheck skips running all test suites.
+ OptSkipCheck = 1 << iota
+ // OptLLVMNoLTO disables LTO in all [LLVM] stages.
+ OptLLVMNoLTO
+)
+
+// S holds a set of [Artifact].
+type S struct {
+ // [ArtifactH] to [Artifact].
+ artifacts sync.Map
+ // Size of artifacts.
+ artifactCount atomic.Uint64
+
+ // Target architecture.
+ arch string
+ // For initialising arch.
+ archOnce sync.Once
+
+ // Options for [pkg.Artifact] created against [S].
+ opts int
+
+ // Cached [pkg.Artifact].
+ c [_toolchainEnd]sync.Map
+
+ // URL of a Gentoo stage3 tarball.
+ gentooStage3 string
+ // Expected checksum of gentooStage3.
+ gentooStage3Checksum pkg.Checksum
+}
+
+// Clone returns a copy of s.
+func (s *S) Clone() *S {
+ v := S{arch: s.arch}
+ s.artifacts.Range(func(key, value any) bool {
+ v.artifacts.Store(key, value)
+ v.artifactCount.Add(1)
+ return true
+ })
+ return &v
+}
+
+// wantsArch must be called before accessing arch.
+func (s *S) wantsArch() {
+ s.archOnce.Do(func() {
+ if s.arch == "" {
+ s.arch = runtime.GOARCH
+ }
+ })
+}
+
+// Arch returns the target architecture.
+func (s *S) Arch() string { s.wantsArch(); return s.arch }
+
+// Flags returns the current preset flags.
+func (s *S) Flags() int { return s.opts }
+
+// DropCaches arranges for all cached [pkg.Artifact] to be freed some time after
+// it returns. Must not be used concurrently with any other method.
+func (s *S) DropCaches(targetArch string, flags int) {
+ if targetArch == "" {
+ targetArch = runtime.GOARCH
+ }
+
+ s.arch = targetArch
+ s.opts = flags
+ for i := range s.c {
+ s.c[i].Clear()
+ }
+}
+
+// Get returns the address of the named [Artifact].
+func (s *S) Get(p ArtifactH) (meta *Artifact) {
+ s.wantsArch()
+ v, ok := s.artifacts.Load(p)
+ if ok {
+ meta = v.(*Artifact)
+ }
+ return
+}
+
+// MustGet is like Get, but panics if the named [Artifact] is not registered.
+func (s *S) MustGet(p ArtifactH) (meta *Artifact) {
+ meta = s.Get(p)
+ if meta == nil {
+ panic("artifact " + strconv.Quote(p.String()) + " not available")
+ }
+ return
+}
+
+// Load returns the resulting [pkg.Artifact] of [ArtifactH].
+func (s *S) Load(t Toolchain, p ArtifactH) (pkg.Artifact, string) {
+ s.wantsArch()
+ e, ok := s.c[t].Load(p)
+ if ok {
+ r := e.(cachedArtifact)
+ return r.a, r.v
+ }
+
+ meta := s.Get(p)
+ if meta == nil {
+ return nil, ""
+ }
+ var r cachedArtifact
+ r.a, r.v = meta.f(t, s)
+ s.c[t].Store(p, r)
+ return r.a, r.v
+}
+
+// MustLoad is like Load, but panics if the named [Artifact] is not registered.
+func (s *S) MustLoad(t Toolchain, p ArtifactH) (pkg.Artifact, string) {
+ a, version := s.Load(t, p)
+ if a == nil {
+ panic("artifact " + strconv.Quote(p.String()) + " not available")
+ }
+ return a, version
+}
+
+// Register arranges for a new [Artifact] to be cured under s. It returns false
+// if another [Artifact] is already registered under the same name.
+func (s *S) Register(meta *Artifact) bool {
+ if meta.Name == "" {
+ return false
+ }
+ p := ArtifactH(unique.Make(meta.Name))
+ _, ok := s.artifacts.LoadOrStore(p, meta)
+ if !ok {
+ s.artifactCount.Add(1)
+ }
+ return !ok
+}
+
+// MustRegister is like Register, but panics if registration fails.
+func (s *S) MustRegister(meta *Artifact) {
+ if !s.Register(meta) {
+ panic("attempting to register " + strconv.Quote(meta.Name) + " twice")
+ }
+}
+
+// Count returns the number of [Artifact] registered to s.
+func (s *S) Count() int {
+ return int(s.artifactCount.Load())
+}
+
+// Collect returns all [ArtifactH] registered to s.
+func (s *S) Collect() (handles []ArtifactH) {
+ handles = make([]ArtifactH, 0, s.Count())
+ s.artifacts.Range(func(key, _ any) bool {
+ handles = append(handles, key.(ArtifactH))
+ return true
+ })
+ slices.SortFunc(handles, func(a, b ArtifactH) int {
+ return strings.Compare(a.String(), b.String())
+ })
+ return
+}
+
+// SetGentooStage3 sets the Gentoo stage3 tarball url and checksum. It panics
+// if given zero values or if these values have already been set.
+func (s *S) SetGentooStage3(url string, checksum pkg.Checksum) {
+ if s.gentooStage3 != "" {
+ panic(errors.New("attempting to set Gentoo stage3 url twice"))
+ }
+ if url == "" {
+ panic(errors.New("attempting to set Gentoo stage3 url to the zero value"))
+ }
+ s.gentooStage3, s.gentooStage3Checksum = url, checksum
+ s.DropCaches(s.Arch(), s.Flags())
+}