Three faults in one path. A merge rebuilt every module built from the repository, so one change in a repository holding twenty-six of them meant twenty-six builds. A merge into a repository a module only *packages* source from rebuilt nothing — two modules are built from the control plane's own repository and neither had ever been rebuilt when it moved — because the manifest the mesh keeps carries no build section, so a build now says which repositories it read and the mesh keeps that beside what it stood on. And a module handed over by hand could record a repository with no directory inside it, which is a module nothing can ever rebuild (novox/hq 04-ISSUES/131, /132). A change inside no module's own directory is a change to what they share, and everything built from that repository is rebuilt: rebuilding too much is the safe direction, because the fault this whole path exists for is a mesh that believes it is current and is not.
182 lines
6.7 KiB
Go
182 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/internal/builder"
|
|
)
|
|
|
|
// buildOnce is the builder doing one build and stopping, with no broker and no mesh.
|
|
//
|
|
// **This is how a mesh is raised** (novox/hq ADR 0073). The installer carries this program and runs
|
|
// it once, before anything else exists, to produce the control plane from the same repository and
|
|
// path that every later rebuild of it will use. What raises the mesh is therefore the same thing
|
|
// that maintains it — not a second mechanism that has to be kept in step with the first and is
|
|
// exercised once per new mesh, which is how often enough to rot.
|
|
//
|
|
// It takes no work from a queue and answers nobody: there is no broker yet, and the only thing
|
|
// waiting for the answer is the installer that started it. So the result goes to standard output as
|
|
// JSON, which is what the installer reads.
|
|
//
|
|
// With no --registry it publishes nowhere and the image stays in this machine's container runtime,
|
|
// named by the digest of its own configuration (see builder.Local). That is the genesis case. With
|
|
// one, it publishes as it always does — the same command is how an operator builds a module by hand
|
|
// on a mesh that already exists.
|
|
func buildOnce(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("build", flag.ContinueOnError)
|
|
path := set.String("path", "", "the module's directory inside the repository (default: its root)")
|
|
ref := set.String("ref", "", "the commit to build; a branch is a moving target somebody else controls")
|
|
registry := set.String("registry", "",
|
|
"host:port to publish to. Without it the artifacts stay in this machine's container runtime, which is the genesis case")
|
|
workspace := set.String("workspace", "", "where to clone and build (default: a temporary directory)")
|
|
on := multiple(set, "on",
|
|
"a base this build stands on, as <module>/<artifact>=<reference>. Repeatable")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New("mesh-builder build <repository> [--path P] [--ref COMMIT] [--registry HOST:PORT]")
|
|
}
|
|
repository := positionals[0]
|
|
|
|
where := *workspace
|
|
if where == "" {
|
|
where = os.TempDir() + "/mesh-builder-once"
|
|
}
|
|
|
|
// What this build may stand on, said on the command line because there is no mesh to ask.
|
|
//
|
|
// **This is the genesis case and it is meant to be awkward.** On a running mesh the control
|
|
// plane answers this, because only it knows what this mesh holds. Here nothing has been built
|
|
// yet, so whoever runs this says what to use — and for the control plane, which is the one
|
|
// module raised before anything else exists, the answer is ordinarily nothing at all.
|
|
bases := map[string]string{}
|
|
for _, pair := range *on {
|
|
key, reference, found := strings.Cut(pair, "=")
|
|
if !found || key == "" || reference == "" {
|
|
return fmt.Errorf(
|
|
"--on takes <module>/<artifact>=<reference>, and %q is not that", pair)
|
|
}
|
|
bases[key] = reference
|
|
}
|
|
|
|
// Local unless told otherwise, because the moment this exists for has nowhere to publish. A
|
|
// default pointing at a registry would mean genesis failing at a push to something that is not
|
|
// there yet, one step away from the thing that could explain it.
|
|
var publisher builder.Publisher = builder.Local{Run: builder.Command}
|
|
if *registry != "" {
|
|
publisher = builder.Registry{Address: *registry, Run: builder.Command}
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "building %s", repository)
|
|
if *path != "" {
|
|
fmt.Fprintf(os.Stderr, " at %s", *path)
|
|
}
|
|
if *ref != "" {
|
|
fmt.Fprintf(os.Stderr, " at %s", *ref)
|
|
}
|
|
fmt.Fprintln(os.Stderr)
|
|
|
|
npmrc, err := packagesFrom()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases, npmrc,
|
|
forgeFrom(),
|
|
func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) })
|
|
if buildErr != nil {
|
|
return buildErr
|
|
}
|
|
|
|
// To standard output, and everything else to standard error, so the caller can read this
|
|
// without having to separate it from progress.
|
|
out := onceResult{
|
|
Module: built.Manifest.Module,
|
|
Commit: built.Commit,
|
|
Repository: repository,
|
|
Path: *path,
|
|
Ref: *ref,
|
|
Manifest: built.Manifest,
|
|
Against: built.Against,
|
|
}
|
|
for _, r := range built.Read {
|
|
out.Read = append(out.Read, readRepository{Repository: r.Repository, Ref: r.Ref})
|
|
}
|
|
for _, made := range built.Built {
|
|
out.Made = append(out.Made, madeArtifact{Name: made.Name, Kind: made.Kind, Reference: made.Reference})
|
|
}
|
|
body, err := json.MarshalIndent(out, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(body))
|
|
fmt.Fprintf(os.Stderr, " built %s from %s\n", built.Manifest.Module, short(built.Commit))
|
|
return nil
|
|
}
|
|
|
|
// onceResult is what one build reports to whoever started it.
|
|
//
|
|
// The same fields the mesh records for a build, so a reader comparing a genesis build against an
|
|
// ordinary one is comparing the same thing said the same way.
|
|
type onceResult struct {
|
|
Module string `json:"module"`
|
|
Commit string `json:"commit"`
|
|
Repository string `json:"repository"`
|
|
Path string `json:"path,omitempty"`
|
|
Ref string `json:"ref,omitempty"`
|
|
Manifest any `json:"manifest"`
|
|
Made []madeArtifact `json:"made"`
|
|
Against []string `json:"against,omitempty"`
|
|
Read []readRepository `json:"read,omitempty"`
|
|
}
|
|
|
|
// readRepository is a repository this build read source from besides the module's own.
|
|
type readRepository struct {
|
|
Repository string `json:"repository"`
|
|
Ref string `json:"ref,omitempty"`
|
|
}
|
|
|
|
type madeArtifact struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Reference string `json:"reference"`
|
|
}
|
|
|
|
// parseAround lets flags appear on either side of the repository, because a person writing this by
|
|
// hand will put them wherever reads best and the standard parser stops at the first thing that is
|
|
// not a flag. The same helper the control plane's commands use, for the same reason.
|
|
func parseAround(set *flag.FlagSet, args []string) ([]string, error) {
|
|
var positionals []string
|
|
rest := args
|
|
for {
|
|
if err := set.Parse(rest); err != nil {
|
|
return nil, err
|
|
}
|
|
rest = set.Args()
|
|
if len(rest) == 0 {
|
|
return positionals, nil
|
|
}
|
|
positionals = append(positionals, rest[0])
|
|
rest = rest[1:]
|
|
}
|
|
}
|
|
|
|
// multiple is a flag that may be given more than once.
|
|
type repeated []string
|
|
|
|
func (r *repeated) String() string { return strings.Join(*r, ", ") }
|
|
func (r *repeated) Set(v string) error { *r = append(*r, v); return nil }
|
|
|
|
func multiple(set *flag.FlagSet, name, usage string) *[]string {
|
|
var values repeated
|
|
set.Var(&values, name, usage)
|
|
return (*[]string)(&values)
|
|
}
|