Files
jschoubben e3748bc06f One module, several languages, each bundle packed alone
A module is one piece of software and may still carry a daemon in one language,
tools in another and a package in a third. The first cut compiled every bundle
into the toolchain's single output directory, so two of them would have
overwritten each other and then been packed together — one artifact containing
both, published twice.

So output is a property of the artifact, not of the toolchain, and the toolchain
says how it is told where to write rather than where it writes. Under a directory
named for the build rather than beside the source, so a pack never sweeps up the
module's own working files.

A second toolchain is declared so the multi-language path is exercised rather
than asserted — a list with one entry cannot fail the way a list with four will.

And the fake compiler in the tests now writes where it was TOLD to. One that
always wrote to a fixed place would have passed whether or not each artifact got
its own directory, which is the whole of what these tests are for.

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

126 lines
5.2 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
}
// 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",
},
{
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: "",
},
}
// 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
}