Skip to content

Commit cf09191

Browse files
committed
Keep Gondolin runtime dependency and auto-resolve guest assets
1 parent 3d92f5c commit cf09191

8 files changed

Lines changed: 44 additions & 202 deletions

File tree

README.md

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Docker containers share the host kernel. Gondolin runs workloads inside a VM, so
1616

1717
## Current features
1818

19-
- zero runtime npm dependencies (`dependencies: {}`)
19+
- pinned Gondolin runtime dependency (`@earendil-works/gondolin@0.2.1`) for guest-asset retrieval
2020
- `oci2gondolin` core converter
2121
- input: `--image`, `--oci-layout`, `--oci-tar` (exactly one)
2222
- platform: `linux/amd64`, `linux/arm64`
@@ -39,32 +39,27 @@ Docker containers share the host kernel. Gondolin runs workloads inside a VM, so
3939

4040
- Bun >= 1.2
4141
- `e2fsprogs` (`mke2fs`, `debugfs`)
42-
- QEMU (for runtime smoke checks via `gondolin exec`)
43-
- Gondolin CLI installed separately (tested with `@earendil-works/gondolin@0.2.1`)
42+
- QEMU (for runtime smoke checks)
4443
- Docker (only required for `dockerfile2gondolin`)
4544

46-
macOS helpers:
45+
`docker2vm` uses `@earendil-works/gondolin@0.2.1` as a runtime dependency and resolves/downloads guest assets automatically during conversion.
4746

48-
```bash
49-
brew install e2fsprogs qemu
50-
```
51-
52-
Ubuntu helpers:
47+
If you also want to run generated assets with `gondolin exec`, install the CLI separately:
5348

5449
```bash
55-
sudo apt-get install -y e2fsprogs qemu-system-x86
50+
bun add -g @earendil-works/gondolin@0.2.1
5651
```
5752

58-
Install Gondolin CLI (tested version):
53+
macOS helpers:
5954

6055
```bash
61-
bun add -g @earendil-works/gondolin@0.2.1
56+
brew install e2fsprogs qemu
6257
```
6358

64-
Prime guest assets once:
59+
Ubuntu helpers:
6560

6661
```bash
67-
gondolin exec -- /bin/true
62+
sudo apt-get install -y e2fsprogs qemu-system-x86
6863
```
6964

7065
> On macOS, `docker2vm` checks common Homebrew `e2fsprogs` locations automatically; updating `PATH` is usually optional.

bun.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/linux.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This guide is for running `docker2vm` on Linux hosts.
44

5-
`docker2vm` itself has **0 runtime npm dependencies**; system/runtime tools are installed separately.
5+
`docker2vm` includes a pinned Gondolin runtime dependency (`@earendil-works/gondolin@0.2.1`) to resolve guest assets during conversion.
66

77
## 1) Install required tools
88

@@ -23,24 +23,16 @@ export PATH="$BUN_INSTALL/bin:$PATH"
2323

2424
If you want Dockerfile conversion (`dockerfile2gondolin`), install Docker and Buildx.
2525

26-
## 2) Install Gondolin CLI separately (tested version)
26+
## 2) Optional: install Gondolin CLI (for running generated assets)
2727

28-
`docker2vm` is tested with:
28+
`docker2vm` is tested with `@earendil-works/gondolin@0.2.1` and can fetch guest assets automatically during conversion.
2929

30-
- `@earendil-works/gondolin@0.2.1`
31-
32-
Install (global):
30+
Install the CLI globally if you want to execute generated assets via `gondolin exec`:
3331

3432
```bash
3533
bun add -g @earendil-works/gondolin@0.2.1
3634
```
3735

38-
Prime guest assets once:
39-
40-
```bash
41-
gondolin exec -- /bin/true
42-
```
43-
4436
## 3) Verify toolchain
4537

