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
205 lines
9.5 KiB
Go
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()...)
|
|
}
|