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,184 @@
|
|
|
1
|
+
// build-ubuntu.ts — the ubuntu-vmware build flow: the port of
|
|
2
|
+
// images/ubuntu-arm64-vmware/build.sh (the autoinstall seed server on a
|
|
3
|
+
// fixed port, the vmnet8 subnet read for the watchdog's grub command,
|
|
4
|
+
// the packer pipeline, the optional post-build hardware upgrade).
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { commandExists, isAlive, run, sleep, spawnDetached } from '../lib/exec.js';
|
|
8
|
+
import { logger } from '../lib/logger.js';
|
|
9
|
+
import { announceBuild, buildDirLayout, ensureCacheDir, materializeContext, reportOutputDir, requireAppleSilicon, requireCmd, runPackerBuild, runPackerFmtCheck, runPackerInit, stringVar, upgradeArtifactHardware, verifyIsoSha256, vmnet8SubnetFromDhcpConf, watchdogBootCommand, } from './build-shared.js';
|
|
10
|
+
import { clearBootTypedMarker, startBuildWatchdog, stopBuildWatchdog } from './build-watchdog.js';
|
|
11
|
+
const DEFAULT_FUSION_APP = '/Applications/VMware Fusion.app';
|
|
12
|
+
/** The autoinstall seed port (pinned so the grub URL is known upfront). */
|
|
13
|
+
const SEED_PORT = 8004;
|
|
14
|
+
/** Fusion's vmnet8 DHCP config (the NAT subnet of the guest network). */
|
|
15
|
+
const VMNET8_DHCP = '/Library/Preferences/VMware Fusion/vmnet8/dhcpd.conf';
|
|
16
|
+
/** Builds an ubuntu-vmware image.
|
|
17
|
+
*
|
|
18
|
+
* @param image - The catalog image.
|
|
19
|
+
* @param options - Force/watchdog overrides.
|
|
20
|
+
*/
|
|
21
|
+
export async function buildUbuntuImage(image, options) {
|
|
22
|
+
requireAppleSilicon('the Ubuntu image can only be built on Apple Silicon (VMware\n' +
|
|
23
|
+
' Fusion cannot run ARM64 guests on Intel Macs).');
|
|
24
|
+
requireCmd('packer', 'brew install packer');
|
|
25
|
+
const fusionPath = optionalFusionApp();
|
|
26
|
+
const dirs = buildDirLayout('ubuntu-vmware');
|
|
27
|
+
ensureCacheDir(dirs.cache);
|
|
28
|
+
const context = materializeContext(image);
|
|
29
|
+
announceBuild(image, context);
|
|
30
|
+
const iso = requireUbuntuIso(image);
|
|
31
|
+
await verifyIsoSha256(iso, stringVar(image, 'iso_sha256'), 'Ubuntu ISO', image.varsFile);
|
|
32
|
+
await runUbuntuPacker(image, options, dirs, context, iso);
|
|
33
|
+
await verifyUbuntuOutput(image, dirs, fusionPath);
|
|
34
|
+
}
|
|
35
|
+
/** The seed server + watchdog + packer pipeline (torn down after). */
|
|
36
|
+
async function runUbuntuPacker(image, options, dirs, context, iso) {
|
|
37
|
+
const seedPid = await startSeedServer(context.platformDir, dirs.cache);
|
|
38
|
+
let watchdogPid;
|
|
39
|
+
try {
|
|
40
|
+
await runPackerInit(context.templateFile);
|
|
41
|
+
await runPackerFmtCheck(context.platformDir);
|
|
42
|
+
if (options.watchdog !== false) {
|
|
43
|
+
watchdogPid = await startWatchdogWithBootCmd(dirs.cache);
|
|
44
|
+
}
|
|
45
|
+
await runPackerBuild({
|
|
46
|
+
platformDir: context.platformDir,
|
|
47
|
+
templateFile: context.templateFile,
|
|
48
|
+
varsFile: image.varsFile,
|
|
49
|
+
buildDir: dirs.build,
|
|
50
|
+
force: options.force,
|
|
51
|
+
env: { PKR_VAR_iso_path: iso },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await stopBuildWatchdog(watchdogPid);
|
|
56
|
+
await stopSeedServer(seedPid);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Verifies the built vmx and upgrades/checks the hardware version. */
|
|
60
|
+
async function verifyUbuntuOutput(image, dirs, fusionPath) {
|
|
61
|
+
const output = join(dirs.output, `${image.name}.vmx`);
|
|
62
|
+
if (!existsSync(output)) {
|
|
63
|
+
throw new Error(`build produced no ${output}\n` +
|
|
64
|
+
` Expected the vmware-iso builder's export in ${dirs.output}/.`);
|
|
65
|
+
}
|
|
66
|
+
if (fusionPath) {
|
|
67
|
+
await upgradeArtifactHardware(output, fusionPath);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
logger.warn(`skipped the hardware upgrade (no Fusion at ${DEFAULT_FUSION_APP}).`);
|
|
71
|
+
}
|
|
72
|
+
await reportOutputDir(dirs.output);
|
|
73
|
+
logger.ok(`Done: ${output} (export with agent-dev-env deploy ${image.name})`);
|
|
74
|
+
}
|
|
75
|
+
/** Upgrades the artifact to the host Fusion's hardware version. */
|
|
76
|
+
/** The bring-your-own Ubuntu ISO (UBUNTU_ISO_PATH).
|
|
77
|
+
*
|
|
78
|
+
* @param image - The catalog image (vars file for the error message).
|
|
79
|
+
* @returns The ISO path.
|
|
80
|
+
*/
|
|
81
|
+
function requireUbuntuIso(image) {
|
|
82
|
+
const iso = process.env.UBUNTU_ISO_PATH;
|
|
83
|
+
if (!iso) {
|
|
84
|
+
throw new Error('UBUNTU_ISO_PATH is not set.\n' +
|
|
85
|
+
' Download the Ubuntu Server 24.04 ARM64 ISO (live-server-arm64)\n' +
|
|
86
|
+
' from https://cdimage.ubuntu.com/releases/24.04/release/,\n' +
|
|
87
|
+
' then set UBUNTU_ISO_PATH to its absolute path.\n' +
|
|
88
|
+
' Paste the SHA256 from the release SHA256SUMS into iso_sha256 in\n' +
|
|
89
|
+
` ${image.varsFile} to enable integrity verification.`);
|
|
90
|
+
}
|
|
91
|
+
if (!existsSync(iso)) {
|
|
92
|
+
throw new Error(`UBUNTU_ISO_PATH points to a file that does not exist:\n ${iso}`);
|
|
93
|
+
}
|
|
94
|
+
return iso;
|
|
95
|
+
}
|
|
96
|
+
/** Optional Fusion (the build works without it; only the post-build
|
|
97
|
+
* hardware upgrade is skipped).
|
|
98
|
+
*
|
|
99
|
+
* @returns The Fusion app path, or undefined when missing (warns).
|
|
100
|
+
*/
|
|
101
|
+
function optionalFusionApp() {
|
|
102
|
+
const path = process.env.FUSION_APP_PATH ?? DEFAULT_FUSION_APP;
|
|
103
|
+
if (!existsSync(path)) {
|
|
104
|
+
logger.warn(`VMware Fusion not found at ${path} —\n` +
|
|
105
|
+
' the post-build hardware upgrade will be skipped.\n' +
|
|
106
|
+
' Install VMware Fusion (free, Broadcom) or set FUSION_APP_PATH.');
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
/** Serves the autoinstall seed on the fixed port (the vmware plugin's
|
|
112
|
+
* http_directory port is random; the grub URL must be known upfront).
|
|
113
|
+
*
|
|
114
|
+
* @param platformDir - The materialized platform dir (autoinstall/).
|
|
115
|
+
* @param cacheDir - The build cache dir (seed-server.log lands there).
|
|
116
|
+
* @returns The server pid.
|
|
117
|
+
*/
|
|
118
|
+
async function startSeedServer(platformDir, cacheDir) {
|
|
119
|
+
const logFile = join(cacheDir, 'seed-server.log');
|
|
120
|
+
const taken = await run('lsof', ['-nP', `-iTCP:${SEED_PORT}`, '-sTCP:LISTEN']);
|
|
121
|
+
if (taken.code === 0) {
|
|
122
|
+
throw new Error(`port ${SEED_PORT} is taken (something else is listening).\n` +
|
|
123
|
+
' Stop the other listener before building.');
|
|
124
|
+
}
|
|
125
|
+
if (!commandExists('python3')) {
|
|
126
|
+
throw new Error('python3 is required to serve the autoinstall seed.');
|
|
127
|
+
}
|
|
128
|
+
logger.step(`serving the autoinstall seed at http://<host>:${SEED_PORT}/ (autoinstall/ dir)`);
|
|
129
|
+
const pid = spawnDetached('python3', [
|
|
130
|
+
'-m',
|
|
131
|
+
'http.server',
|
|
132
|
+
String(SEED_PORT),
|
|
133
|
+
'--bind',
|
|
134
|
+
'0.0.0.0',
|
|
135
|
+
'--directory',
|
|
136
|
+
join(platformDir, 'autoinstall'),
|
|
137
|
+
], { logFile });
|
|
138
|
+
await sleep(1000);
|
|
139
|
+
if (!isAlive(pid)) {
|
|
140
|
+
throw new Error(`the seed server exited — see ${logFile}.`);
|
|
141
|
+
}
|
|
142
|
+
return pid;
|
|
143
|
+
}
|
|
144
|
+
/** Kills the seed server when still alive. */
|
|
145
|
+
async function stopSeedServer(pid) {
|
|
146
|
+
if (isAlive(pid)) {
|
|
147
|
+
try {
|
|
148
|
+
process.kill(pid);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// already gone
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Reads the vmnet8 subnet from Fusion's dhcpd.conf (best effort). */
|
|
156
|
+
function readVmnet8Subnet() {
|
|
157
|
+
try {
|
|
158
|
+
return vmnet8SubnetFromDhcpConf(readFileSync(VMNET8_DHCP, 'utf8'));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Starts the watchdog with the grub autoinstall command (typed once per
|
|
165
|
+
* build; the seed URL points at the vmnet8 host address).
|
|
166
|
+
*
|
|
167
|
+
* @param cacheDir - The build cache dir.
|
|
168
|
+
* @returns The watchdog pid, or undefined when skipped.
|
|
169
|
+
*/
|
|
170
|
+
async function startWatchdogWithBootCmd(cacheDir) {
|
|
171
|
+
const env = {};
|
|
172
|
+
const subnet = readVmnet8Subnet();
|
|
173
|
+
if (!subnet) {
|
|
174
|
+
logger.warn("could not read the vmnet8 subnet from Fusion's DHCP config —\n" +
|
|
175
|
+
' the watchdog will not type the autoinstall command.');
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const natHost = `${subnet.slice(0, subnet.lastIndexOf('.'))}.1`;
|
|
179
|
+
env.WATCH_BUILD_BOOT_CMD = watchdogBootCommand(natHost, SEED_PORT);
|
|
180
|
+
clearBootTypedMarker(cacheDir);
|
|
181
|
+
logger.step(`watchdog will type the autoinstall command for grub (seed at http://${natHost}:${SEED_PORT}/)`);
|
|
182
|
+
}
|
|
183
|
+
return startBuildWatchdog({ cacheDir, env });
|
|
184
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// build-watchdog.ts — the VNC build watchdog the wrapper build flows
|
|
2
|
+
// share (scripts/watch-build.sh + scripts/watch-build.py): dependency
|
|
3
|
+
// checks (python3 + vncdotool, swiftc) with warn+skip, the OCR helper
|
|
4
|
+
// compiled if-stale into the watchdog dir, and a detached spawn of the
|
|
5
|
+
// bundled watch-build.py with a log file. The wrapper scripts'
|
|
6
|
+
// start_watchdog/stop_watchdog helpers land here.
|
|
7
|
+
import { existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { commandExists, isExecutable, killTree, run, spawnDetached } from '../lib/exec.js';
|
|
11
|
+
import { logger } from '../lib/logger.js';
|
|
12
|
+
/** The VNC port the packer templates pin (vnc_port_min/max = 5901). */
|
|
13
|
+
const WATCHDOG_VNC_PORT = 5901;
|
|
14
|
+
/** The bundled watchdog asset root (dist/assets/watchdog); the compiled
|
|
15
|
+
* module sits at dist/lifecycle/, one level above the assets.
|
|
16
|
+
*
|
|
17
|
+
* @returns The watchdog assets dir.
|
|
18
|
+
*/
|
|
19
|
+
function watchdogAssetsRoot() {
|
|
20
|
+
return fileURLToPath(new URL('../assets/watchdog', import.meta.url));
|
|
21
|
+
}
|
|
22
|
+
/** The watchdog asset paths.
|
|
23
|
+
*
|
|
24
|
+
* @param root - The assets root (defaults to the bundled dir).
|
|
25
|
+
* @returns The python + swift paths.
|
|
26
|
+
*/
|
|
27
|
+
export function watchdogAssets(root = watchdogAssetsRoot()) {
|
|
28
|
+
return { python: join(root, 'watch-build.py'), ocrSwift: join(root, 'watch-build-ocr.swift') };
|
|
29
|
+
}
|
|
30
|
+
/** @internal — test-only export of the compile-if-stale check (the
|
|
31
|
+
* shell's `[ ! -x $ocr ] || [ src -nt $ocr ]`).
|
|
32
|
+
*
|
|
33
|
+
* @param ocrPath - The compiled OCR binary.
|
|
34
|
+
* @param srcPath - The swift source.
|
|
35
|
+
* @returns True when a compile is due.
|
|
36
|
+
*/
|
|
37
|
+
export function ocrNeedsCompile(ocrPath, srcPath) {
|
|
38
|
+
if (!existsSync(ocrPath) || !isExecutable(ocrPath) || !existsSync(srcPath)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return statSync(srcPath).mtimeMs > statSync(ocrPath).mtimeMs;
|
|
42
|
+
}
|
|
43
|
+
/** @internal — swiftc compile argv (test-only export).
|
|
44
|
+
*
|
|
45
|
+
* @param src - The swift source.
|
|
46
|
+
* @param out - The output binary.
|
|
47
|
+
* @returns The swiftc argv.
|
|
48
|
+
*/
|
|
49
|
+
export function swiftcArgs(src, out) {
|
|
50
|
+
return ['-O', src, '-o', out];
|
|
51
|
+
}
|
|
52
|
+
/** @internal — the watch-build.py argv (test-only export).
|
|
53
|
+
*
|
|
54
|
+
* @param port - The VNC port.
|
|
55
|
+
* @param outdir - The frames dir.
|
|
56
|
+
* @param ocr - The compiled OCR binary.
|
|
57
|
+
* @param python - The watch-build.py path.
|
|
58
|
+
* @returns The argv after python3.
|
|
59
|
+
*/
|
|
60
|
+
export function watchdogPyArgs(port, outdir, ocr, python) {
|
|
61
|
+
return [python, String(port), outdir, ocr];
|
|
62
|
+
}
|
|
63
|
+
/** Whether python3 + vncdotool are available.
|
|
64
|
+
*
|
|
65
|
+
* @returns True when the module imports.
|
|
66
|
+
*/
|
|
67
|
+
export async function pythonVncdotoolAvailable() {
|
|
68
|
+
const res = await run('python3', ['-c', 'import vncdotool']);
|
|
69
|
+
return res.code === 0;
|
|
70
|
+
}
|
|
71
|
+
/** Warns and returns false when the watchdog deps are missing (the
|
|
72
|
+
* build continues without a watchdog).
|
|
73
|
+
*
|
|
74
|
+
* @returns True when vncdotool + swiftc are available.
|
|
75
|
+
*/
|
|
76
|
+
async function watchdogDepsAvailable() {
|
|
77
|
+
if (!(await pythonVncdotoolAvailable())) {
|
|
78
|
+
logger.warn('vncdotool not installed (pip3 install vncdotool) —\n' +
|
|
79
|
+
' the build may stall at build dialogs without the watchdog.');
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!commandExists('swiftc')) {
|
|
83
|
+
logger.warn('swiftc not found — the watchdog OCR helper needs the\n' +
|
|
84
|
+
' Xcode command line tools; skipping the watchdog.');
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Compiles the OCR helper when stale; returns its path.
|
|
90
|
+
*
|
|
91
|
+
* @param outdir - The watchdog frames dir (binary lands there).
|
|
92
|
+
* @param assets - The watchdog assets.
|
|
93
|
+
* @returns The OCR binary path, or undefined when the compile fails.
|
|
94
|
+
*/
|
|
95
|
+
export async function compileOcrHelper(outdir, assets) {
|
|
96
|
+
mkdirSync(outdir, { recursive: true });
|
|
97
|
+
const ocr = join(outdir, 'watch-build-ocr');
|
|
98
|
+
if (!ocrNeedsCompile(ocr, assets.ocrSwift)) {
|
|
99
|
+
return ocr;
|
|
100
|
+
}
|
|
101
|
+
logger.step(`compiling the OCR helper (${ocr})`);
|
|
102
|
+
const res = await run('swiftc', swiftcArgs(assets.ocrSwift, ocr));
|
|
103
|
+
if (res.code !== 0) {
|
|
104
|
+
logger.warn(`swiftc failed — skipping the watchdog (${res.stderr.trim()})`);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return ocr;
|
|
108
|
+
}
|
|
109
|
+
/** Starts the detached watchdog (warn+skip when deps are missing).
|
|
110
|
+
*
|
|
111
|
+
* @param options - Cache dir / env / assets.
|
|
112
|
+
* @returns The supervisor pid, or undefined when skipped.
|
|
113
|
+
*/
|
|
114
|
+
export async function startBuildWatchdog(options) {
|
|
115
|
+
const assets = options.assets ?? watchdogAssets();
|
|
116
|
+
if (!(await watchdogDepsAvailable())) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const outdir = join(options.cacheDir, 'watchdog');
|
|
120
|
+
const ocr = await compileOcrHelper(outdir, assets);
|
|
121
|
+
if (!ocr) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
logger.step(`starting build watchdog (VNC port ${WATCHDOG_VNC_PORT}, frames in ${outdir})`);
|
|
125
|
+
const pid = spawnDetached('python3', watchdogPyArgs(WATCHDOG_VNC_PORT, outdir, ocr, assets.python), { logFile: join(options.cacheDir, 'watchdog.log'), env: options.env });
|
|
126
|
+
return pid > 0 ? pid : undefined;
|
|
127
|
+
}
|
|
128
|
+
/** Stops the watchdog: kill the supervisor tree, then the belt-and-braces
|
|
129
|
+
* pkill of any leftover `--worker` subprocesses holding the VNC port.
|
|
130
|
+
*
|
|
131
|
+
* @param pid - The supervisor pid (optional).
|
|
132
|
+
*/
|
|
133
|
+
export async function stopBuildWatchdog(pid) {
|
|
134
|
+
if (pid !== undefined && pid > 0) {
|
|
135
|
+
await killTree(pid);
|
|
136
|
+
}
|
|
137
|
+
await run('pkill', ['-f', 'watch-build.py --worker']);
|
|
138
|
+
}
|
|
139
|
+
/** Removes the .boot-typed marker (the wrappers do this at watchdog
|
|
140
|
+
* start so the grub command is typed once per build).
|
|
141
|
+
*
|
|
142
|
+
* @param cacheDir - The build cache dir.
|
|
143
|
+
*/
|
|
144
|
+
export function clearBootTypedMarker(cacheDir) {
|
|
145
|
+
rmSync(join(cacheDir, 'watchdog', '.boot-typed'), { force: true });
|
|
146
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// build-windows-vmware.ts — the windows-vmware build flow: the port of
|
|
2
|
+
// images/windows-arm64-vmware/build.sh (Fusion app + ARM64 boot-driver
|
|
3
|
+
// paths, the vmxnet3 trio staged into the unattend CD, the watchdog,
|
|
4
|
+
// packer with the vmware-iso builder, the post-build hardware upgrade).
|
|
5
|
+
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { run, runChecked } from '../lib/exec.js';
|
|
8
|
+
import { logger } from '../lib/logger.js';
|
|
9
|
+
import { announceBuild, buildDirLayout, ensureCacheDir, materializeContext, reportOutputDir, requireAppleSilicon, requireCommands, requireWindowsIso, runPackerBuild, runPackerFmtCheck, runPackerInit, stringVar, unzipVmxnet3Args, upgradeArtifactHardware, verifyIsoSha256, } from './build-shared.js';
|
|
10
|
+
import { startBuildWatchdog, stopBuildWatchdog } from './build-watchdog.js';
|
|
11
|
+
const DEFAULT_FUSION_APP = '/Applications/VMware Fusion.app';
|
|
12
|
+
/** Builds a windows-vmware image.
|
|
13
|
+
*
|
|
14
|
+
* @param image - The catalog image.
|
|
15
|
+
* @param options - Force/watchdog overrides.
|
|
16
|
+
*/
|
|
17
|
+
export async function buildWindowsVmwareImage(image, options) {
|
|
18
|
+
requireAppleSilicon('the Windows image can only be built on Apple Silicon (VMware\n' +
|
|
19
|
+
' Fusion cannot run ARM64 guests on Intel Macs).');
|
|
20
|
+
requireCommands([
|
|
21
|
+
['packer', 'brew install packer'],
|
|
22
|
+
['unzip', 'comes with macOS'],
|
|
23
|
+
['xmllint', 'comes with macOS'],
|
|
24
|
+
]);
|
|
25
|
+
const fusion = requireFusion(image);
|
|
26
|
+
const dirs = buildDirLayout('windows-vmware');
|
|
27
|
+
ensureCacheDir(dirs.cache);
|
|
28
|
+
const context = materializeContext(image);
|
|
29
|
+
announceBuild(image, context);
|
|
30
|
+
const winIso = requireWindowsIso(image);
|
|
31
|
+
await verifyIsoSha256(winIso, stringVar(image, 'iso_sha256'), 'Windows ISO', image.varsFile);
|
|
32
|
+
stageVmxnet3(fusion.driversZip, dirs.staging);
|
|
33
|
+
let watchdogPid;
|
|
34
|
+
if (options.watchdog !== false) {
|
|
35
|
+
watchdogPid = await startBuildWatchdog({ cacheDir: dirs.cache });
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
logger.step('xmllint autounattend.xml');
|
|
39
|
+
await runChecked('xmllint', ['--noout', join(context.platformDir, 'autounattend.xml')]);
|
|
40
|
+
await runPackerInit(context.templateFile);
|
|
41
|
+
await runPackerFmtCheck(context.platformDir);
|
|
42
|
+
await runPackerBuild({
|
|
43
|
+
platformDir: context.platformDir,
|
|
44
|
+
templateFile: context.templateFile,
|
|
45
|
+
varsFile: image.varsFile,
|
|
46
|
+
buildDir: dirs.build,
|
|
47
|
+
force: options.force,
|
|
48
|
+
env: {
|
|
49
|
+
PKR_VAR_iso_path: winIso,
|
|
50
|
+
PKR_VAR_vmware_fusion_app_path: fusion.path,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await stopBuildWatchdog(watchdogPid);
|
|
56
|
+
}
|
|
57
|
+
const output = requireVmxOutput(dirs.output, image.name);
|
|
58
|
+
await upgradeArtifactHardware(output, fusion.path);
|
|
59
|
+
await reportOutputDir(dirs.output);
|
|
60
|
+
logger.ok(`Done: ${output} (export with agent-dev-env deploy ${image.name})`);
|
|
61
|
+
}
|
|
62
|
+
/** Resolves the Fusion app (FUSION_APP_PATH → vars → default) and checks
|
|
63
|
+
* the ARM64 drivers zip + tools ISO exist.
|
|
64
|
+
*
|
|
65
|
+
* @param image - The catalog image.
|
|
66
|
+
* @returns The fusion paths.
|
|
67
|
+
*/
|
|
68
|
+
function requireFusion(image) {
|
|
69
|
+
const path = process.env.FUSION_APP_PATH ?? stringVar(image, 'vmware_fusion_app_path') ?? DEFAULT_FUSION_APP;
|
|
70
|
+
if (!existsSync(path)) {
|
|
71
|
+
throw new Error(`VMware Fusion not found at ${path}.\n Install VMware Fusion (free, Broadcom) or set FUSION_APP_PATH.`);
|
|
72
|
+
}
|
|
73
|
+
logger.step(`using VMware Fusion at ${path}`);
|
|
74
|
+
const driversZip = join(path, 'Contents', 'Library', 'isoimages', 'arm64', 'drivers-arm64.zip');
|
|
75
|
+
const toolsIso = join(path, 'Contents', 'Library', 'isoimages', 'arm64', 'windows.iso');
|
|
76
|
+
if (!existsSync(driversZip)) {
|
|
77
|
+
throw new Error(`ARM64 boot drivers not found at ${driversZip}.\n` +
|
|
78
|
+
' This Fusion version does not ship the ARM64 Windows drivers —\n' +
|
|
79
|
+
' use Fusion 13.6+ (checked from the app bundle).');
|
|
80
|
+
}
|
|
81
|
+
if (!existsSync(toolsIso)) {
|
|
82
|
+
throw new Error(`ARM64 VMware Tools ISO not found at ${toolsIso}.\n` +
|
|
83
|
+
' This Fusion version does not ship ARM64 tools —\n' +
|
|
84
|
+
' use Fusion 13.6+ (checked from the app bundle).');
|
|
85
|
+
}
|
|
86
|
+
return { path, driversZip };
|
|
87
|
+
}
|
|
88
|
+
/** Extracts the vmxnet3 ARM64 inf/sys/cat trio flat into the staging dir
|
|
89
|
+
* (packed into the unattend CD).
|
|
90
|
+
*
|
|
91
|
+
* @param driversZip - Fusion's drivers-arm64.zip.
|
|
92
|
+
* @param stagingDir - The staging dir.
|
|
93
|
+
*/
|
|
94
|
+
async function stageVmxnet3(driversZip, stagingDir) {
|
|
95
|
+
logger.step(`staging vmxnet3 ARM64 drivers into ${stagingDir}`);
|
|
96
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
97
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
98
|
+
const res = await run('unzip', unzipVmxnet3Args(driversZip, stagingDir));
|
|
99
|
+
if (res.code !== 0) {
|
|
100
|
+
throw new Error(`vmxnet3 ARM64 driver missing from ${driversZip}.\n` +
|
|
101
|
+
' Expected vmxnet3/Win10_1709/ARM64/vmxnet3.{inf,sys,cat}.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** The built vmx (must exist after the vmware-iso build). */
|
|
105
|
+
function requireVmxOutput(outputDir, imageName) {
|
|
106
|
+
const output = join(outputDir, `${imageName}.vmx`);
|
|
107
|
+
if (!existsSync(output)) {
|
|
108
|
+
throw new Error(`build produced no ${output}\n` +
|
|
109
|
+
` Expected the vmware-iso builder's export in ${outputDir}/.`);
|
|
110
|
+
}
|
|
111
|
+
return output;
|
|
112
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// build.ts — `agent-dev-env build`: the dispatcher (the port of
|
|
2
|
+
// scripts/build.sh). Resolves the requested images (all catalog images
|
|
3
|
+
// when none are given), then runs the per-platform build flow; the first
|
|
4
|
+
// failure aborts the run (the CLI's top level turns the throw into
|
|
5
|
+
// die()).
|
|
6
|
+
import { logger } from '../lib/logger.js';
|
|
7
|
+
import { buildMacosImage } from './build-macos.js';
|
|
8
|
+
import { buildQemuImage } from './build-qemu.js';
|
|
9
|
+
import { buildUbuntuImage } from './build-ubuntu.js';
|
|
10
|
+
import { buildWindowsVmwareImage } from './build-windows-vmware.js';
|
|
11
|
+
import { resolveRequestedImages } from './catalog.js';
|
|
12
|
+
/** Builds one image with its platform's flow.
|
|
13
|
+
*
|
|
14
|
+
* @param image - The catalog image.
|
|
15
|
+
* @param options - Force/watchdog overrides.
|
|
16
|
+
*/
|
|
17
|
+
async function buildImage(image, options) {
|
|
18
|
+
const flow = {
|
|
19
|
+
force: options.force,
|
|
20
|
+
watchdog: options.watchdog !== false,
|
|
21
|
+
};
|
|
22
|
+
switch (image.platform) {
|
|
23
|
+
case 'macos':
|
|
24
|
+
await buildMacosImage(image, flow);
|
|
25
|
+
return;
|
|
26
|
+
case 'windows-qemu':
|
|
27
|
+
await buildQemuImage(image, flow);
|
|
28
|
+
return;
|
|
29
|
+
case 'windows-vmware':
|
|
30
|
+
await buildWindowsVmwareImage(image, flow);
|
|
31
|
+
return;
|
|
32
|
+
case 'ubuntu-vmware':
|
|
33
|
+
await buildUbuntuImage(image, flow);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** `agent-dev-env build [image...] [--force] [--no-watchdog]`.
|
|
38
|
+
*
|
|
39
|
+
* @param requested - Image names (all images when empty).
|
|
40
|
+
* @param options - Force/watchdog overrides.
|
|
41
|
+
* @returns The exit code (0 on success).
|
|
42
|
+
*/
|
|
43
|
+
export async function buildCmd(requested = [], options = {}) {
|
|
44
|
+
const targets = resolveRequestedImages(requested);
|
|
45
|
+
if (requested.length === 0) {
|
|
46
|
+
logger.title('Building all images:');
|
|
47
|
+
for (const image of targets) {
|
|
48
|
+
logger.info(image.name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const image of targets) {
|
|
52
|
+
await buildImage(image, options);
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// catalog.ts — the image catalog: discovers images from the per-image
|
|
2
|
+
// vars files (images/<platform>/vars/<image>.pkrvars.hcl) and maps name
|
|
3
|
+
// <-> platform <-> vars file <-> defaults.
|
|
4
|
+
//
|
|
5
|
+
// Replaces list_images()/find_vars_file() from scripts/build.sh,
|
|
6
|
+
// scripts/deploy.sh, scripts/tag.sh and the wrapper resolution in each
|
|
7
|
+
// runner.
|
|
8
|
+
//
|
|
9
|
+
// Roots (first match wins, deduped by image name):
|
|
10
|
+
// 1. the checkout's images/ — when this process runs inside a clone
|
|
11
|
+
// (dev mode / repo tooling)
|
|
12
|
+
// 2. the bundled snapshot under dist/assets/images/ — when installed
|
|
13
|
+
// from npm (the package ships everything it needs)
|
|
14
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { basename, dirname, join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { findRepoRoot } from '../lib/git.js';
|
|
18
|
+
import { PLATFORM_DIR_MAP } from '../lib/platform.js';
|
|
19
|
+
import { parseVars, readQuotedVar } from '../lib/vars.js';
|
|
20
|
+
function bundledImagesRoot() {
|
|
21
|
+
// dist/lifecycle/catalog.js -> dist/assets/images (the package ships the
|
|
22
|
+
// images/ snapshot next to the compiled CLI).
|
|
23
|
+
return fileURLToPath(new URL('../assets/images', import.meta.url));
|
|
24
|
+
}
|
|
25
|
+
/** @internal — The images roots to search, repo checkout first. */
|
|
26
|
+
export function imagesRoots(cwd = process.cwd()) {
|
|
27
|
+
const roots = [];
|
|
28
|
+
const repo = findRepoRoot(cwd);
|
|
29
|
+
if (repo) {
|
|
30
|
+
roots.push(join(repo, 'images'));
|
|
31
|
+
}
|
|
32
|
+
roots.push(bundledImagesRoot());
|
|
33
|
+
return [...new Set(roots)];
|
|
34
|
+
}
|
|
35
|
+
/** Vars files under <root>/<platform>/vars/*.pkrvars.hcl, sorted by image
|
|
36
|
+
* name for deterministic output. */
|
|
37
|
+
function varsFilesIn(root) {
|
|
38
|
+
if (!existsSync(root)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const platformDir of readdirSync(root, { withFileTypes: true })) {
|
|
43
|
+
if (!platformDir.isDirectory()) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const varsDir = join(root, platformDir.name, 'vars');
|
|
47
|
+
if (!existsSync(varsDir)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const entry of readdirSync(varsDir)) {
|
|
51
|
+
if (entry.endsWith('.pkrvars.hcl')) {
|
|
52
|
+
out.push(join(varsDir, entry));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out.sort();
|
|
57
|
+
}
|
|
58
|
+
function platformForDir(dir) {
|
|
59
|
+
const platform = PLATFORM_DIR_MAP[basename(dir)];
|
|
60
|
+
if (!platform) {
|
|
61
|
+
throw new Error(`unknown platform directory: ${dir}`);
|
|
62
|
+
}
|
|
63
|
+
return platform;
|
|
64
|
+
}
|
|
65
|
+
/** Lists every image in the catalog (repo root first, deduped by name).
|
|
66
|
+
*
|
|
67
|
+
* @param options - Explicit images roots (defaults to checkout + bundled).
|
|
68
|
+
* @returns The catalog entries, sorted by image name.
|
|
69
|
+
*/
|
|
70
|
+
export function listImages(options = {}) {
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const root of options.roots ?? imagesRoots()) {
|
|
74
|
+
for (const varsFile of varsFilesIn(root)) {
|
|
75
|
+
const name = basename(varsFile, '.pkrvars.hcl');
|
|
76
|
+
if (seen.has(name)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
seen.add(name);
|
|
80
|
+
out.push({
|
|
81
|
+
name,
|
|
82
|
+
platform: platformForDir(dirname(dirname(varsFile))),
|
|
83
|
+
varsFile,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/** Looks an image up by name; throws with the shell's error message.
|
|
90
|
+
*
|
|
91
|
+
* @param name - The image name.
|
|
92
|
+
* @param options - Catalog roots (defaults to the repo + bundled assets).
|
|
93
|
+
* @returns The catalog image.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveImage(name, options = {}) {
|
|
96
|
+
const image = listImages(options).find((i) => i.name === name);
|
|
97
|
+
if (!image) {
|
|
98
|
+
throw new Error(`No vars file found for image '${name}'. ` +
|
|
99
|
+
`Expected images/<platform>/vars/${name}.pkrvars.hcl`);
|
|
100
|
+
}
|
|
101
|
+
return image;
|
|
102
|
+
}
|
|
103
|
+
/** Returns the catalog's default image for a platform.
|
|
104
|
+
*
|
|
105
|
+
* @param platform - The platform to look up.
|
|
106
|
+
* @param options - Explicit images roots (defaults to checkout + bundled).
|
|
107
|
+
* @returns The platform's catalog image.
|
|
108
|
+
* @throws Error when the platform has no image in the catalog.
|
|
109
|
+
*/
|
|
110
|
+
export function defaultImageFor(platform, options = {}) {
|
|
111
|
+
const image = listImages(options).find((i) => i.platform === platform);
|
|
112
|
+
if (!image) {
|
|
113
|
+
throw new Error(`No image found for platform '${platform}' — expected ` +
|
|
114
|
+
`images/<platform>/vars/*.pkrvars.hcl`);
|
|
115
|
+
}
|
|
116
|
+
return image;
|
|
117
|
+
}
|
|
118
|
+
/** Resolves the requested image names — explicit names in order, or the
|
|
119
|
+
* whole catalog when empty (no-arg build/deploy/tag semantics).
|
|
120
|
+
*
|
|
121
|
+
* @param requested - Image names (empty = all images).
|
|
122
|
+
* @returns The catalog images, in catalog order for "all".
|
|
123
|
+
* @throws Error for an unknown name or an empty catalog.
|
|
124
|
+
*/
|
|
125
|
+
export function resolveRequestedImages(requested = []) {
|
|
126
|
+
if (requested.length > 0) {
|
|
127
|
+
return requested.map((name) => resolveImage(name));
|
|
128
|
+
}
|
|
129
|
+
const all = listImages();
|
|
130
|
+
if (all.length === 0) {
|
|
131
|
+
throw new Error('No images found: no vars files under images/*/vars/.');
|
|
132
|
+
}
|
|
133
|
+
return all;
|
|
134
|
+
}
|
|
135
|
+
const varsCache = new Map();
|
|
136
|
+
/** Parses all assignments in the image's vars file (cached per file).
|
|
137
|
+
*
|
|
138
|
+
* @param image - The catalog image.
|
|
139
|
+
* @returns The vars file's assignments as a name → value map.
|
|
140
|
+
*/
|
|
141
|
+
export function varsFor(image) {
|
|
142
|
+
const cached = varsCache.get(image.varsFile);
|
|
143
|
+
if (cached) {
|
|
144
|
+
return cached;
|
|
145
|
+
}
|
|
146
|
+
const text = readFileSync(image.varsFile, 'utf8');
|
|
147
|
+
const parsed = parseVars(text);
|
|
148
|
+
varsCache.set(image.varsFile, parsed);
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
/** Reads the image's image_version — required, same error as
|
|
152
|
+
* deploy.sh/tag.sh.
|
|
153
|
+
*
|
|
154
|
+
* @param image - The catalog image.
|
|
155
|
+
* @returns The version string.
|
|
156
|
+
* @throws Error when the vars file has no image_version.
|
|
157
|
+
*/
|
|
158
|
+
export function imageVersion(image) {
|
|
159
|
+
const text = readFileSync(image.varsFile, 'utf8');
|
|
160
|
+
const version = readQuotedVar(text, 'image_version');
|
|
161
|
+
if (version === undefined) {
|
|
162
|
+
throw new Error(`Could not read image_version from ${image.varsFile}`);
|
|
163
|
+
}
|
|
164
|
+
return version;
|
|
165
|
+
}
|