diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-05-17 13:07:12 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-05-17 15:56:53 +0900 |
| commit | 30eb0d6a613ad268f6805ccbcdcabf158e7f526f (patch) | |
| tree | 07e55baa52748fd139e183017eeb43f5f64cee59 /internal/rosa/state.go | |
| parent | c2ff9c9fa55ea569a5e70ee90d2eb0b2ee2a0540 (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.go | 307 |
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()) +} |