4638
```bash

docs/macos.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This guide is for running `docker2vm` on macOS (Apple Silicon or Intel).
44

5-
`docker2vm` itself has **0 runtime npm dependencies**; system/runtime tools are installed separately.
5+
`docker2vm` includes a pinned Gondolin runtime dependency (`@earendil-works/gondolin@0.2.1`) to resolve guest assets during conversion.
66

77
## 1) Install required tools
88

@@ -24,24 +24,16 @@ export PATH="$(brew --prefix e2fsprogs)/sbin:$PATH"
2424

2525
To persist it, add that `export PATH=...` line to your shell profile (`~/.zshrc`, `~/.bashrc`, `~/.profile`, etc.).
2626

27-
## 3) Install Gondolin CLI separately (tested version)
27+
## 3) Optional: install Gondolin CLI (for running generated assets)
2828

29-
`docker2vm` is tested with:
29+
`docker2vm` is tested with `@earendil-works/gondolin@0.2.1` and can fetch guest assets automatically during conversion.
3030

31-
- `@earendil-works/gondolin@0.2.1`
32-
33-
Install (global):
31+
Install the CLI globally if you want to execute generated assets via `gondolin exec`:
3432

3533
```bash
3634
bun add -g @earendil-works/gondolin@0.2.1
3735
```
3836

39-
Prime guest assets once:
40-
41-
```bash
42-
gondolin exec -- /bin/true
43-
```
44-
4537
## 4) Verify toolchain
4638

4739
```bash

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
"dockerfile2gondolin": "bun run src/bin/dockerfile2gondolin.ts"
2020
},
2121
"devDependencies": {
22-
"@earendil-works/gondolin": "0.2.1",
2322
"@types/node": "^22.13.10",
2423
"typescript": "^5.7.3"
2524
},
2625
"engines": {
2726
"bun": ">=1.2.0"
2827
},
2928
"packageManager": "bun@1.3.6",
30-
"dependencies": {}
29+
"dependencies": {
30+
"@earendil-works/gondolin": "0.2.1"
31+
}
3132
}

