From d7375c9811fee2e04f1a620666d7081b3d2e232d Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 16:12:23 +0200 Subject: [PATCH] Add mesh-pack: a hand build with the build module's recipe and mesh-sdk's own pack (F1) --- go/cmd/mesh-pack/main.go | 111 ++++++++++++++++++++++++++++++++++ go/cmd/mesh-pack/main_test.go | 52 ++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 go/cmd/mesh-pack/main.go create mode 100644 go/cmd/mesh-pack/main_test.go diff --git a/go/cmd/mesh-pack/main.go b/go/cmd/mesh-pack/main.go new file mode 100644 index 0000000..80e7728 --- /dev/null +++ b/go/cmd/mesh-pack/main.go @@ -0,0 +1,111 @@ +// mesh-pack builds a Go bundle by hand with the recipe of the mesh-build-go module and packs it with mesh-sdk's own +// buildstep.Pack, printing the digest, so a hand build has the form of a built artifact and, with the same Go version, +// its digest (novox/hq ADR 0526; fallback record F1; issue 1499 for the Go version). +// +// mesh-pack -dir -from -binary [-system arch] [-out ] +// mesh-pack -pack [-out ] (a `files` archive: pack a tree as it is) +// +// The recipe is mesh-catalog modules/mesh-build-go cmd/mesh-build-go/build.go (recipeFlags, recipeLinkerFlags, +// recipeSystemStamp, buildEnv): CGO_ENABLED=0, GOFLAGS "-trimpath -buildvcs=false -mod=mod" (-mod=vendor when the +// directory vendors), GOTOOLCHAIN=local, GOPROXY=off, and -ldflags "-s -w -X main.builtFor=". +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "git.novox.be/novox/mesh-sdk/go/buildstep" +) + +const ( + recipeFlags = "-trimpath -buildvcs=false" + recipeLinkerFlags = "-s -w" + recipeSystemStamp = "main.builtFor" +) + +// Env is the build module's environment for the compile of dir, added to the caller's own. +func Env(dir string) []string { + mod := "-mod=mod" + if _, err := os.Stat(filepath.Join(dir, "vendor", "modules.txt")); err == nil { + mod = "-mod=vendor" + } + return append(os.Environ(), + "CGO_ENABLED=0", + "GOFLAGS="+recipeFlags+" "+mod, + "GOTOOLCHAIN=local", + "GOPROXY=off", + ) +} + +// Args is the arguments of the go command of the recipe. +func Args(dir, from, out, binary, system string) []string { + linker := recipeLinkerFlags + if s := strings.TrimSpace(system); s != "" { + linker += " -X " + recipeSystemStamp + "=" + s + } + return []string{"-C", dir, "build", "-ldflags", linker, "-o", filepath.Join(out, binary), "./" + strings.Trim(from, "./")} +} + +// Build compiles the package into a fresh directory and answers it. +func Build(dir, from, binary, system string) (string, error) { + out, err := os.MkdirTemp("", "mesh-pack-") + if err != nil { + return "", err + } + cmd := exec.Command("go", Args(dir, from, out, binary, system)...) + cmd.Env = Env(dir) + cmd.Stdout, cmd.Stderr = os.Stderr, os.Stderr + if err := cmd.Run(); err != nil { + os.RemoveAll(out) + return "", fmt.Errorf("go build of %s: %w", from, err) + } + return out, nil +} + +func run(args []string) (string, error) { + fs := flag.NewFlagSet("mesh-pack", flag.ContinueOnError) + dir := fs.String("dir", ".", "the Go module's directory") + from := fs.String("from", "", "the package to build, relative to -dir (the artifact's from)") + binary := fs.String("binary", "", "the binary's name in the archive") + system := fs.String("system", "", "the system stamped into main.builtFor (an artifact's system, e.g. arch)") + pack := fs.String("pack", "", "pack this directory as it is (a files archive) instead of building") + outFile := fs.String("out", "", "write the archive here") + if err := fs.Parse(args); err != nil { + return "", err + } + root := *pack + if root == "" { + if *from == "" || *binary == "" { + return "", fmt.Errorf("-from and -binary are needed (or -pack )") + } + out, err := Build(*dir, *from, *binary, *system) + if err != nil { + return "", err + } + defer os.RemoveAll(out) + root = out + } + archive, digest, err := buildstep.Pack(root) + if err != nil { + return "", err + } + if *outFile != "" { + if err := os.WriteFile(*outFile, archive, 0o644); err != nil { + return "", err + } + } + return digest, nil +} + +func main() { + digest, err := run(os.Args[1:]) + if err != nil { + fmt.Fprintln(os.Stderr, "mesh-pack:", err) + os.Exit(1) + } + fmt.Println(digest) +} diff --git a/go/cmd/mesh-pack/main_test.go b/go/cmd/mesh-pack/main_test.go new file mode 100644 index 0000000..7f026d1 --- /dev/null +++ b/go/cmd/mesh-pack/main_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "git.novox.be/novox/mesh-sdk/go/buildstep" +) + +func fixture(t *testing.T) string { + t.Helper() + d := t.TempDir() + os.MkdirAll(filepath.Join(d, "sub"), 0o755) + os.WriteFile(filepath.Join(d, "a.txt"), []byte("alpha"), 0o644) + os.WriteFile(filepath.Join(d, "sub", "run"), []byte("#!/bin/sh\n"), 0o755) + return d +} + +func TestPackFixtureTwiceGivesBuildstepsDigest(t *testing.T) { + d := fixture(t) + _, want, err := buildstep.Pack(d) + if err != nil { + t.Fatal(err) + } + for i := 0; i < 2; i++ { + got, err := run([]string{"-pack", d, "-out", filepath.Join(t.TempDir(), "x.tgz")}) + if err != nil || got != want { + t.Fatalf("run %d: %q, %v; want %q", i, got, err, want) + } + } +} + +func TestBuildTwiceGivesOneDigestAndRecipeArgs(t *testing.T) { + m := t.TempDir() + os.WriteFile(filepath.Join(m, "go.mod"), []byte("module x\n\ngo 1.21\n"), 0o644) + os.MkdirAll(filepath.Join(m, "cmd", "p"), 0o755) + os.WriteFile(filepath.Join(m, "cmd", "p", "main.go"), []byte("package main\n\nvar builtFor string\n\nfunc main() { println(builtFor) }\n"), 0o644) + args := []string{"-dir", m, "-from", "cmd/p", "-binary", "p", "-system", "arch"} + a, err := run(args) + if err != nil { + t.Skipf("no go toolchain that builds offline: %v", err) + } + b, err := run(args) + if err != nil || a != b { + t.Fatalf("two builds: %q, %q, %v", a, b, err) + } + got := Args("/d", "./cmd/p/", "/o", "p", "arch") + if got[len(got)-1] != "./cmd/p" || got[4] != "-s -w -X main.builtFor=arch" { + t.Fatalf("args %v", got) + } +}