internal/rosa: initial azalea bindings
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

Supported fields are still rather minimal, but evaluation works, and resulting artifacts cure correctly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-05-18 01:59:51 +09:00
parent 3e236333a7
commit 0360e779f3
4 changed files with 267 additions and 15 deletions

View File

@@ -140,6 +140,8 @@ var (
// IdentInputs is a special array argument in a package declaration whose
// values of [Ident] are kept as is when passed to a [PF].
IdentInputs = unique.Make(Ident("inputs"))
// IdentRuntime has the same semantics as [IdentInputs].
IdentRuntime = unique.Make(Ident("runtime"))
// ErrInvalidInputs is panicked for an [IdentInputs] argument to [PF]
// sharing its value or set for R.
@@ -274,6 +276,7 @@ func evaluateAny(d PF, s []Frame, expr, rp any) bool {
fp.Val = maps.Clone(f.V)
}
args:
for _, arg := range e.Args {
names := make([]unique.Handle[Ident], len(arg.K))
for i, name := range arg.K {
@@ -281,16 +284,23 @@ func evaluateAny(d PF, s []Frame, expr, rp any) bool {
}
farg := FArg{R: arg.R}
if e.Package && slices.Contains(names, IdentInputs) {
if len(names) != 1 || len(arg.V) != 1 || arg.R {
panic(ErrInvalidInputs)
if e.Package {
for _, special := range [...]unique.Handle[Ident]{
IdentInputs,
IdentRuntime,
} {
if slices.Contains(names, special) {
if len(names) != 1 || len(arg.V) != 1 || arg.R {
panic(ErrInvalidInputs)
}
farg.K = names[0]
if err := storeE(&farg.V, arg.V[0]); err != nil {
panic(err)
}
fargs = append(fargs, farg)
continue args
}
}
farg.K = names[0]
if err := storeE(&farg.V, arg.V[0]); err != nil {
panic(err)
}
fargs = append(fargs, farg)
continue
}
if !evaluateAny(d, s, arg.V, &farg.V) {
@@ -333,7 +343,7 @@ func evaluateAny(d PF, s []Frame, expr, rp any) bool {
}
// Evaluate evaluates a statement and returns its value.
func Evaluate[T Value](d PF, s []Frame, expr any) (v T, set bool, err error) {
func Evaluate[T any](d PF, s []Frame, expr any) (v T, set bool, err error) {
defer func() {
r := recover()
if r == nil {
@@ -346,6 +356,6 @@ func Evaluate[T Value](d PF, s []Frame, expr any) (v T, set bool, err error) {
}
err = _err
}()
set = evaluate[T](d, s, expr, &v)
set = evaluateAny(d, s, expr, &v)
return
}

View File

@@ -125,6 +125,18 @@ func TestEvaluate(t *testing.T) {
Err: ErrInvalidInputs,
}},
{"bound runtime", `package name { runtime* = []; }`, nil, "", EvaluationError{
Expr: Func{
Ident: Ident("name"),
Package: true,
Args: []Arg{
{K: []Ident{"runtime"}, V: Val{Array(nil)}, R: true},
},
},
Err: ErrInvalidInputs,
}},
{"concat inputs", `package name { inputs = ""+""; }`, nil, "", EvaluationError{
Expr: Func{
Ident: Ident("name"),

View File

@@ -42,7 +42,7 @@ func mustDecode(s string) pkg.Checksum {
}
// KV is a key-value pair of strings.
type KV [2]string
type KV = [2]string
var (
// AbsUsrSrc is the conventional directory to place source code under.
@@ -601,6 +601,9 @@ func newFromGitHubRelease(
)
}
// native contains natively-implemented and built-in azalea-based [Artifact].
// It is generally recommended to clone this instance for custom [Artifact]
// registrations.
var native S
// Native returns the global [S].

View File

@@ -4,7 +4,10 @@ import (
"context"
"encoding/json"
"errors"
"io"
"io/fs"
"net/http"
"path/filepath"
"runtime"
"slices"
"strconv"
@@ -12,8 +15,10 @@ import (
"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].
@@ -179,6 +184,11 @@ type S struct {
// 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
@@ -300,10 +310,18 @@ func (s *S) Register(meta *Artifact) bool {
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("attempting to register " + strconv.Quote(meta.Name) + " twice")
panic(RegisterError(H(meta.Name)))
}
}
@@ -313,8 +331,8 @@ func (s *S) Count() int {
}
// Collect returns all [ArtifactH] registered to s.
func (s *S) Collect() (handles []ArtifactH) {
handles = make([]ArtifactH, 0, s.Count())
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
@@ -325,6 +343,215 @@ func (s *S) Collect() (handles []ArtifactH) {
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) {