Files
mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go/build.go
T
jschoubben 82d00aeb5e Builders, the artifact store's tools and distribution read and write the registry reference form through the registry clients' reference package, and name the sweep seat from its contract (ADR 0634 §4, S-8; issue 1966)
catalogue's SeatReference, InRegistrySeat, Routed, Recorded, InArtifactStore and ArtifactSweepSeat leave mesh-sdk;
each package gives the two registry seats' names from their contracts (or the build step's copies of them).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 17:26:32 +01:00

205 lines
9.5 KiB
Go

package main
// The build step (novox/hq ADR 0405 §3 §7, ADR 0453 §7; issue 1487): a Go bundle artifact of a module of a fetched
// workspace, built with today's recipe — the build agent's, mesh-controller's internal/builder/toolchain.go (the Go
// entry) and builder.go (compile, binaryName, pack) — by the installed Go and no network, packed with mesh-sdk's Pack
// so that its digest is the build agent's for the same tree, and pushed to the artifact store by this process once the
// build ended (ADR 0498 §1 §2, push.go).
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
artifactref "git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient/reference"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The Go recipe of the build agent (toolchain.go, the entry of language go): static, trimmed of its own paths and of
// the commit stamp, stripped of symbols and debug info, and told the system it was built for.
const (
recipeFlags = "-trimpath -buildvcs=false"
recipeLinkerFlags = "-s -w"
recipeSystemStamp = "main.builtFor"
// outDir is where a build leaves an artifact in the module's directory (builder.Out).
outDir = ".mesh-build"
)
// BuildResult is what a passed build step says.
type BuildResult struct {
Artifact string `json:"artifact"`
Binary string `json:"binary"`
Digest string `json:"digest"`
Size int `json:"size"`
// Reference is where the archive is, in the form the seat it was published through answers (novox/hq ADR 0605
// §9): oci-artifact-registry/<module>/<artifact>@<digest>. The builder writes the older form beside it while a
// counted node-engine reads only that (ADR 0624 §1, §3).
Reference string `json:"reference"`
}
func (m *Module) buildTool() StepTool {
return StepTool{
Name: "build_go_build",
Description: "Build one Go bundle, archive or Go package artifact of a module of a fetched workspace with the build agent's " +
"recipe, by the installed Go and no network, and pack it (a Go package as Go's module zip); a bundle or archive is " +
"published on oci-artifact-registry, a Go package given a version on go-package-registry under its module path. " +
"Answers a step id at once; the step ends on its key with the artifact's digest, size and reference.",
Input: map[string]any{
"type": "object",
"properties": map[string]any{
"workspace": map[string]any{"type": "string", "description": "the workspace id build_go_fetch answered"},
"module": map[string]any{"type": "string", "description": "the module's directory in the workspace, . for the repository's root"},
"artifact": map[string]any{"type": "string", "description": "the artifact's name in the module's manifest"},
"version": map[string]any{"type": "string", "description": "the version the builder gave a package at the trunk's claim; absent for a candidate, whose package is not published"},
"seat": map[string]any{"type": "string", "description": "the registry seat the artifact is published through, as the build ask names it"},
"publish": map[string]any{"type": "boolean", "description": "true for a merged trunk commit alone: the build writes the shared Go caches; without it (a candidate) they are read beneath a throwaway layer"},
},
"required": []string{"workspace", "module", "artifact"},
},
Long: true,
Run: m.build,
}
}
// binaryName is what a compiled bundle's executable is called (builder.go binaryName): what the artifact says, or the
// name of the package it is built from, or the artifact's.
func binaryName(a catalogue.Artifact) string {
if name := catalogue.BinaryOf(a); name != "" {
return name
}
if name := strings.TrimSpace(a.Binary); name != "" {
return name
}
if from := strings.Trim(a.From, "./"); from != "" {
return filepath.Base(from)
}
return a.Name
}
func (m *Module) build(ctx context.Context, args map[string]any) (any, error) {
workspace, _ := args["workspace"].(string)
module, _ := args["module"].(string)
name, _ := args["artifact"].(string)
publish, _ := args["publish"].(bool)
if !workspaceOK.MatchString(workspace) {
return nil, fmt.Errorf("%q is not a workspace id", workspace)
}
tree := filepath.Join(m.Work, workspace, "tree")
dir := filepath.Join(tree, filepath.FromSlash(module))
if rel, err := filepath.Rel(tree, dir); err != nil || strings.HasPrefix(rel, "..") {
return nil, fmt.Errorf("the module directory %q lies outside the workspace", module)
}
raw, err := os.ReadFile(filepath.Join(dir, "module.json"))
if err != nil {
return nil, fmt.Errorf("no module in %q of workspace %s: %w", module, workspace, err)
}
manifest, err := catalogue.Decode(raw)
if err != nil {
return nil, fmt.Errorf("the manifest in %q: %w", module, err)
}
var art *catalogue.Artifact
names := []string{}
if manifest.Build != nil {
for i := range manifest.Build.Artifacts {
names = append(names, manifest.Build.Artifacts[i].Name)
if manifest.Build.Artifacts[i].Name == name {
art = &manifest.Build.Artifacts[i]
}
}
}
if art == nil {
return nil, fmt.Errorf("the module %s has no artifact %q (it has %s)", manifest.Module, name, strings.Join(names, ", "))
}
if art.Kind == catalogue.ArtifactPackage && art.Language == "go" {
version, _ := args["version"].(string)
seat, _ := args["seat"].(string)
return m.buildGoPackage(ctx, workspace, dir, *art, version, seat)
}
if art.Kind == "archive" {
return m.buildArchive(ctx, workspace, module, manifest.Module, dir, *art)
}
if art.Kind != "bundle" || art.Language != "go" {
return nil, fmt.Errorf("the artifact %s of %s is a %s of language %q: this module builds Go bundles and archives", name, manifest.Module, art.Kind, art.Language)
}
if strings.TrimSpace(art.From) == "" {
return nil, fmt.Errorf("the artifact %s of %s says nothing it is built from", name, manifest.Module)
}
// Its own output directory, made empty: a build of the same tree twice is one build.
out := filepath.Join(dir, outDir, art.Name)
if err := m.insideModule(workspace, dir, out); err != nil {
return nil, fmt.Errorf("the output directory of %s: %w", art.Name, err)
}
if err := os.RemoveAll(out); err != nil {
return nil, err
}
if err := os.MkdirAll(out, 0o755); err != nil {
return nil, fmt.Errorf("making the output directory for %s: %w", art.Name, err)
}
binary := binaryName(*art)
// One -ldflags, composed: a repeated flag is not a merged one (the build agent's issue 161).
linker := recipeLinkerFlags
if system := strings.TrimSpace(art.System); system != "" {
linker += " -X " + recipeSystemStamp + "=" + system
}
cmd := []string{"-C", dir, "build", "-ldflags", linker, "-o", filepath.Join(out, binary), "./" + strings.Trim(art.From, "./")}
// Shut off (ADR 0448): the shared caches are written by a trunk build alone (publish), a candidate's go build writes
// beneath a throwaway layer.
run, err := m.stepRunner(workspace, publish)
if err != nil {
return nil, err
}
if r := run(ctx, m.buildEnv(dir), "go", cmd...); r.Status != 0 || r.Err != "" {
return nil, fmt.Errorf("go build of %s: %s", art.Name, causeLine(r.Err+"\n"+r.Stderr+"\n"+r.Stdout))
}
if err := m.insideModule(workspace, dir, out); err != nil {
return nil, fmt.Errorf("the output directory of %s: %w", art.Name, err)
}
res, err := m.packInto(ctx, workspace, module, manifest.Module, art.Name, "bundle", out)
if err != nil {
return nil, err
}
res.Binary = binary
return res, nil
}
// packInto packs a directory with mesh-sdk's Pack, as the build agent packs every artifact, and publishes the archive
// on the seat oci-artifact-registry from this process, after the build step ended (design 51 *Publishing*; publish.go):
// a candidate's as a trunk build's, under <module>/<artifact> by its digest, of kind (bundle or archive). The step
// answers the reference; a publish the seat refuses fails the step `not stored:`. The packed bytes never stay on the
// node: they are held in this process until the publish answered (the lead's decision L9).
func (m *Module) packInto(ctx context.Context, workspace, module, name, artifact, kind, root string) (BuildResult, error) {
archive, digest, err := buildstep.Pack(root)
if err != nil {
return BuildResult{}, fmt.Errorf("packing %s: %w", artifact, err)
}
if err := m.publishArchive(ctx, name, artifact, kind, digest, archive); err != nil {
return BuildResult{}, fmt.Errorf("%s%s: %v", builds.NotStored, artifact, err)
}
return BuildResult{Artifact: artifact, Digest: digest, Size: len(archive), Reference: artifactref.SeatReference(ArtifactSeat, name, artifact, digest)}, nil
}
// buildEnv is the environment of the compile: the build agent's (CGO_ENABLED=0, GOFLAGS) with the installed go
// alone (GOTOOLCHAIN=local), the modules the fetch put in the module's cache and no way to fetch another (GOPROXY=off), so a build never
// reaches the network. A directory that vendors is built from its vendor directory.
func (m *Module) buildEnv(dir string) []string {
mod := "-mod=mod"
if _, err := os.Stat(filepath.Join(dir, "vendor", "modules.txt")); err == nil {
mod = "-mod=vendor"
} else if modfile, ok := m.pinModfile(dir); ok {
mod += " -modfile=" + modfile // a pin on a pull request's head the fetch placed beside the workspace (pins.go)
}
return append([]string{
"CGO_ENABLED=0",
"GOFLAGS=" + recipeFlags + " " + mod,
"GOTOOLCHAIN=local",
"GOMODCACHE=" + filepath.Join(m.Cache, "gomod"),
"GOCACHE=" + filepath.Join(m.Cache, "gobuild"),
"GOPROXY=off",
}, m.threadEnv()...)
}