agent-dev-env 0.1.0-canary.2.fbbd044
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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/assets/bridge/bridge.js +202 -0
- package/dist/assets/guest/guest-agent-mac.js +586 -0
- package/dist/assets/guest/guest-agent-ubuntu.js +481 -0
- package/dist/assets/guest/guest-agent-windows.js +466 -0
- package/dist/assets/images/mac/CHANGELOG.md +265 -0
- package/dist/assets/images/mac/README.md +63 -0
- package/dist/assets/images/mac/sandbox.pkr.hcl +468 -0
- package/dist/assets/images/mac/vars/sandbox-macos-tahoe.pkrvars.hcl +39 -0
- package/dist/assets/images/ubuntu-arm64-vmware/CHANGELOG.md +239 -0
- package/dist/assets/images/ubuntu-arm64-vmware/README.md +177 -0
- package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/meta-data +2 -0
- package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/user-data +34 -0
- package/dist/assets/images/ubuntu-arm64-vmware/sandbox.pkr.hcl +776 -0
- package/dist/assets/images/ubuntu-arm64-vmware/vars/sandbox-ubuntu-24-04-arm64-vmware.pkrvars.hcl +71 -0
- package/dist/assets/images/windows-arm64-qemu/CHANGELOG.md +209 -0
- package/dist/assets/images/windows-arm64-qemu/README.md +142 -0
- package/dist/assets/images/windows-arm64-qemu/autounattend.xml +325 -0
- package/dist/assets/images/windows-arm64-qemu/qemu-with-tpm.sh +170 -0
- package/dist/assets/images/windows-arm64-qemu/sandbox.pkr.hcl +1012 -0
- package/dist/assets/images/windows-arm64-qemu/vars/sandbox-windows-11-arm64-qemu.pkrvars.hcl +83 -0
- package/dist/assets/images/windows-arm64-vmware/CHANGELOG.md +270 -0
- package/dist/assets/images/windows-arm64-vmware/README.md +171 -0
- package/dist/assets/images/windows-arm64-vmware/autounattend.xml +311 -0
- package/dist/assets/images/windows-arm64-vmware/sandbox.pkr.hcl +1036 -0
- package/dist/assets/images/windows-arm64-vmware/vars/sandbox-windows-11-arm64-vmware.pkrvars.hcl +83 -0
- package/dist/assets/rules/agent-rules-linux.md +68 -0
- package/dist/assets/rules/agent-rules.md +71 -0
- package/dist/assets/watchdog/watch-build-ocr.swift +40 -0
- package/dist/assets/watchdog/watch-build.py +275 -0
- package/dist/cli.js +52 -0
- package/dist/commands/delete.js +179 -0
- package/dist/commands/doctor.js +165 -0
- package/dist/commands/list.js +38 -0
- package/dist/commands/not-yet.js +15 -0
- package/dist/commands/register.js +150 -0
- package/dist/commands/run.js +65 -0
- package/dist/commands/status.js +142 -0
- package/dist/commands/stop.js +186 -0
- package/dist/commands/sync.js +98 -0
- package/dist/lib/exec.js +250 -0
- package/dist/lib/ghcr.js +82 -0
- package/dist/lib/git.js +100 -0
- package/dist/lib/logger.js +87 -0
- package/dist/lib/network.js +63 -0
- package/dist/lib/paths.js +116 -0
- package/dist/lib/platform.js +87 -0
- package/dist/lib/prompt.js +66 -0
- package/dist/lib/qemu.js +373 -0
- package/dist/lib/regex.js +9 -0
- package/dist/lib/ssh.js +230 -0
- package/dist/lib/tart.js +210 -0
- package/dist/lib/template.js +35 -0
- package/dist/lib/vars.js +99 -0
- package/dist/lib/vmrun.js +334 -0
- package/dist/lifecycle/build-macos.js +23 -0
- package/dist/lifecycle/build-qemu.js +204 -0
- package/dist/lifecycle/build-shared.js +355 -0
- package/dist/lifecycle/build-ubuntu.js +184 -0
- package/dist/lifecycle/build-watchdog.js +146 -0
- package/dist/lifecycle/build-windows-vmware.js +112 -0
- package/dist/lifecycle/build.js +55 -0
- package/dist/lifecycle/catalog.js +165 -0
- package/dist/lifecycle/deploy.js +161 -0
- package/dist/lifecycle/tag.js +77 -0
- package/dist/lifecycle/watch-build.js +64 -0
- package/dist/runners/bridges.js +156 -0
- package/dist/runners/framework.js +66 -0
- package/dist/runners/macos-bridges.js +143 -0
- package/dist/runners/macos-guest.js +76 -0
- package/dist/runners/macos-rules.js +77 -0
- package/dist/runners/macos-summary.js +145 -0
- package/dist/runners/macos.js +258 -0
- package/dist/runners/openchamber.js +60 -0
- package/dist/runners/options.js +56 -0
- package/dist/runners/qemu-image.js +183 -0
- package/dist/runners/rules.js +49 -0
- package/dist/runners/ubuntu-bridges.js +154 -0
- package/dist/runners/ubuntu-guest.js +136 -0
- package/dist/runners/ubuntu-image.js +26 -0
- package/dist/runners/ubuntu-rules.js +86 -0
- package/dist/runners/ubuntu-shared.js +65 -0
- package/dist/runners/ubuntu-summary.js +141 -0
- package/dist/runners/ubuntu.js +117 -0
- package/dist/runners/vmware-common.js +158 -0
- package/dist/runners/vmware-image.js +242 -0
- package/dist/runners/windows-autologon.js +119 -0
- package/dist/runners/windows-bridges.js +136 -0
- package/dist/runners/windows-guest.js +213 -0
- package/dist/runners/windows-image.js +23 -0
- package/dist/runners/windows-qemu-summary.js +85 -0
- package/dist/runners/windows-qemu.js +189 -0
- package/dist/runners/windows-shared.js +68 -0
- package/dist/runners/windows-summary.js +104 -0
- package/dist/runners/windows.js +96 -0
- package/dist/settings/common.js +107 -0
- package/dist/settings/macos-copy.js +223 -0
- package/dist/settings/macos.js +41 -0
- package/dist/settings/ubuntu-copy.js +223 -0
- package/dist/settings/ubuntu.js +68 -0
- package/package.json +52 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// build-macos.ts — the macOS build flow: the plain `packer init` +
|
|
2
|
+
// `packer build -var-file` path of scripts/build.sh's fallback (no
|
|
3
|
+
// staging, no swtpm — the tart plugin owns the VM under ~/.tart/vms/).
|
|
4
|
+
import { logger } from '../lib/logger.js';
|
|
5
|
+
import { announceBuild, materializeContext, requireCmd, runPackerBuild, runPackerInit, } from './build-shared.js';
|
|
6
|
+
/** Builds a macOS image with Packer.
|
|
7
|
+
*
|
|
8
|
+
* @param image - The catalog image.
|
|
9
|
+
* @param options - Force/watchdog overrides.
|
|
10
|
+
*/
|
|
11
|
+
export async function buildMacosImage(image, options) {
|
|
12
|
+
requireCmd('packer', 'brew install packer');
|
|
13
|
+
const context = materializeContext(image);
|
|
14
|
+
announceBuild(image, context);
|
|
15
|
+
await runPackerInit(context.templateFile);
|
|
16
|
+
await runPackerBuild({
|
|
17
|
+
platformDir: context.platformDir,
|
|
18
|
+
templateFile: context.templateFile,
|
|
19
|
+
varsFile: image.varsFile,
|
|
20
|
+
force: options.force,
|
|
21
|
+
});
|
|
22
|
+
logger.ok(`Done: ${image.name}`);
|
|
23
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// build-qemu.ts — the windows-qemu build flow: the port of
|
|
2
|
+
// images/windows-arm64-qemu/build.sh (ISO + virtio-win staging, swtpm,
|
|
3
|
+
// the watchdog, the packer pipeline with qemu-with-tpm.sh, zstd
|
|
4
|
+
// compression). Failures throw; a kink in the shell flow is the bare
|
|
5
|
+
// start_watchdog call (a missing vncdotool aborts the build under set
|
|
6
|
+
// -e) — the CLI warn+skips like the VMware wrappers do.
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
8
|
+
import { mkdtempSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { basename, join } from 'node:path';
|
|
11
|
+
import { isAlive, readPidFile, run, runChecked, sleep } from '../lib/exec.js';
|
|
12
|
+
import { logger } from '../lib/logger.js';
|
|
13
|
+
import { announceBuild, buildDirLayout, ensureCacheDir, materializeContext, qemuImgCompressArgs, requireAppleSilicon, requireCommands, requireWindowsIso, runPackerBuild, runPackerFmtCheck, runPackerInit, stringVar, swtpmArgs, verifyIsoSha256, } from './build-shared.js';
|
|
14
|
+
import { startBuildWatchdog, stopBuildWatchdog } from './build-watchdog.js';
|
|
15
|
+
/** The virtio drivers staged into the unattend CD (viostor = boot disk,
|
|
16
|
+
* vioscsi belt-and-braces, NetKVM = the NIC, viogpudo = display). */
|
|
17
|
+
const WINPE_DRIVERS = ['viostor', 'vioscsi', 'NetKVM', 'viogpudo'];
|
|
18
|
+
/** Builds a windows-qemu image.
|
|
19
|
+
*
|
|
20
|
+
* @param image - The catalog image.
|
|
21
|
+
* @param options - Force/watchdog overrides.
|
|
22
|
+
*/
|
|
23
|
+
export async function buildQemuImage(image, options) {
|
|
24
|
+
requireAppleSilicon('the Windows image can only be built on Apple Silicon (QEMU + HVF).');
|
|
25
|
+
requireCommands([
|
|
26
|
+
['packer', 'brew install packer'],
|
|
27
|
+
['qemu-system-aarch64', 'brew install qemu'],
|
|
28
|
+
['qemu-img', 'brew install qemu'],
|
|
29
|
+
['swtpm', 'brew install swtpm'],
|
|
30
|
+
['curl', 'comes with macOS'],
|
|
31
|
+
['hdiutil', 'comes with macOS'],
|
|
32
|
+
['xmllint', 'comes with macOS'],
|
|
33
|
+
]);
|
|
34
|
+
const dirs = buildDirLayout('windows-qemu');
|
|
35
|
+
ensureCacheDir(dirs.cache);
|
|
36
|
+
const context = materializeContext(image);
|
|
37
|
+
announceBuild(image, context);
|
|
38
|
+
const { winIso, virtioIso } = await prepareQemuInputs(image, dirs);
|
|
39
|
+
const swtpm = await startSwtpm(dirs.cache);
|
|
40
|
+
await runQemuPacker(image, options, dirs, context, { winIso, virtioIso, swtpm });
|
|
41
|
+
await compressQemuOutput(dirs.output, image.name);
|
|
42
|
+
}
|
|
43
|
+
/** Windows ISO + virtio-win.iso verification and driver staging. */
|
|
44
|
+
async function prepareQemuInputs(image, dirs) {
|
|
45
|
+
const winIso = requireWindowsIso(image);
|
|
46
|
+
await verifyIsoSha256(winIso, stringVar(image, 'iso_sha256'), 'Windows ISO', image.varsFile);
|
|
47
|
+
const virtioIso = await requireVirtioWinIso(image, dirs.cache);
|
|
48
|
+
await stageVirtioDrivers(virtioIso, dirs.staging);
|
|
49
|
+
return { winIso, virtioIso };
|
|
50
|
+
}
|
|
51
|
+
/** The watchdog + packer pipeline (xmllint/init/fmt/build), with the
|
|
52
|
+
* watchdog and swtpm torn down afterwards. */
|
|
53
|
+
async function runQemuPacker(image, options, dirs, context, inputs) {
|
|
54
|
+
let watchdogPid;
|
|
55
|
+
if (options.watchdog !== false) {
|
|
56
|
+
watchdogPid = await startBuildWatchdog({ cacheDir: dirs.cache });
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await runXmllint(context.platformDir);
|
|
60
|
+
await runPackerInit(context.templateFile);
|
|
61
|
+
await runPackerFmtCheck(context.platformDir);
|
|
62
|
+
await runPackerBuild({
|
|
63
|
+
platformDir: context.platformDir,
|
|
64
|
+
templateFile: context.templateFile,
|
|
65
|
+
varsFile: image.varsFile,
|
|
66
|
+
buildDir: dirs.build,
|
|
67
|
+
force: options.force,
|
|
68
|
+
env: {
|
|
69
|
+
SWTPM_SOCK: inputs.swtpm.sock,
|
|
70
|
+
VIRTIO_WIN_ISO_PATH: inputs.virtioIso,
|
|
71
|
+
QEMU_WITH_TPM_LOG: join(dirs.cache, 'qemu-with-tpm.cmd.log'),
|
|
72
|
+
PKR_VAR_iso_path: inputs.winIso,
|
|
73
|
+
PKR_VAR_virtio_win_iso_path: inputs.virtioIso,
|
|
74
|
+
PKR_VAR_qemu_binary: join(context.platformDir, 'qemu-with-tpm.sh'),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await stopBuildWatchdog(watchdogPid);
|
|
80
|
+
await stopSwtpm(inputs.swtpm.pidFile);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Resolution of virtio-win.iso: the env override, or a cached download
|
|
84
|
+
* from the vars file's virtio_win_url.
|
|
85
|
+
*
|
|
86
|
+
* @param image - The catalog image.
|
|
87
|
+
* @param cacheDir - The build cache dir.
|
|
88
|
+
* @returns The virtio-win.iso path.
|
|
89
|
+
*/
|
|
90
|
+
async function requireVirtioWinIso(image, cacheDir) {
|
|
91
|
+
const cachedPath = join(cacheDir, 'virtio-win.iso');
|
|
92
|
+
const path = process.env.VIRTIO_WIN_ISO_PATH ?? cachedPath;
|
|
93
|
+
if (!process.env.VIRTIO_WIN_ISO_PATH) {
|
|
94
|
+
logger.step('VIRTIO_WIN_ISO_PATH unset — downloading virtio-win.iso');
|
|
95
|
+
if (existsSync(cachedPath)) {
|
|
96
|
+
logger.info(`already cached: ${cachedPath}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const url = stringVar(image, 'virtio_win_url');
|
|
100
|
+
if (!url) {
|
|
101
|
+
throw new Error(`virtio_win_url is empty in ${image.varsFile}`);
|
|
102
|
+
}
|
|
103
|
+
await runChecked('curl', ['-fSL', '-o', cachedPath, url]);
|
|
104
|
+
}
|
|
105
|
+
await verifyIsoSha256(cachedPath, stringVar(image, 'virtio_win_sha256') ?? '', 'virtio-win.iso', image.varsFile, `Delete ${cachedPath} and rebuild, or update virtio_win_sha256 in ${image.varsFile}.`);
|
|
106
|
+
}
|
|
107
|
+
if (!existsSync(path)) {
|
|
108
|
+
throw new Error(`VIRTIO_WIN_ISO_PATH points to a file that does not exist:\n ${path}`);
|
|
109
|
+
}
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
112
|
+
/** Extracts the ARM64 drivers from virtio-win.iso (hdiutil) into the
|
|
113
|
+
* staging dir; a failed/missing build still detaches the mount.
|
|
114
|
+
*
|
|
115
|
+
* @param virtioIso - The virtio-win.iso path.
|
|
116
|
+
* @param stagingDir - The staging dir.
|
|
117
|
+
*/
|
|
118
|
+
async function stageVirtioDrivers(virtioIso, stagingDir) {
|
|
119
|
+
const mountDir = mkdtempSync(join(tmpdir(), 'agent-dev-env-virtio-win.'));
|
|
120
|
+
const missing = [];
|
|
121
|
+
logger.step(`mounting ${basename(virtioIso)}`);
|
|
122
|
+
try {
|
|
123
|
+
await runChecked('hdiutil', [
|
|
124
|
+
'attach',
|
|
125
|
+
'-nobrowse',
|
|
126
|
+
'-readonly',
|
|
127
|
+
'-mountpoint',
|
|
128
|
+
mountDir,
|
|
129
|
+
virtioIso,
|
|
130
|
+
]);
|
|
131
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
132
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
133
|
+
for (const driver of WINPE_DRIVERS) {
|
|
134
|
+
const src = join(mountDir, driver, 'w11', 'ARM64');
|
|
135
|
+
if (!existsSync(src)) {
|
|
136
|
+
missing.push(driver);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
cpSync(src, join(stagingDir, driver), { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await run('hdiutil', ['detach', mountDir, '-quiet']);
|
|
144
|
+
rmSync(mountDir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
if (missing.length > 0) {
|
|
147
|
+
throw new Error('ARM64 driver tree missing from virtio-win.iso for:\n' +
|
|
148
|
+
` ${missing.join(' ')}\n` +
|
|
149
|
+
' Expected <driver>/w11/ARM64/ to exist. Your virtio-win.iso\n' +
|
|
150
|
+
' may pre-date 0.1.240 (the first release with ARM64 builds).');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Runs swtpm as a daemon and waits for its Unix socket. */
|
|
154
|
+
async function startSwtpm(cacheDir) {
|
|
155
|
+
const swtpmDir = join(cacheDir, 'swtpm');
|
|
156
|
+
const sock = join(swtpmDir, 'sock');
|
|
157
|
+
const pidFile = join(swtpmDir, 'pid');
|
|
158
|
+
mkdirSync(swtpmDir, { recursive: true });
|
|
159
|
+
rmSync(sock, { force: true });
|
|
160
|
+
rmSync(pidFile, { force: true });
|
|
161
|
+
rmSync(join(swtpmDir, 'tpm2-00.permall'), { recursive: true, force: true });
|
|
162
|
+
logger.step('starting swtpm (TPM 2.0 emulator)');
|
|
163
|
+
await runChecked('swtpm', swtpmArgs(swtpmDir, sock, pidFile));
|
|
164
|
+
await sleep(1000);
|
|
165
|
+
if (!existsSync(sock)) {
|
|
166
|
+
throw new Error(`swtpm socket ${sock} did not appear.\n Check ${swtpmDir}/log.`);
|
|
167
|
+
}
|
|
168
|
+
return { pidFile, sock };
|
|
169
|
+
}
|
|
170
|
+
/** Kills swtpm by its pid file and removes the pid file. */
|
|
171
|
+
async function stopSwtpm(pidFile) {
|
|
172
|
+
const pid = readPidFile(pidFile);
|
|
173
|
+
if (pid !== undefined && isAlive(pid)) {
|
|
174
|
+
try {
|
|
175
|
+
process.kill(pid);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// already gone
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
rmSync(pidFile, { force: true });
|
|
182
|
+
}
|
|
183
|
+
/** zstd-compresses the built qcow2 and prints its info lines. */
|
|
184
|
+
async function compressQemuOutput(outputDir, imageName) {
|
|
185
|
+
const output = join(outputDir, `${imageName}.qcow2`);
|
|
186
|
+
if (!existsSync(output)) {
|
|
187
|
+
throw new Error(`build produced no ${output}`);
|
|
188
|
+
}
|
|
189
|
+
logger.step(`compressing ${output} (zstd)`);
|
|
190
|
+
await runChecked('qemu-img', qemuImgCompressArgs(output));
|
|
191
|
+
renameSync(`${output}.tmp`, output);
|
|
192
|
+
const info = await run('qemu-img', ['info', output]);
|
|
193
|
+
for (const line of info.stdout.split('\n')) {
|
|
194
|
+
if (/virtual size|disk size/.test(line)) {
|
|
195
|
+
logger.info(line);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
logger.ok(`Done: ${output}`);
|
|
199
|
+
}
|
|
200
|
+
/** `xmllint --noout` on the unattend seed (catches XML typos early). */
|
|
201
|
+
async function runXmllint(platformDir) {
|
|
202
|
+
logger.step('xmllint autounattend.xml');
|
|
203
|
+
await runChecked('xmllint', ['--noout', join(platformDir, 'autounattend.xml')]);
|
|
204
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// build-shared.ts — helpers shared by the per-platform build flows (the
|
|
2
|
+
// port of scripts/build.sh plus the three images/*/build.sh wrappers):
|
|
3
|
+
// host prereqs, the build-dir layout, ISO hash verification, the
|
|
4
|
+
// materialized Packer context, the packer pipeline (init / fmt -check /
|
|
5
|
+
// build), and the pure arg builders the wrappers inline (swtpm,
|
|
6
|
+
// qemu-img, unzip, the Ubuntu seed + grub command).
|
|
7
|
+
//
|
|
8
|
+
// Failures throw (the CLI's top level turns them into die()); helpers
|
|
9
|
+
// keep the shell's exact error wording.
|
|
10
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
|
|
11
|
+
import { basename, dirname, join } from 'node:path';
|
|
12
|
+
import { commandExists, run, runChecked } from '../lib/exec.js';
|
|
13
|
+
import { logger } from '../lib/logger.js';
|
|
14
|
+
import { buildDir, paths } from '../lib/paths.js';
|
|
15
|
+
import { upgradeVmHardware, vmwareHwVersion } from '../lib/vmrun.js';
|
|
16
|
+
import { varsFor } from './catalog.js';
|
|
17
|
+
/** The build dirs for a platform.
|
|
18
|
+
*
|
|
19
|
+
* @param platform - The platform id.
|
|
20
|
+
* @returns The layout (build/output/cache/staging paths).
|
|
21
|
+
*/
|
|
22
|
+
export function buildDirLayout(platform) {
|
|
23
|
+
const build = buildDir(platform);
|
|
24
|
+
return {
|
|
25
|
+
build,
|
|
26
|
+
output: join(build, 'output'),
|
|
27
|
+
cache: join(build, 'packer_cache'),
|
|
28
|
+
staging: join(build, 'drivers', 'staging'),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Host check: macOS Apple Silicon (QEMU/VMware cannot virtualize ARM64
|
|
32
|
+
* guests on Intel Macs).
|
|
33
|
+
*
|
|
34
|
+
* @returns True on an Apple Silicon Mac.
|
|
35
|
+
*/
|
|
36
|
+
function hostIsAppleSilicon() {
|
|
37
|
+
return process.platform === 'darwin' && process.arch === 'arm64';
|
|
38
|
+
}
|
|
39
|
+
/** Dies with the platform's message when the host is not Apple Silicon.
|
|
40
|
+
*
|
|
41
|
+
* @param message - The platform-specific error message.
|
|
42
|
+
*/
|
|
43
|
+
export function requireAppleSilicon(message) {
|
|
44
|
+
if (!hostIsAppleSilicon()) {
|
|
45
|
+
throw new Error(message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** `command -v` check with the shell's install hint.
|
|
49
|
+
*
|
|
50
|
+
* @param command - The command name.
|
|
51
|
+
* @param hint - The install hint (brew install … / comes with macOS).
|
|
52
|
+
*/
|
|
53
|
+
export function requireCmd(command, hint) {
|
|
54
|
+
if (!commandExists(command)) {
|
|
55
|
+
throw new Error(`required command '${command}' not found on PATH.\n Install with: ${hint}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Runs requireCmd for each [command, hint] pair.
|
|
59
|
+
*
|
|
60
|
+
* @param checks - The command/hint pairs to check.
|
|
61
|
+
*/
|
|
62
|
+
export function requireCommands(checks) {
|
|
63
|
+
for (const [command, hint] of checks) {
|
|
64
|
+
requireCmd(command, hint);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Reads a quoted-string variable from the image's vars file.
|
|
68
|
+
*
|
|
69
|
+
* @param image - The catalog image.
|
|
70
|
+
* @param name - The variable name.
|
|
71
|
+
* @returns The value, or undefined when absent/not a string.
|
|
72
|
+
*/
|
|
73
|
+
export function stringVar(image, name) {
|
|
74
|
+
const value = varsFor(image)[name];
|
|
75
|
+
return typeof value === 'string' ? value : undefined;
|
|
76
|
+
}
|
|
77
|
+
/** @internal — test-only export of the digest normalizer.
|
|
78
|
+
*
|
|
79
|
+
* @param raw - The digest as written.
|
|
80
|
+
* @returns The normalized digest (lowercase, no whitespace).
|
|
81
|
+
*/
|
|
82
|
+
export function normalizeSha(raw) {
|
|
83
|
+
return raw.toLowerCase().replace(/\s+/g, '');
|
|
84
|
+
}
|
|
85
|
+
/** SHA256 of a file (shasum -a 256), normalized.
|
|
86
|
+
*
|
|
87
|
+
* @param file - The file to hash.
|
|
88
|
+
* @returns The lowercase hex digest.
|
|
89
|
+
*/
|
|
90
|
+
async function sha256Of(file) {
|
|
91
|
+
const res = await run('shasum', ['-a', '256', file]);
|
|
92
|
+
if (res.code !== 0) {
|
|
93
|
+
throw new Error(`shasum failed: ${res.stderr.trim() || res.stdout.trim()}`);
|
|
94
|
+
}
|
|
95
|
+
return normalizeSha(res.stdout.split(/\s+/)[0] ?? '');
|
|
96
|
+
}
|
|
97
|
+
/** Verifies a file against its vars-file sha256 (warns and skips when the
|
|
98
|
+
* expected digest is empty, matching the wrappers' behavior).
|
|
99
|
+
*
|
|
100
|
+
* @param file - The file to verify.
|
|
101
|
+
* @param expected - The expected digest (empty = skip).
|
|
102
|
+
* @param label - The digest context label (e.g. "Windows ISO").
|
|
103
|
+
* @param varsFile - The vars file (for the error message).
|
|
104
|
+
* @param failureNote - Optional custom failure hint.
|
|
105
|
+
*/
|
|
106
|
+
export async function verifyIsoSha256(file, expected, label, varsFile, failureNote) {
|
|
107
|
+
logger.step(`verifying SHA256 of ${basename(file)}`);
|
|
108
|
+
const want = normalizeSha(expected ?? '');
|
|
109
|
+
if (!want) {
|
|
110
|
+
logger.warn(`iso_sha256 is empty in ${varsFile} — skipping ISO verification.`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const got = await sha256Of(file);
|
|
114
|
+
if (got !== want) {
|
|
115
|
+
const note = failureNote ?? `Re-download the ISO or update iso_sha256 in ${varsFile}.`;
|
|
116
|
+
throw new Error(`SHA256 mismatch for the ${label}.\n` + ` expected: ${want}\n actual: ${got}\n ${note}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Copies the image's platform dir into <data>/build-context/ so the
|
|
120
|
+
* packed context is writable and self-contained (npm installs cannot
|
|
121
|
+
* run builds from the read-only package snapshot), chmod'ing the
|
|
122
|
+
* qemu-with-tpm.sh wrapper on demand.
|
|
123
|
+
*
|
|
124
|
+
* @param image - The catalog image.
|
|
125
|
+
* @param options - data-root override (tests).
|
|
126
|
+
* @returns The materialized context (platform dir + template).
|
|
127
|
+
*/
|
|
128
|
+
export function materializeContext(image, options = {}) {
|
|
129
|
+
const platformDir = dirname(dirname(image.varsFile));
|
|
130
|
+
const contextRoot = join(options.dataRoot ?? paths.data, 'build-context', basename(platformDir));
|
|
131
|
+
rmSync(contextRoot, { recursive: true, force: true });
|
|
132
|
+
cpSync(platformDir, contextRoot, { recursive: true });
|
|
133
|
+
chmodQemuWrapper(contextRoot);
|
|
134
|
+
return { platformDir: contextRoot, templateFile: findTemplateFile(contextRoot) };
|
|
135
|
+
}
|
|
136
|
+
/** chmod +x the qemu-with-tpm.sh wrapper (packer execs it as the qemu
|
|
137
|
+
* binary; a copied context may lose the mode).
|
|
138
|
+
*
|
|
139
|
+
* @param contextRoot - The materialized platform dir.
|
|
140
|
+
*/
|
|
141
|
+
function chmodQemuWrapper(contextRoot) {
|
|
142
|
+
const qemu = join(contextRoot, 'qemu-with-tpm.sh');
|
|
143
|
+
if (existsSync(qemu)) {
|
|
144
|
+
chmodSync(qemu, 0o755);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** @internal — the first *.pkr.hcl template in the platform dir
|
|
148
|
+
* (test-only export; also used by materializeContext).
|
|
149
|
+
*
|
|
150
|
+
* @param platformDir - The platform dir.
|
|
151
|
+
* @returns The template path.
|
|
152
|
+
* @throws Error when no template exists (shell parity).
|
|
153
|
+
*/
|
|
154
|
+
export function findTemplateFile(platformDir) {
|
|
155
|
+
const templates = readdirSync(platformDir)
|
|
156
|
+
.filter((entry) => entry.endsWith('.pkr.hcl'))
|
|
157
|
+
.sort();
|
|
158
|
+
if (templates.length === 0) {
|
|
159
|
+
throw new Error(`No Packer template (*.pkr.hcl) found in ${platformDir}`);
|
|
160
|
+
}
|
|
161
|
+
return join(platformDir, templates[0]);
|
|
162
|
+
}
|
|
163
|
+
/** Logs the legacy per-image build header.
|
|
164
|
+
*
|
|
165
|
+
* @param image - The image being built.
|
|
166
|
+
* @param context - The materialized context.
|
|
167
|
+
*/
|
|
168
|
+
export function announceBuild(image, context) {
|
|
169
|
+
logger.title(`Building image: ${image.name}`);
|
|
170
|
+
logger.info(`Using template: ${context.templateFile}`);
|
|
171
|
+
logger.info(`Using vars: ${image.varsFile}`);
|
|
172
|
+
}
|
|
173
|
+
/** `packer init <template>` (installs required plugins).
|
|
174
|
+
*
|
|
175
|
+
* @param templateFile - The template to init.
|
|
176
|
+
*/
|
|
177
|
+
export async function runPackerInit(templateFile) {
|
|
178
|
+
logger.step('packer init');
|
|
179
|
+
await runChecked('packer', ['init', templateFile]);
|
|
180
|
+
}
|
|
181
|
+
/** `packer fmt -check` — warns (does not fail) when formatting differs.
|
|
182
|
+
*
|
|
183
|
+
* @param platformDir - The dir to format-check.
|
|
184
|
+
*/
|
|
185
|
+
export async function runPackerFmtCheck(platformDir) {
|
|
186
|
+
logger.step('packer fmt -check');
|
|
187
|
+
const res = await run('packer', ['fmt', '-check', platformDir]);
|
|
188
|
+
if (res.code !== 0) {
|
|
189
|
+
logger.warn(`'packer fmt' would change formatting. Run 'packer fmt ${platformDir}' to fix.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** `packer build [-force] [-var build_dir=…] -var-file=… <template>`.
|
|
193
|
+
*
|
|
194
|
+
* @param options - The build options.
|
|
195
|
+
*/
|
|
196
|
+
export async function runPackerBuild(options) {
|
|
197
|
+
logger.step('packer build');
|
|
198
|
+
const args = ['build'];
|
|
199
|
+
if (options.force) {
|
|
200
|
+
args.push('-force');
|
|
201
|
+
}
|
|
202
|
+
if (options.buildDir) {
|
|
203
|
+
args.push('-var', `build_dir=${options.buildDir}`);
|
|
204
|
+
}
|
|
205
|
+
args.push('-var-file', options.varsFile, options.templateFile);
|
|
206
|
+
await runChecked('packer', args, { cwd: options.platformDir, env: options.env });
|
|
207
|
+
}
|
|
208
|
+
/** The human size of a path (`du -sh`).
|
|
209
|
+
*
|
|
210
|
+
* @param path - The path to measure.
|
|
211
|
+
* @returns The size string (e.g. "12G"), or '' when du fails.
|
|
212
|
+
*/
|
|
213
|
+
export async function duHuman(path) {
|
|
214
|
+
const res = await run('du', ['-sh', path]);
|
|
215
|
+
if (res.code !== 0) {
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
return res.stdout.trim().split(/\s+/)[0] ?? '';
|
|
219
|
+
}
|
|
220
|
+
/** swtpm invocation (TPM 2.0 emulator, Unix socket control).
|
|
221
|
+
*
|
|
222
|
+
* @param tpmDir - The tpmstate dir.
|
|
223
|
+
* @param sockPath - The control socket path.
|
|
224
|
+
* @param pidFile - The pid file swtpm writes.
|
|
225
|
+
* @returns The swtpm argv.
|
|
226
|
+
*/
|
|
227
|
+
export function swtpmArgs(tpmDir, sockPath, pidFile) {
|
|
228
|
+
return [
|
|
229
|
+
'socket',
|
|
230
|
+
'--tpmstate',
|
|
231
|
+
`dir=${tpmDir}`,
|
|
232
|
+
'--ctrl',
|
|
233
|
+
`type=unixio,path=${sockPath}`,
|
|
234
|
+
'--log',
|
|
235
|
+
`file=${join(tpmDir, 'log')},level=20`,
|
|
236
|
+
'--pid',
|
|
237
|
+
`file=${pidFile}`,
|
|
238
|
+
'--tpm2',
|
|
239
|
+
'--daemon',
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
/** qemu-img zstd conversion of the built qcow2.
|
|
243
|
+
*
|
|
244
|
+
* @param output - The qcow2 to compress.
|
|
245
|
+
* @returns The qemu-img argv (tmp output, renamed by the caller).
|
|
246
|
+
*/
|
|
247
|
+
export function qemuImgCompressArgs(output) {
|
|
248
|
+
return ['convert', '-c', '-O', 'qcow2', '-o', 'compression_type=zstd', output, `${output}.tmp`];
|
|
249
|
+
}
|
|
250
|
+
/** The vmxnet3 ARM64 driver extraction (flat, at the CD root).
|
|
251
|
+
*
|
|
252
|
+
* @param driversZip - Fusion's drivers-arm64.zip.
|
|
253
|
+
* @param stagingDir - The staging dir.
|
|
254
|
+
* @returns The unzip argv.
|
|
255
|
+
*/
|
|
256
|
+
export function unzipVmxnet3Args(driversZip, stagingDir) {
|
|
257
|
+
return [
|
|
258
|
+
'-jo',
|
|
259
|
+
driversZip,
|
|
260
|
+
'vmxnet3/Win10_1709/ARM64/vmxnet3.cat',
|
|
261
|
+
'vmxnet3/Win10_1709/ARM64/vmxnet3.inf',
|
|
262
|
+
'vmxnet3/Win10_1709/ARM64/vmxnet3.sys',
|
|
263
|
+
'-d',
|
|
264
|
+
stagingDir,
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
/** The vmnet8 NAT subnet from Fusion's dhcpd.conf
|
|
268
|
+
* (`subnet x.y.z.0 netmask …`).
|
|
269
|
+
*
|
|
270
|
+
* @param content - The dhcpd.conf content.
|
|
271
|
+
* @returns The subnet, or undefined when not found.
|
|
272
|
+
*/
|
|
273
|
+
export function vmnet8SubnetFromDhcpConf(content) {
|
|
274
|
+
for (const line of content.split('\n')) {
|
|
275
|
+
const match = line.match(/^subnet ([0-9.]+) netmask/);
|
|
276
|
+
if (match) {
|
|
277
|
+
return match[1];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
/** The grub autoinstall command the watchdog types (WATCH_BUILD_BOOT_CMD;
|
|
283
|
+
* `\;` escapes the grub command splitter).
|
|
284
|
+
*
|
|
285
|
+
* @param natHost - The vmnet8 host address (x.y.z.1).
|
|
286
|
+
* @param seedPort - The seed server port.
|
|
287
|
+
* @returns The multi-line grub command.
|
|
288
|
+
*/
|
|
289
|
+
export function watchdogBootCommand(natHost, seedPort) {
|
|
290
|
+
return (`linux /casper/vmlinuz autoinstall ds=nocloud-net\\;s=http://${natHost}:${seedPort}/ ---\n` +
|
|
291
|
+
'initrd /casper/initrd\n' +
|
|
292
|
+
'boot');
|
|
293
|
+
}
|
|
294
|
+
/** Ensures the cache dir exists (never the output dir — packer owns it).
|
|
295
|
+
*
|
|
296
|
+
* @param cacheDir - The per-platform packer_cache dir.
|
|
297
|
+
*/
|
|
298
|
+
export function ensureCacheDir(cacheDir) {
|
|
299
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
300
|
+
}
|
|
301
|
+
/** The bring-your-own Windows ISO (WINDOWS_ISO_PATH).
|
|
302
|
+
*
|
|
303
|
+
* @param image - The catalog image (vars file for the error message).
|
|
304
|
+
* @returns The ISO path.
|
|
305
|
+
*/
|
|
306
|
+
export function requireWindowsIso(image) {
|
|
307
|
+
const winIso = process.env.WINDOWS_ISO_PATH;
|
|
308
|
+
if (!winIso) {
|
|
309
|
+
throw new Error('WINDOWS_ISO_PATH is not set.\n' +
|
|
310
|
+
' Download the Windows 11 ARM64 ISO from\n' +
|
|
311
|
+
' https://www.microsoft.com/software-download/windows11arm64,\n' +
|
|
312
|
+
' then set WINDOWS_ISO_PATH to its absolute path.\n' +
|
|
313
|
+
' Paste the SHA256 from the download page into iso_sha256 in\n' +
|
|
314
|
+
` ${image.varsFile} to enable integrity verification.`);
|
|
315
|
+
}
|
|
316
|
+
if (!existsSync(winIso)) {
|
|
317
|
+
throw new Error(`WINDOWS_ISO_PATH points to a file that does not exist:\n ${winIso}`);
|
|
318
|
+
}
|
|
319
|
+
return winIso;
|
|
320
|
+
}
|
|
321
|
+
/** Upgrades a built vmx to the host Fusion's hardware version (vmrun
|
|
322
|
+
* upgradevm never exits — the 180 s cap lives in upgradeVmHardware).
|
|
323
|
+
*
|
|
324
|
+
* @param vmx - The built vmx.
|
|
325
|
+
* @param fusionPath - The Fusion app path (vmrun resolution).
|
|
326
|
+
*/
|
|
327
|
+
export async function upgradeArtifactHardware(vmx, fusionPath) {
|
|
328
|
+
const env = { FUSION_APP_PATH: fusionPath };
|
|
329
|
+
const before = vmwareHwVersion(vmx);
|
|
330
|
+
let after;
|
|
331
|
+
try {
|
|
332
|
+
after = await upgradeVmHardware(vmx, { env });
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
logger.warn(`${err.message} — cannot upgrade the build output; a GUI start may prompt once.`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (before && after && after !== before) {
|
|
339
|
+
logger.step(`build output upgraded to hardware version ${after}`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
logger.step(`build output hardware version: ${after ?? 'unknown'}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** `du -sh` of the build output dir (the legacy "==> build output" log).
|
|
346
|
+
*
|
|
347
|
+
* @param outputDir - The output dir to report.
|
|
348
|
+
*/
|
|
349
|
+
export async function reportOutputDir(outputDir) {
|
|
350
|
+
logger.step('build output');
|
|
351
|
+
const size = await duHuman(outputDir);
|
|
352
|
+
if (size) {
|
|
353
|
+
logger.info(`${outputDir} (${size})`);
|
|
354
|
+
}
|
|
355
|
+
}
|