Files
hakurei/internal/rosa/state.go
Ophestra 3e236333a7 internal/rosa: panic error for invalid handle
This enables recovery and better error handling for errors originating from external azalea files.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 00:07:39 +09:00

340 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]
// H is a convenient function for acquiring an [ArtifactH] by name.
func H(name string) ArtifactH { return ArtifactH(unique.Make(name)) }
// String returns the name of p.
func (handle ArtifactH) String() string {
return unique.Handle[string](handle).Value()
}
// MarshalJSON represents [ArtifactH] by its [Artifact.Name].
func (handle ArtifactH) MarshalJSON() ([]byte, error) {
return json.Marshal(handle.String())
}
// UnmarshalJSON resolves [ArtifactH] by its [Artifact.Name].
func (handle *ArtifactH) UnmarshalJSON(data []byte) error {
var name string
if err := json.Unmarshal(data, &name); err != nil {
return err
}
*handle = ArtifactH(unique.Make(name))
return nil
}
// A HandleError is panicked when attempting to acquire an [Artifact] via an
// invalid [ArtifactH] with the Must suite of methods.
type HandleError ArtifactH
func (e HandleError) Error() string {
return "artifact " + strconv.Quote(ArtifactH(e).String()) + " not available"
}
type (
// Stage denotes the infrastructure to compile a [pkg.Artifact] on.
Stage uint32
// Toolchain refers to an instance of [S], and a [Stage] to compile on.
Toolchain struct {
stage Stage
*S
}
)
// 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) (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 [_stageEnd]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(handle ArtifactH) (meta *Artifact) {
s.wantsArch()
v, ok := s.artifacts.Load(handle)
if ok {
meta = v.(*Artifact)
}
return
}
// MustGet is like Get, but panics if the named [Artifact] is not registered.
func (s *S) MustGet(handle ArtifactH) (meta *Artifact) {
meta = s.Get(handle)
if meta == nil {
panic(HandleError(handle))
}
return
}
// New returns a [Toolchain] for the specified [Stage].
func (s *S) New(stage Stage) Toolchain {
return Toolchain{S: s, stage: stage}
}
// Std is a convenience method that returns a [Toolchain] for the [Std] stage.
func (s *S) Std() Toolchain { return s.New(Std) }
// Load returns the resulting [pkg.Artifact] of [ArtifactH].
func (t Toolchain) Load(handle ArtifactH) (pkg.Artifact, string) {
t.wantsArch()
e, ok := t.c[t.stage].Load(handle)
if ok {
r := e.(cachedArtifact)
return r.a, r.v
}
meta := t.Get(handle)
if meta == nil {
return nil, ""
}
var r cachedArtifact
r.a, r.v = meta.f(t)
t.c[t.stage].Store(handle, r)
return r.a, r.v
}
// MustLoad is like Load, but panics if the named [Artifact] is not registered.
func (t Toolchain) MustLoad(handle ArtifactH) (pkg.Artifact, string) {
a, version := t.Load(handle)
if a == nil {
panic(HandleError(handle))
}
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())
}