Files
hakurei/internal/rosa/state.go
Ophestra 0360e779f3
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m51s
Test / ShareFS (push) Successful in 3m50s
Test / Hakurei (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 5m27s
Test / Hakurei (race detector) (push) Successful in 6m28s
Test / Flake checks (push) Successful in 1m22s
internal/rosa: initial azalea bindings
Supported fields are still rather minimal, but evaluation works, and resulting artifacts cure correctly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-18 02:56:38 +09:00

567 lines
14 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"
"io"
"io/fs"
"net/http"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"unique"
"unsafe"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa/azalea"
)
// 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
// Built-in functions.
s []azalea.Frame
// For initialising s.
sOnce 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
}
// RegisterError is returned or panicked when attempting to register multiple
// [Artifact] with the same name.
type RegisterError ArtifactH
func (e RegisterError) Error() string {
return "attempting to register " + strconv.Quote(ArtifactH(e).String()) + " twice"
}
// MustRegister is like Register, but panics if registration fails.
func (s *S) MustRegister(meta *Artifact) {
if !s.Register(meta) {
panic(RegisterError(H(meta.Name)))
}
}
// 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 P) {
handles = make(P, 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
}
// getS must be called before accessing s. This value is not currently safe for
// concurrent use, but the underlying frame is immutable.
func (s *S) getS() []azalea.Frame {
s.sOnce.Do(func() {
s.wantsArch()
k := func(name string) unique.Handle[azalea.Ident] {
return unique.Make(azalea.Ident(name))
}
s.s = make([]azalea.Frame, 1, 1<<4)
s.s[0].Func = map[unique.Handle[azalea.Ident]]azalea.F{
// intenral/pkg built-ins
unique.Make(azalea.Ident("remoteTar")): {F: func(
args azalea.FArgs,
) (v any, set bool, err error) {
var url, checksum string
var compress uint32
if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
k("url"): &url,
k("checksum"): &checksum,
k("compress"): &compress,
}); err != nil {
return
}
v = newTar(url, checksum, compress)
set = true
return
}, V: map[unique.Handle[azalea.Ident]]any{
k("uncompressed"): uint32(pkg.TarUncompressed),
k("gzip"): uint32(pkg.TarGzip),
k("bzip2"): uint32(pkg.TarBzip2),
}},
// high-level helpers
unique.Make(azalea.Ident("make")): {F: func(
args azalea.FArgs,
) (v any, set bool, err error) {
var attr MakeHelper
if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
k("omitDefaults"): &attr.OmitDefaults,
k("generate"): &attr.Generate,
k("preMake"): &attr.ScriptMakeEarly,
k("preCheck"): &attr.ScriptCheckEarly,
k("postInstall"): &attr.Script,
k("inPlace"): &attr.InPlace,
k("skipConfigure"): &attr.SkipConfigure,
k("configureName"): &attr.ConfigureName,
k("configure"): &attr.Configure,
k("host"): &attr.Host,
k("build"): &attr.Build,
k("make"): &attr.Make,
k("skipCheck"): &attr.SkipCheck,
k("check"): &attr.Check,
k("install"): &attr.Install,
}); err != nil {
return
}
v = &attr
set = true
return
}},
}
})
return s.s
}
// toHandles makes handles out of an [azalea.Array] of identifiers.
func toHandles(idents azalea.Array) (P, error) {
handles := make(P, len(idents))
for i, p := range idents {
if len(p) != 1 {
return nil, azalea.EvaluationError{
Expr: p,
Err: errors.New("concatenation not allowed for handles"),
}
}
s, ok := p[0].(azalea.Ident)
if !ok {
return nil, azalea.EvaluationError{
Expr: p[0],
Err: errors.New("identifiers expected for handles"),
}
}
handles[i] = H(string(s))
}
return handles, nil
}
// evalContext holds per-reader context.
type evalContext struct{ b fs.FS }
// f implements [azalea.PF].
func (ctx *evalContext) f(
name azalea.Ident,
args azalea.FArgs,
) (v any, set bool, err error) {
k := func(name string) unique.Handle[azalea.Ident] {
return unique.Make(azalea.Ident(name))
}
meta := Artifact{Name: string(name)}
var (
attr PackageAttr
patches []string
anitya int64
version string
source pkg.Artifact
helper Helper
inputs, runtimes azalea.Array
)
if err = args.Apply(map[unique.Handle[azalea.Ident]]any{
k("description"): &meta.Description,
k("website"): &meta.Website,
k("anitya"): &anitya,
k("version"): &version,
k("source"): &source,
k("writable"): &attr.Writable,
k("chmod"): &attr.Chmod,
k("enterSource"): &attr.EnterSource,
k("env"): &attr.Env,
k("early"): &attr.ScriptEarly,
k("patches"): &patches,
k("exec"): &helper,
k("inputs"): &inputs,
k("runtime"): &runtimes,
}); err != nil {
return
}
var inputsH P
if inputsH, err = toHandles(inputs); err != nil {
return
} else if meta.Dependencies, err = toHandles(runtimes); err != nil {
return
}
for _, pathname := range patches {
var p []byte
p, err = fs.ReadFile(ctx.b, pathname)
if err != nil {
return
}
attr.Patches = append(attr.Patches, KV{
strings.TrimSuffix(filepath.Base(pathname), ".patch"),
unsafe.String(unsafe.SliceData(p), len(p)),
})
}
meta.ID = int(anitya)
meta.f = func(t Toolchain) (pkg.Artifact, string) {
return t.NewPackage(
meta.Name,
version,
source,
&attr,
helper,
inputsH...,
), version
}
v = meta
set = true
return
}
var (
// ErrToplevel is returned by [S.Evaluate] when encountering a toplevel
// expression other than a package declaration.
ErrToplevel = errors.New("top level must only contain package declarations")
)
// Evaluate defines all package declarations from r. The backing filesystem is
// directly exposed to azalea pathnames.
func (s *S) Evaluate(r io.Reader, b fs.FS) error {
var pending []*azalea.Func
if expressions, err := azalea.Parse(r); err != nil {
return err
} else {
pending = make([]*azalea.Func, len(expressions))
for i, expr := range expressions {
f, ok := expr.(azalea.Func)
if !ok || !f.Package {
return ErrToplevel
}
pending[i] = &f
}
}
ctx := evalContext{b}
for _, f := range pending {
meta, set, err := azalea.Evaluate[Artifact](ctx.f, s.getS(), *f)
if err != nil {
return err
} else if !set {
return errors.New("unexpected unset")
}
if !s.Register(&meta) {
return RegisterError(H(string(f.Ident)))
}
}
return nil
}
// 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())
}