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) + 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 } 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..d69aeb60 100644 --- a/internal/rosa/state.go +++ b/internal/rosa/state.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "io" "net/http" "runtime" "slices" @@ -14,6 +15,7 @@ import ( "unique" "hakurei.app/internal/pkg" + "hakurei.app/internal/rosa/azalea" ) // ArtifactH is a handle of the unique name of a prepared [pkg.Artifact]. @@ -179,6 +181,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 +307,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 +328,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 +340,188 @@ 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 +} + +// azaleaPackage implements [azalea.PF]. +func (s *S) azaleaPackage( + 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 ( + 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("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 + } + + meta.ID = int(anitya) + meta.f = func(t Toolchain) (pkg.Artifact, string) { + return t.NewPackage( + meta.Name, + version, + source, + nil, + 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. +func (s *S) Evaluate(r io.Reader) 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 + } + } + + for _, f := range pending { + meta, set, err := azalea.Evaluate[Artifact](s.azaleaPackage, 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) {