diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-05-18 01:59:51 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-05-18 02:56:38 +0900 |
| commit | 0360e779f3fb9dc770e1f33420ca5b8f8f638666 (patch) | |
| tree | e9278ec16068938633b04404d1169e89b9572778 /internal/rosa/state.go | |
| parent | 3e236333a77233cfcd96b6fc93be91e14ab7c680 (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.go | 233 |
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) { |
