@bun-native/cli 0.0.0-stage → 0.33.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/LICENSE +21 -0
- package/README.md +4 -2
- package/bun-native-source.json +131 -0
- package/package.json +33 -4
- package/src/.oxfmtrc.json +20 -0
- package/src/.oxlintrc.json +12 -0
- package/src/experiments/aot-gpui/compile.ts +41 -0
- package/src/experiments/aot-gpui/inspect-runtime.ts +303 -0
- package/src/experiments/aot-gpui/inspect.ts +65 -0
- package/src/scripts/native/artifact.ts +32 -0
- package/src/scripts/native/build.ts +250 -0
- package/src/scripts/native/doctor.ts +68 -0
- package/src/scripts/native/host-download.ts +139 -0
- package/src/scripts/native/host-header.ts +33 -0
- package/src/scripts/native/host.ts +22 -0
- package/src/scripts/native/inputs.ts +65 -0
- package/src/scripts/native/lint.ts +43 -0
- package/src/scripts/native/package-archive.ts +103 -0
- package/src/scripts/native/package-lock.ts +220 -0
- package/src/scripts/native/package-set.ts +408 -0
- package/src/scripts/native/paths.ts +20 -0
- package/src/scripts/native/project.ts +251 -0
- package/src/scripts/native/quality.ts +61 -0
- package/src/scripts/native/session.ts +296 -0
- package/src/scripts/native/standalone.ts +83 -0
- package/src/scripts/native/watch.ts +104 -0
- package/src/scripts/native.ts +219 -0
- package/src/scripts/solid/transform.ts +16 -0
- package/src/scripts/solid-build.ts +43 -0
- package/src/toolchain.json +64 -0
- package/src/tsconfig.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bunly contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bun-native/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create, check, run and AOT-build Bun Native desktop applications.
|
|
4
|
+
|
|
5
|
+
Part of [Bun Native](https://github.com/0xPlayerOne/bun-native), an experimental developer preview. Version 0.33.0 was built from [fa8bef754122](https://github.com/0xPlayerOne/bun-native/tree/fa8bef754122621d4235eb06e30bb1085b4f7b1e) and published from GitHub Actions with npm provenance. Use the Bun revision pinned in that commit's toolchain.json. Start with the [getting-started guide](https://github.com/0xPlayerOne/bun-native/blob/fa8bef754122621d4235eb06e30bb1085b4f7b1e/docs/getting-started.md); `bun-native host install` fetches the matching release host. The license notice is in LICENSE.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"commit": "fa8bef754122621d4235eb06e30bb1085b4f7b1e",
|
|
4
|
+
"tree": "f805e2eb19a4e10fc29d1dc827cf5bc617b5773b",
|
|
5
|
+
"files": {
|
|
6
|
+
"package.json": {
|
|
7
|
+
"bytes": 2164,
|
|
8
|
+
"sha256": "db50b6f81f25c113a46f8c9b038bc548a92effaeb4ad9562e5e95044baa7b66f"
|
|
9
|
+
},
|
|
10
|
+
".oxfmtrc.json": {
|
|
11
|
+
"bytes": 402,
|
|
12
|
+
"sha256": "ead97e1ad225cb7b960d838c1a8130b98c88940634c366b45e622e0da586e8be"
|
|
13
|
+
},
|
|
14
|
+
".oxlintrc.json": {
|
|
15
|
+
"bytes": 297,
|
|
16
|
+
"sha256": "25e27847d6fd50c27314506d50c83a68764489b84bf91f9c35dceb937134e882"
|
|
17
|
+
},
|
|
18
|
+
"experiments/aot-gpui/compile.ts": {
|
|
19
|
+
"bytes": 1661,
|
|
20
|
+
"sha256": "832cec3a32205995551dd270fdda0fd4bd26f0dfdf5b11d65457c73b88ec2b8f"
|
|
21
|
+
},
|
|
22
|
+
"experiments/aot-gpui/inspect-runtime.ts": {
|
|
23
|
+
"bytes": 12491,
|
|
24
|
+
"sha256": "071d377315ba801faad088a2dcf0140598536d38c7112139fd927a649a1ea285"
|
|
25
|
+
},
|
|
26
|
+
"experiments/aot-gpui/inspect.ts": {
|
|
27
|
+
"bytes": 2383,
|
|
28
|
+
"sha256": "f80f7ec7e8a070f6e9d8478380d193a0120f9b89dedd88422c7f7fd6b3490d48"
|
|
29
|
+
},
|
|
30
|
+
"scripts/native.ts": {
|
|
31
|
+
"bytes": 9688,
|
|
32
|
+
"sha256": "2290cf2b6508cc8a08c044ff363edc4a6b9b53b5a69169773d14807c3d4ed6c6"
|
|
33
|
+
},
|
|
34
|
+
"scripts/native/artifact.ts": {
|
|
35
|
+
"bytes": 1131,
|
|
36
|
+
"sha256": "e0496b30b935f02efb8297b41fd2e7ad310fc58d8a6056c3bd4b0d8c142044c1"
|
|
37
|
+
},
|
|
38
|
+
"scripts/native/build.ts": {
|
|
39
|
+
"bytes": 10234,
|
|
40
|
+
"sha256": "a72c2639c1a5dd4d31eb48cea48c0a010e71b0ea7794ba1eace2d5f35f7677d1"
|
|
41
|
+
},
|
|
42
|
+
"scripts/native/doctor.ts": {
|
|
43
|
+
"bytes": 2610,
|
|
44
|
+
"sha256": "ca2f57dacf6c929bb336324fc71228c03557b3c38ab9f5f8b77da86cb5048f7c"
|
|
45
|
+
},
|
|
46
|
+
"scripts/native/host-download.ts": {
|
|
47
|
+
"bytes": 6430,
|
|
48
|
+
"sha256": "d27ef6968d506ad8adc0b850a1283fe8c41487d9009dd690e75ad5ad379cdc44"
|
|
49
|
+
},
|
|
50
|
+
"scripts/native/host-header.ts": {
|
|
51
|
+
"bytes": 1248,
|
|
52
|
+
"sha256": "67e7cdeff0a9d16449c5ab1d5d6cdbbb5cc8881cf20591183f60a2f419ee5103"
|
|
53
|
+
},
|
|
54
|
+
"scripts/native/host.ts": {
|
|
55
|
+
"bytes": 990,
|
|
56
|
+
"sha256": "5e790bbf787bc22e37aecf031520942d96677717b201fd9889ddcb8eaa2715e1"
|
|
57
|
+
},
|
|
58
|
+
"scripts/native/inputs.ts": {
|
|
59
|
+
"bytes": 2839,
|
|
60
|
+
"sha256": "3e98bf1a0208104cc2c18235cfe84fbde719e5e6cf5fc8a4181c3fa69772ce94"
|
|
61
|
+
},
|
|
62
|
+
"scripts/native/lint.ts": {
|
|
63
|
+
"bytes": 1819,
|
|
64
|
+
"sha256": "eaedd87b266d54722c22a4a5b9ddc5606c96cedeb20d21ea9d2381ed5aaadd33"
|
|
65
|
+
},
|
|
66
|
+
"scripts/native/package-archive.ts": {
|
|
67
|
+
"bytes": 4945,
|
|
68
|
+
"sha256": "249cda6f7edf4eade989c324e473826899bcc0062cdf9cafde76b0d1c074247e"
|
|
69
|
+
},
|
|
70
|
+
"scripts/native/package-lock.ts": {
|
|
71
|
+
"bytes": 9244,
|
|
72
|
+
"sha256": "ad8f2b73ca5b88075b503a18c2f0405f13ec60a6c1d9a9d172056d707c0b5e10"
|
|
73
|
+
},
|
|
74
|
+
"scripts/native/package-set.ts": {
|
|
75
|
+
"bytes": 16263,
|
|
76
|
+
"sha256": "b7207b884b90303a0d44dc9a181617c03e24683850fc17a6db802fc740f0a26a"
|
|
77
|
+
},
|
|
78
|
+
"scripts/native/paths.ts": {
|
|
79
|
+
"bytes": 806,
|
|
80
|
+
"sha256": "6ccf669071878d65dbe4686e9b8585bbb217b4a9cd834f7c1866c9448c0c9a55"
|
|
81
|
+
},
|
|
82
|
+
"scripts/native/project.ts": {
|
|
83
|
+
"bytes": 11175,
|
|
84
|
+
"sha256": "c62044f4e0baa004bba1bd27a0ae3a53beb6aa02d558e356464c39fc471b0c61"
|
|
85
|
+
},
|
|
86
|
+
"scripts/native/quality.ts": {
|
|
87
|
+
"bytes": 2380,
|
|
88
|
+
"sha256": "5d2c68657e9a85db9e654b1be6ffc437bcd4fdc623854a98eda5f9da842e38cc"
|
|
89
|
+
},
|
|
90
|
+
"scripts/native/session.ts": {
|
|
91
|
+
"bytes": 9635,
|
|
92
|
+
"sha256": "def47965ad2d36d95e0f9a5b1f778443e5d811e8a82b9fb094e4ee79916eb26f"
|
|
93
|
+
},
|
|
94
|
+
"scripts/native/standalone.ts": {
|
|
95
|
+
"bytes": 6195,
|
|
96
|
+
"sha256": "c464f48f3c054dbbb057e621b1f4b55e2679b797bca3282561eed39cfe510f6f"
|
|
97
|
+
},
|
|
98
|
+
"scripts/native/watch.ts": {
|
|
99
|
+
"bytes": 3793,
|
|
100
|
+
"sha256": "b5d7f98516135ae1577823ccd6d4647f53d84df7a0f1fa68d4cad5a2c62cdd0f"
|
|
101
|
+
},
|
|
102
|
+
"scripts/solid-build.ts": {
|
|
103
|
+
"bytes": 1615,
|
|
104
|
+
"sha256": "5f263562cc299642e2971cce1dd913533ff092bc5543abd11d07048e8b3e5908"
|
|
105
|
+
},
|
|
106
|
+
"scripts/solid/transform.ts": {
|
|
107
|
+
"bytes": 642,
|
|
108
|
+
"sha256": "281fdbd71b10e613d9080e0494320161e51cfaf9a1c7b9966707eba0dd24ef1c"
|
|
109
|
+
},
|
|
110
|
+
"toolchain.json": {
|
|
111
|
+
"bytes": 3012,
|
|
112
|
+
"sha256": "00c3f707aae8abe9ca75cd2e162f5e1c7c34362a03675da39afead60d856066a"
|
|
113
|
+
},
|
|
114
|
+
"tsconfig.json": {
|
|
115
|
+
"bytes": 1104,
|
|
116
|
+
"sha256": "8d8121ab905345fe75ff994fc40706f683dfc120699ca48269e85b90a82197a7"
|
|
117
|
+
},
|
|
118
|
+
"LICENSE": {
|
|
119
|
+
"bytes": 1075,
|
|
120
|
+
"sha256": "86673fb485f12fa9aaa3941dfccc9f69a7f8322f8141d0d5c73db4323de10ac5"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"toolchain": {
|
|
124
|
+
"bytes": 3012,
|
|
125
|
+
"sha256": "00c3f707aae8abe9ca75cd2e162f5e1c7c34362a03675da39afead60d856066a"
|
|
126
|
+
},
|
|
127
|
+
"dependencyLock": {
|
|
128
|
+
"bytes": 28357,
|
|
129
|
+
"sha256": "65e3dbd01be4a1889367690faf1a9c3f08fb31d22dbd273d9bb9f39dd6baa6fb"
|
|
130
|
+
}
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bun-native/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.33.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md",
|
|
10
|
+
"bun-native-source.json"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"bun": "1.4.4-canary.1"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"bun-native": "./src/scripts/native.ts"
|
|
17
|
+
},
|
|
18
|
+
"description": "Create, check, run and AOT-build Bun Native desktop applications.",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@babel/core": "7.29.7",
|
|
21
|
+
"babel-preset-solid": "1.9.16",
|
|
22
|
+
"oxlint": "1.92.0"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/0xPlayerOne/bun-native.git",
|
|
27
|
+
"directory": "scripts"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/0xPlayerOne/bun-native#readme",
|
|
30
|
+
"bugs": "https://github.com/0xPlayerOne/bun-native/issues",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"provenance": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"semi": false,
|
|
3
|
+
"singleQuote": true,
|
|
4
|
+
"trailingComma": "es5",
|
|
5
|
+
"printWidth": 100,
|
|
6
|
+
"ignorePatterns": [
|
|
7
|
+
"CHANGELOG.md",
|
|
8
|
+
".github/.code-foundry",
|
|
9
|
+
".github/actions/",
|
|
10
|
+
"node_modules/**",
|
|
11
|
+
"native/target/**",
|
|
12
|
+
"dist/**",
|
|
13
|
+
".bun-native/**"
|
|
14
|
+
],
|
|
15
|
+
"tabWidth": 2,
|
|
16
|
+
"endOfLine": "lf",
|
|
17
|
+
"sortImports": false,
|
|
18
|
+
"sortPackageJson": false,
|
|
19
|
+
"embeddedLanguageFormatting": "off"
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"categories": {
|
|
4
|
+
"correctness": "error",
|
|
5
|
+
"suspicious": "off"
|
|
6
|
+
},
|
|
7
|
+
"ignorePatterns": ["node_modules/**", "packages/*/dist/**", "examples/*/dist/**"],
|
|
8
|
+
"rules": {
|
|
9
|
+
"no-debugger": "error",
|
|
10
|
+
"no-console": "off"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { inspectBunExecutable } from './inspect'
|
|
4
|
+
import { inspectBunRuntime } from './inspect-runtime'
|
|
5
|
+
import toolchain from '../../toolchain.json'
|
|
6
|
+
|
|
7
|
+
/** Shared real AOT compiler and executable checks for fixtures and application tooling. */
|
|
8
|
+
export function compileApplication(input: string, output: string, cwd: string, trim = false) {
|
|
9
|
+
if (Bun.revision !== toolchain.bun.revision) throw new Error('Use the pinned Bun AOT compiler')
|
|
10
|
+
const compileCommand = [
|
|
11
|
+
process.execPath,
|
|
12
|
+
'--no-install',
|
|
13
|
+
'compile',
|
|
14
|
+
'--no-script',
|
|
15
|
+
'--production',
|
|
16
|
+
...(trim ? ['--trim'] : []),
|
|
17
|
+
'--explain=json',
|
|
18
|
+
input,
|
|
19
|
+
'--outfile',
|
|
20
|
+
output,
|
|
21
|
+
]
|
|
22
|
+
const start = performance.now()
|
|
23
|
+
const result = Bun.spawnSync(compileCommand, {
|
|
24
|
+
cwd,
|
|
25
|
+
stdout: 'pipe',
|
|
26
|
+
stderr: 'pipe',
|
|
27
|
+
timeout: 120_000,
|
|
28
|
+
})
|
|
29
|
+
writeFileSync(join(dirname(output), 'compile-aot.stdout.log'), result.stdout)
|
|
30
|
+
writeFileSync(join(dirname(output), 'compile-aot.stderr.log'), result.stderr)
|
|
31
|
+
if (result.exitCode !== 0)
|
|
32
|
+
throw new Error(`compile-aot failed (${result.exitCode}): ${result.stderr}`)
|
|
33
|
+
const buildMs = performance.now() - start
|
|
34
|
+
const inspection = inspectBunExecutable(output)
|
|
35
|
+
if (!inspection.hasCompiledProgram)
|
|
36
|
+
throw new Error('Compiler output has no compiled-program record')
|
|
37
|
+
const runtimeFeatures = inspectBunRuntime(output)
|
|
38
|
+
if (runtimeFeatures.jit === 'present' || runtimeFeatures.jitRemovedRanges.count === 0)
|
|
39
|
+
throw new Error('Compiler output does not record removal of the JIT compiler')
|
|
40
|
+
return { compileCommand, buildMs, inspection, runtimeFeatures }
|
|
41
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
// Pinned Bun da867c60: src/trim/removable_features/state.rs and
|
|
4
|
+
// scripts/build/trim/features.ts. engine.def assigns JIT=0, Interpreter=1,
|
|
5
|
+
// ImageCompiler=3. Executable layouts follow src/exe_format/{elf,macho,pe}.rs.
|
|
6
|
+
// Named, file-backed sections locate records; a magic-byte search is not proof.
|
|
7
|
+
const stateSize = 536
|
|
8
|
+
const removalOffset = 24
|
|
9
|
+
const featureCount = 512
|
|
10
|
+
const states = ['present', 'unused', 'requested'] as const
|
|
11
|
+
|
|
12
|
+
type Section = {
|
|
13
|
+
name: string
|
|
14
|
+
address: bigint
|
|
15
|
+
offset: bigint
|
|
16
|
+
size: bigint
|
|
17
|
+
mapped: boolean
|
|
18
|
+
mappedSize: bigint
|
|
19
|
+
readOnlyData: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bounded(bytes: Buffer, offset: bigint, length: bigint): number {
|
|
23
|
+
if (offset < 0n || length < 0n || offset + length > BigInt(bytes.length))
|
|
24
|
+
throw new Error('Executable range extends outside the file')
|
|
25
|
+
return Number(offset)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function elfSections(bytes: Buffer): Section[] {
|
|
29
|
+
if (
|
|
30
|
+
bytes.length < 64 ||
|
|
31
|
+
bytes.readUInt32LE(0) !== 0x464c457f ||
|
|
32
|
+
bytes[4] !== 2 ||
|
|
33
|
+
bytes[5] !== 1 ||
|
|
34
|
+
bytes[6] !== 1 ||
|
|
35
|
+
![2, 3].includes(bytes.readUInt16LE(16)) ||
|
|
36
|
+
bytes.readUInt32LE(20) !== 1 ||
|
|
37
|
+
bytes.readUInt16LE(52) !== 64 ||
|
|
38
|
+
bytes.readUInt16LE(58) !== 64
|
|
39
|
+
)
|
|
40
|
+
throw new Error('Runtime feature inspection requires a little-endian ELF64 executable')
|
|
41
|
+
const count = bytes.readUInt16LE(60)
|
|
42
|
+
const namesIndex = bytes.readUInt16LE(62)
|
|
43
|
+
if (count === 0 || namesIndex === 0 || namesIndex >= count)
|
|
44
|
+
throw new Error('Missing or unsupported ELF section table')
|
|
45
|
+
const table = bounded(bytes, bytes.readBigUInt64LE(40), BigInt(count * 64))
|
|
46
|
+
const raw = Array.from({ length: count }, (_, index) => {
|
|
47
|
+
const offset = table + index * 64
|
|
48
|
+
return {
|
|
49
|
+
nameOffset: bytes.readUInt32LE(offset),
|
|
50
|
+
type: bytes.readUInt32LE(offset + 4),
|
|
51
|
+
flags: bytes.readBigUInt64LE(offset + 8),
|
|
52
|
+
address: bytes.readBigUInt64LE(offset + 16),
|
|
53
|
+
offset: bytes.readBigUInt64LE(offset + 24),
|
|
54
|
+
size: bytes.readBigUInt64LE(offset + 32),
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
const names = raw[namesIndex]
|
|
58
|
+
if (!names || names.type !== 3) throw new Error('Invalid ELF section-name table')
|
|
59
|
+
const namesStart = bounded(bytes, names.offset, names.size)
|
|
60
|
+
const nameBytes = bytes.subarray(namesStart, namesStart + Number(names.size))
|
|
61
|
+
return raw.map((section) => {
|
|
62
|
+
if (BigInt(section.nameOffset) >= names.size)
|
|
63
|
+
throw new Error('ELF section name is outside the string table')
|
|
64
|
+
const end = nameBytes.indexOf(0, section.nameOffset)
|
|
65
|
+
if (end < section.nameOffset) throw new Error('Unterminated ELF section name')
|
|
66
|
+
return {
|
|
67
|
+
...section,
|
|
68
|
+
name: nameBytes.toString('utf8', section.nameOffset, end),
|
|
69
|
+
mapped: (section.flags & 2n) !== 0n,
|
|
70
|
+
mappedSize: section.size,
|
|
71
|
+
readOnlyData: section.type === 1 && (section.flags & 3n) === 2n,
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fixedName(bytes: Buffer, offset: number, length: number): string {
|
|
77
|
+
const name = bytes.subarray(offset, offset + length)
|
|
78
|
+
const end = name.indexOf(0)
|
|
79
|
+
// Node's ASCII decoder clears the high bit and would alias malformed names.
|
|
80
|
+
return name.toString('latin1', 0, end < 0 ? length : end)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function segmentMatches(owner: string, declared: string): boolean {
|
|
84
|
+
if (owner === declared) return true
|
|
85
|
+
// Pinned Bun macho_ranges.rs splits trimmed segments into NAME, NAME.1, ...
|
|
86
|
+
// and retains the original section.segname. Match its bounded name rewrite.
|
|
87
|
+
const suffix = owner.match(/\.[1-9][0-9]*$/)?.[0]
|
|
88
|
+
return suffix !== undefined && owner === declared.slice(0, 16 - suffix.length) + suffix
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function machoSections(bytes: Buffer): Section[] {
|
|
92
|
+
if (
|
|
93
|
+
bytes.length < 32 ||
|
|
94
|
+
![0x01000007, 0x0100000c].includes(bytes.readUInt32LE(4)) ||
|
|
95
|
+
bytes.readUInt32LE(12) !== 2
|
|
96
|
+
)
|
|
97
|
+
throw new Error('Runtime feature inspection requires a thin Mach-O64 executable')
|
|
98
|
+
const count = bytes.readUInt32LE(16)
|
|
99
|
+
const length = bytes.readUInt32LE(20)
|
|
100
|
+
const end = bounded(bytes, 32n, BigInt(length)) + length
|
|
101
|
+
if (count === 0 || count > length / 8) throw new Error('Invalid Mach-O load-command count')
|
|
102
|
+
const sections: Section[] = []
|
|
103
|
+
let command = 32
|
|
104
|
+
for (let index = 0; index < count; index++) {
|
|
105
|
+
if (command + 8 > end) throw new Error('Truncated Mach-O load command')
|
|
106
|
+
const kind = bytes.readUInt32LE(command)
|
|
107
|
+
const size = bytes.readUInt32LE(command + 4)
|
|
108
|
+
if (size < 8 || size % 8 !== 0 || command + size > end)
|
|
109
|
+
throw new Error('Invalid Mach-O load-command size')
|
|
110
|
+
if (kind === 0x19) {
|
|
111
|
+
if (size < 72) throw new Error('Truncated Mach-O segment')
|
|
112
|
+
const name = fixedName(bytes, command + 8, 16)
|
|
113
|
+
const address = bytes.readBigUInt64LE(command + 24)
|
|
114
|
+
const virtualSize = bytes.readBigUInt64LE(command + 32)
|
|
115
|
+
const offset = bytes.readBigUInt64LE(command + 40)
|
|
116
|
+
const fileSize = bytes.readBigUInt64LE(command + 48)
|
|
117
|
+
const protection = bytes.readUInt32LE(command + 60)
|
|
118
|
+
const sectionCount = bytes.readUInt32LE(command + 64)
|
|
119
|
+
if (size !== 72 + sectionCount * 80 || address + virtualSize > 0xffffffffffffffffn)
|
|
120
|
+
throw new Error('Invalid Mach-O segment layout')
|
|
121
|
+
bounded(bytes, offset, fileSize)
|
|
122
|
+
for (let sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) {
|
|
123
|
+
const header = command + 72 + sectionIndex * 80
|
|
124
|
+
const sectionName = fixedName(bytes, header, 16)
|
|
125
|
+
const segmentName = fixedName(bytes, header + 16, 16)
|
|
126
|
+
const sectionAddress = bytes.readBigUInt64LE(header + 32)
|
|
127
|
+
const sectionSize = bytes.readBigUInt64LE(header + 40)
|
|
128
|
+
const sectionOffset = BigInt(bytes.readUInt32LE(header + 48))
|
|
129
|
+
const kind = bytes.readUInt32LE(header + 64) & 0xff
|
|
130
|
+
const fileBacked = ![1, 0xc, 0x12].includes(kind)
|
|
131
|
+
if (
|
|
132
|
+
!segmentMatches(name, segmentName) ||
|
|
133
|
+
sectionAddress < address ||
|
|
134
|
+
sectionAddress + sectionSize > address + virtualSize ||
|
|
135
|
+
(fileBacked &&
|
|
136
|
+
(sectionOffset < offset ||
|
|
137
|
+
sectionOffset + sectionSize > offset + fileSize ||
|
|
138
|
+
sectionAddress - address !== sectionOffset - offset))
|
|
139
|
+
)
|
|
140
|
+
throw new Error('Invalid Mach-O section mapping')
|
|
141
|
+
sections.push({
|
|
142
|
+
name: `${segmentName},${sectionName}`,
|
|
143
|
+
address: sectionAddress,
|
|
144
|
+
offset: sectionOffset,
|
|
145
|
+
size: sectionSize,
|
|
146
|
+
mapped: true,
|
|
147
|
+
mappedSize: sectionSize,
|
|
148
|
+
readOnlyData: kind === 0 && (protection & 3) === 1,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
command += size
|
|
153
|
+
}
|
|
154
|
+
if (command !== end) throw new Error('Mach-O load commands do not fill their declared table')
|
|
155
|
+
return sections
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function peSections(bytes: Buffer): Section[] {
|
|
159
|
+
if (bytes.length < 64) throw new Error('Truncated PE DOS header')
|
|
160
|
+
const header = bounded(bytes, BigInt(bytes.readUInt32LE(60)), 24n)
|
|
161
|
+
const optionalSize = bytes.readUInt16LE(header + 20)
|
|
162
|
+
const count = bytes.readUInt16LE(header + 6)
|
|
163
|
+
if (
|
|
164
|
+
bytes.readUInt32LE(header) !== 0x4550 ||
|
|
165
|
+
![0x8664, 0xaa64].includes(bytes.readUInt16LE(header + 4)) ||
|
|
166
|
+
(bytes.readUInt16LE(header + 22) & 0x2002) !== 2 ||
|
|
167
|
+
optionalSize < 112 ||
|
|
168
|
+
count === 0 ||
|
|
169
|
+
count > 96
|
|
170
|
+
)
|
|
171
|
+
throw new Error('Runtime feature inspection requires a PE32+ executable')
|
|
172
|
+
const optional = bounded(bytes, BigInt(header + 24), BigInt(optionalSize))
|
|
173
|
+
if (bytes.readUInt16LE(optional) !== 0x20b)
|
|
174
|
+
throw new Error('Runtime feature inspection requires a PE32+ optional header')
|
|
175
|
+
const imageBase = bytes.readBigUInt64LE(optional + 24)
|
|
176
|
+
const table = bounded(bytes, BigInt(optional + optionalSize), BigInt(count * 40))
|
|
177
|
+
return Array.from({ length: count }, (_, index) => {
|
|
178
|
+
const section = table + index * 40
|
|
179
|
+
const size = bytes.readUInt32LE(section + 8)
|
|
180
|
+
const address = imageBase + BigInt(bytes.readUInt32LE(section + 12))
|
|
181
|
+
const fileSize = bytes.readUInt32LE(section + 16)
|
|
182
|
+
const offset = BigInt(bytes.readUInt32LE(section + 20))
|
|
183
|
+
const flags = bytes.readUInt32LE(section + 36)
|
|
184
|
+
bounded(bytes, offset, BigInt(fileSize))
|
|
185
|
+
if (address + BigInt(Math.max(size, fileSize)) > 0xffffffffffffffffn)
|
|
186
|
+
throw new Error('Invalid PE section address')
|
|
187
|
+
return {
|
|
188
|
+
name: fixedName(bytes, section, 8),
|
|
189
|
+
address,
|
|
190
|
+
offset,
|
|
191
|
+
size: BigInt(size),
|
|
192
|
+
mapped: true,
|
|
193
|
+
mappedSize: BigInt(Math.max(size, fileSize)),
|
|
194
|
+
readOnlyData: (flags & 0xc0000040) === 0x40000040 && size <= fileSize,
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function imageSections(bytes: Buffer) {
|
|
200
|
+
const magic = bytes.length >= 4 ? bytes.readUInt32LE(0) : 0
|
|
201
|
+
if (magic === 0x464c457f)
|
|
202
|
+
return { format: 'elf64-le' as const, name: '.bun_features', table: elfSections(bytes) }
|
|
203
|
+
if (magic === 0xfeedfacf)
|
|
204
|
+
return {
|
|
205
|
+
format: 'macho64-le' as const,
|
|
206
|
+
name: '__TEXT,__bun_features',
|
|
207
|
+
table: machoSections(bytes),
|
|
208
|
+
}
|
|
209
|
+
if (bytes.length >= 2 && bytes.readUInt16LE(0) === 0x5a4d)
|
|
210
|
+
return { format: 'pe32+-le' as const, name: '.bunfeat', table: peSections(bytes) }
|
|
211
|
+
throw new Error('Runtime feature inspection requires an ELF64, Mach-O64 or PE32+ executable')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function overlappingMappings(table: Section[], start: bigint, length: bigint): Section[] {
|
|
215
|
+
return table.filter(
|
|
216
|
+
(entry) =>
|
|
217
|
+
entry.mapped && entry.address < start + length && start < entry.address + entry.mappedSize
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function inspectBunRuntime(path: string) {
|
|
222
|
+
const bytes = readFileSync(path)
|
|
223
|
+
const { format, name, table } = imageSections(bytes)
|
|
224
|
+
const candidates = table.filter((section) => section.name === name)
|
|
225
|
+
const section = candidates[0]
|
|
226
|
+
if (candidates.length !== 1 || !section) throw new Error(`Expected exactly one ${name} section`)
|
|
227
|
+
if (
|
|
228
|
+
!section.readOnlyData ||
|
|
229
|
+
section.size !== BigInt(stateSize) ||
|
|
230
|
+
section.address % 8n !== 0n ||
|
|
231
|
+
section.offset % 8n !== 0n
|
|
232
|
+
)
|
|
233
|
+
throw new Error(`Invalid ${name} section layout`)
|
|
234
|
+
if (overlappingMappings(table, section.address, section.size).length !== 1)
|
|
235
|
+
throw new Error('Ambiguous Bun runtime feature mapping')
|
|
236
|
+
const offset = bounded(bytes, section.offset, section.size)
|
|
237
|
+
const state = bytes.subarray(offset, offset + stateSize)
|
|
238
|
+
if (!state.subarray(0, 8).equals(Buffer.from('BUNFEAT\0')))
|
|
239
|
+
throw new Error('Invalid Bun runtime feature magic')
|
|
240
|
+
const trimmed = state.readUInt32LE(8)
|
|
241
|
+
const rangeCount = state.readUInt32LE(12)
|
|
242
|
+
const rangeOffset = state.readBigInt64LE(16)
|
|
243
|
+
if (
|
|
244
|
+
trimmed > 1 ||
|
|
245
|
+
rangeCount > 32768 ||
|
|
246
|
+
rangeOffset % 8n !== 0n ||
|
|
247
|
+
(rangeCount === 0 && rangeOffset !== 0n) ||
|
|
248
|
+
state.subarray(removalOffset).some((value) => value > 2)
|
|
249
|
+
)
|
|
250
|
+
throw new Error('Invalid Bun runtime feature state')
|
|
251
|
+
|
|
252
|
+
let jitRangeCount = 0
|
|
253
|
+
let jitRangeBytes = 0
|
|
254
|
+
if (rangeCount > 0) {
|
|
255
|
+
const start = section.address + rangeOffset
|
|
256
|
+
const length = BigInt(rangeCount * 16)
|
|
257
|
+
// Check every mapped alias before testing permissions. Filtering writable
|
|
258
|
+
// mappings first could falsely qualify an ambiguous table as read-only.
|
|
259
|
+
const ranges = overlappingMappings(table, start, length)
|
|
260
|
+
const rangeSection = ranges[0]
|
|
261
|
+
if (
|
|
262
|
+
ranges.length !== 1 ||
|
|
263
|
+
!rangeSection ||
|
|
264
|
+
!rangeSection.readOnlyData ||
|
|
265
|
+
rangeSection.address > start ||
|
|
266
|
+
start + length > rangeSection.address + rangeSection.size
|
|
267
|
+
)
|
|
268
|
+
throw new Error('Missing or ambiguous Bun removed-range table')
|
|
269
|
+
bounded(bytes, rangeSection.offset, rangeSection.size)
|
|
270
|
+
const rangeStart = bounded(bytes, rangeSection.offset + start - rangeSection.address, length)
|
|
271
|
+
let previousEnd = 0n
|
|
272
|
+
for (let index = 0; index < rangeCount; index++) {
|
|
273
|
+
const record = rangeStart + index * 16
|
|
274
|
+
const address = section.address + bytes.readBigInt64LE(record)
|
|
275
|
+
const size = bytes.readUInt32LE(record + 8)
|
|
276
|
+
const feature = bytes.readUInt16LE(record + 12)
|
|
277
|
+
if (
|
|
278
|
+
address < previousEnd ||
|
|
279
|
+
address + BigInt(size) > 0xffffffffffffffffn ||
|
|
280
|
+
size === 0 ||
|
|
281
|
+
feature >= featureCount ||
|
|
282
|
+
bytes.readUInt16LE(record + 14) !== 0
|
|
283
|
+
)
|
|
284
|
+
throw new Error('Invalid Bun removed-range record')
|
|
285
|
+
previousEnd = address + BigInt(size)
|
|
286
|
+
if (feature === 0) {
|
|
287
|
+
jitRangeCount++
|
|
288
|
+
jitRangeBytes += size
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
format,
|
|
294
|
+
section: { name: section.name, offset, bytes: stateSize },
|
|
295
|
+
rawRecordHex: state.toString('hex'),
|
|
296
|
+
trimmedProgramFeatures: trimmed === 1,
|
|
297
|
+
removedRangeCount: rangeCount,
|
|
298
|
+
jit: states[state[removalOffset]!]!,
|
|
299
|
+
interpreter: states[state[removalOffset + 1]!]!,
|
|
300
|
+
imageCompiler: states[state[removalOffset + 3]!]!,
|
|
301
|
+
jitRemovedRanges: { count: jitRangeCount, virtualBytes: jitRangeBytes },
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
// Layout is pinned to Bun da867c60: src/standalone_graph/StandaloneModuleGraph.rs
|
|
4
|
+
// Offsets is repr(C), 32 bytes on all six supported 64-bit targets. A .bun_aot
|
|
5
|
+
// section also exists in JIT executables; the actual module graph flag matters.
|
|
6
|
+
const trailer = Buffer.from('\n---- Bun! ----\n')
|
|
7
|
+
const offsetsSize = 32
|
|
8
|
+
const moduleRecordSize = 52
|
|
9
|
+
const compiledProgramFlag = 1 << 14
|
|
10
|
+
|
|
11
|
+
export function inspectBunExecutable(path: string): {
|
|
12
|
+
format: 'elf' | 'macho' | 'pe'
|
|
13
|
+
payloadOffset: number
|
|
14
|
+
payloadBytes: number
|
|
15
|
+
flags: number
|
|
16
|
+
hasCompiledProgram: boolean
|
|
17
|
+
} {
|
|
18
|
+
const bytes = readFileSync(path)
|
|
19
|
+
const magic = bytes.length >= 4 ? bytes.readUInt32LE(0) : 0
|
|
20
|
+
const format =
|
|
21
|
+
magic === 0x464c457f
|
|
22
|
+
? 'elf'
|
|
23
|
+
: magic === 0xfeedfacf
|
|
24
|
+
? 'macho'
|
|
25
|
+
: bytes.subarray(0, 2).toString() === 'MZ'
|
|
26
|
+
? 'pe'
|
|
27
|
+
: undefined
|
|
28
|
+
if (!format) throw new Error('Unsupported executable format')
|
|
29
|
+
|
|
30
|
+
let trailerOffset = bytes.lastIndexOf(trailer)
|
|
31
|
+
while (trailerOffset >= offsetsSize + 8) {
|
|
32
|
+
const offsets = trailerOffset - offsetsSize
|
|
33
|
+
const count64 = bytes.readBigUInt64LE(offsets)
|
|
34
|
+
if (count64 <= BigInt(offsets - 8)) {
|
|
35
|
+
const count = Number(count64)
|
|
36
|
+
const payloadOffset = offsets - count
|
|
37
|
+
const payloadBytes = count + offsetsSize + trailer.length
|
|
38
|
+
const modulesOffset = bytes.readUInt32LE(offsets + 8)
|
|
39
|
+
const modulesLength = bytes.readUInt32LE(offsets + 12)
|
|
40
|
+
const entryPoint = bytes.readUInt32LE(offsets + 16)
|
|
41
|
+
const argvOffset = bytes.readUInt32LE(offsets + 20)
|
|
42
|
+
const argvLength = bytes.readUInt32LE(offsets + 24)
|
|
43
|
+
const flags = bytes.readUInt32LE(offsets + 28)
|
|
44
|
+
if (
|
|
45
|
+
bytes.readBigUInt64LE(payloadOffset - 8) === BigInt(payloadBytes) &&
|
|
46
|
+
modulesLength > 0 &&
|
|
47
|
+
modulesLength % moduleRecordSize === 0 &&
|
|
48
|
+
modulesOffset + modulesLength <= count &&
|
|
49
|
+
entryPoint < modulesLength / moduleRecordSize &&
|
|
50
|
+
argvOffset + argvLength <= count &&
|
|
51
|
+
(flags & ~0x7fff) === 0
|
|
52
|
+
) {
|
|
53
|
+
return {
|
|
54
|
+
format,
|
|
55
|
+
payloadOffset,
|
|
56
|
+
payloadBytes,
|
|
57
|
+
flags,
|
|
58
|
+
hasCompiledProgram: (flags & compiledProgramFlag) !== 0,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
trailerOffset = bytes.lastIndexOf(trailer, trailerOffset - 1)
|
|
63
|
+
}
|
|
64
|
+
throw new Error('No valid pinned Bun standalone module graph found')
|
|
65
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { open, readFile, stat } from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
export type FileDigest = { bytes: number; sha256: string }
|
|
5
|
+
|
|
6
|
+
// Source files and manifests are read whole; compilers and host binaries are
|
|
7
|
+
// hashed through one reused buffer so memory stays bounded.
|
|
8
|
+
const WHOLE_FILE_BYTES = 4 * 1024 * 1024
|
|
9
|
+
const CHUNK_BYTES = 1024 * 1024
|
|
10
|
+
|
|
11
|
+
export async function fileDigest(path: string): Promise<FileDigest> {
|
|
12
|
+
const hash = createHash('sha256')
|
|
13
|
+
// The size only picks a strategy; both read to EOF and count what they read.
|
|
14
|
+
if ((await stat(path)).size <= WHOLE_FILE_BYTES) {
|
|
15
|
+
const data = await readFile(path)
|
|
16
|
+
return { bytes: data.length, sha256: hash.update(data).digest('hex') }
|
|
17
|
+
}
|
|
18
|
+
let bytes = 0
|
|
19
|
+
const file = await open(path, 'r')
|
|
20
|
+
try {
|
|
21
|
+
const buffer = Buffer.allocUnsafe(CHUNK_BYTES)
|
|
22
|
+
for (;;) {
|
|
23
|
+
const { bytesRead } = await file.read(buffer, 0, CHUNK_BYTES, null)
|
|
24
|
+
if (bytesRead === 0) break
|
|
25
|
+
hash.update(buffer.subarray(0, bytesRead))
|
|
26
|
+
bytes += bytesRead
|
|
27
|
+
}
|
|
28
|
+
} finally {
|
|
29
|
+
await file.close()
|
|
30
|
+
}
|
|
31
|
+
return { bytes, sha256: hash.digest('hex') }
|
|
32
|
+
}
|