@namzu/sandbox 23.0.1 → 25.0.0
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/CHANGELOG.md +290 -0
- package/dist/backends/http-worker-client.d.ts +3 -1
- package/dist/backends/http-worker-client.d.ts.map +1 -1
- package/dist/backends/http-worker-client.js +15 -4
- package/dist/backends/http-worker-client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/local-virtual-computer/client.d.ts +21 -0
- package/dist/local-virtual-computer/client.d.ts.map +1 -0
- package/dist/local-virtual-computer/client.js +503 -0
- package/dist/local-virtual-computer/client.js.map +1 -0
- package/dist/local-virtual-computer/detached-process.d.ts +15 -0
- package/dist/local-virtual-computer/detached-process.d.ts.map +1 -0
- package/dist/local-virtual-computer/detached-process.js +111 -0
- package/dist/local-virtual-computer/detached-process.js.map +1 -0
- package/dist/local-virtual-computer/detached-worker.d.ts +11 -0
- package/dist/local-virtual-computer/detached-worker.d.ts.map +1 -0
- package/dist/local-virtual-computer/detached-worker.js +97 -0
- package/dist/local-virtual-computer/detached-worker.js.map +1 -0
- package/dist/local-virtual-computer/dialect.d.ts +26 -0
- package/dist/local-virtual-computer/dialect.d.ts.map +1 -0
- package/dist/local-virtual-computer/dialect.js +61 -0
- package/dist/local-virtual-computer/dialect.js.map +1 -0
- package/dist/local-virtual-computer/engine.d.ts +63 -0
- package/dist/local-virtual-computer/engine.d.ts.map +1 -0
- package/dist/local-virtual-computer/engine.js +125 -0
- package/dist/local-virtual-computer/engine.js.map +1 -0
- package/dist/local-virtual-computer/guest-stdio.d.ts +15 -0
- package/dist/local-virtual-computer/guest-stdio.d.ts.map +1 -0
- package/dist/local-virtual-computer/guest-stdio.js +330 -0
- package/dist/local-virtual-computer/guest-stdio.js.map +1 -0
- package/dist/local-virtual-computer/index.d.ts +36 -0
- package/dist/local-virtual-computer/index.d.ts.map +1 -0
- package/dist/local-virtual-computer/index.js +341 -0
- package/dist/local-virtual-computer/index.js.map +1 -0
- package/dist/local-virtual-computer/podman-machine.d.ts +17 -0
- package/dist/local-virtual-computer/podman-machine.d.ts.map +1 -0
- package/dist/local-virtual-computer/podman-machine.js +181 -0
- package/dist/local-virtual-computer/podman-machine.js.map +1 -0
- package/dist/local-virtual-computer/setup/contract.d.ts +360 -0
- package/dist/local-virtual-computer/setup/contract.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/contract.js +3 -0
- package/dist/local-virtual-computer/setup/contract.js.map +1 -0
- package/dist/local-virtual-computer/setup/elevated.d.ts +78 -0
- package/dist/local-virtual-computer/setup/elevated.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/elevated.js +548 -0
- package/dist/local-virtual-computer/setup/elevated.js.map +1 -0
- package/dist/local-virtual-computer/setup/host.d.ts +12 -0
- package/dist/local-virtual-computer/setup/host.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/host.js +93 -0
- package/dist/local-virtual-computer/setup/host.js.map +1 -0
- package/dist/local-virtual-computer/setup/idle.d.ts +18 -0
- package/dist/local-virtual-computer/setup/idle.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/idle.js +38 -0
- package/dist/local-virtual-computer/setup/idle.js.map +1 -0
- package/dist/local-virtual-computer/setup/image.d.ts +7 -0
- package/dist/local-virtual-computer/setup/image.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/image.js +124 -0
- package/dist/local-virtual-computer/setup/image.js.map +1 -0
- package/dist/local-virtual-computer/setup/index.d.ts +17 -0
- package/dist/local-virtual-computer/setup/index.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/index.js +17 -0
- package/dist/local-virtual-computer/setup/index.js.map +1 -0
- package/dist/local-virtual-computer/setup/limits.d.ts +30 -0
- package/dist/local-virtual-computer/setup/limits.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/limits.js +58 -0
- package/dist/local-virtual-computer/setup/limits.js.map +1 -0
- package/dist/local-virtual-computer/setup/plan.d.ts +8 -0
- package/dist/local-virtual-computer/setup/plan.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/plan.js +131 -0
- package/dist/local-virtual-computer/setup/plan.js.map +1 -0
- package/dist/local-virtual-computer/setup/readiness.d.ts +11 -0
- package/dist/local-virtual-computer/setup/readiness.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/readiness.js +320 -0
- package/dist/local-virtual-computer/setup/readiness.js.map +1 -0
- package/dist/local-virtual-computer/setup/registry.d.ts +18 -0
- package/dist/local-virtual-computer/setup/registry.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/registry.js +50 -0
- package/dist/local-virtual-computer/setup/registry.js.map +1 -0
- package/dist/local-virtual-computer/setup/restart.d.ts +8 -0
- package/dist/local-virtual-computer/setup/restart.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/restart.js +60 -0
- package/dist/local-virtual-computer/setup/restart.js.map +1 -0
- package/dist/local-virtual-computer/setup/resume.d.ts +14 -0
- package/dist/local-virtual-computer/setup/resume.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/resume.js +58 -0
- package/dist/local-virtual-computer/setup/resume.js.map +1 -0
- package/dist/local-virtual-computer/setup/run.d.ts +8 -0
- package/dist/local-virtual-computer/setup/run.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/run.js +200 -0
- package/dist/local-virtual-computer/setup/run.js.map +1 -0
- package/dist/local-virtual-computer/setup/windows-folder.d.ts +11 -0
- package/dist/local-virtual-computer/setup/windows-folder.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/windows-folder.js +14 -0
- package/dist/local-virtual-computer/setup/windows-folder.js.map +1 -0
- package/dist/local-virtual-computer/setup/wsl-package.d.ts +19 -0
- package/dist/local-virtual-computer/setup/wsl-package.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/wsl-package.js +242 -0
- package/dist/local-virtual-computer/setup/wsl-package.js.map +1 -0
- package/dist/local-virtual-computer/setup/wslc-shared.d.ts +27 -0
- package/dist/local-virtual-computer/setup/wslc-shared.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/wslc-shared.js +137 -0
- package/dist/local-virtual-computer/setup/wslc-shared.js.map +1 -0
- package/dist/local-virtual-computer/ssh-forward.d.ts +19 -0
- package/dist/local-virtual-computer/ssh-forward.d.ts.map +1 -0
- package/dist/local-virtual-computer/ssh-forward.js +179 -0
- package/dist/local-virtual-computer/ssh-forward.js.map +1 -0
- package/dist/local-virtual-computer/wslc-engine.d.ts +27 -0
- package/dist/local-virtual-computer/wslc-engine.d.ts.map +1 -0
- package/dist/local-virtual-computer/wslc-engine.js +98 -0
- package/dist/local-virtual-computer/wslc-engine.js.map +1 -0
- package/local-computer/Dockerfile +99 -0
- package/local-computer/app-catalog.cjs +240 -0
- package/local-computer/app-native-host.cjs +144 -0
- package/local-computer/browser-home.cjs +249 -0
- package/local-computer/build-image.mjs +310 -0
- package/local-computer/desktop-worker.cjs +477 -0
- package/local-computer/entrypoint.sh +161 -0
- package/local-computer/home.html +28 -0
- package/local-computer/new-tab/home.css +255 -0
- package/local-computer/new-tab/home.js +65 -0
- package/local-computer/new-tab/manifest.json +12 -0
- package/local-computer/new-tab/wordmark.svg +33 -0
- package/local-computer/openbox-rc.xml +41 -0
- package/local-computer/prepare-home.cjs +121 -0
- package/local-computer/tint2rc +44 -0
- package/package.json +13 -4
- package/src/backends/http-worker-client.ts +31 -3
- package/src/index.ts +13 -0
- package/src/local-virtual-computer/client.ts +573 -0
- package/src/local-virtual-computer/detached-process.ts +133 -0
- package/src/local-virtual-computer/detached-worker.ts +124 -0
- package/src/local-virtual-computer/dialect.ts +83 -0
- package/src/local-virtual-computer/engine.ts +197 -0
- package/src/local-virtual-computer/guest-stdio.ts +332 -0
- package/src/local-virtual-computer/index.ts +447 -0
- package/src/local-virtual-computer/podman-machine.ts +264 -0
- package/src/local-virtual-computer/setup/contract.ts +395 -0
- package/src/local-virtual-computer/setup/elevated.ts +655 -0
- package/src/local-virtual-computer/setup/host.ts +100 -0
- package/src/local-virtual-computer/setup/idle.ts +39 -0
- package/src/local-virtual-computer/setup/image.ts +159 -0
- package/src/local-virtual-computer/setup/index.ts +16 -0
- package/src/local-virtual-computer/setup/limits.ts +68 -0
- package/src/local-virtual-computer/setup/plan.ts +144 -0
- package/src/local-virtual-computer/setup/readiness.ts +386 -0
- package/src/local-virtual-computer/setup/registry.ts +50 -0
- package/src/local-virtual-computer/setup/restart.ts +64 -0
- package/src/local-virtual-computer/setup/resume.ts +65 -0
- package/src/local-virtual-computer/setup/run.ts +225 -0
- package/src/local-virtual-computer/setup/windows-folder.ts +13 -0
- package/src/local-virtual-computer/setup/wsl-package.ts +242 -0
- package/src/local-virtual-computer/setup/wslc-shared.ts +147 -0
- package/src/local-virtual-computer/ssh-forward.ts +195 -0
- package/src/local-virtual-computer/wslc-engine.ts +124 -0
- package/worker/server.js +1499 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build one stage of the Pal computer image with a container engine, save it,
|
|
3
|
+
// compress it and write a manifest the installer carries beside it.
|
|
4
|
+
//
|
|
5
|
+
// node build-image.mjs --engine docker --stage base --out ./out
|
|
6
|
+
// node build-image.mjs --engine wslc --stage base --tag namzu-local-computer:2 --out ./out --format zst
|
|
7
|
+
//
|
|
8
|
+
// The engine is any binary with `build`, `image inspect` and `save` (docker, podman,
|
|
9
|
+
// wslc). Every external command goes through an injected runner, so the tests drive
|
|
10
|
+
// this module without an engine.
|
|
11
|
+
import { spawn } from 'node:child_process'
|
|
12
|
+
import { createHash } from 'node:crypto'
|
|
13
|
+
import { createReadStream, createWriteStream } from 'node:fs'
|
|
14
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
|
|
15
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
16
|
+
import { pipeline } from 'node:stream/promises'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import zlib from 'node:zlib'
|
|
19
|
+
|
|
20
|
+
export const STAGES = ['base', 'creative']
|
|
21
|
+
export const PROTOCOL_LABEL = 'org.namzu.local-computer.protocol'
|
|
22
|
+
/** `wslc load -i` sniffs the content, so any of these loads without a separate decompression step. */
|
|
23
|
+
export const FORMATS = ['zst', 'xz', 'gz', 'tar']
|
|
24
|
+
const DEFAULT_TAGS = { base: 'namzu-local-computer:2', creative: 'namzu-local-computer:2-creative' }
|
|
25
|
+
const EXTENSIONS = { zst: 'tar.zst', xz: 'tar.xz', gz: 'tar.gz', tar: 'tar' }
|
|
26
|
+
const DEFAULT_LEVELS = { zst: 19, xz: 9, gz: 9, tar: 0 }
|
|
27
|
+
|
|
28
|
+
export class BuildImageError extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message)
|
|
31
|
+
this.name = 'BuildImageError'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
36
|
+
|
|
37
|
+
export function defaultTag(stage) {
|
|
38
|
+
return DEFAULT_TAGS[stage]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The integer revision a tag names: `ns/name:2` and `ns/name:2-creative` are both revision 2. */
|
|
42
|
+
export function revisionFromTag(tag) {
|
|
43
|
+
const match = /:(\d+)(?:-[A-Za-z0-9._]+)?$/.exec(tag)
|
|
44
|
+
return match ? Number(match[1]) : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function usage() {
|
|
48
|
+
return [
|
|
49
|
+
'Usage: build-image.mjs --engine <docker|podman|wslc|path> --out <dir> [options]',
|
|
50
|
+
'',
|
|
51
|
+
' --engine <bin> container engine binary that builds, inspects and saves',
|
|
52
|
+
' --stage <name> base (default) or creative',
|
|
53
|
+
' --tag <ref> image reference (default namzu-local-computer:2, or :2-creative)',
|
|
54
|
+
' --out <dir> directory for the archive and manifest.json',
|
|
55
|
+
' --format <fmt> zst (default), xz, gz or tar; all load directly with wslc load -i',
|
|
56
|
+
' --level <n> compression level (zst 19, xz 9, gz 9)',
|
|
57
|
+
' --revision <n> manifest revision (default: the number in the tag)',
|
|
58
|
+
' --context <dir> build context (default packages/sandbox)',
|
|
59
|
+
' --dockerfile <file> Dockerfile (default local-computer/Dockerfile)',
|
|
60
|
+
' --no-cache build without the engine cache',
|
|
61
|
+
' --skip-build save the existing image instead of building it',
|
|
62
|
+
].join('\n')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseArguments(argv) {
|
|
66
|
+
const options = {
|
|
67
|
+
engine: undefined,
|
|
68
|
+
stage: 'base',
|
|
69
|
+
tag: undefined,
|
|
70
|
+
out: undefined,
|
|
71
|
+
format: 'zst',
|
|
72
|
+
level: undefined,
|
|
73
|
+
revision: undefined,
|
|
74
|
+
context: resolve(HERE, '..'),
|
|
75
|
+
dockerfile: join(HERE, 'Dockerfile'),
|
|
76
|
+
noCache: false,
|
|
77
|
+
skipBuild: false,
|
|
78
|
+
}
|
|
79
|
+
const value = (index, flag) => {
|
|
80
|
+
const next = argv[index + 1]
|
|
81
|
+
if (next === undefined || next.startsWith('--'))
|
|
82
|
+
throw new BuildImageError(`${flag} needs a value`)
|
|
83
|
+
return next
|
|
84
|
+
}
|
|
85
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
86
|
+
const flag = argv[index]
|
|
87
|
+
switch (flag) {
|
|
88
|
+
case '--engine':
|
|
89
|
+
options.engine = value(index++, flag)
|
|
90
|
+
break
|
|
91
|
+
case '--stage':
|
|
92
|
+
options.stage = value(index++, flag)
|
|
93
|
+
break
|
|
94
|
+
case '--tag':
|
|
95
|
+
options.tag = value(index++, flag)
|
|
96
|
+
break
|
|
97
|
+
case '--out':
|
|
98
|
+
options.out = resolve(value(index++, flag))
|
|
99
|
+
break
|
|
100
|
+
case '--format':
|
|
101
|
+
options.format = value(index++, flag)
|
|
102
|
+
break
|
|
103
|
+
case '--level':
|
|
104
|
+
options.level = Number(value(index++, flag))
|
|
105
|
+
break
|
|
106
|
+
case '--revision':
|
|
107
|
+
options.revision = Number(value(index++, flag))
|
|
108
|
+
break
|
|
109
|
+
case '--context':
|
|
110
|
+
options.context = resolve(value(index++, flag))
|
|
111
|
+
break
|
|
112
|
+
case '--dockerfile':
|
|
113
|
+
options.dockerfile = resolve(value(index++, flag))
|
|
114
|
+
break
|
|
115
|
+
case '--no-cache':
|
|
116
|
+
options.noCache = true
|
|
117
|
+
break
|
|
118
|
+
case '--skip-build':
|
|
119
|
+
options.skipBuild = true
|
|
120
|
+
break
|
|
121
|
+
default:
|
|
122
|
+
throw new BuildImageError(`Unknown argument: ${flag}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!options.engine) throw new BuildImageError('--engine is required')
|
|
126
|
+
if (!options.out) throw new BuildImageError('--out is required')
|
|
127
|
+
if (!STAGES.includes(options.stage))
|
|
128
|
+
throw new BuildImageError(`--stage must be one of ${STAGES.join(', ')}`)
|
|
129
|
+
if (!FORMATS.includes(options.format))
|
|
130
|
+
throw new BuildImageError(`--format must be one of ${FORMATS.join(', ')}`)
|
|
131
|
+
options.tag ??= defaultTag(options.stage)
|
|
132
|
+
if (!/^[a-z0-9][a-z0-9._/-]*:[A-Za-z0-9._-]+$/.test(options.tag))
|
|
133
|
+
throw new BuildImageError(`--tag is not an image reference: ${options.tag}`)
|
|
134
|
+
options.revision ??= revisionFromTag(options.tag)
|
|
135
|
+
if (!Number.isInteger(options.revision) || options.revision < 1)
|
|
136
|
+
throw new BuildImageError('--revision must be a positive integer (or a tag ending in :<n>)')
|
|
137
|
+
options.level ??= DEFAULT_LEVELS[options.format]
|
|
138
|
+
if (!Number.isInteger(options.level) || options.level < 0 || options.level > 22)
|
|
139
|
+
throw new BuildImageError('--level must be an integer from 0 to 22')
|
|
140
|
+
return options
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Real command runner: argv array, no shell, output captured (inherited for long builds). */
|
|
144
|
+
export function spawnRunner(file, args, { cwd, inherit = false } = {}) {
|
|
145
|
+
return new Promise((resolvePromise, reject) => {
|
|
146
|
+
const child = spawn(file, args, { cwd, stdio: inherit ? ['ignore', 'inherit', 'inherit'] : 'pipe' })
|
|
147
|
+
let stdout = ''
|
|
148
|
+
let stderr = ''
|
|
149
|
+
child.stdout?.on('data', (chunk) => {
|
|
150
|
+
stdout += chunk
|
|
151
|
+
})
|
|
152
|
+
child.stderr?.on('data', (chunk) => {
|
|
153
|
+
stderr += chunk
|
|
154
|
+
})
|
|
155
|
+
child.once('error', reject)
|
|
156
|
+
child.once('close', (code) => resolvePromise({ code: code ?? 1, stdout, stderr }))
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function exec(run, file, args, options) {
|
|
161
|
+
const result = await run(file, args, options)
|
|
162
|
+
if (result.code !== 0)
|
|
163
|
+
throw new BuildImageError(
|
|
164
|
+
`${basename(file)} ${args[0]} failed (${result.code}): ${(result.stderr || result.stdout || '').trim().slice(-500)}`,
|
|
165
|
+
)
|
|
166
|
+
return result
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function sha256File(path) {
|
|
170
|
+
const hash = createHash('sha256')
|
|
171
|
+
await pipeline(createReadStream(path), hash)
|
|
172
|
+
return hash.digest('hex')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Compress `source` to `destination`. xz has no Node codec, so it runs the engine host's `xz`. */
|
|
176
|
+
export async function compressFile({ run, source, destination, format, level }) {
|
|
177
|
+
if (format === 'tar') {
|
|
178
|
+
await rename(source, destination)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (format === 'xz') {
|
|
182
|
+
await exec(run, 'xz', [`-${level}e`, '--keep', '--force', '--threads=0', source])
|
|
183
|
+
await rename(`${source}.xz`, destination)
|
|
184
|
+
await rm(source, { force: true })
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
let codec
|
|
188
|
+
if (format === 'gz') codec = zlib.createGzip({ level })
|
|
189
|
+
else {
|
|
190
|
+
if (typeof zlib.createZstdCompress !== 'function')
|
|
191
|
+
throw new BuildImageError('zstd needs Node 22.15 or newer, or use --format gz or xz')
|
|
192
|
+
codec = zlib.createZstdCompress({
|
|
193
|
+
params: {
|
|
194
|
+
// ZSTD_c_nbWorkers stays at its default of none: Node's zstd stream fails with
|
|
195
|
+
// "stream.push() after EOF" when workers are on (checked on Node 22.20 and 24.19). One
|
|
196
|
+
// thread takes about 5 minutes at level 19 (measured on the base stage).
|
|
197
|
+
[zlib.constants.ZSTD_c_compressionLevel]: level,
|
|
198
|
+
[zlib.constants.ZSTD_c_checksumFlag]: 1,
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
await pipeline(createReadStream(source), codec, createWriteStream(destination))
|
|
203
|
+
await rm(source, { force: true })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build, save and compress one stage, then write manifest.json beside the archive.
|
|
208
|
+
* `run(file, args, { cwd, inherit })` resolves { code, stdout, stderr }.
|
|
209
|
+
*/
|
|
210
|
+
export async function buildImage(options, { run = spawnRunner, log = () => {} } = {}) {
|
|
211
|
+
const { engine, stage, tag, out, format, level, revision, context, dockerfile } = options
|
|
212
|
+
await mkdir(out, { recursive: true })
|
|
213
|
+
const started = Date.now()
|
|
214
|
+
if (!options.skipBuild) {
|
|
215
|
+
const args = ['build', '--file', dockerfile, '--target', stage, '--tag', tag]
|
|
216
|
+
if (options.noCache) args.push('--no-cache')
|
|
217
|
+
args.push(context)
|
|
218
|
+
log(`building ${tag} (${stage})`)
|
|
219
|
+
await exec(run, engine, args, { inherit: true })
|
|
220
|
+
}
|
|
221
|
+
const inspected = await exec(run, engine, ['image', 'inspect', tag])
|
|
222
|
+
let record
|
|
223
|
+
try {
|
|
224
|
+
record = JSON.parse(inspected.stdout)
|
|
225
|
+
if (Array.isArray(record)) record = record[0]
|
|
226
|
+
} catch {
|
|
227
|
+
throw new BuildImageError(`image inspect ${tag} did not print JSON`)
|
|
228
|
+
}
|
|
229
|
+
const imageId = record?.Id ?? record?.ID
|
|
230
|
+
const protocol = record?.Config?.Labels?.[PROTOCOL_LABEL]
|
|
231
|
+
if (typeof imageId !== 'string' || !imageId) throw new BuildImageError(`${tag} has no image id`)
|
|
232
|
+
if (typeof protocol !== 'string' || !protocol)
|
|
233
|
+
throw new BuildImageError(`${tag} has no ${PROTOCOL_LABEL} label`)
|
|
234
|
+
const built = Date.now()
|
|
235
|
+
|
|
236
|
+
const stem = `${tag.replace(/[/:]/g, '-')}`
|
|
237
|
+
const tarPath = join(out, `${stem}.tar`)
|
|
238
|
+
log(`saving ${tag}`)
|
|
239
|
+
await exec(run, engine, ['save', '--output', tarPath, tag])
|
|
240
|
+
const sizeBytes = (await stat(tarPath)).size
|
|
241
|
+
const tarSha256 = await sha256File(tarPath)
|
|
242
|
+
const archive = join(out, `${stem}.${EXTENSIONS[format]}`)
|
|
243
|
+
log(`compressing to ${basename(archive)}`)
|
|
244
|
+
await compressFile({ run, source: tarPath, destination: archive, format, level })
|
|
245
|
+
const compressedBytes = (await stat(archive)).size
|
|
246
|
+
const sha256 = await sha256File(archive)
|
|
247
|
+
const manifest = {
|
|
248
|
+
revision,
|
|
249
|
+
imageRef: tag,
|
|
250
|
+
imageId,
|
|
251
|
+
protocol,
|
|
252
|
+
sha256,
|
|
253
|
+
sizeBytes,
|
|
254
|
+
compressedBytes,
|
|
255
|
+
format,
|
|
256
|
+
file: basename(archive),
|
|
257
|
+
stage,
|
|
258
|
+
tarSha256,
|
|
259
|
+
}
|
|
260
|
+
const manifestPath = join(out, 'manifest.json')
|
|
261
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
262
|
+
return {
|
|
263
|
+
manifest,
|
|
264
|
+
manifestPath,
|
|
265
|
+
archive,
|
|
266
|
+
buildMs: built - started,
|
|
267
|
+
totalMs: Date.now() - started,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Read a manifest back and check it against the archive beside it. */
|
|
272
|
+
export async function verifyManifest(manifestPath) {
|
|
273
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
274
|
+
const archive = join(dirname(manifestPath), manifest.file)
|
|
275
|
+
const info = await stat(archive)
|
|
276
|
+
if (info.size !== manifest.compressedBytes)
|
|
277
|
+
throw new BuildImageError(`${manifest.file} is ${info.size} bytes, manifest says ${manifest.compressedBytes}`)
|
|
278
|
+
if ((await sha256File(archive)) !== manifest.sha256)
|
|
279
|
+
throw new BuildImageError(`${manifest.file} does not match the manifest sha256`)
|
|
280
|
+
return manifest
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function main(argv, deps = {}) {
|
|
284
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
285
|
+
process.stdout.write(`${usage()}\n`)
|
|
286
|
+
return 0
|
|
287
|
+
}
|
|
288
|
+
let options
|
|
289
|
+
try {
|
|
290
|
+
options = parseArguments(argv)
|
|
291
|
+
} catch (error) {
|
|
292
|
+
process.stderr.write(`${error.message}\n\n${usage()}\n`)
|
|
293
|
+
return 2
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const result = await buildImage(options, {
|
|
297
|
+
...deps,
|
|
298
|
+
log: (line) => process.stderr.write(`${line}\n`),
|
|
299
|
+
})
|
|
300
|
+
process.stdout.write(`${JSON.stringify({ ...result.manifest, buildMs: result.buildMs, totalMs: result.totalMs })}\n`)
|
|
301
|
+
return 0
|
|
302
|
+
} catch (error) {
|
|
303
|
+
process.stderr.write(`${error.message}\n`)
|
|
304
|
+
return 1
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
309
|
+
process.exitCode = await main(process.argv.slice(2))
|
|
310
|
+
}
|