forked from rosa/hakurei
This cleans up many function signatures. Signed-off-by: Ophestra <cat@gensokyo.uk>
329 lines
8.2 KiB
Go
329 lines
8.2 KiB
Go
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 (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
|
||
}
|
||
|
||
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 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 [_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("artifact " + strconv.Quote(handle.String()) + " not available")
|
||
}
|
||
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("artifact " + strconv.Quote(handle.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())
|
||
}
|