src/oci2gondolin/materialize/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export async function materializeOutput(
5050
let assetManifestPath: string | undefined;
5151

5252
if (options.mode === "assets") {
53-
const baseAssets = resolveGondolinGuestAssets();
53+
const baseAssets = await resolveGondolinGuestAssets();
5454
const kernelPath = path.join(outDir, KERNEL_FILENAME);
5555
const initramfsPath = path.join(outDir, INITRAMFS_FILENAME);
5656

src/oci2gondolin/materialize/runtime-injection.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,13 @@ export interface RuntimeInjectionResult {
8888
}
8989

9090
export async function extractBaseRootfsTree(destinationDir: string): Promise<string> {
91-
const guestAssets = resolveGondolinGuestAssets();
91+
const guestAssets = await resolveGondolinGuestAssets();
9292
const baseRootfsPath = guestAssets.rootfsPath;
9393

9494
if (!fs.existsSync(baseRootfsPath)) {
9595
throw new CliUsageError("Gondolin base rootfs.ext4 was not found.", [
9696
`Expected path: ${baseRootfsPath}`,
97-
"Run a gondolin command once to download guest assets, then retry.",
97+
"Guest assets should be downloaded automatically via @earendil-works/gondolin; verify network access and retry.",
9898
]);
9999
}
100100

@@ -118,13 +118,13 @@ export async function extractBaseRootfsTree(destinationDir: string): Promise<str
118118
}
119119

120120
export async function injectGondolinRuntime(rootfsDir: string): Promise<RuntimeInjectionResult> {
121-
const guestAssets = resolveGondolinGuestAssets();
121+
const guestAssets = await resolveGondolinGuestAssets();
122122
const baseRootfsPath = guestAssets.rootfsPath;
123123

124124
if (!fs.existsSync(baseRootfsPath)) {
125125
throw new CliUsageError("Gondolin base rootfs.ext4 was not found.", [
126126
`Expected path: ${baseRootfsPath}`,
127-
"Run a gondolin command once to download guest assets, then retry.",
127+
"Guest assets should be downloaded automatically via @earendil-works/gondolin; verify network access and retry.",
128128
]);
129129
}
130130

src/shared/gondolin-assets.ts

Lines changed: 16 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import fs from "node:fs";
2-
import os from "node:os";
31
import path from "node:path";
42

3+
import { ensureGuestAssets } from "@earendil-works/gondolin";
4+
55
import { CliUsageError } from "./cli-errors";
66

77
export const TESTED_GONDOLIN_VERSION = "0.2.1";
@@ -13,163 +13,23 @@ export interface GondolinGuestAssets {
1313
rootfsPath: string;
1414
}
1515

16-
type AssetFileNames = {
17-
kernel: string;
18-
initramfs: string;
19-
rootfs: string;
20-
};
21-
22-
const DEFAULT_FILE_NAMES: AssetFileNames = {
23-
kernel: "vmlinuz-virt",
24-
initramfs: "initramfs.cpio.lz4",
25-
rootfs: "rootfs.ext4",
26-
};
27-
28-
export function resolveGondolinGuestAssets(): GondolinGuestAssets {
29-
const explicitDir = process.env.GONDOLIN_GUEST_DIR;
30-
if (explicitDir && explicitDir.trim().length > 0) {
31-
return loadAssetsFromDirectory(path.resolve(explicitDir), "GONDOLIN_GUEST_DIR");
32-
}
33-
34-
for (const candidateDir of discoverCachedGuestAssetDirectories()) {
35-
const loaded = tryLoadAssetsFromDirectory(candidateDir);
36-
if (loaded) {
37-
return loaded;
38-
}
39-
}
40-
41-
throw new CliUsageError("Gondolin guest assets were not found.", [
42-
"Install gondolin CLI separately (tested with @earendil-works/gondolin@0.2.1).",
43-
"Run once to populate guest assets: gondolin exec -- /bin/true",
44-
"Or set GONDOLIN_GUEST_DIR to a directory containing: vmlinuz-virt, initramfs.cpio.lz4, rootfs.ext4.",
45-
"Expected cache location: ~/.cache/gondolin/<version>/",
46-
]);
47-
}
48-
49-
function discoverCachedGuestAssetDirectories(): string[] {
50-
const cacheBase = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache");
51-
const gondolinCacheRoot = path.resolve(cacheBase, "gondolin");
52-
53-
if (!fs.existsSync(gondolinCacheRoot)) {
54-
return [];
55-
}
56-
57-
const dirs = fs
58-
.readdirSync(gondolinCacheRoot, { withFileTypes: true })
59-
.filter((entry) => entry.isDirectory())
60-
.map((entry) => path.join(gondolinCacheRoot, entry.name));
61-
62-
dirs.sort((a, b) => compareCacheDirectoryPriority(path.basename(a), path.basename(b)));
63-
64-
return dirs;
65-
}
66-
67-
function compareCacheDirectoryPriority(a: string, b: string): number {
68-
const parsedA = parseSemverTag(a);
69-
const parsedB = parseSemverTag(b);
70-
71-
if (parsedA && parsedB) {
72-
for (let i = 0; i < parsedA.length; i += 1) {
73-
if (parsedA[i] !== parsedB[i]) {
74-
return parsedB[i] - parsedA[i];
75-
}
76-
}
77-
return 0;
78-
}
79-
80-
if (parsedA) {
81-
return -1;
82-
}
83-
84-
if (parsedB) {
85-
return 1;
86-
}
87-
88-
return b.localeCompare(a);
89-
}
90-
91-
function parseSemverTag(value: string): [number, number, number] | null {
92-
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
93-
if (!match) {
94-
return null;
95-
}
96-
97-
return [Number(match[1]), Number(match[2]), Number(match[3])];
98-
}
99-
100-
function tryLoadAssetsFromDirectory(candidateDir: string): GondolinGuestAssets | null {
101-
try {
102-
return loadAssetsFromDirectory(candidateDir, "cache");
103-
} catch {
104-
return null;
105-
}
106-
}
107-
108-
function loadAssetsFromDirectory(assetDir: string, source: "GONDOLIN_GUEST_DIR" | "cache"): GondolinGuestAssets {
109-
const fileNames = resolveAssetFileNames(assetDir);
110-
111-
const kernelPath = path.join(assetDir, fileNames.kernel);
112-
const initrdPath = path.join(assetDir, fileNames.initramfs);
113-
const rootfsPath = path.join(assetDir, fileNames.rootfs);
114-
115-
const missing = [
116-
[fileNames.kernel, kernelPath],
117-
[fileNames.initramfs, initrdPath],
118-
[fileNames.rootfs, rootfsPath],
119-
]
120-
.filter(([, filePath]) => !fs.existsSync(filePath))
121-
.map(([name]) => name);
122-
123-
if (missing.length > 0) {
124-
const hint =
125-
source === "GONDOLIN_GUEST_DIR"
126-
? "Verify GONDOLIN_GUEST_DIR points to a valid gondolin guest asset directory."
127-
: "Run 'gondolin exec -- /bin/true' to download guest assets into the cache.";
128-
129-
throw new CliUsageError("Gondolin guest assets are incomplete.", [
130-
`Directory: ${assetDir}`,
131-
`Missing files: ${missing.join(", ")}`,
132-
hint,
133-
]);
134-
}
135-
136-
return {
137-
assetDir,
138-
kernelPath,
139-
initrdPath,
140-
rootfsPath,
141-
};
142-
}
143-
144-
function resolveAssetFileNames(assetDir: string): AssetFileNames {
145-
const manifestPath = path.join(assetDir, "manifest.json");
146-
if (!fs.existsSync(manifestPath)) {
147-
return DEFAULT_FILE_NAMES;
148-
}
149-
16+
export async function resolveGondolinGuestAssets(): Promise<GondolinGuestAssets> {
15017
try {
151-
const raw = fs.readFileSync(manifestPath, "utf8");
152-
const parsed = JSON.parse(raw) as {
153-
assets?: {
154-
kernel?: unknown;
155-
initramfs?: unknown;
156-
rootfs?: unknown;
157-
};
158-
};
159-
160-
const kernel = typeof parsed.assets?.kernel === "string" ? parsed.assets.kernel : DEFAULT_FILE_NAMES.kernel;
161-
const initramfs =
162-
typeof parsed.assets?.initramfs === "string"
163-
? parsed.assets.initramfs
164-
: DEFAULT_FILE_NAMES.initramfs;
165-
const rootfs = typeof parsed.assets?.rootfs === "string" ? parsed.assets.rootfs : DEFAULT_FILE_NAMES.rootfs;
18+
const assets = await ensureGuestAssets();
16619

16720
return {
168-
kernel,
169-
initramfs,
170-
rootfs,
21+
assetDir: path.dirname(assets.rootfsPath),
22+
kernelPath: assets.kernelPath,
23+
initrdPath: assets.initrdPath,
24+
rootfsPath: assets.rootfsPath,
17125
};
172-
} catch {
173-
return DEFAULT_FILE_NAMES;
26+
} catch (error) {
27+
const message = error instanceof Error ? error.message : String(error);
28+
29+
throw new CliUsageError("Failed to resolve Gondolin guest assets.", [
30+
message,
31+
`Ensure @earendil-works/gondolin@${TESTED_GONDOLIN_VERSION} is installed.`,
32+
"To use custom assets, set GONDOLIN_GUEST_DIR to a directory containing kernel/initramfs/rootfs assets.",
33+
]);
17434
}
17535
}

0 commit comments

Comments
 (0)