forked from rosa/hakurei
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>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user