@specific.dev/spectest 0.119.5 → 0.119.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,117 +0,0 @@
1
- /**
2
- * The image cache as the guest sees it (`CONTAINER_STORE.md`).
3
- *
4
- * The control plane writes one manifest per VM at start naming the two
5
- * cache disks it attached: the **root** (containerd's own root,
6
- * read-write, this VM's clone) and the **layers** disk (read-only, one
7
- * EROFS file per layer, shared by every VM of a generation). Both paths
8
- * are fixed by the control plane; the manifest is how a harness learns
9
- * whether this VM carries a cache at all (the fake backend does not,
10
- * and neither does a server older than the cache).
11
- *
12
- * Read synchronously as well as asynchronously: a component's service
13
- * definition is built inside `defineEnvironment`, which is synchronous,
14
- * and `k3s()` decides its mounts there.
15
- */
16
- /** Written by `env.rs` before the harness starts. */
17
- export declare const IMAGE_CACHE_MANIFEST = "/run/spectest-image-cache.json";
18
- /** Where the cache's paths are, when this VM carries one. */
19
- export interface ImageCachePaths {
20
- /** containerd's root: read-write, this VM's own clone. */
21
- root: string;
22
- /** The layers disk: read-only, shared by every VM of a generation. */
23
- layers: string;
24
- }
25
- /**
26
- * Directory under the root disk holding a nested runtime's containerd
27
- * root, one per service: `<root>/spectest-nested/<service>`. The merge
28
- * (`image_cache/merge.rs::NESTED_DIR`) reads every store it finds there
29
- * exactly as it reads the disk's own.
30
- */
31
- export declare const NESTED_STORES_DIR = "spectest-nested";
32
- /** The guest's static `mkfs.erofs`, which a nested runtime's EROFS
33
- * differ needs and no runtime image ships. */
34
- export declare const MKFS_EROFS_PATH = "/usr/local/bin/mkfs.erofs";
35
- /** The guest's adopt helper (`base.rs::STORE_ADOPT_SH`), POSIX sh so a
36
- * nested runtime's busybox can run the same file. */
37
- export declare const STORE_ADOPT_PATH = "/usr/local/bin/spectest-store-adopt";
38
- /**
39
- * The BuildKit cache directory under each cache disk (`merge.rs::CACHE_DIR`):
40
- * on the layers disk the merged export of every earlier build, read-only;
41
- * on the root disk this lineage's own exports, which the merge harvests.
42
- */
43
- export declare const BUILDKIT_CACHE_DIR = "spectest-buildkit-cache";
44
- /**
45
- * One directory that is both: an overlayfs with the layers disk's cache
46
- * directory as the read-only lower and the root disk's as the upper,
47
- * mounted here by the harness when the in-VM builder comes up
48
- * (`daemon.ts::ensureBuildkitCacheUnion`). It is for the PROJECT'S OWN
49
- * BuildKit — the `/services` docs page tells it to mount this one path
50
- * for both of its cache flags. BuildKit's local exporter skips a blob its
51
- * destination already holds, and through the union every merged blob is
52
- * already there, so a fully cached build exports its index and manifest
53
- * and nothing else (a fresh directory on the root cost every
54
- * uncompressed layer again, 1.1 GB and 20 s for one service, measured
55
- * 2026-09-06). New layers land in the upper, which the merge reads.
56
- *
57
- * The harness's own service builds used it too until SDK 0.76.0; since
58
- * then their cache is the in-VM daemon's own state, which lives on the
59
- * root disk and rides the base root into the next cold build
60
- * (CONTAINER_STORE.md §20).
61
- */
62
- export declare const BUILDKIT_CACHE_UNION = "/var/lib/spectest-buildkit-cache";
63
- /**
64
- * The prefix the docs reserve for spectest in that directory's one flat
65
- * tag namespace. The harness tagged its service builds under it from
66
- * SDK 0.75.0 to 0.76.0, after Specific's `nginx-mod` service tagged its
67
- * own `buildctl` cache `nginx-mod` and the two overwrote each other on
68
- * alternate boots (2026-09-06); the harness writes no tags now, and the
69
- * prefix stays reserved so nothing a project names can ever collide
70
- * with something spectest writes there later.
71
- */
72
- export declare const BUILDKIT_CACHE_TAG_PREFIX = "spectest/service/";
73
- /** The tag a service's Dockerfile build would carry under the reserved
74
- * prefix: the service name, lowercased with anything outside `[a-z0-9-]`
75
- * folded to `-`. Unused by the harness since SDK 0.76.0. */
76
- export declare function buildkitCacheTag(service: string): string;
77
- /** The cache's paths, or `null` when this VM carries none.
78
- * `SPECTEST_IMAGE_CACHE_MANIFEST` points a test at another file. */
79
- export declare function imageCachePathsSync(manifest?: string): ImageCachePaths | null;
80
- /** The host directory a nested runtime keeps its containerd root in. */
81
- export declare function nestedStoreDir(paths: ImageCachePaths, service: string): string;
82
- /**
83
- * Is a volume's host path on a cache disk? Such a directory is a cache
84
- * the lineage keeps — like the container store one level up — and the
85
- * delta-restore teardown must not wipe it.
86
- */
87
- export declare function isOnImageCache(hostPath: string, paths: ImageCachePaths | null): boolean;
88
- /** The shape of an OCI layout's `index.json` the BuildKit local cache
89
- * exporter writes: one manifest per tag, the tag in the ref-name
90
- * annotation. */
91
- export interface OciIndex {
92
- schemaVersion?: number;
93
- mediaType?: string;
94
- manifests: Array<{
95
- mediaType?: string;
96
- digest: string;
97
- size?: number;
98
- annotations?: Record<string, string>;
99
- }>;
100
- }
101
- /**
102
- * The index the BuildKit cache union must show: every tag of the merged
103
- * cache (lower), overridden by this lineage's own exports (upper) where
104
- * the upper still holds the manifest.
105
- *
106
- * overlayfs shadows whole files, so an `index.json` in the upper hides
107
- * the lower's completely — and a root disk can carry one: the exports
108
- * of a lineage that built before the union existed, or an index left
109
- * behind with its blobs gone. Either way BuildKit then imports from an
110
- * index naming a few tags whose manifests may not exist, skips the
111
- * import, and every step runs (seen 2026-09-06: a cold build with
112
- * every service's cache on the layers disk and 4 stale tags in the
113
- * upper built everything from scratch). `upperHasBlob` says whether the
114
- * upper holds a manifest's blob; an upper entry without one is dropped,
115
- * since it would fail the whole import.
116
- */
117
- export declare function mergeCacheIndex(lower: OciIndex | null, upper: OciIndex | null, upperHasBlob: (hex: string) => boolean): OciIndex;
@@ -1,150 +0,0 @@
1
- /**
2
- * The image cache as the guest sees it (`CONTAINER_STORE.md`).
3
- *
4
- * The control plane writes one manifest per VM at start naming the two
5
- * cache disks it attached: the **root** (containerd's own root,
6
- * read-write, this VM's clone) and the **layers** disk (read-only, one
7
- * EROFS file per layer, shared by every VM of a generation). Both paths
8
- * are fixed by the control plane; the manifest is how a harness learns
9
- * whether this VM carries a cache at all (the fake backend does not,
10
- * and neither does a server older than the cache).
11
- *
12
- * Read synchronously as well as asynchronously: a component's service
13
- * definition is built inside `defineEnvironment`, which is synchronous,
14
- * and `k3s()` decides its mounts there.
15
- */
16
- import { existsSync, readFileSync } from "node:fs";
17
- /** Written by `env.rs` before the harness starts. */
18
- export const IMAGE_CACHE_MANIFEST = "/run/spectest-image-cache.json";
19
- /**
20
- * Directory under the root disk holding a nested runtime's containerd
21
- * root, one per service: `<root>/spectest-nested/<service>`. The merge
22
- * (`image_cache/merge.rs::NESTED_DIR`) reads every store it finds there
23
- * exactly as it reads the disk's own.
24
- */
25
- export const NESTED_STORES_DIR = "spectest-nested";
26
- /** The guest's static `mkfs.erofs`, which a nested runtime's EROFS
27
- * differ needs and no runtime image ships. */
28
- export const MKFS_EROFS_PATH = "/usr/local/bin/mkfs.erofs";
29
- /** The guest's adopt helper (`base.rs::STORE_ADOPT_SH`), POSIX sh so a
30
- * nested runtime's busybox can run the same file. */
31
- export const STORE_ADOPT_PATH = "/usr/local/bin/spectest-store-adopt";
32
- /**
33
- * The BuildKit cache directory under each cache disk (`merge.rs::CACHE_DIR`):
34
- * on the layers disk the merged export of every earlier build, read-only;
35
- * on the root disk this lineage's own exports, which the merge harvests.
36
- */
37
- export const BUILDKIT_CACHE_DIR = "spectest-buildkit-cache";
38
- /**
39
- * One directory that is both: an overlayfs with the layers disk's cache
40
- * directory as the read-only lower and the root disk's as the upper,
41
- * mounted here by the harness when the in-VM builder comes up
42
- * (`daemon.ts::ensureBuildkitCacheUnion`). It is for the PROJECT'S OWN
43
- * BuildKit — the `/services` docs page tells it to mount this one path
44
- * for both of its cache flags. BuildKit's local exporter skips a blob its
45
- * destination already holds, and through the union every merged blob is
46
- * already there, so a fully cached build exports its index and manifest
47
- * and nothing else (a fresh directory on the root cost every
48
- * uncompressed layer again, 1.1 GB and 20 s for one service, measured
49
- * 2026-09-06). New layers land in the upper, which the merge reads.
50
- *
51
- * The harness's own service builds used it too until SDK 0.76.0; since
52
- * then their cache is the in-VM daemon's own state, which lives on the
53
- * root disk and rides the base root into the next cold build
54
- * (CONTAINER_STORE.md §20).
55
- */
56
- export const BUILDKIT_CACHE_UNION = "/var/lib/spectest-buildkit-cache";
57
- /**
58
- * The prefix the docs reserve for spectest in that directory's one flat
59
- * tag namespace. The harness tagged its service builds under it from
60
- * SDK 0.75.0 to 0.76.0, after Specific's `nginx-mod` service tagged its
61
- * own `buildctl` cache `nginx-mod` and the two overwrote each other on
62
- * alternate boots (2026-09-06); the harness writes no tags now, and the
63
- * prefix stays reserved so nothing a project names can ever collide
64
- * with something spectest writes there later.
65
- */
66
- export const BUILDKIT_CACHE_TAG_PREFIX = "spectest/service/";
67
- /** The tag a service's Dockerfile build would carry under the reserved
68
- * prefix: the service name, lowercased with anything outside `[a-z0-9-]`
69
- * folded to `-`. Unused by the harness since SDK 0.76.0. */
70
- export function buildkitCacheTag(service) {
71
- return `${BUILDKIT_CACHE_TAG_PREFIX}${service.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}`;
72
- }
73
- function parse(raw) {
74
- const parsed = JSON.parse(raw);
75
- const root = (parsed.disks ?? []).find((d) => d.role === "root" && d.path)?.path;
76
- const layers = (parsed.disks ?? []).find((d) => d.role === "layers" && d.path)?.path;
77
- return root && layers ? { root, layers } : null;
78
- }
79
- /** The cache's paths, or `null` when this VM carries none.
80
- * `SPECTEST_IMAGE_CACHE_MANIFEST` points a test at another file. */
81
- export function imageCachePathsSync(manifest = process.env.SPECTEST_IMAGE_CACHE_MANIFEST || IMAGE_CACHE_MANIFEST) {
82
- try {
83
- if (!existsSync(manifest))
84
- return null;
85
- return parse(readFileSync(manifest, "utf8"));
86
- }
87
- catch {
88
- return null;
89
- }
90
- }
91
- /** The host directory a nested runtime keeps its containerd root in. */
92
- export function nestedStoreDir(paths, service) {
93
- return `${paths.root}/${NESTED_STORES_DIR}/${service}`;
94
- }
95
- /**
96
- * Is a volume's host path on a cache disk? Such a directory is a cache
97
- * the lineage keeps — like the container store one level up — and the
98
- * delta-restore teardown must not wipe it.
99
- */
100
- export function isOnImageCache(hostPath, paths) {
101
- if (!paths)
102
- return false;
103
- // The union is the root's cache directory seen through the layers
104
- // disk: wiping it would wipe the upper, i.e. this lineage's exports.
105
- if (hostPath === BUILDKIT_CACHE_UNION || hostPath.startsWith(`${BUILDKIT_CACHE_UNION}/`))
106
- return true;
107
- return hostPath === paths.root || hostPath.startsWith(`${paths.root}/`) || hostPath === paths.layers || hostPath.startsWith(`${paths.layers}/`);
108
- }
109
- const REF_NAME = "org.opencontainers.image.ref.name";
110
- /**
111
- * The index the BuildKit cache union must show: every tag of the merged
112
- * cache (lower), overridden by this lineage's own exports (upper) where
113
- * the upper still holds the manifest.
114
- *
115
- * overlayfs shadows whole files, so an `index.json` in the upper hides
116
- * the lower's completely — and a root disk can carry one: the exports
117
- * of a lineage that built before the union existed, or an index left
118
- * behind with its blobs gone. Either way BuildKit then imports from an
119
- * index naming a few tags whose manifests may not exist, skips the
120
- * import, and every step runs (seen 2026-09-06: a cold build with
121
- * every service's cache on the layers disk and 4 stale tags in the
122
- * upper built everything from scratch). `upperHasBlob` says whether the
123
- * upper holds a manifest's blob; an upper entry without one is dropped,
124
- * since it would fail the whole import.
125
- */
126
- export function mergeCacheIndex(lower, upper, upperHasBlob) {
127
- const byTag = new Map();
128
- const untagged = [];
129
- for (const m of lower?.manifests ?? []) {
130
- const tag = m.annotations?.[REF_NAME];
131
- if (tag)
132
- byTag.set(tag, m);
133
- else
134
- untagged.push(m);
135
- }
136
- for (const m of upper?.manifests ?? []) {
137
- if (!upperHasBlob(m.digest.replace(/^sha256:/, "")))
138
- continue;
139
- const tag = m.annotations?.[REF_NAME];
140
- if (tag)
141
- byTag.set(tag, m);
142
- else
143
- untagged.push(m);
144
- }
145
- return {
146
- schemaVersion: 2,
147
- mediaType: lower?.mediaType ?? upper?.mediaType ?? "application/vnd.oci.image.index.v1+json",
148
- manifests: [...untagged, ...byTag.values()],
149
- };
150
- }
@@ -1,81 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { BUILDKIT_CACHE_UNION, isOnImageCache } from "./image-cache";
4
-
5
- const paths = { root: "/var/lib/containerd", layers: "/var/lib/spectest-image-cache" };
6
-
7
- describe("isOnImageCache", () => {
8
- test("the two cache disks and anything under them", () => {
9
- expect(isOnImageCache("/var/lib/containerd", paths)).toBe(true);
10
- expect(isOnImageCache("/var/lib/containerd/spectest-nested/k8s", paths)).toBe(true);
11
- expect(isOnImageCache("/var/lib/spectest-image-cache/spectest-buildkit-cache", paths)).toBe(true);
12
- });
13
-
14
- /** The union is the root's export directory seen through the layers
15
- * disk; wiping it at a delta restore would wipe this lineage's exports. */
16
- test("the BuildKit cache union counts as cache", () => {
17
- expect(isOnImageCache(BUILDKIT_CACHE_UNION, paths)).toBe(true);
18
- expect(isOnImageCache(`${BUILDKIT_CACHE_UNION}/blobs`, paths)).toBe(true);
19
- });
20
-
21
- test("an ordinary project volume does not", () => {
22
- expect(isOnImageCache("/var/cache/spectest/volumes/db", paths)).toBe(false);
23
- expect(isOnImageCache("/var/lib/containerd-not", paths)).toBe(false);
24
- });
25
-
26
- test("no cache, nothing counts", () => {
27
- expect(isOnImageCache(BUILDKIT_CACHE_UNION, null)).toBe(false);
28
- });
29
- });
30
-
31
- import { BUILDKIT_CACHE_TAG_PREFIX, buildkitCacheTag, mergeCacheIndex, type OciIndex } from "./image-cache";
32
-
33
- const entry = (tag: string, hex: string) => ({ mediaType: "application/vnd.oci.image.manifest.v1+json", digest: `sha256:${hex}`, size: 1, annotations: { "org.opencontainers.image.ref.name": tag } });
34
-
35
- describe("mergeCacheIndex", () => {
36
- const lower: OciIndex = { schemaVersion: 2, manifests: [entry("api", "a1"), entry("cli", "c1")] };
37
-
38
- test("no upper index: the merged cache's tags, unchanged", () => {
39
- const out = mergeCacheIndex(lower, null, () => false);
40
- expect(out.manifests.map((m) => m.annotations?.["org.opencontainers.image.ref.name"])).toEqual(["api", "cli"]);
41
- });
42
-
43
- /** The lineage's own export of a service wins over the merged one. */
44
- test("an upper entry whose manifest exists overrides the lower's tag", () => {
45
- const upper: OciIndex = { schemaVersion: 2, manifests: [entry("api", "a2")] };
46
- const out = mergeCacheIndex(lower, upper, (hex) => hex === "a2");
47
- const api = out.manifests.find((m) => m.annotations?.["org.opencontainers.image.ref.name"] === "api");
48
- expect(api?.digest).toBe("sha256:a2");
49
- expect(out.manifests).toHaveLength(2);
50
- });
51
-
52
- /** A stale upper index — blobs emptied, index left — would hide every
53
- * lower tag and fail the import; its entries are dropped instead. */
54
- test("an upper entry without its manifest blob is dropped", () => {
55
- const upper: OciIndex = { schemaVersion: 2, manifests: [entry("api", "a2"), entry("worker", "w1")] };
56
- const out = mergeCacheIndex(lower, upper, () => false);
57
- expect(out.manifests.map((m) => m.digest)).toEqual(["sha256:a1", "sha256:c1"]);
58
- });
59
-
60
- test("a valid upper tag the lower lacks is added", () => {
61
- const upper: OciIndex = { schemaVersion: 2, manifests: [entry("worker", "w1")] };
62
- const out = mergeCacheIndex(lower, upper, () => true);
63
- expect(out.manifests.map((m) => m.annotations?.["org.opencontainers.image.ref.name"])).toEqual(["api", "cli", "worker"]);
64
- });
65
-
66
- /** The directory is one tag namespace shared with the project's own
67
- * BuildKit. The harness's tags are prefixed so a project tag named after
68
- * the same service sits beside it instead of replacing it. */
69
- test("a project's tag and the harness's tag for the same service both survive", () => {
70
- const both: OciIndex = { schemaVersion: 2, manifests: [entry("nginx-mod", "p1"), entry(buildkitCacheTag("nginx-mod"), "h1")] };
71
- const out = mergeCacheIndex(both, null, () => false);
72
- expect(out.manifests.map((m) => m.digest)).toEqual(["sha256:p1", "sha256:h1"]);
73
- });
74
- });
75
-
76
- describe("buildkitCacheTag", () => {
77
- test("the reserved prefix plus the normalised service name", () => {
78
- expect(buildkitCacheTag("nginx-mod")).toBe(`${BUILDKIT_CACHE_TAG_PREFIX}nginx-mod`);
79
- expect(buildkitCacheTag("Api_v2")).toBe("spectest/service/api-v2");
80
- });
81
- });
@@ -1,175 +0,0 @@
1
- /**
2
- * The image cache as the guest sees it (`CONTAINER_STORE.md`).
3
- *
4
- * The control plane writes one manifest per VM at start naming the two
5
- * cache disks it attached: the **root** (containerd's own root,
6
- * read-write, this VM's clone) and the **layers** disk (read-only, one
7
- * EROFS file per layer, shared by every VM of a generation). Both paths
8
- * are fixed by the control plane; the manifest is how a harness learns
9
- * whether this VM carries a cache at all (the fake backend does not,
10
- * and neither does a server older than the cache).
11
- *
12
- * Read synchronously as well as asynchronously: a component's service
13
- * definition is built inside `defineEnvironment`, which is synchronous,
14
- * and `k3s()` decides its mounts there.
15
- */
16
-
17
- import { existsSync, readFileSync } from "node:fs";
18
-
19
- /** Written by `env.rs` before the harness starts. */
20
- export const IMAGE_CACHE_MANIFEST = "/run/spectest-image-cache.json";
21
-
22
- /** Where the cache's paths are, when this VM carries one. */
23
- export interface ImageCachePaths {
24
- /** containerd's root: read-write, this VM's own clone. */
25
- root: string;
26
- /** The layers disk: read-only, shared by every VM of a generation. */
27
- layers: string;
28
- }
29
-
30
- /**
31
- * Directory under the root disk holding a nested runtime's containerd
32
- * root, one per service: `<root>/spectest-nested/<service>`. The merge
33
- * (`image_cache/merge.rs::NESTED_DIR`) reads every store it finds there
34
- * exactly as it reads the disk's own.
35
- */
36
- export const NESTED_STORES_DIR = "spectest-nested";
37
-
38
- /** The guest's static `mkfs.erofs`, which a nested runtime's EROFS
39
- * differ needs and no runtime image ships. */
40
- export const MKFS_EROFS_PATH = "/usr/local/bin/mkfs.erofs";
41
-
42
- /** The guest's adopt helper (`base.rs::STORE_ADOPT_SH`), POSIX sh so a
43
- * nested runtime's busybox can run the same file. */
44
- export const STORE_ADOPT_PATH = "/usr/local/bin/spectest-store-adopt";
45
-
46
- /**
47
- * The BuildKit cache directory under each cache disk (`merge.rs::CACHE_DIR`):
48
- * on the layers disk the merged export of every earlier build, read-only;
49
- * on the root disk this lineage's own exports, which the merge harvests.
50
- */
51
- export const BUILDKIT_CACHE_DIR = "spectest-buildkit-cache";
52
-
53
- /**
54
- * One directory that is both: an overlayfs with the layers disk's cache
55
- * directory as the read-only lower and the root disk's as the upper,
56
- * mounted here by the harness when the in-VM builder comes up
57
- * (`daemon.ts::ensureBuildkitCacheUnion`). It is for the PROJECT'S OWN
58
- * BuildKit — the `/services` docs page tells it to mount this one path
59
- * for both of its cache flags. BuildKit's local exporter skips a blob its
60
- * destination already holds, and through the union every merged blob is
61
- * already there, so a fully cached build exports its index and manifest
62
- * and nothing else (a fresh directory on the root cost every
63
- * uncompressed layer again, 1.1 GB and 20 s for one service, measured
64
- * 2026-09-06). New layers land in the upper, which the merge reads.
65
- *
66
- * The harness's own service builds used it too until SDK 0.76.0; since
67
- * then their cache is the in-VM daemon's own state, which lives on the
68
- * root disk and rides the base root into the next cold build
69
- * (CONTAINER_STORE.md §20).
70
- */
71
- export const BUILDKIT_CACHE_UNION = "/var/lib/spectest-buildkit-cache";
72
-
73
- /**
74
- * The prefix the docs reserve for spectest in that directory's one flat
75
- * tag namespace. The harness tagged its service builds under it from
76
- * SDK 0.75.0 to 0.76.0, after Specific's `nginx-mod` service tagged its
77
- * own `buildctl` cache `nginx-mod` and the two overwrote each other on
78
- * alternate boots (2026-09-06); the harness writes no tags now, and the
79
- * prefix stays reserved so nothing a project names can ever collide
80
- * with something spectest writes there later.
81
- */
82
- export const BUILDKIT_CACHE_TAG_PREFIX = "spectest/service/";
83
-
84
- /** The tag a service's Dockerfile build would carry under the reserved
85
- * prefix: the service name, lowercased with anything outside `[a-z0-9-]`
86
- * folded to `-`. Unused by the harness since SDK 0.76.0. */
87
- export function buildkitCacheTag(service: string): string {
88
- return `${BUILDKIT_CACHE_TAG_PREFIX}${service.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}`;
89
- }
90
-
91
- function parse(raw: string): ImageCachePaths | null {
92
- const parsed = JSON.parse(raw) as { disks?: { role?: string; path?: string }[] };
93
- const root = (parsed.disks ?? []).find((d) => d.role === "root" && d.path)?.path;
94
- const layers = (parsed.disks ?? []).find((d) => d.role === "layers" && d.path)?.path;
95
- return root && layers ? { root, layers } : null;
96
- }
97
-
98
- /** The cache's paths, or `null` when this VM carries none.
99
- * `SPECTEST_IMAGE_CACHE_MANIFEST` points a test at another file. */
100
- export function imageCachePathsSync(
101
- manifest: string = process.env.SPECTEST_IMAGE_CACHE_MANIFEST || IMAGE_CACHE_MANIFEST,
102
- ): ImageCachePaths | null {
103
- try {
104
- if (!existsSync(manifest)) return null;
105
- return parse(readFileSync(manifest, "utf8"));
106
- } catch {
107
- return null;
108
- }
109
- }
110
-
111
- /** The host directory a nested runtime keeps its containerd root in. */
112
- export function nestedStoreDir(paths: ImageCachePaths, service: string): string {
113
- return `${paths.root}/${NESTED_STORES_DIR}/${service}`;
114
- }
115
-
116
- /**
117
- * Is a volume's host path on a cache disk? Such a directory is a cache
118
- * the lineage keeps — like the container store one level up — and the
119
- * delta-restore teardown must not wipe it.
120
- */
121
- export function isOnImageCache(hostPath: string, paths: ImageCachePaths | null): boolean {
122
- if (!paths) return false;
123
- // The union is the root's cache directory seen through the layers
124
- // disk: wiping it would wipe the upper, i.e. this lineage's exports.
125
- if (hostPath === BUILDKIT_CACHE_UNION || hostPath.startsWith(`${BUILDKIT_CACHE_UNION}/`)) return true;
126
- return hostPath === paths.root || hostPath.startsWith(`${paths.root}/`) || hostPath === paths.layers || hostPath.startsWith(`${paths.layers}/`);
127
- }
128
-
129
- /** The shape of an OCI layout's `index.json` the BuildKit local cache
130
- * exporter writes: one manifest per tag, the tag in the ref-name
131
- * annotation. */
132
- export interface OciIndex {
133
- schemaVersion?: number;
134
- mediaType?: string;
135
- manifests: Array<{ mediaType?: string; digest: string; size?: number; annotations?: Record<string, string> }>;
136
- }
137
-
138
- const REF_NAME = "org.opencontainers.image.ref.name";
139
-
140
- /**
141
- * The index the BuildKit cache union must show: every tag of the merged
142
- * cache (lower), overridden by this lineage's own exports (upper) where
143
- * the upper still holds the manifest.
144
- *
145
- * overlayfs shadows whole files, so an `index.json` in the upper hides
146
- * the lower's completely — and a root disk can carry one: the exports
147
- * of a lineage that built before the union existed, or an index left
148
- * behind with its blobs gone. Either way BuildKit then imports from an
149
- * index naming a few tags whose manifests may not exist, skips the
150
- * import, and every step runs (seen 2026-09-06: a cold build with
151
- * every service's cache on the layers disk and 4 stale tags in the
152
- * upper built everything from scratch). `upperHasBlob` says whether the
153
- * upper holds a manifest's blob; an upper entry without one is dropped,
154
- * since it would fail the whole import.
155
- */
156
- export function mergeCacheIndex(lower: OciIndex | null, upper: OciIndex | null, upperHasBlob: (hex: string) => boolean): OciIndex {
157
- const byTag = new Map<string, OciIndex["manifests"][number]>();
158
- const untagged: OciIndex["manifests"] = [];
159
- for (const m of lower?.manifests ?? []) {
160
- const tag = m.annotations?.[REF_NAME];
161
- if (tag) byTag.set(tag, m);
162
- else untagged.push(m);
163
- }
164
- for (const m of upper?.manifests ?? []) {
165
- if (!upperHasBlob(m.digest.replace(/^sha256:/, ""))) continue;
166
- const tag = m.annotations?.[REF_NAME];
167
- if (tag) byTag.set(tag, m);
168
- else untagged.push(m);
169
- }
170
- return {
171
- schemaVersion: 2,
172
- mediaType: lower?.mediaType ?? upper?.mediaType ?? "application/vnd.oci.image.index.v1+json",
173
- manifests: [...untagged, ...byTag.values()],
174
- };
175
- }