aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/rosa/state.go
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/rosa/state.go
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/rosa/state.go')
-rw-r--r--internal/rosa/state.go233
1 files changed, 230 insertions, 3 deletions
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) {