aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-05-18 01:59:51 +0900
committerOphestra <cat@gensokyo.uk>2026-05-18 02:56:38 +0900
commit0360e779f3fb9dc770e1f33420ca5b8f8f638666 (patch)
treee9278ec16068938633b04404d1169e89b9572778 /internal
parent3e236333a77233cfcd96b6fc93be91e14ab7c680 (diff)
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>
Diffstat (limited to 'internal')
-rw-r--r--internal/rosa/azalea/evaluate.go32
-rw-r--r--internal/rosa/azalea/evaluate_test.go12
-rw-r--r--internal/rosa/rosa.go5
-rw-r--r--internal/rosa/state.go233
4 files changed, 267 insertions, 15 deletions
diff --git a/internal/rosa/azalea/evaluate.go b/internal/rosa/azalea/evaluate.go
index daf38849..063471e1 100644
--- a/internal/rosa/azalea/evaluate.go
+++ b/internal/rosa/azalea/evaluate.go
@@ -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)
- }
- farg.K = names[0]
- if err := storeE(&farg.V, arg.V[0]); err != nil {
- panic(err)
+ 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
+ }
}
- 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
}
diff --git a/internal/rosa/azalea/evaluate_test.go b/internal/rosa/azalea/evaluate_test.go
index aa7de27e..26c4e668 100644
--- a/internal/rosa/azalea/evaluate_test.go
+++ b/internal/rosa/azalea/evaluate_test.go
@@ -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"),
diff --git a/internal/rosa/rosa.go b/internal/rosa/rosa.go
index 8a1b59a3..709b8420 100644
--- a/internal/rosa/rosa.go
+++ b/internal/rosa/rosa.go
@@ -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].
diff --git a/internal/rosa/state.go b/internal/rosa/state.go
index 67cfdf97..71f25520 100644
--- a/internal/rosa/state.go
+++ b/internal/rosa/state.go
@@ -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) {