novox/hq 04-ISSUES/161. The mesh compiled the host, published it, delivered it, and the launcher started it — and it would have refused the first declaration it was asked to apply, because it asks which system it was built for before applying anything and the answer was empty. The Makefile links that in. The mesh's toolchain deliberately takes nothing from the module, so it linked in nothing. The system is the stated exception, and ADR 0142 says why: the target is a property of the artifact rather than of the recipe, because a compiled binary is per system and a toolchain accepting it from the module would be accepting a build instruction. So the toolchain names the variable it fills and the artifact supplies the value. Named in the toolchain rather than inferred, and empty for a language whose output is not pinned to a system — which is every interpreted one, and a manifest declaring a system for those is already refused.
194 lines
8.9 KiB
Go
194 lines
8.9 KiB
Go
package builder
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// What a language implies, so a module does not have to say it.
|
|
//
|
|
// **A module says what it is written in; this says what that means.** The alternative is what the
|
|
// mesh had: every module carrying a Dockerfile that repeated the same incantation, and most of the
|
|
// catalogue never converted because the incantation is easy to get wrong in ways that fail
|
|
// somewhere else (novox/hq 03-DESIGN/01-to-be/18-building-a-module.md).
|
|
//
|
|
// A toolchain is deliberately not configurable by the module. Anything a module could override
|
|
// here it would be writing a Dockerfile to override, and then this bought nothing.
|
|
|
|
// Toolchain is how one language is compiled into a bundle.
|
|
type Toolchain struct {
|
|
// Language is what a module declares to select this.
|
|
Language string
|
|
// Base is the module whose artifact provides the compiler, named rather than pinned: the mesh
|
|
// answers with the copy it holds, so a recipe never names one particular build of it
|
|
// (novox/hq 04-ISSUES/044).
|
|
Base string
|
|
// Artifact is which of that module's artifacts is the compiling one.
|
|
Artifact string
|
|
// Compile is what runs inside it, relative to the module's own directory.
|
|
//
|
|
// The output directory is appended by the builder, per artifact, because one module may
|
|
// declare several bundles — a daemon in one language, tools in another, a package in a third —
|
|
// and a toolchain with one fixed output would have them overwrite each other and then be
|
|
// packed together.
|
|
Compile []string
|
|
// OutputFlag is how this compiler is told where to put its output.
|
|
OutputFlag string
|
|
// Unit is what this compiler is pointed at: UnitSources, the entrypoint files the module named,
|
|
// or UnitPackage, the one directory the artifact is built `from`.
|
|
//
|
|
// **The difference is the language and not the module.** A TypeScript bundle is a set of files
|
|
// compiled into a set of files, so what to compile is the module's entrypoints with their source
|
|
// extension. A Go bundle is a package compiled into one binary, and there is no per-file
|
|
// compilation to name — pointing `go build` at a file list builds a program out of exactly those
|
|
// files and ignores the rest of the package, which fails as a missing symbol rather than as a
|
|
// wrong instruction.
|
|
Unit string
|
|
// SourceExt is the extension an entrypoint has in the repository, for UnitSources. An entrypoint
|
|
// is named as it will be FOUND, inside the unpacked bundle, so the source is the same path with
|
|
// the output directory taken off the front and this on the end.
|
|
SourceExt string
|
|
// SystemStamp is the variable this language's linker fills with the artifact's declared system,
|
|
// for a language whose binaries are pinned to one at link time (novox/hq ADR 0005).
|
|
//
|
|
// **The one thing a toolchain takes from the artifact, and 0142 says why**: the target is a
|
|
// property of the artifact rather than of the recipe, because a compiled binary is per system
|
|
// and a toolchain that accepted it from the module would be accepting a build instruction. This
|
|
// is the narrow exception, named here rather than inferred.
|
|
//
|
|
// Empty for a language that compiles to nothing pinned. A host built without it refuses every
|
|
// declaration before applying anything — safely, totally, and with nothing reporting it
|
|
// (novox/hq 04-ISSUES/161).
|
|
SystemStamp string
|
|
}
|
|
|
|
// What a toolchain is pointed at.
|
|
const (
|
|
// UnitSources is a list of files, derived from the module's entrypoints.
|
|
UnitSources = "sources"
|
|
// UnitPackage is the single directory the artifact is built `from`, compiled whole.
|
|
UnitPackage = "package"
|
|
)
|
|
|
|
// Out is where one artifact's compiled output lands, inside the module's own directory.
|
|
//
|
|
// **Per artifact, never per toolchain.** A module is one piece of software and may still be
|
|
// written in several languages — a daemon in one, a tool in another, a package in a third (ADR
|
|
// 0040). Each bundle is compiled and packed alone, so what a machine unpacks is that artifact and
|
|
// nothing else.
|
|
//
|
|
// Under a directory named for the build rather than beside the source, so a pack never sweeps up
|
|
// the module's own working files, and two builds of one commit see the same tree.
|
|
func Out(artifact string) string { return ".mesh-build/" + artifact }
|
|
|
|
// toolchains is every language the mesh can build.
|
|
//
|
|
// **A closed list, and adding to it is a decision rather than a configuration.** Every language is
|
|
// permanent: it needs an SDK carrying the broker client, sealed-credential reading, the event
|
|
// envelope and tool serving, and the contracts every module shares change rarely and cascade when
|
|
// they do (novox/hq ADR 0039). A mesh whose languages disagree about the envelope fails by ignoring
|
|
// messages rather than by failing to compile, so a new entry here is a commitment to keeping N
|
|
// implementations of one contract in step.
|
|
var toolchains = []Toolchain{
|
|
{
|
|
Language: "typescript",
|
|
Base: "mesh-tools",
|
|
Artifact: "build",
|
|
// Invoked by its real path rather than through node_modules/.bin, whose entries are
|
|
// symlinks to a launcher that requires its library relatively — and the base image's own
|
|
// assembly resolves them away, leaving a launcher whose relative require points nowhere.
|
|
// Every module's hand-written Dockerfile had to know this. Now none of them does.
|
|
Compile: []string{
|
|
"node", "/app/node_modules/typescript/bin/tsc",
|
|
"--module", "NodeNext", "--moduleResolution", "NodeNext",
|
|
"--target", "ES2022",
|
|
},
|
|
OutputFlag: "--outDir",
|
|
Unit: UnitSources,
|
|
SourceExt: ".ts",
|
|
},
|
|
{
|
|
Language: "go",
|
|
Base: "mesh-tools-go",
|
|
Artifact: "build",
|
|
// **The mesh's own components, and not modules.** The warning above this list — that every
|
|
// language is another implementation of the contracts modules share, so adding one commits
|
|
// to keeping N implementations in step — does not attach here. Go is how the host, the
|
|
// control plane and the builder are written, and none of them is a module in that sense:
|
|
// the host is what APPLIES modules. So there is no SDK obligation, and the reason this
|
|
// entry did not exist was that nothing needed to compile the mesh itself
|
|
// (novox/hq ADR 0142, and 04-ISSUES/142 where that is why nothing delivers the host).
|
|
//
|
|
// Static, because what a machine ends up holding is a file rather than a container, and a
|
|
// binary that needs a libc it did not bring is a delivery that works until a machine
|
|
// differs. Trimmed of its own paths for the same reason a version comes from where it sits
|
|
// rather than from the linker: two builds of one commit produce the same bytes.
|
|
Compile: []string{
|
|
"env", "CGO_ENABLED=0", "GOFLAGS=-trimpath",
|
|
"go", "build", "-ldflags", "-s -w",
|
|
},
|
|
OutputFlag: "-o",
|
|
// Pointed at the package the artifact is built `from`, compiled whole. Go writes the binary
|
|
// into the output directory, named after the package — so the bundle a machine unpacks is a
|
|
// directory holding one executable, which is what the delivery mechanism expects
|
|
// (novox/hq ADR 0141).
|
|
Unit: UnitPackage,
|
|
// The mesh's own Go components read the system they were built for from this variable, and
|
|
// refuse to touch a machine without one.
|
|
SystemStamp: "main.builtFor",
|
|
},
|
|
{
|
|
Language: "python",
|
|
Base: "mesh-tools-python",
|
|
Artifact: "build",
|
|
// Nothing to compile: what a bundle needs is the module's own code and its dependencies
|
|
// resolved, so the "compile" is an install into the output directory. Named here rather
|
|
// than left implicit because a reader comparing two toolchains should be able to see what
|
|
// each actually does.
|
|
Compile: []string{"python", "-m", "pip", "install", "--no-compile", "--target"},
|
|
OutputFlag: "",
|
|
Unit: UnitSources,
|
|
SourceExt: ".py",
|
|
},
|
|
}
|
|
|
|
// ToolchainFor is what builds this language, or says what it can build.
|
|
func ToolchainFor(language string) (Toolchain, error) {
|
|
want := strings.ToLower(strings.TrimSpace(language))
|
|
if want == "" {
|
|
return Toolchain{}, fmt.Errorf(
|
|
"a bundle must say what language it is written in: the mesh chooses the compiler, and "+
|
|
"it cannot choose one for a module that has not said. It can build %s", spoken())
|
|
}
|
|
for _, t := range toolchains {
|
|
if t.Language == want {
|
|
return t, nil
|
|
}
|
|
}
|
|
return Toolchain{}, fmt.Errorf(
|
|
"%q is not a language this mesh builds. It can build %s — and adding one is a decision "+
|
|
"rather than a setting, because every language is another implementation of the "+
|
|
"contracts every module shares", language, spoken())
|
|
}
|
|
|
|
// spoken lists the languages, so a refusal says what would have worked.
|
|
func spoken() string {
|
|
names := make([]string, 0, len(toolchains))
|
|
for _, t := range toolchains {
|
|
names = append(names, t.Language)
|
|
}
|
|
sort.Strings(names)
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
// Languages is every language the mesh can build, for anything that wants to say so.
|
|
func Languages() []string {
|
|
names := make([]string, 0, len(toolchains))
|
|
for _, t := range toolchains {
|
|
names = append(names, t.Language)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|