@bun-native/cli 0.33.0 → 0.33.1
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/README.md +1 -1
- package/bun-native-source.json +64 -44
- package/package.json +31 -1
- package/src/experiments/aot-gpui/compile.ts +12 -9
- package/src/experiments/aot-gpui/inspect-runtime.ts +3 -2
- package/src/experiments/aot-gpui/inspect.ts +3 -2
- package/src/scripts/native/artifact.ts +44 -3
- package/src/scripts/native/build.ts +22 -41
- package/src/scripts/native/bundle.ts +121 -0
- package/src/scripts/native/consumer-files.ts +46 -0
- package/src/scripts/native/context.ts +95 -0
- package/src/scripts/native/doctor.ts +3 -2
- package/src/scripts/native/host-download.ts +64 -24
- package/src/scripts/native/inputs.ts +50 -20
- package/src/scripts/native/lint.ts +2 -2
- package/src/scripts/native/package-archive.ts +7 -16
- package/src/scripts/native/package-set.ts +24 -15
- package/src/scripts/native/package-stamp.ts +209 -0
- package/src/scripts/native/project.ts +44 -24
- package/src/scripts/native/quality.ts +36 -21
- package/src/scripts/native/registry-project.ts +90 -0
- package/src/scripts/native/session.ts +4 -1
- package/src/scripts/native/standalone.ts +8 -27
- package/src/scripts/native/ustar.ts +29 -0
- package/src/scripts/native.ts +62 -44
- package/src/toolchain.json +12 -2
- package/src/scripts/solid-build.ts +0 -43
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { basename, dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where this CLI runs from, computed once per invocation.
|
|
6
|
+
*
|
|
7
|
+
* - `checkout`: `scripts/native.ts` in a Bun Native checkout. The version is the
|
|
8
|
+
* workspace's root `package.json`; released hosts are cached in the checkout's
|
|
9
|
+
* `.bun-native/hosts`.
|
|
10
|
+
* - `package`: the installed `@bun-native/cli`, whose sources live under `src/`.
|
|
11
|
+
* The version is the package's own manifest. Released hosts are cached in the
|
|
12
|
+
* per-user cache, never inside the digest-verified CLI package and never in the
|
|
13
|
+
* consumer project, whose `.bun-native/` holds only `dev` modules.
|
|
14
|
+
*/
|
|
15
|
+
export type CliContext = {
|
|
16
|
+
mode: 'checkout' | 'package'
|
|
17
|
+
/** The directory that holds `scripts/native.ts`: the checkout root, or the package's `src`. */
|
|
18
|
+
root: string
|
|
19
|
+
/** Absent only for a bare copy of the CLI sources; host downloads then need --version. */
|
|
20
|
+
version: string | undefined
|
|
21
|
+
hostCache: string
|
|
22
|
+
/** How the usage text spells this CLI. */
|
|
23
|
+
command: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Environment = Record<string, string | undefined>
|
|
27
|
+
|
|
28
|
+
const packageCommand = 'bun --no-install ./node_modules/@bun-native/cli/src/scripts/native.ts'
|
|
29
|
+
|
|
30
|
+
async function manifest(path: string): Promise<{ name?: unknown; version?: unknown } | undefined> {
|
|
31
|
+
const file = Bun.file(path)
|
|
32
|
+
return (await file.exists()) ? file.json() : undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The per-user cache for state the CLI keeps between runs (released hosts,
|
|
37
|
+
* verification stamps, the compiler digest). Projects stay free of it.
|
|
38
|
+
*/
|
|
39
|
+
export function userCache(
|
|
40
|
+
environment: Environment = process.env,
|
|
41
|
+
platform: NodeJS.Platform = process.platform,
|
|
42
|
+
home: string = homedir()
|
|
43
|
+
): string {
|
|
44
|
+
const base =
|
|
45
|
+
environment['XDG_CACHE_HOME'] ||
|
|
46
|
+
(platform === 'darwin'
|
|
47
|
+
? join(home, 'Library', 'Caches')
|
|
48
|
+
: platform === 'win32'
|
|
49
|
+
? environment['LOCALAPPDATA'] || join(home, 'AppData', 'Local')
|
|
50
|
+
: join(home, '.cache'))
|
|
51
|
+
return join(base, 'bun-native')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Released hosts of an installed CLI. */
|
|
55
|
+
export function userHostCache(
|
|
56
|
+
environment: Environment = process.env,
|
|
57
|
+
platform: NodeJS.Platform = process.platform,
|
|
58
|
+
home: string = homedir()
|
|
59
|
+
): string {
|
|
60
|
+
return join(userCache(environment, platform, home), 'hosts')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function cliContext(
|
|
64
|
+
root: string,
|
|
65
|
+
environment: Environment = process.env
|
|
66
|
+
): Promise<CliContext> {
|
|
67
|
+
const packageRoot = dirname(root)
|
|
68
|
+
const installed =
|
|
69
|
+
basename(root) === 'src' ? await manifest(join(packageRoot, 'package.json')) : undefined
|
|
70
|
+
if (installed?.name === '@bun-native/cli') {
|
|
71
|
+
return {
|
|
72
|
+
mode: 'package',
|
|
73
|
+
root,
|
|
74
|
+
version: typeof installed.version === 'string' ? installed.version : undefined,
|
|
75
|
+
hostCache: userHostCache(environment),
|
|
76
|
+
command: packageCommand,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const workspace = await manifest(join(root, 'package.json'))
|
|
80
|
+
return {
|
|
81
|
+
mode: 'checkout',
|
|
82
|
+
root,
|
|
83
|
+
version: typeof workspace?.version === 'string' ? workspace.version : undefined,
|
|
84
|
+
hostCache: join(root, '.bun-native', 'hosts'),
|
|
85
|
+
command: 'bun run native',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The release version for host downloads: an explicit one, else this CLI's own. */
|
|
90
|
+
export function releaseVersion(context: CliContext, explicit?: string): string {
|
|
91
|
+
const version = explicit ?? context.version
|
|
92
|
+
if (version === undefined)
|
|
93
|
+
throw new Error('This CLI has no package version; pass host install --version x.y.z')
|
|
94
|
+
return version
|
|
95
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import toolchain from '../../toolchain.json'
|
|
2
|
+
import { cliContext } from './context.ts'
|
|
2
3
|
import { prebuiltHost } from './host.ts'
|
|
3
|
-
import { readProject } from './project.ts'
|
|
4
4
|
|
|
5
5
|
type Diagnosis = { name: string; status: 'ok' | 'failed' | 'skipped'; detail: string }
|
|
6
6
|
|
|
@@ -55,11 +55,12 @@ export async function diagnose(
|
|
|
55
55
|
results.push({ name: 'project', status: 'skipped', detail: 'pass a project directory' })
|
|
56
56
|
else
|
|
57
57
|
try {
|
|
58
|
+
const { readProject } = await import('./project.ts')
|
|
58
59
|
const project = await readProject(repository, options.project)
|
|
59
60
|
results.push({
|
|
60
61
|
name: 'project',
|
|
61
62
|
status: 'ok',
|
|
62
|
-
detail: `${project.framework} project ${project.directory}${project.packageSet ? ' with verified installed package set' : ' in this checkout'}`,
|
|
63
|
+
detail: `${project.framework} project ${project.directory}${project.packageSet ? ' with verified installed package set' : (await cliContext(repository)).mode === 'package' ? ' with registry packages' : ' in this checkout'}`,
|
|
63
64
|
})
|
|
64
65
|
} catch (error) {
|
|
65
66
|
results.push({ name: 'project', status: 'failed', detail: message(error) })
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
1
|
import { chmod, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises'
|
|
3
2
|
import { dirname, join } from 'node:path'
|
|
4
3
|
import { gunzipSync } from 'node:zlib'
|
|
4
|
+
import { sha256 } from './artifact.ts'
|
|
5
5
|
import { prebuiltHost } from './host.ts'
|
|
6
|
+
import { ustarChecksumValid, ustarNumber } from './ustar.ts'
|
|
6
7
|
|
|
7
8
|
/** The platforms whose release host archives the release workflow attaches (ADR 0021). */
|
|
8
9
|
export const releaseHostTargets = [
|
|
@@ -16,6 +17,7 @@ export type ReleaseHostTarget = (typeof releaseHostTargets)[number]
|
|
|
16
17
|
const releases = 'https://github.com/0xPlayerOne/bun-native/releases/download'
|
|
17
18
|
const maximumArchiveBytes = 512 * 1024 * 1024
|
|
18
19
|
const maximumChecksumBytes = 64 * 1024
|
|
20
|
+
const defaultTimeoutMs = 5 * 60_000
|
|
19
21
|
|
|
20
22
|
export function releaseHostTarget(
|
|
21
23
|
platform: string = process.platform,
|
|
@@ -44,12 +46,6 @@ export function checksumFor(sums: string, asset: string): string {
|
|
|
44
46
|
return matches[0]![1]!
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
function octal(header: Buffer, offset: number, length: number): number {
|
|
48
|
-
const text = header.toString('latin1', offset, offset + length).replace(/[\0 ]+$/, '')
|
|
49
|
-
if (!/^[0-7]+$/.test(text)) throw new Error('Host archive has an invalid USTAR number')
|
|
50
|
-
return Number.parseInt(text, 8)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
49
|
/**
|
|
54
50
|
* The single `bun-native-host` member of a host archive made by host-archive.ts:
|
|
55
51
|
* a gzip-compressed USTAR stream with exactly one regular file, mode 0755.
|
|
@@ -64,11 +60,9 @@ export function hostFromArchive(archive: Uint8Array): Buffer {
|
|
|
64
60
|
if (header[156] !== 0x30 && header[156] !== 0)
|
|
65
61
|
throw new Error('Host member is not a regular file')
|
|
66
62
|
if (header.toString('latin1', 257, 262) !== 'ustar') throw new Error('Host archive is not USTAR')
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (checksum !== octal(header, 148, 8)) throw new Error('Host archive header checksum mismatch')
|
|
71
|
-
const size = octal(header, 124, 12)
|
|
63
|
+
if (!ustarChecksumValid(header)) throw new Error('Host archive header checksum mismatch')
|
|
64
|
+
const size = ustarNumber(header.subarray(124, 136))
|
|
65
|
+
if (size === undefined) throw new Error('Host archive has an invalid USTAR number')
|
|
72
66
|
const end = 512 + Math.ceil(size / 512) * 512
|
|
73
67
|
if (size === 0 || end + 1024 > tar.length) throw new Error('Host archive is truncated')
|
|
74
68
|
if (tar.subarray(end).some((byte) => byte !== 0))
|
|
@@ -76,14 +70,55 @@ export function hostFromArchive(archive: Uint8Array): Buffer {
|
|
|
76
70
|
return Buffer.from(tar.subarray(512, 512 + size))
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
/**
|
|
74
|
+
* One bounded download: the same deadline covers the request and the whole body,
|
|
75
|
+
* which is streamed into a byte-limited buffer (the install-bun/download.mjs pattern).
|
|
76
|
+
* A stalled release server fails the command instead of hanging it.
|
|
77
|
+
*/
|
|
78
|
+
async function download(url: string, limit: number, timeoutMs: number): Promise<Uint8Array> {
|
|
79
|
+
const controller = new AbortController()
|
|
80
|
+
const expired = () => new Error(`Download exceeded ${timeoutMs} ms: ${url}`)
|
|
81
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
82
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
83
|
+
timer = setTimeout(() => {
|
|
84
|
+
controller.abort()
|
|
85
|
+
reject(expired())
|
|
86
|
+
}, timeoutMs)
|
|
87
|
+
})
|
|
88
|
+
const operation = (async () => {
|
|
89
|
+
let response: Response
|
|
90
|
+
try {
|
|
91
|
+
response = await fetch(url, { redirect: 'follow', signal: controller.signal })
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (controller.signal.aborted) throw expired()
|
|
94
|
+
throw error
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
if (!response.ok) throw new Error(`Download failed (${response.status}): ${url}`)
|
|
98
|
+
const declared = Number(response.headers.get('content-length') ?? '0')
|
|
99
|
+
if (declared > limit) throw new Error(`Download is larger than ${limit} bytes: ${url}`)
|
|
100
|
+
const parts: Uint8Array[] = []
|
|
101
|
+
let total = 0
|
|
102
|
+
for await (const part of response.body ?? []) {
|
|
103
|
+
total += part.byteLength
|
|
104
|
+
if (total > limit) throw new Error(`Download is larger than ${limit} bytes: ${url}`)
|
|
105
|
+
parts.push(part)
|
|
106
|
+
}
|
|
107
|
+
return Buffer.concat(parts, total)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (controller.signal.aborted) throw expired()
|
|
110
|
+
throw error
|
|
111
|
+
} finally {
|
|
112
|
+
// Release the connection on every early exit; a completed body is a no-op.
|
|
113
|
+
controller.abort()
|
|
114
|
+
}
|
|
115
|
+
})()
|
|
116
|
+
try {
|
|
117
|
+
return await Promise.race([operation, deadline])
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timer)
|
|
120
|
+
operation.catch(() => {})
|
|
121
|
+
}
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
export type HostInstallOptions = {
|
|
@@ -94,9 +129,11 @@ export type HostInstallOptions = {
|
|
|
94
129
|
cache: string
|
|
95
130
|
force?: boolean
|
|
96
131
|
target?: ReleaseHostTarget
|
|
132
|
+
/** Deadline for each download, request and body together. Defaults to 5 minutes. */
|
|
133
|
+
timeoutMs?: number
|
|
97
134
|
}
|
|
98
135
|
|
|
99
|
-
|
|
136
|
+
function installedHostPath(options: HostInstallOptions): string {
|
|
100
137
|
const target = options.target ?? releaseHostTarget()
|
|
101
138
|
return join(options.cache, `v${options.version}`, target, 'bun-native-host')
|
|
102
139
|
}
|
|
@@ -117,10 +154,13 @@ export async function installReleaseHost(options: HostInstallOptions): Promise<s
|
|
|
117
154
|
return prebuiltHost(destination)
|
|
118
155
|
const base = `${(options.baseUrl ?? process.env['BUN_NATIVE_HOST_RELEASE_URL'] ?? releases).replace(/\/+$/, '')}/v${options.version}`
|
|
119
156
|
const asset = releaseHostAsset(target)
|
|
120
|
-
const
|
|
157
|
+
const timeoutMs = options.timeoutMs ?? defaultTimeoutMs
|
|
158
|
+
const sums = new TextDecoder().decode(
|
|
159
|
+
await download(`${base}/SHA256SUMS`, maximumChecksumBytes, timeoutMs)
|
|
160
|
+
)
|
|
121
161
|
const expected = checksumFor(sums, asset)
|
|
122
|
-
const archive = await download(`${base}/${asset}`, maximumArchiveBytes)
|
|
123
|
-
const actual =
|
|
162
|
+
const archive = await download(`${base}/${asset}`, maximumArchiveBytes, timeoutMs)
|
|
163
|
+
const actual = sha256(archive)
|
|
124
164
|
if (actual !== expected)
|
|
125
165
|
throw new Error(`${asset} digest ${actual} does not match SHA256SUMS ${expected}`)
|
|
126
166
|
const host = hostFromArchive(archive)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
import { createReadStream } from 'node:fs'
|
|
3
1
|
import { readdir, realpath, stat } from 'node:fs/promises'
|
|
4
2
|
import { join, relative } from 'node:path'
|
|
5
3
|
|
|
@@ -22,9 +20,40 @@ function disappeared(error: unknown): boolean {
|
|
|
22
20
|
)
|
|
23
21
|
}
|
|
24
22
|
|
|
23
|
+
type Input = { name: string; path: string; link: boolean }
|
|
24
|
+
|
|
25
|
+
/** The digest entry for one input, or undefined when it vanished or is not a file. */
|
|
26
|
+
async function inputDigest(
|
|
27
|
+
directory: string,
|
|
28
|
+
input: Input,
|
|
29
|
+
signal?: AbortSignal
|
|
30
|
+
): Promise<string | undefined> {
|
|
31
|
+
try {
|
|
32
|
+
// File links are hashed through their targets. Do not follow directory
|
|
33
|
+
// links: recursive aliases can escape the project or introduce cycles.
|
|
34
|
+
if (input.link && !(await stat(input.path)).isFile()) return undefined
|
|
35
|
+
const target = input.link ? relative(directory, await realpath(input.path)) : null
|
|
36
|
+
const contents = await Bun.file(input.path).bytes()
|
|
37
|
+
signal?.throwIfAborted()
|
|
38
|
+
return JSON.stringify([
|
|
39
|
+
input.name,
|
|
40
|
+
target,
|
|
41
|
+
new Bun.CryptoHasher('sha256').update(contents).digest('hex'),
|
|
42
|
+
])
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Deletion/atomic replacement during a scan is an absent input; a
|
|
45
|
+
// queued create/change event requests another scan. Other IO fails.
|
|
46
|
+
if (!disappeared(error)) throw error
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Enough concurrent reads to overlap file system latency without exhausting descriptors.
|
|
52
|
+
const READERS = 16
|
|
53
|
+
|
|
25
54
|
/** One content digest, with no retained file contents or earlier generations. */
|
|
26
55
|
export async function projectFingerprint(directory: string, signal?: AbortSignal): Promise<string> {
|
|
27
|
-
const
|
|
56
|
+
const inputs: Input[] = []
|
|
28
57
|
const visit = async (folder: string, prefix: string): Promise<void> => {
|
|
29
58
|
signal?.throwIfAborted()
|
|
30
59
|
let entries
|
|
@@ -36,30 +65,31 @@ export async function projectFingerprint(directory: string, signal?: AbortSignal
|
|
|
36
65
|
}
|
|
37
66
|
entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))
|
|
38
67
|
for (const entry of entries) {
|
|
39
|
-
signal?.throwIfAborted()
|
|
40
68
|
if (ignored.has(entry.name)) continue
|
|
41
69
|
const path = join(folder, entry.name)
|
|
42
70
|
const name = prefix + entry.name
|
|
43
71
|
if (entry.isDirectory()) await visit(path, name + '/')
|
|
44
|
-
else if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const target = entry.isSymbolicLink() ? relative(directory, await realpath(path)) : null
|
|
50
|
-
const contents = createHash('sha256')
|
|
51
|
-
for await (const chunk of createReadStream(path, { signal })) contents.update(chunk)
|
|
52
|
-
signal?.throwIfAborted()
|
|
53
|
-
fingerprint.update(JSON.stringify([name, target, contents.digest('hex')]))
|
|
54
|
-
} catch (error) {
|
|
55
|
-
// Deletion/atomic replacement during a scan is an absent input; a
|
|
56
|
-
// queued create/change event requests another scan. Other IO fails.
|
|
57
|
-
if (!disappeared(error)) throw error
|
|
58
|
-
}
|
|
59
|
-
}
|
|
72
|
+
else if (
|
|
73
|
+
/\.(?:[cm]?[jt]s|[jt]sx|jsonc?|toml)$/.test(name) &&
|
|
74
|
+
(entry.isFile() || entry.isSymbolicLink())
|
|
75
|
+
)
|
|
76
|
+
inputs.push({ name, path, link: entry.isSymbolicLink() })
|
|
60
77
|
}
|
|
61
78
|
}
|
|
62
79
|
await visit(directory, '')
|
|
80
|
+
// Read and hash files concurrently, then fold them in the sorted traversal order,
|
|
81
|
+
// so the digest is identical to a serial scan.
|
|
82
|
+
const digests: (string | undefined)[] = Array.from({ length: inputs.length })
|
|
83
|
+
let next = 0
|
|
84
|
+
const reader = async () => {
|
|
85
|
+
for (let index = next++; index < inputs.length; index = next++) {
|
|
86
|
+
signal?.throwIfAborted()
|
|
87
|
+
digests[index] = await inputDigest(directory, inputs[index]!, signal)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
await Promise.all(Array.from({ length: Math.min(READERS, inputs.length) }, reader))
|
|
63
91
|
signal?.throwIfAborted()
|
|
92
|
+
const fingerprint = new Bun.CryptoHasher('sha256')
|
|
93
|
+
for (const digest of digests) if (digest !== undefined) fingerprint.update(digest)
|
|
64
94
|
return fingerprint.digest('hex')
|
|
65
95
|
}
|
|
@@ -38,6 +38,6 @@ export function lintArguments(project: Project): string[] {
|
|
|
38
38
|
return args
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export function lintProject(project: Project): QualityStep {
|
|
42
|
-
return { name: 'lint', ...runQualityStep(project, lintArguments(project)) }
|
|
41
|
+
export async function lintProject(project: Project): Promise<QualityStep> {
|
|
42
|
+
return { name: 'lint', ...(await runQualityStep(project, lintArguments(project))) }
|
|
43
43
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
1
|
import { gunzipSync } from 'node:zlib'
|
|
3
|
-
import type
|
|
2
|
+
import { sha256, type FileDigest } from './artifact.ts'
|
|
4
3
|
import { packageArchivePath } from './paths.ts'
|
|
4
|
+
import { USTAR_BLOCK as block, ustarChecksumValid, ustarNumber } from './ustar.ts'
|
|
5
5
|
|
|
6
|
-
const block = 512
|
|
7
6
|
const maximumBytes = 64 * 1024 * 1024
|
|
8
7
|
|
|
9
8
|
function text(field: Buffer): string {
|
|
@@ -14,10 +13,8 @@ function text(field: Buffer): string {
|
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
function octal(field: Buffer): number {
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
const number = Number.parseInt(value.replaceAll('\0', '').trim(), 8)
|
|
20
|
-
if (!Number.isSafeInteger(number)) throw new Error('Invalid package tar size')
|
|
16
|
+
const number = ustarNumber(field)
|
|
17
|
+
if (number === undefined) throw new Error('Invalid package tar numeric field')
|
|
21
18
|
return number
|
|
22
19
|
}
|
|
23
20
|
|
|
@@ -57,11 +54,8 @@ export async function verifyPackageArchive(
|
|
|
57
54
|
ended = true
|
|
58
55
|
break
|
|
59
56
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
for (let index = 0; index < block; index += 1)
|
|
63
|
-
sum += index >= 148 && index < 156 ? 32 : header[index]!
|
|
64
|
-
if (sum !== checksum) throw new Error('Invalid package tar header checksum')
|
|
57
|
+
octal(header.subarray(148, 156))
|
|
58
|
+
if (!ustarChecksumValid(header)) throw new Error('Invalid package tar header checksum')
|
|
65
59
|
if (!['ustar\0', 'ustar '].includes(header.subarray(257, 263).toString('latin1')))
|
|
66
60
|
throw new Error('Package archive must use the supported USTAR format')
|
|
67
61
|
const kind = header[156] === 0 ? '0' : String.fromCharCode(header[156]!)
|
|
@@ -88,10 +82,7 @@ export async function verifyPackageArchive(
|
|
|
88
82
|
} else {
|
|
89
83
|
const expected = files.get(name)
|
|
90
84
|
if (!expected) throw new Error(`Packed archive file inventory differs: ${name}`)
|
|
91
|
-
if (
|
|
92
|
-
size !== expected.bytes ||
|
|
93
|
-
createHash('sha256').update(data.subarray(start, end)).digest('hex') !== expected.sha256
|
|
94
|
-
)
|
|
85
|
+
if (size !== expected.bytes || sha256(data.subarray(start, end)) !== expected.sha256)
|
|
95
86
|
throw new Error(`Packed archive digest mismatch: ${name}`)
|
|
96
87
|
regular.add(name)
|
|
97
88
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
1
|
import { readdir, realpath, stat } from 'node:fs/promises'
|
|
3
2
|
import { dirname, join } from 'node:path'
|
|
4
3
|
import { isDeepStrictEqual } from 'node:util'
|
|
@@ -11,7 +10,7 @@ import {
|
|
|
11
10
|
type DependencyLock,
|
|
12
11
|
} from './package-lock.ts'
|
|
13
12
|
import { inside, packageArchivePath, packagePath } from './paths.ts'
|
|
14
|
-
import { fileDigest, type FileDigest } from './artifact.ts'
|
|
13
|
+
import { fileDigest, sha256, type FileDigest } from './artifact.ts'
|
|
15
14
|
import { verifyPackageArchive } from './package-archive.ts'
|
|
16
15
|
|
|
17
16
|
export const packageNames = [
|
|
@@ -160,7 +159,7 @@ function packageManifest(
|
|
|
160
159
|
if (
|
|
161
160
|
!isDeepStrictEqual(contents['package.json'], {
|
|
162
161
|
bytes: Buffer.byteLength(text),
|
|
163
|
-
sha256:
|
|
162
|
+
sha256: sha256(text),
|
|
164
163
|
})
|
|
165
164
|
)
|
|
166
165
|
throw new Error(`Package manifest digest mismatch: ${name}`)
|
|
@@ -302,6 +301,12 @@ async function containedFile(directory: string, path: string): Promise<string> {
|
|
|
302
301
|
return target
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
/** Run checks concurrently, but report the first failure in their listed order. */
|
|
305
|
+
async function settledInOrder(tasks: Promise<void>[]): Promise<void> {
|
|
306
|
+
for (const result of await Promise.allSettled(tasks))
|
|
307
|
+
if (result.status === 'rejected') throw result.reason
|
|
308
|
+
}
|
|
309
|
+
|
|
305
310
|
export async function readPackageSet(path: string): Promise<ReadPackageSet> {
|
|
306
311
|
if (Bun.revision !== toolchain.bun.revision)
|
|
307
312
|
throw new Error('Use the pinned Bun for this package set')
|
|
@@ -311,12 +316,14 @@ export async function readPackageSet(path: string): Promise<ReadPackageSet> {
|
|
|
311
316
|
const manifest = parsePackageSet(JSON.parse(text))
|
|
312
317
|
if (text !== JSON.stringify(manifest, null, 2) + '\n')
|
|
313
318
|
throw new Error('Package-set manifest must use its original canonical JSON encoding')
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
319
|
+
await settledInOrder(
|
|
320
|
+
Object.entries(manifest.packages).map(async ([name, pkg]) => {
|
|
321
|
+
const file = await containedFile(directory, pkg.archive.file)
|
|
322
|
+
if (!isDeepStrictEqual(await fileDigest(file), digest(pkg.archive)))
|
|
323
|
+
throw new Error(`Package archive digest mismatch: ${name}`)
|
|
324
|
+
await verifyPackageArchive(file, pkg.files)
|
|
325
|
+
})
|
|
326
|
+
)
|
|
320
327
|
return { directory, manifest, text }
|
|
321
328
|
}
|
|
322
329
|
|
|
@@ -362,7 +369,7 @@ export async function verifyInstalledPackages(
|
|
|
362
369
|
...pkg.files,
|
|
363
370
|
'.bun-tag': {
|
|
364
371
|
bytes: Buffer.byteLength(tag),
|
|
365
|
-
sha256:
|
|
372
|
+
sha256: sha256(tag),
|
|
366
373
|
},
|
|
367
374
|
}
|
|
368
375
|
const expected = Object.keys(installedFiles).sort()
|
|
@@ -385,11 +392,13 @@ export async function verifyInstalledPackages(
|
|
|
385
392
|
await visit(root, '')
|
|
386
393
|
if (!isDeepStrictEqual(inventory.sort(), expected))
|
|
387
394
|
throw new Error(`Installed package file inventory differs: ${name}`)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
395
|
+
await settledInOrder(
|
|
396
|
+
Object.entries(installedFiles).map(async ([path, expected]) => {
|
|
397
|
+
const file = await containedFile(root, path)
|
|
398
|
+
if (!isDeepStrictEqual(await fileDigest(file), expected))
|
|
399
|
+
throw new Error(`Installed package digest mismatch: ${name}/${path}`)
|
|
400
|
+
})
|
|
401
|
+
)
|
|
393
402
|
}
|
|
394
403
|
for (const name of required) {
|
|
395
404
|
const root = locations.get(name)!
|