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 /=. Repeatable") positionals, err := parseAround(set, args) if err != nil { return err } if len(positionals) != 1 { return errors.New("mesh-builder build [--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 /=, 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) }