novox/hq 04-ISSUES/142, and ADR 0141's own progressive insight naming this as the first of two things missing: "nothing can compile it". The toolchain list was a closed set of typescript and python, whose warning — every language is another implementation of the contracts modules share — does not attach to Go. 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. The toolchain names mesh-tools-go as its base rather than pinning an upstream release here (ADR 0142, 0044): named and not pinned means the mesh answers with the copy it holds, and moving compiler is a build instead of an edit to this file. Two things beyond the list also assumed one language, and both would have failed after the entry was added: sourcesFor turned every entrypoint into a `.ts` file. The extension is the toolchain's now — one language's file extension written into the code that serves every language is a wall the next one hits. The output directory was the compiler's to create. tsc --outDir makes one; go build -o writes into a directory and does not make it, failing with a message about a path rather than about a build. Made here for every toolchain, because which compilers are forgiving is not something a reader should have to know. And a toolchain now says what it is pointed at: a file list from the module's entrypoints, or the one package the artifact is built `from`. Pointing `go build` at a file list builds a program out of exactly those files and ignores the rest of the package — a missing symbol rather than a legible refusal. Static and -trimpath: what a machine holds is a file, not a container, so a binary needing a libc it did not bring is a delivery that works until a machine differs; and a version comes from where a component sits rather than from its linker, so two builds of one commit are the same bytes.
179 lines
8.0 KiB
Go
179 lines
8.0 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
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
{
|
|
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
|
|
}
|