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 // LinkerFlags are passed to the linker as one flag, together with the system stamp below. // // **Separate from Compile because a repeated flag is not a merged one.** They were in the compile // line, and appending the stamp as a second `-ldflags` meant the Go command took the last and // dropped the first — so the binary gained its system and lost `-s -w`, growing by half and // carrying its debug info. The mistake was believing a comment rather than reading the file it // produced (novox/hq 04-ISSUES/161). LinkerFlags []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", }, // Stripped of symbols and debug info: what a machine holds is a file it runs, not one it // debugs, and the difference measured 12.2MB against 8.5MB. LinkerFlags: []string{"-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 }