A runtime compiled to a binary runs itself (hq ADR 0193)

A compiled bundle records the binary it is (BinaryOf, shared by the builder and the composer), and
the node's runtime, when it is one, is run as ./<binary> from its own unpacked bundle rather than by
an interpreter and an entrypoint.
This commit is contained in:
jochen
2026-10-03 21:24:12 +02:00
parent 06ea2168d8
commit 9204190445
5 changed files with 69 additions and 9 deletions
+3
View File
@@ -1171,6 +1171,9 @@ func readBy(manifest catalogue.Manifest) []catalogue.ArtifactContext {
// binaryName is what a compiled bundle's executable is called: what the artifact says, or the name of // binaryName is what a compiled bundle's executable is called: what the artifact says, or the name of
// the package it is built from, which is what a compiler would have chosen anyway. // the package it is built from, which is what a compiler would have chosen anyway.
func binaryName(a catalogue.Artifact) string { func binaryName(a catalogue.Artifact) string {
if name := catalogue.BinaryOf(a); name != "" {
return name
}
if name := strings.TrimSpace(a.Binary); name != "" { if name := strings.TrimSpace(a.Binary); name != "" {
return name return name
} }
+19
View File
@@ -2,6 +2,7 @@ package catalogue
import ( import (
"fmt" "fmt"
"path"
"sort" "sort"
"strings" "strings"
) )
@@ -96,6 +97,7 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
Name: a.Name, Source: Recorded(made.Reference), Digest: made.Digest, Name: a.Name, Source: Recorded(made.Reference), Digest: made.Digest,
Language: a.Language, Entrypoints: append([]string(nil), a.Entrypoints...), Language: a.Language, Entrypoints: append([]string(nil), a.Entrypoints...),
Loads: loads, Env: copyWords(a.Env), Launchers: copyWords(made.Launchers), Loads: loads, Env: copyWords(a.Env), Launchers: copyWords(made.Launchers),
Binary: BinaryOf(a),
}) })
} }
sort.Slice(out.Bundles, func(i, j int) bool { return out.Bundles[i].Name < out.Bundles[j].Name }) sort.Slice(out.Bundles, func(i, j int) bool { return out.Bundles[i].Name < out.Bundles[j].Name })
@@ -417,3 +419,20 @@ func copyWords(in map[string]string) map[string]string {
} }
return out return out
} }
// BinaryOf is what a bundle compiled to a binary is called once built: what the artifact names, else
// the package it is built from, else the artifact's own name (novox/hq 04-ISSUES/142). Empty for a
// language that does not compile to one. The builder writes the binary under this name, and the
// composer runs it by it, so both ask here.
func BinaryOf(a Artifact) string {
if !compilesToABinary(a.Language) {
return ""
}
if name := strings.TrimSpace(a.Binary); name != "" {
return name
}
if from := strings.Trim(a.From, "./"); from != "" {
return path.Base(from)
}
return a.Name
}
+3
View File
@@ -607,6 +607,9 @@ type Bundle struct {
// Launchers are the executables the build wrote beside its entrypoints, by entrypoint (novox/hq // Launchers are the executables the build wrote beside its entrypoints, by entrypoint (novox/hq
// ADR 0193). A bundle built before them has none, and is served as it was built. // ADR 0193). A bundle built before them has none, and is served as it was built.
Launchers map[string]string `json:"launchers,omitempty"` Launchers map[string]string `json:"launchers,omitempty"`
// Binary is the executable a bundle compiled to a binary is, at its root (novox/hq ADR 0193):
// what runs it, where an interpreted bundle names an interpreter and an entrypoint.
Binary string `json:"binary,omitempty"`
} }
// Build says how to produce this module's artifacts from its source. // Build says how to produce this module's artifacts from its source.
+17 -9
View File
@@ -129,14 +129,22 @@ func (r Resolution) runtimeProcess(with Rendering) (map[string]any, error) {
RuntimeModule, r.Node, len(runtime.Bundles)) RuntimeModule, r.Node, len(runtime.Bundles))
} }
bundle := runtime.Bundles[0] bundle := runtime.Bundles[0]
if len(bundle.Entrypoints) != 1 { // What runs it (novox/hq ADR 0193): a runtime compiled to a binary runs itself, from its own
return nil, fmt.Errorf( // unpacked bundle; an interpreted one is its language's interpreter and its one entrypoint.
"%s's bundle %q names %d entrypoint(s); the runtime is run from one, so the module "+ var run []any
"declares exactly one (novox/hq to-be 38)", RuntimeModule, bundle.Name, len(bundle.Entrypoints)) if bundle.Binary != "" {
} run = []any{"./" + bundle.Binary}
interpreter, err := interpreterFor(bundle.Language) } else {
if err != nil { if len(bundle.Entrypoints) != 1 {
return nil, err return nil, fmt.Errorf(
"%s's bundle %q names %d entrypoint(s); the runtime is run from one, so the module "+
"declares exactly one (novox/hq to-be 38)", RuntimeModule, bundle.Name, len(bundle.Entrypoints))
}
interpreter, err := interpreterFor(bundle.Language)
if err != nil {
return nil, err
}
run = []any{interpreter, bundle.Entrypoints[0]}
} }
credential, declared := runtime.OwnSecrets["broker"] credential, declared := runtime.OwnSecrets["broker"]
if !declared { if !declared {
@@ -198,7 +206,7 @@ func (r Resolution) runtimeProcess(with Rendering) (map[string]any, error) {
process := map[string]any{ process := map[string]any{
"id": RuntimeModule + "." + RuntimeProcessID(), "type": "process", "name": RuntimeModule, "id": RuntimeModule + "." + RuntimeProcessID(), "type": "process", "name": RuntimeModule,
"source": bundle.Source, "digest": bundle.Digest, "source": bundle.Source, "digest": bundle.Digest,
"run": []any{interpreter, bundle.Entrypoints[0]}, "run": run,
"env": env, "env": env,
"restart-on": toAny(restartOn), "restart-on": toAny(restartOn),
} }
+27
View File
@@ -362,3 +362,30 @@ func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) {
t.Errorf("the runtime is told %q, want %q", env[RuntimeToolModules], want) t.Errorf("the runtime is told %q, want %q", env[RuntimeToolModules], want)
} }
} }
// novox/hq ADR 0193: a runtime compiled to a binary runs itself from its own unpacked bundle.
func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) {
with := Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}}
goRuntime := Manifest{Module: RuntimeModule, Version: "1",
OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}},
Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "go",
System: "arch", From: "cmd/node-tools"}}}}
goRuntime, err := goRuntime.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle,
Reference: ArtifactStoreScheme + RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{aToolsModule(t, "nftables", "tools/index.js"), goRuntime}}.Declaration(with)
if err != nil {
t.Fatal(err)
}
process := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())
if fmt.Sprint(process["run"]) != "[./node-tools]" {
t.Errorf("a Go runtime is run as %v, want its own binary", process["run"])
}
env := process["env"].(map[string]string)
if env[RuntimeToolModules] == "" || process["user"] != "ops" {
t.Errorf("the Go runtime is not told what to serve or whose it is: %v %v", env, process["user"])
}
}