diff --git a/internal/builder/binary_name_test.go b/internal/builder/binary_name_test.go
new file mode 100644
index 0000000..de0bfd2
--- /dev/null
+++ b/internal/builder/binary_name_test.go
@@ -0,0 +1,44 @@
+package builder
+
+import (
+ "testing"
+
+ "github.com/novox/mesh-controller/internal/catalogue"
+)
+
+// The name a machine runs a binary by is not always the name of the package that built it. The host's
+// command is cmd/mesh-host and every machine runs it as nox-mesh-host — the path it is installed at,
+// the name in its unit, and the name its launcher looks for inside a delivered version.
+//
+// A bundle carrying the package's name was delivered to a machine correctly, reported "created … 1
+// file(s)", and was invisible to the launcher (novox/hq 04-ISSUES/142). Found by reading the delivered
+// directory rather than by trusting the line that said it worked.
+
+func TestACompiledArtifactNamesTheBinaryAMachineWillRun(t *testing.T) {
+ got := binaryName(catalogue.Artifact{
+ Name: "host-arch", From: "cmd/mesh-host", Binary: "nox-mesh-host",
+ })
+ if got != "nox-mesh-host" {
+ t.Fatalf("the binary is named %q, and the launcher looks for nox-mesh-host", got)
+ }
+}
+
+func TestSayingNothingKeepsWhatTheCompilerWouldHaveChosen(t *testing.T) {
+ // go build names its output after the package, so an artifact that says nothing gets the same
+ // thing it got before this existed.
+ if got := binaryName(catalogue.Artifact{Name: "host-arch", From: "cmd/mesh-host"}); got != "mesh-host" {
+ t.Fatalf("an artifact naming no binary produced %q", got)
+ }
+ if got := binaryName(catalogue.Artifact{Name: "host-arch", From: "./cmd/agent/"}); got != "agent" {
+ t.Fatalf("a from with slashes produced %q", got)
+ }
+}
+
+func TestABundleBuiltFromTheModuleRootFallsBackToItsArtifactName(t *testing.T) {
+ // A single-command repository names no package, and `go build -o
` would then write a file
+ // named after the module directory — which is not something the manifest states. The artifact's
+ // own name is what the manifest does state.
+ if got := binaryName(catalogue.Artifact{Name: "tool"}); got != "tool" {
+ t.Fatalf("a bundle built from the root produced %q", got)
+ }
+}
diff --git a/internal/builder/builder.go b/internal/builder/builder.go
index 2519378..02e12de 100644
--- a/internal/builder/builder.go
+++ b/internal/builder/builder.go
@@ -878,7 +878,16 @@ func compile(ctx context.Context, run Runner, tree string, chain Toolchain,
}
invocation = append(invocation, chain.Compile...)
if chain.OutputFlag != "" {
- invocation = append(invocation, chain.OutputFlag, out)
+ // A compiler pointed at a package is told the file to write, not the directory: the name a
+ // machine runs it by is not always the name of the package that built it. The host's command
+ // is `cmd/mesh-host` and every machine runs it as `nox-mesh-host` — so a bundle carrying the
+ // package's name lands correctly, reports success, and is invisible to whatever looks for it
+ // (novox/hq 04-ISSUES/142).
+ target := out
+ if chain.Unit == UnitPackage {
+ target = filepath.Join(out, binaryName(a))
+ }
+ invocation = append(invocation, chain.OutputFlag, target)
}
// What to compile. Named by the module rather than discovered, so adding a file does not
// silently change what a build produces.
@@ -1067,3 +1076,15 @@ func readBy(manifest catalogue.Manifest) []catalogue.ArtifactContext {
})
return out
}
+
+// binaryName is what a compiled bundle's executable is called: what the artifact says, or the name of
+// the package it is built from, which is what a compiler would have chosen anyway.
+func binaryName(a catalogue.Artifact) string {
+ if name := strings.TrimSpace(a.Binary); name != "" {
+ return name
+ }
+ if from := strings.Trim(a.From, "./"); from != "" {
+ return filepath.Base(from)
+ }
+ return a.Name
+}
diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go
index 27c80ac..eb2f554 100644
--- a/internal/catalogue/manifest.go
+++ b/internal/catalogue/manifest.go
@@ -597,6 +597,16 @@ type Artifact struct {
// Empty for every other kind, which do not compile.
Language string `json:"language,omitempty"`
+ // Binary is what the compiled executable is called, for a bundle in a language that compiles to
+ // one. Empty means the package's own name, which is what a compiler does by default.
+ //
+ // **Because the name a machine runs it by is not always the name of the package that built it.**
+ // The host's command is `cmd/mesh-host` and every machine runs it as `nox-mesh-host` — the path
+ // it is installed at, the name in its unit, and the name its launcher looks for inside a
+ // delivered version. A bundle that carried the package's name was delivered correctly, reported
+ // success, and was invisible to the launcher (novox/hq 04-ISSUES/142).
+ Binary string `json:"binary,omitempty"`
+
// Entrypoints are the compiled files a tool host should load from this module, relative to the
// bundle's root.
//