@adhdev/ghostty-vt-node 1.0.78 → 1.0.82-rc.38
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/CMakeLists.txt +284 -0
- package/README.md +151 -9
- package/binding-candidates.js +40 -0
- package/index.d.ts +107 -0
- package/index.js +92 -48
- package/package.json +6 -2
- package/prebuilt/darwin-arm64-node137/ghostty_vt_node.node +0 -0
- package/prebuilt/darwin-arm64-node137/libghostty-vt.0.1.0.dylib +0 -0
- package/prebuilt/darwin-arm64-node137/libghostty-vt.dylib +0 -0
- package/prebuilt/darwin-arm64-node137/manifest.json +34 -0
- package/prebuilt/linux-x64-node137/ghostty_vt_node.node +0 -0
- package/prebuilt/linux-x64-node137/libghostty-vt.so +0 -0
- package/prebuilt/linux-x64-node137/libghostty-vt.so.0 +0 -0
- package/prebuilt/linux-x64-node137/libghostty-vt.so.0.1.0 +0 -0
- package/prebuilt/linux-x64-node137/manifest.json +38 -0
- package/prebuilt/win32-x64-node137/ghostty-vt.dll +0 -0
- package/prebuilt/win32-x64-node137/ghostty_vt_node.node +0 -0
- package/prebuilt/win32-x64-node137/manifest.json +30 -0
- package/scripts/binding-digest.mjs +60 -0
- package/scripts/build-or-use-prebuilt.mjs +60 -31
- package/scripts/check-prebuilts.mjs +113 -0
- package/scripts/clean-dir.cjs +32 -0
- package/scripts/host-path-scan.mjs +115 -0
- package/scripts/native-format.mjs +219 -0
- package/scripts/prebuilt-manifest.mjs +231 -0
- package/scripts/probe-build-provenance.mjs +288 -0
- package/scripts/verify-prebuilt.mjs +115 -0
- package/src/addon.cc +403 -23
- package/src/ghostty_bridge.c +344 -0
- package/src/ghostty_bridge.h +96 -0
- package/toolchain.json +3 -0
- package/prebuilt/darwin-arm64-node115/ghostty_vt_node.node +0 -0
- package/prebuilt/darwin-arm64-node115/libghostty-vt.0.1.0.dylib +0 -0
- package/prebuilt/darwin-arm64-node115/libghostty-vt.dylib +0 -0
- package/prebuilt/darwin-arm64-node127/ghostty_vt_node.node +0 -0
- package/prebuilt/darwin-arm64-node127/libghostty-vt.0.1.0.dylib +0 -0
- package/prebuilt/darwin-arm64-node127/libghostty-vt.dylib +0 -0
- package/prebuilt/darwin-arm64-node141/ghostty_vt_node.node +0 -0
- package/prebuilt/darwin-arm64-node141/libghostty-vt.0.1.0.dylib +0 -0
- package/prebuilt/darwin-arm64-node141/libghostty-vt.dylib +0 -0
- package/prebuilt/linux-x64-node115/ghostty_vt_node.node +0 -0
- package/prebuilt/linux-x64-node115/libghostty-vt.so +0 -0
- package/prebuilt/linux-x64-node115/libghostty-vt.so.0 +0 -0
- package/prebuilt/linux-x64-node115/libghostty-vt.so.0.1.0 +0 -0
- package/prebuilt/linux-x64-node127/ghostty_vt_node.node +0 -0
- package/prebuilt/linux-x64-node127/libghostty-vt.so +0 -0
- package/prebuilt/linux-x64-node127/libghostty-vt.so.0 +0 -0
- package/prebuilt/linux-x64-node127/libghostty-vt.so.0.1.0 +0 -0
- package/prebuilt/linux-x64-node141/ghostty_vt_node.node +0 -0
- package/prebuilt/linux-x64-node141/libghostty-vt.so +0 -0
- package/prebuilt/linux-x64-node141/libghostty-vt.so.0 +0 -0
- package/prebuilt/linux-x64-node141/libghostty-vt.so.0.1.0 +0 -0
- package/prebuilt/win32-x64-node127/ghostty-vt.dll +0 -0
- package/prebuilt/win32-x64-node127/ghostty_vt_node.node +0 -0
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('node:fs');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
|
|
4
4
|
const ADDON_FILE = 'ghostty_vt_node.node';
|
|
5
|
+
const { orderPrebuiltTriplets } = require('./binding-candidates.js');
|
|
5
6
|
const PREBUILT_ROOT = path.join(__dirname, 'prebuilt');
|
|
6
7
|
|
|
7
8
|
function getTriplet() {
|
|
@@ -19,9 +20,10 @@ function getTriplet() {
|
|
|
19
20
|
// Consequently the loader resolves in three tiers:
|
|
20
21
|
// 1. an explicit override dir (ADHDEV_GHOSTTY_VT_PREBUILT_DIR),
|
|
21
22
|
// 2. a locally compiled build/ output,
|
|
22
|
-
// 3.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
23
|
+
// 3. same-`platform-arch` prebuilts ranked by manifest apiLevel (highest
|
|
24
|
+
// first), exact ABI as tie-breaker (see binding-candidates.js).
|
|
25
|
+
// Not preferring the exact ABI is deliberate: stale API-level-1 binaries for
|
|
26
|
+
// ABI 115/127 once shadowed the current API-level-2 build. This is what makes a brand-new Node ABI (e.g. a future node26) work without
|
|
25
27
|
// shipping a new directory — the previous failure mode where node137 runtimes
|
|
26
28
|
// hit "binding unavailable" purely because no `node137` directory existed.
|
|
27
29
|
|
|
@@ -38,66 +40,69 @@ function listAvailablePrebuiltTriplets() {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Last-resort: every other installed prebuilt, regardless of platform/arch.
|
|
50
|
-
// Requiring a foreign-OS `.node` throws a load error (ELF/PE mismatch,
|
|
51
|
-
// "invalid ELF header", "not a valid Win32 application"), which loadBinding()
|
|
52
|
-
// simply skips — so this tier is inert on a correctly-addressed runtime. Its
|
|
53
|
-
// sole purpose is test isolation: suites that mock `process.platform` (e.g.
|
|
54
|
-
// win32 PTY-write / submit driver coverage) corrupt getTriplet() into an
|
|
55
|
-
// address for which no loadable binary exists on the CI runner; walking the
|
|
56
|
-
// remaining triplets lets the real host binary (the one that actually loads)
|
|
57
|
-
// still resolve, instead of the whole ProviderCliAdapter construction throwing.
|
|
58
|
-
function otherInstalledTriplets(triplet, alreadyTried) {
|
|
59
|
-
return listAvailablePrebuiltTriplets().filter(
|
|
60
|
-
(name) => name !== triplet && !alreadyTried.has(name),
|
|
61
|
-
);
|
|
43
|
+
function readManifestApiLevel(name) {
|
|
44
|
+
try {
|
|
45
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(PREBUILT_ROOT, name, 'manifest.json'), 'utf8'));
|
|
46
|
+
return Number.isFinite(manifest.apiLevel) ? manifest.apiLevel : 0;
|
|
47
|
+
} catch {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
62
50
|
}
|
|
63
51
|
|
|
52
|
+
// Selection rule (see binding-candidates.js): among same platform-arch
|
|
53
|
+
// prebuilts the highest manifest apiLevel wins, then the exact running ABI.
|
|
54
|
+
// Foreign-platform dirs are a last-resort test-isolation fallback (requiring a
|
|
55
|
+
// foreign-OS `.node` throws and is skipped; it matters only for suites that
|
|
56
|
+
// mock process.platform).
|
|
57
|
+
//
|
|
58
|
+
// A locally compiled build/ output is tried before the prebuilts, but only
|
|
59
|
+
// accepted if it reports apiLevel >= the best prebuilt manifest level — a stale
|
|
60
|
+
// build/ directory must not shadow a newer prebuilt. A rejected build is kept
|
|
61
|
+
// as the very last resort.
|
|
64
62
|
function loadBinding() {
|
|
65
|
-
const
|
|
63
|
+
const abi = process.versions.modules;
|
|
66
64
|
const explicitPrebuiltDir = process.env.ADHDEV_GHOSTTY_VT_PREBUILT_DIR
|
|
67
65
|
? path.resolve(process.env.ADHDEV_GHOSTTY_VT_PREBUILT_DIR)
|
|
68
66
|
: null;
|
|
67
|
+
const triplet = getTriplet();
|
|
68
|
+
const ordered = orderPrebuiltTriplets({
|
|
69
|
+
names: listAvailablePrebuiltTriplets(),
|
|
70
|
+
platform: process.platform,
|
|
71
|
+
arch: process.arch,
|
|
72
|
+
abi,
|
|
73
|
+
readApiLevel: readManifestApiLevel,
|
|
74
|
+
});
|
|
75
|
+
const prebuiltFile = (name) => path.join(PREBUILT_ROOT, name, ADDON_FILE);
|
|
69
76
|
|
|
77
|
+
// { file, minApiLevel } — minApiLevel > 0 means "verify after load".
|
|
70
78
|
const candidates = [
|
|
71
|
-
|
|
72
|
-
explicitPrebuiltDir ? path.join(explicitPrebuiltDir,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// 3. exact-ABI prebuilt
|
|
78
|
-
path.join(PREBUILT_ROOT, triplet, ADDON_FILE),
|
|
79
|
-
// 4. N-API portability fallback: any same platform-arch prebuilt
|
|
80
|
-
...samePlatformArchTriplets(triplet).map((name) =>
|
|
81
|
-
path.join(PREBUILT_ROOT, name, ADDON_FILE),
|
|
82
|
-
),
|
|
83
|
-
// 5. test-isolation fallback: any remaining installed triplet (see
|
|
84
|
-
// otherInstalledTriplets). Foreign-OS binaries throw on require and
|
|
85
|
-
// are skipped; the real host binary still resolves under a mocked
|
|
86
|
-
// process.platform.
|
|
87
|
-
...otherInstalledTriplets(
|
|
88
|
-
triplet,
|
|
89
|
-
new Set([triplet, ...samePlatformArchTriplets(triplet)]),
|
|
90
|
-
).map((name) => path.join(PREBUILT_ROOT, name, ADDON_FILE)),
|
|
79
|
+
explicitPrebuiltDir ? { file: path.join(explicitPrebuiltDir, triplet, ADDON_FILE) } : null,
|
|
80
|
+
explicitPrebuiltDir ? { file: path.join(explicitPrebuiltDir, ADDON_FILE) } : null,
|
|
81
|
+
{ file: path.join(__dirname, 'build', 'Release', ADDON_FILE), minApiLevel: ordered.bestApiLevel },
|
|
82
|
+
{ file: path.join(__dirname, 'build', 'Debug', ADDON_FILE), minApiLevel: ordered.bestApiLevel },
|
|
83
|
+
...ordered.samePlatform.map((name) => ({ file: prebuiltFile(name) })),
|
|
84
|
+
...ordered.otherPlatform.map((name) => ({ file: prebuiltFile(name) })),
|
|
91
85
|
].filter(Boolean);
|
|
92
86
|
|
|
93
87
|
const errors = [];
|
|
88
|
+
const rejected = [];
|
|
94
89
|
for (const candidate of candidates) {
|
|
95
90
|
try {
|
|
96
|
-
|
|
91
|
+
const loaded = require(candidate.file);
|
|
92
|
+
if (candidate.minApiLevel) {
|
|
93
|
+
const level = describeBinding(loaded).apiLevel;
|
|
94
|
+
if (level < candidate.minApiLevel) {
|
|
95
|
+
errors.push(`${candidate.file}: apiLevel ${level} < best prebuilt ${candidate.minApiLevel}`);
|
|
96
|
+
rejected.push(loaded);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return loaded;
|
|
97
101
|
} catch (error) {
|
|
98
|
-
errors.push(`${candidate}: ${error && error.message ? error.message : String(error)}`);
|
|
102
|
+
errors.push(`${candidate.file}: ${error && error.message ? error.message : String(error)}`);
|
|
99
103
|
}
|
|
100
104
|
}
|
|
105
|
+
if (rejected.length) return rejected[0];
|
|
101
106
|
|
|
102
107
|
const available = listAvailablePrebuiltTriplets();
|
|
103
108
|
throw new Error(
|
|
@@ -108,4 +113,43 @@ function loadBinding() {
|
|
|
108
113
|
);
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
// Surface added at API level 2. A binary built before it (an older prebuilt or
|
|
117
|
+
// a stale build/ output that shadows the prebuilt) still loads and still
|
|
118
|
+
// serves the legacy methods; it simply lacks these. Consumers feature-detect
|
|
119
|
+
// via `capabilities.viewport` (or `typeof handle.getViewport === 'function'`)
|
|
120
|
+
// and must treat the exact-read path as unavailable rather than falling back
|
|
121
|
+
// to formatter output.
|
|
122
|
+
const VIEWPORT_METHODS = ['getViewport', 'readRow', 'scrollViewport'];
|
|
123
|
+
|
|
124
|
+
function describeBinding(binding) {
|
|
125
|
+
if (typeof binding.getBindingInfo === 'function') {
|
|
126
|
+
const info = binding.getBindingInfo();
|
|
127
|
+
return {
|
|
128
|
+
apiLevel: info.apiLevel,
|
|
129
|
+
sourceDigest: info.sourceDigest || null,
|
|
130
|
+
ghosttyRevision: info.ghosttyRevision || null,
|
|
131
|
+
napiVersion: info.napiVersion ?? null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { apiLevel: 1, sourceDigest: null, ghosttyRevision: null, napiVersion: null };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function probeViewportSupport(binding, info) {
|
|
138
|
+
if (info.apiLevel < 2 || typeof binding.createTerminal !== 'function') return false;
|
|
139
|
+
let terminal;
|
|
140
|
+
try {
|
|
141
|
+
terminal = binding.createTerminal({ cols: 1, rows: 1, scrollback: 0 });
|
|
142
|
+
return VIEWPORT_METHODS.every((name) => typeof terminal[name] === 'function');
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
} finally {
|
|
146
|
+
try { terminal?.dispose(); } catch { /* best effort */ }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const binding = loadBinding();
|
|
151
|
+
const bindingInfo = describeBinding(binding);
|
|
152
|
+
binding.bindingInfo = bindingInfo;
|
|
153
|
+
binding.capabilities = Object.freeze({ viewport: probeViewportSupport(binding, bindingInfo) });
|
|
154
|
+
|
|
155
|
+
module.exports = binding;
|
package/package.json
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/ghostty-vt-node",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.82-rc.38",
|
|
4
4
|
"description": "Native Node binding for libghostty-vt",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.js",
|
|
9
|
+
"binding-candidates.js",
|
|
9
10
|
"index.d.ts",
|
|
10
11
|
"prebuilt",
|
|
11
12
|
"scripts",
|
|
12
13
|
"CMakeLists.txt",
|
|
14
|
+
"toolchain.json",
|
|
13
15
|
"src",
|
|
14
16
|
"README.md"
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
17
19
|
"build": "node ./scripts/build-or-use-prebuilt.mjs",
|
|
18
20
|
"rebuild": "cmake-js rebuild",
|
|
19
|
-
"clean": "cmake-js clean"
|
|
21
|
+
"clean": "cmake-js clean",
|
|
22
|
+
"test": "node --import ../../scripts/test-tmp-guard.mjs --test test/*.test.mjs",
|
|
23
|
+
"check:prebuilts": "node scripts/check-prebuilts.mjs"
|
|
20
24
|
},
|
|
21
25
|
"author": "vilmire",
|
|
22
26
|
"dependencies": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 2,
|
|
3
|
+
"platform": "darwin",
|
|
4
|
+
"arch": "arm64",
|
|
5
|
+
"apiLevel": 2,
|
|
6
|
+
"sourceDigest": "e4ca3372235a1289373e172996a5e7804bc4d5f3eac630d91d9160dd932e0627",
|
|
7
|
+
"ghosttyRevision": "d4a38c8661453026bd1b1bb022ba2c37b347adce",
|
|
8
|
+
"napiVersion": 8,
|
|
9
|
+
"builtWithNode": "v22.17.0",
|
|
10
|
+
"release": true,
|
|
11
|
+
"provenance": {
|
|
12
|
+
"problems": [],
|
|
13
|
+
"ghosttySourceRevision": "d4a38c8661453026bd1b1bb022ba2c37b347adce",
|
|
14
|
+
"ghosttySourceClean": true,
|
|
15
|
+
"zigVersion": "0.15.2",
|
|
16
|
+
"cCompiler": "AppleClang 17.0.0.17000316",
|
|
17
|
+
"cxxCompiler": "AppleClang 17.0.0.17000316",
|
|
18
|
+
"buildType": "Release"
|
|
19
|
+
},
|
|
20
|
+
"files": {
|
|
21
|
+
"ghostty_vt_node.node": {
|
|
22
|
+
"sha256": "fbdbf5e197b99d43b609b875d3621ce3e873ac8e0e04608cd057fba987a8555a",
|
|
23
|
+
"size": 100536
|
|
24
|
+
},
|
|
25
|
+
"libghostty-vt.0.1.0.dylib": {
|
|
26
|
+
"sha256": "d2f92f4bdbaa734b014fe8d38615ccaa5a97f9f07847e5476e87a4c7a64f3c00",
|
|
27
|
+
"size": 1528392
|
|
28
|
+
},
|
|
29
|
+
"libghostty-vt.dylib": {
|
|
30
|
+
"sha256": "d2f92f4bdbaa734b014fe8d38615ccaa5a97f9f07847e5476e87a4c7a64f3c00",
|
|
31
|
+
"size": 1528392
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 2,
|
|
3
|
+
"platform": "linux",
|
|
4
|
+
"arch": "x64",
|
|
5
|
+
"apiLevel": 2,
|
|
6
|
+
"sourceDigest": "e4ca3372235a1289373e172996a5e7804bc4d5f3eac630d91d9160dd932e0627",
|
|
7
|
+
"ghosttyRevision": "d4a38c8661453026bd1b1bb022ba2c37b347adce",
|
|
8
|
+
"napiVersion": 8,
|
|
9
|
+
"builtWithNode": "v22.22.0",
|
|
10
|
+
"release": true,
|
|
11
|
+
"provenance": {
|
|
12
|
+
"problems": [],
|
|
13
|
+
"ghosttySourceRevision": "d4a38c8661453026bd1b1bb022ba2c37b347adce",
|
|
14
|
+
"ghosttySourceClean": true,
|
|
15
|
+
"zigVersion": "0.15.2",
|
|
16
|
+
"cCompiler": "GNU 13.3.0",
|
|
17
|
+
"cxxCompiler": "GNU 13.3.0",
|
|
18
|
+
"buildType": "Release"
|
|
19
|
+
},
|
|
20
|
+
"files": {
|
|
21
|
+
"ghostty_vt_node.node": {
|
|
22
|
+
"sha256": "aefc9e332147bc2980f3810558d4f80ad3d407dfb3b19fcd9dd0d416e62c38bc",
|
|
23
|
+
"size": 130096
|
|
24
|
+
},
|
|
25
|
+
"libghostty-vt.so": {
|
|
26
|
+
"sha256": "2dc10d68318c378a35584ee36580f836c91d64428fcd5bf4466b08f3716c3c23",
|
|
27
|
+
"size": 7496328
|
|
28
|
+
},
|
|
29
|
+
"libghostty-vt.so.0": {
|
|
30
|
+
"sha256": "2dc10d68318c378a35584ee36580f836c91d64428fcd5bf4466b08f3716c3c23",
|
|
31
|
+
"size": 7496328
|
|
32
|
+
},
|
|
33
|
+
"libghostty-vt.so.0.1.0": {
|
|
34
|
+
"sha256": "2dc10d68318c378a35584ee36580f836c91d64428fcd5bf4466b08f3716c3c23",
|
|
35
|
+
"size": 7496328
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 2,
|
|
3
|
+
"platform": "win32",
|
|
4
|
+
"arch": "x64",
|
|
5
|
+
"apiLevel": 2,
|
|
6
|
+
"sourceDigest": "e4ca3372235a1289373e172996a5e7804bc4d5f3eac630d91d9160dd932e0627",
|
|
7
|
+
"ghosttyRevision": "d4a38c8661453026bd1b1bb022ba2c37b347adce",
|
|
8
|
+
"napiVersion": 8,
|
|
9
|
+
"builtWithNode": "v22.14.0",
|
|
10
|
+
"release": true,
|
|
11
|
+
"provenance": {
|
|
12
|
+
"problems": [],
|
|
13
|
+
"ghosttySourceRevision": "d4a38c8661453026bd1b1bb022ba2c37b347adce",
|
|
14
|
+
"ghosttySourceClean": true,
|
|
15
|
+
"zigVersion": "0.15.2",
|
|
16
|
+
"cCompiler": "MSVC 19.44.35229.0",
|
|
17
|
+
"cxxCompiler": "MSVC 19.44.35229.0",
|
|
18
|
+
"buildType": "Release"
|
|
19
|
+
},
|
|
20
|
+
"files": {
|
|
21
|
+
"ghostty-vt.dll": {
|
|
22
|
+
"sha256": "7d2ce8845d559a7279bde0080c99ccf795dd9f13d4236add3bf0c4e312fdad7a",
|
|
23
|
+
"size": 1305600
|
|
24
|
+
},
|
|
25
|
+
"ghostty_vt_node.node": {
|
|
26
|
+
"sha256": "6d29fd06b15d1f05930ce5c969ee316f0135c42d334763c4a07ff584e3fb9a6d",
|
|
27
|
+
"size": 70656
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Binding source digest: the identity of the sources a native binary was
|
|
3
|
+
// compiled from. CMakeLists.txt computes the same value at configure time and
|
|
4
|
+
// embeds it in the addon (getBindingInfo().sourceDigest), so a prebuilt can be
|
|
5
|
+
// checked against the sources in this checkout.
|
|
6
|
+
//
|
|
7
|
+
// Algorithm (keep in sync with CMakeLists.txt): for each input in order,
|
|
8
|
+
// "<path>:<sha256 of content with CRLF -> LF>\n"; digest = sha256 of the
|
|
9
|
+
// concatenation. LF normalization keeps a win32 autocrlf checkout from
|
|
10
|
+
// reporting a different digest for identical sources.
|
|
11
|
+
//
|
|
12
|
+
// node scripts/binding-digest.mjs # prints the digest
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
export const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
|
|
20
|
+
export const BINDING_DIGEST_INPUTS = [
|
|
21
|
+
'CMakeLists.txt',
|
|
22
|
+
'toolchain.json',
|
|
23
|
+
'src/addon.cc',
|
|
24
|
+
'src/ghostty_bridge.c',
|
|
25
|
+
'src/ghostty_bridge.h',
|
|
26
|
+
'scripts/clean-dir.cjs',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const sha256 = (data) => createHash('sha256').update(data).digest('hex');
|
|
30
|
+
|
|
31
|
+
export function computeBindingDigest(packageDir = PACKAGE_DIR) {
|
|
32
|
+
let manifest = '';
|
|
33
|
+
for (const input of BINDING_DIGEST_INPUTS) {
|
|
34
|
+
const content = readFileSync(join(packageDir, input), 'utf8').replace(/\r\n/g, '\n');
|
|
35
|
+
manifest += `${input}:${sha256(Buffer.from(content, 'utf8'))}\n`;
|
|
36
|
+
}
|
|
37
|
+
return sha256(Buffer.from(manifest, 'utf8'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The zig version toolchain.json pins: the single source the native workflow
|
|
41
|
+
// installs, CMakeLists.txt enforces and the release gate compares against.
|
|
42
|
+
export function pinnedZigVersion(packageDir = PACKAGE_DIR) {
|
|
43
|
+
const { zig } = JSON.parse(readFileSync(join(packageDir, 'toolchain.json'), 'utf8'));
|
|
44
|
+
if (typeof zig !== 'string' || !/^\d+\.\d+\.\d+$/.test(zig)) throw new Error('toolchain.json does not pin "zig" to x.y.z');
|
|
45
|
+
return zig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The ghostty revision CMakeLists.txt pins by default.
|
|
49
|
+
export function pinnedGhosttyRevision(packageDir = PACKAGE_DIR) {
|
|
50
|
+
const cmake = readFileSync(join(packageDir, 'CMakeLists.txt'), 'utf8');
|
|
51
|
+
const match = cmake.match(/set\(GHOSTTY_GIT_TAG "([0-9a-f]{40})"/);
|
|
52
|
+
const pin = cmake.match(/set\(ADHDEV_GHOSTTY_PINNED_REVISION "([0-9a-f]{40})"\)/);
|
|
53
|
+
if (!match || !pin) throw new Error('CMakeLists.txt does not pin GHOSTTY_GIT_TAG / ADHDEV_GHOSTTY_PINNED_REVISION to a 40-hex revision');
|
|
54
|
+
if (match[1] !== pin[1]) throw new Error(`CMakeLists.txt pins disagree: GHOSTTY_GIT_TAG ${match[1]} != ADHDEV_GHOSTTY_PINNED_REVISION ${pin[1]}`);
|
|
55
|
+
return pin[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
59
|
+
console.log(computeBindingDigest());
|
|
60
|
+
}
|
|
@@ -3,6 +3,8 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
|
+
import { emitPrebuilt } from './prebuilt-manifest.mjs';
|
|
7
|
+
|
|
6
8
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
9
|
const packageDir = path.resolve(scriptDir, '..');
|
|
8
10
|
const outputDir = path.join(packageDir, 'build', 'Release');
|
|
@@ -10,9 +12,11 @@ const outputFile = path.join(outputDir, 'ghostty_vt_node.node');
|
|
|
10
12
|
const triplet = `${process.platform}-${process.arch}-node${process.versions.modules}`;
|
|
11
13
|
const sourceInputs = [
|
|
12
14
|
path.join(packageDir, 'CMakeLists.txt'),
|
|
15
|
+
path.join(packageDir, 'toolchain.json'),
|
|
13
16
|
path.join(packageDir, 'src', 'addon.cc'),
|
|
14
17
|
path.join(packageDir, 'src', 'ghostty_bridge.c'),
|
|
15
18
|
path.join(packageDir, 'src', 'ghostty_bridge.h'),
|
|
19
|
+
path.join(packageDir, 'scripts', 'clean-dir.cjs'),
|
|
16
20
|
];
|
|
17
21
|
|
|
18
22
|
function safeMtime(filePath) {
|
|
@@ -117,45 +121,76 @@ function installPrebuilt(candidateFile) {
|
|
|
117
121
|
if (!fs.existsSync(outputFile)) fs.copyFileSync(candidateFile, outputFile);
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
|
|
124
|
+
// ADHDEV_GHOSTTY_VT_FORCE_COMPILE=1 always compiles from source: no existing
|
|
125
|
+
// build output, no prebuilt. The native CI build uses it — in a fresh checkout
|
|
126
|
+
// the sources are never "locally modified", so without it the tracked
|
|
127
|
+
// prebuilt is copied and a binary that predates the current sources ships.
|
|
128
|
+
// `--plan` prints the decision and exits without touching anything.
|
|
129
|
+
const forceCompile = process.env.ADHDEV_GHOSTTY_VT_FORCE_COMPILE === '1';
|
|
130
|
+
const planOnly = process.argv.includes('--plan');
|
|
131
|
+
|
|
132
|
+
function decide() {
|
|
133
|
+
if (forceCompile) return { action: 'compile', mode: 'rebuild', reason: 'ADHDEV_GHOSTTY_VT_FORCE_COMPILE=1' };
|
|
134
|
+
const sourceMtime = latestSourceMtime();
|
|
135
|
+
const outputMtime = safeMtime(outputFile);
|
|
136
|
+
// A local build output is something this machine compiled, so its mtime
|
|
137
|
+
// against the sources is meaningful: keep it while it is not older.
|
|
138
|
+
if (outputMtime >= sourceMtime && outputMtime > 0) return { action: 'keep-local', file: outputFile };
|
|
121
139
|
for (const candidate of candidatePrebuiltPaths()) {
|
|
122
140
|
if (!fs.existsSync(candidate.file)) continue;
|
|
123
141
|
if (!isUsablePrebuilt(candidate, sourceMtime)) {
|
|
124
142
|
console.log(`[ghostty-vt-node] skipping prebuilt ${candidate.file}: sources were modified locally after it was built`);
|
|
125
143
|
continue;
|
|
126
144
|
}
|
|
127
|
-
|
|
128
|
-
console.log(`[ghostty-vt-node] using prebuilt native binding from ${candidate.file}`);
|
|
129
|
-
return true;
|
|
145
|
+
return { action: 'use-prebuilt', file: candidate.file };
|
|
130
146
|
}
|
|
131
|
-
return
|
|
147
|
+
if (process.env.ADHDEV_SKIP_GHOSTTY_VT_BUILD === '1') return { action: 'skip', reason: 'ADHDEV_SKIP_GHOSTTY_VT_BUILD=1' };
|
|
148
|
+
return { action: 'compile', reason: 'no usable local build or prebuilt' };
|
|
132
149
|
}
|
|
133
150
|
|
|
134
|
-
const
|
|
135
|
-
|
|
151
|
+
const decision = decide();
|
|
152
|
+
if (planOnly) {
|
|
153
|
+
console.log(`[ghostty-vt-node] plan: ${decision.action}${decision.mode ? ` (cmake-js ${decision.mode})` : ''}${decision.file ? ` ${decision.file}` : ''}${decision.reason ? ` (${decision.reason})` : ''}`);
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
// against the sources is meaningful: keep it while it is not older.
|
|
139
|
-
if (outputMtime >= sourceMtime && outputMtime > 0) {
|
|
157
|
+
if (decision.action === 'keep-local') {
|
|
140
158
|
console.log(`[ghostty-vt-node] keeping existing local build at ${outputFile}`);
|
|
141
159
|
process.exit(0);
|
|
142
160
|
}
|
|
143
161
|
|
|
144
|
-
if (
|
|
162
|
+
if (decision.action === 'use-prebuilt') {
|
|
163
|
+
installPrebuilt(decision.file);
|
|
164
|
+
console.log(`[ghostty-vt-node] using prebuilt native binding from ${decision.file}`);
|
|
145
165
|
process.exit(0);
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
if (
|
|
168
|
+
if (decision.action === 'skip') {
|
|
149
169
|
console.log(`[ghostty-vt-node] skipping native build for ${triplet} (ADHDEV_SKIP_GHOSTTY_VT_BUILD=1)`);
|
|
150
170
|
process.exit(0);
|
|
151
171
|
}
|
|
152
172
|
|
|
173
|
+
// A forced compile is a CLEAN rebuild (fresh configure, fresh FetchContent
|
|
174
|
+
// clone, zig-out regenerated): an incremental `compile` would reuse whatever
|
|
175
|
+
// the previous build directory holds. Other compiles stay incremental.
|
|
176
|
+
const cmakeJsCommand = forceCompile ? 'rebuild' : 'compile';
|
|
153
177
|
const isWindows = process.platform === 'win32';
|
|
154
178
|
const command = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'npm';
|
|
155
179
|
const args = isWindows
|
|
156
|
-
? ['/d', '/s', '/c',
|
|
157
|
-
: ['exec', '--', 'cmake-js',
|
|
158
|
-
|
|
180
|
+
? ['/d', '/s', '/c', `npm exec -- cmake-js ${cmakeJsCommand}`]
|
|
181
|
+
: ['exec', '--', 'cmake-js', cmakeJsCommand];
|
|
182
|
+
// cmake-js' own clean (`cmake -E remove_directory`) has the same Windows
|
|
183
|
+
// trailing-dot problem as file(REMOVE_RECURSE), so a forced rebuild removes
|
|
184
|
+
// build/ itself first and refuses to continue on a partial removal.
|
|
185
|
+
if (forceCompile) {
|
|
186
|
+
const buildDir = path.join(packageDir, 'build');
|
|
187
|
+
const clean = spawnSync(process.execPath, [path.join(scriptDir, 'clean-dir.cjs'), buildDir], { stdio: 'inherit', windowsHide: true });
|
|
188
|
+
if (clean.status !== 0 || fs.existsSync(buildDir)) {
|
|
189
|
+
console.error(`[ghostty-vt-node] could not remove ${buildDir}; refusing to rebuild on top of it`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
console.log(`[ghostty-vt-node] compiling ${triplet} (${decision.reason}) via ${command} ${args.join(' ')}`);
|
|
159
194
|
const result = spawnSync(command, args, {
|
|
160
195
|
cwd: packageDir,
|
|
161
196
|
env: process.env,
|
|
@@ -166,26 +201,20 @@ if (result.error) {
|
|
|
166
201
|
console.error(`[ghostty-vt-node] failed to launch native build for ${triplet}:`, result.error);
|
|
167
202
|
}
|
|
168
203
|
|
|
169
|
-
// Mirror a successful local build into
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
204
|
+
// Mirror a successful local build into the committed prebuilt for this
|
|
205
|
+
// platform-arch (prebuilt/<platform>-<arch>-node137, whatever Node runs the
|
|
206
|
+
// build — the binding is Node-API, so the ABI in the name is only an address)
|
|
207
|
+
// and stamp its manifest.json from the compiled binary. Set
|
|
208
|
+
// ADHDEV_GHOSTTY_VT_EMIT_PREBUILT=1 to enable (off by default so ordinary
|
|
209
|
+
// `npm run build` does not churn the dir). Combine with
|
|
210
|
+
// ADHDEV_GHOSTTY_VT_FORCE_COMPILE=1 to rebuild a prebuilt from source.
|
|
175
211
|
if ((result.status ?? 1) === 0 && process.env.ADHDEV_GHOSTTY_VT_EMIT_PREBUILT === '1') {
|
|
176
212
|
try {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
for (const entry of fs.readdirSync(outputDir)) {
|
|
180
|
-
// Ship the addon plus its co-located runtime libs (.dylib/.so/.dll),
|
|
181
|
-
// skip intermediate build artifacts.
|
|
182
|
-
if (/\.(node|dylib|so|so\.\d.*|dll)$/.test(entry)) {
|
|
183
|
-
fs.copyFileSync(path.join(outputDir, entry), path.join(prebuiltDir, entry));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
console.log(`[ghostty-vt-node] emitted prebuilt for ${triplet} → ${prebuiltDir}`);
|
|
213
|
+
const { target, manifest } = emitPrebuilt(outputDir);
|
|
214
|
+
console.log(`[ghostty-vt-node] emitted prebuilt → ${target} (source digest ${manifest.sourceDigest})`);
|
|
187
215
|
} catch (emitError) {
|
|
188
216
|
console.error(`[ghostty-vt-node] failed to emit prebuilt for ${triplet}:`, emitError);
|
|
217
|
+
process.exit(1);
|
|
189
218
|
}
|
|
190
219
|
}
|
|
191
220
|
|