A tool runner is no longer restarted by a build of the engine: owned paths name the directory of every build, and a restart names its cause (issue 1966)

The tool runner's unit carries the paths modules own, for the holder of node-files. The engine's own
archive is unpacked into a place named for its build, so every engine replacement changed that word
and restarted the tool runner with nothing of it changed (novox 21:16, 22:25, 22:38 on 2026-11-09).
A resource unpacked into a place named for its build now owns the directory holding every build.
The restart line now names the unit's directive, word and key that differ, never a value.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-09 22:49:20 +01:00
parent 6e461bd24a
commit bcd815a6bb
4 changed files with 199 additions and 1 deletions
+16 -1
View File
@@ -26,7 +26,7 @@ func declaredPaths(m mcat.Manifest) map[string]bool {
}
for _, r := range m.Resources {
if p, ok := r["path"].(string); ok && strings.HasPrefix(p, "/") {
out[path.Clean(p)] = true
out[path.Clean(versionedPlaceOwner(r, p))] = true
}
}
for _, f := range m.Facts {
@@ -123,3 +123,18 @@ func seatPathsOnMachine(modules []mcat.Manifest) []string {
}
return problems
}
// versionedPlaceOwner is the path a resource owns: its own, unless the resource is an archive unpacked into a place named
// for its build (the artifact's digest, short: `.../versions/<version>`, catalogue `${version}`), whose owner is the
// directory holding every such place, because the place changes with each build and the owned paths are an input of the
// tool runner's unit (the holder of node-files reads them at its start): a build of the engine itself renamed its place
// and restarted the tool runner, with nothing of the tool runner changed (issue 1966).
func versionedPlaceOwner(r map[string]any, p string) string {
digest, _ := r["digest"].(string)
version := strings.TrimPrefix(digest, "sha256:")
base := path.Base(path.Clean(p))
if r["type"] == "archive" && len(base) >= 8 && strings.HasPrefix(version, base) {
return path.Dir(path.Clean(p))
}
return p
}
@@ -54,3 +54,27 @@ func TestAnUplinkDeclaringTheResolverFileAsAResourceIsRefusedBesideTheResolver(t
t.Errorf("the uplink's holder renders %v beside the resolver", got.Facts)
}
}
// The paths a module owns do not change with its build (issue 1966): the holder of node-files is told them in an
// environment word of the tool runner's unit, so a place named for each build moved with every build of its module and
// restarted the tool runner each time.
func TestAPathNamedForABuildIsOwnedAsTheDirectoryHoldingEveryBuild(t *testing.T) {
digest := "sha256:" + strings.Repeat("ab12", 16)
for _, row := range []struct {
name string
res map[string]any
owns string
}{
{"a build's own place", map[string]any{"id": "next", "type": "archive", "digest": digest, "path": "/usr/lib/x/versions/ab12ab12ab12"}, "/usr/lib/x/versions"},
{"another build's place", map[string]any{"id": "next", "type": "archive", "digest": "sha256:" + strings.Repeat("cd34", 16), "path": "/usr/lib/x/versions/cd34cd34cd34"}, "/usr/lib/x/versions"},
{"a fixed place", map[string]any{"id": "app", "type": "archive", "digest": digest, "path": "/opt/app"}, "/opt/app"},
{"a file", map[string]any{"id": "f", "type": "file", "path": "/etc/f"}, "/etc/f"},
} {
t.Run(row.name, func(t *testing.T) {
got := DeclaredPaths(mcat.Manifest{Module: "m", Resources: []map[string]any{row.res}})
if len(got) != 1 || got[0] != row.owns {
t.Fatalf("owns %v, want [%s]", got, row.owns)
}
})
}
}