Files
mesh-controller/internal/builder/toolchain.go
T
jschoubben 4b7bd1b3e2 A module says what it is written in, and needs no Dockerfile
The bundle recipe: the one that both builds and packs. An archive packs a
directory as it stands, so shipping compiled output meant compiling somewhere
first — which meant a Dockerfile repeating the same incantation in every module.
Two base arguments with no defaults, a working directory chosen so the SDK
resolves upward, the compiler invoked by absolute path because the usual symlink
is resolved away when the base image is assembled, a second stage, an environment
variable naming the entrypoints. Most of the catalogue is unconverted and that is
why; two conversions done in one session were each wrong twice with a working
example open in the next window.

A bundle says a language and a list of entrypoints. The mesh knows what the
language implies. Anything a module could override there it would be writing a
Dockerfile to override, so a toolchain is deliberately not configurable.

Declared rather than inferred, both of them: guessing the language from which
files are present makes a build depend on a directory listing, and guessing the
entrypoints makes it change meaning when somebody adds a helper.

A toolchain the mesh does not hold is refused before anything is compiled, naming
what to build first — the same treatment a missing base already gets, because it
is the same question and somebody can answer it. A language the mesh does not
build is refused saying what would have worked, since the author is usually one
word away.

The list of languages is closed and adding to it is a decision. Every language is
another implementation of the contracts every module shares, and those change
rarely and cascade when they do (ADR 0039) — a mesh whose SDKs disagree about the
envelope fails by ignoring messages rather than by failing to compile.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 01:59:07 +02:00

100 lines
3.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 goes to
// Output, which is what gets packed.
Compile []string
// Output is the directory the compiled result lands in, relative to the module's directory.
Output string
}
// 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", "--outDir", "dist",
},
Output: "dist",
},
}
// 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
}