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>
This commit is contained in:
2026-05-17 13:07:12 +09:00
parent c2ff9c9fa5
commit 30eb0d6a61
95 changed files with 1514 additions and 1567 deletions

307
internal/rosa/state.go Normal file
View File

@@ -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 arent 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())
}