A module names the module its build stands on, not a copy of it

A fingerprint written into a recipe names one particular copy of the base — the
copy on whichever machine the person typing it was using. On any other mesh that
copy has never existed, so the build stops on its first line with a message
about an image nobody can look up. Three modules in the catalogue were in
exactly that state, and the line each of them replaced was equally dead.

A module now names the module and artifact instead, and the mesh answers with
what it holds. The builder is still a thing that clones, builds and answers: the
answer travels with the question, because only the mesh knows what it has.

A base the mesh has not built is refused before anything is built, naming which
module has to exist first.
This commit is contained in:
2026-09-13 23:53:22 +02:00
parent cb5108a864
commit cfe2816495
9 changed files with 269 additions and 16 deletions
+1 -1
View File
@@ -184,7 +184,7 @@ func answer(ctx context.Context, channel *amqp.Channel, publisher builder.Publis
fmt.Println()
built, err := builder.Build(ctx, builder.Command, publisher,
request.Repository, request.Path, request.Ref, workspace)
request.Repository, request.Path, request.Ref, workspace, request.Held)
if err != nil {
// A failure is a result. A build that fails and says nothing is indistinguishable from a
// builder that is not running, and those want completely different responses.
+32 -1
View File
@@ -7,6 +7,7 @@ import (
"flag"
"fmt"
"os"
"strings"
"github.com/novox/mesh-control/internal/builder"
)
@@ -34,6 +35,8 @@ func buildOnce(ctx context.Context, args []string) error {
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
@@ -48,6 +51,22 @@ func buildOnce(ctx context.Context, args []string) error {
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.
@@ -65,7 +84,7 @@ func buildOnce(ctx context.Context, args []string) error {
}
fmt.Fprintln(os.Stderr)
built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where)
built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases)
if buildErr != nil {
return buildErr
}
@@ -132,3 +151,15 @@ func parseAround(set *flag.FlagSet, args []string) ([]string, error) {
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)
}
+25
View File
@@ -8,6 +8,7 @@ import (
"errors"
"flag"
"fmt"
"os"
"strings"
"time"
@@ -341,6 +342,7 @@ func buildOne(ctx context.Context, repository, path, ref string, wait time.Durat
Repository: repository,
Path: path,
Ref: ref,
Held: heldBy(ctx),
}
fmt.Printf("asked for %s", request.Repository)
if path != "" {
@@ -417,6 +419,7 @@ func buildAndShow(ctx context.Context, repository, path, ref string, wait time.D
result, err := link.RequestBuild(ctx, server.Channel(), link.BuildRequest{
ID: fmt.Sprintf("%s-%d", "build", time.Now().UnixNano()),
Repository: repository, Path: path, Ref: ref,
Held: heldBy(ctx),
}, wait)
if err != nil {
return err
@@ -460,3 +463,25 @@ type answers struct {
// a mesh whose hub is that node has no hub.
network string
}
// heldBy is every artifact this mesh has built, for a build that may need one as its base.
//
// **A failure here is not a failure to build.** A module that names no base does not need this at
// all, and one that does gets a refusal naming exactly what is missing — which is a better sentence
// than a build command refusing to start because a query did not run. So the store not opening is
// reported and the build goes ahead without it.
func heldBy(ctx context.Context) map[string]string {
open, err := openStores(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read what this mesh has built, so a module naming a "+
"base will be told that base is missing: %v\n", err)
return nil
}
defer open.Close()
held, err := open.inventory.Held(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read what this mesh has built: %v\n", err)
return nil
}
return held
}