crossbind 2.0.0-beta.60 → 2.0.0-beta.63
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 +8 -6
- package/package.json +6 -1
- package/src/actions/buildDependencies.js +37 -9
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildJs.js +1 -1
- package/src/actions/buildLib.js +27 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +190 -0
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +0 -4
- package/src/actions/buildWasm.js +92 -95
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +64 -0
- package/src/actions/createInterface.js +318 -65
- package/src/actions/createLib.js +60 -43
- package/src/actions/getAllBridges.js +1 -1
- package/src/actions/getData.js +4 -1
- package/src/actions/getLinkInputs.js +70 -0
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +175 -17
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +124 -32
- package/src/actions/runnerCommands.js +421 -0
- package/src/actions/writeRuntimeEntry.js +69 -0
- package/src/assets/cmake/CMakeLists.txt +1 -1
- package/src/assets/cmake/command/CMakeLists.txt +21 -0
- package/src/assets/cmake/command/command.cpp +1 -0
- package/src/assets/cmake/dist.cmake +2 -1
- package/src/assets/configure/install.sh +22 -0
- package/src/assets/js-runtime/adapters/path-url.js +6 -3
- package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +87 -56
- package/src/assets/js-runtime/core.js +11 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +27 -13
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +290 -37
- package/src/index.js +6 -1
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +53 -19
- package/src/integration/getDependFilePath.js +18 -12
- package/src/integration/resolveNativeImport.js +14 -0
- package/src/node/dev.mjs +19 -0
- package/src/runner/blobs.js +135 -0
- package/src/runner/files.js +193 -0
- package/src/runner/server.js +320 -0
- package/src/state/attachConanDependencies.js +56 -0
- package/src/state/index.js +31 -20
- package/src/state/loadConfig.js +36 -24
- package/src/state/refreshBuiltDependencies.js +20 -0
- package/src/state/refreshConanDependencies.js +17 -0
- package/src/utils/androidToolchain.js +4 -0
- package/src/utils/appRustCrates.js +17 -1
- package/src/utils/appSources.js +112 -0
- package/src/utils/archiveFlags.js +12 -0
- package/src/utils/boundHeaders.js +79 -0
- package/src/utils/bridgeExtras.js +486 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -0
- package/src/utils/cargoImport.js +44 -0
- package/src/utils/cargoTarget.js +23 -3
- package/src/utils/cloudAccount.js +55 -0
- package/src/utils/conanDependencies.js +66 -0
- package/src/utils/conanImport.js +58 -0
- package/src/utils/conanInstall.js +154 -0
- package/src/utils/conanProfile.js +203 -0
- package/src/utils/conanStage.js +292 -0
- package/src/utils/copyToDistSource.js +13 -0
- package/src/utils/cppDts.js +134 -55
- package/src/utils/darwinToolchain.js +11 -0
- package/src/utils/dirLock.js +137 -28
- package/src/utils/downloadAndExtractFile.js +34 -7
- package/src/utils/embindArgumentGuards.js +84 -0
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/embindRsFingerprint.js +26 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +56 -0
- package/src/utils/hostPlatform.js +21 -0
- package/src/utils/inputStamp.js +23 -0
- package/src/utils/iosToolchain.js +6 -0
- package/src/utils/isSourceCmakePackage.js +10 -0
- package/src/utils/licenseReport.js +11 -2
- package/src/utils/linkLayout.js +8 -1
- package/src/utils/linuxToolchain.js +24 -0
- package/src/utils/makeTreeWritable.js +12 -0
- package/src/utils/nativeImports.js +23 -0
- package/src/utils/nativeSourceStamp.js +36 -0
- package/src/utils/nodeDevBuild.js +92 -0
- package/src/utils/packageLicense.js +101 -0
- package/src/utils/pthreadMainScript.js +14 -0
- package/src/utils/pullDockerImage.js +10 -5
- package/src/utils/relocatePrebuilt.js +119 -0
- package/src/utils/remoteClient.js +127 -0
- package/src/utils/remoteOutputs.js +39 -0
- package/src/utils/remoteRunner.js +97 -0
- package/src/utils/remoteUpload.js +134 -0
- package/src/utils/replaceFile.js +8 -0
- package/src/utils/resolveCompanionPackage.js +51 -0
- package/src/utils/resolveEmbindNapi.js +22 -0
- package/src/utils/resolveEmbindRust.js +17 -52
- package/src/utils/runCargo.js +51 -53
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +903 -253
- package/src/utils/rustMt.js +4 -0
- package/src/utils/scopedEmbind.js +22 -0
- package/src/utils/selectRunner.js +81 -0
- package/src/utils/sourceFingerprint.js +4 -3
- package/src/utils/swigInterface.js +86 -6
- package/src/utils/systemKeys.js +43 -3
- package/src/utils/targets.js +104 -3
- package/src/utils/toolchainNotices.js +5 -0
- package/src/utils/wasiToolchain.js +14 -1
- package/src/utils/windowsToolchain.js +18 -0
- package/vitest.config.js +11 -0
- package/src/utils/cppFieldBindings.js +0 -23
package/src/utils/dirLock.js
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { Worker } from 'node:worker_threads';
|
|
3
5
|
import logger from './logger.js';
|
|
4
6
|
|
|
5
|
-
// A
|
|
6
|
-
//
|
|
7
|
-
const DEFAULT_STALE_MS =
|
|
7
|
+
// A holder refreshes its lock while it works, so a lock nobody refreshed for this long was left by a crash, even when
|
|
8
|
+
// another process has taken over its PID since.
|
|
9
|
+
const DEFAULT_STALE_MS = 30 * 1000;
|
|
10
|
+
// A crossbind from before the refreshing kept its lock as long as its PID ran, and one whose PID cannot be read this long.
|
|
11
|
+
const LEGACY_STALE_MS = 60 * 60 * 1000;
|
|
8
12
|
const DEFAULT_POLL_MS = 2000;
|
|
13
|
+
const DEFAULT_HELD_BY = 'another build is working on this dependency';
|
|
14
|
+
// Breaking a lock takes no time, so a break file this old was left by a waiter that died breaking one.
|
|
15
|
+
const BREAK_STALE_MS = 10 * 1000;
|
|
16
|
+
// Windows keeps the name of a deleted file while another handle has it open.
|
|
17
|
+
const DELETE_PENDING_MS = 2 * 1000;
|
|
9
18
|
|
|
10
19
|
const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
20
|
+
const sleepSync = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
11
21
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
// (no such process), or 'unknown' (missing/unparseable PID).
|
|
15
|
-
export function lockHolderStatus(lockPath) {
|
|
16
|
-
let pid;
|
|
22
|
+
// The holder's PID, and from this crossbind on the token of its acquisition on the next line.
|
|
23
|
+
function readLock(lockPath) {
|
|
17
24
|
try {
|
|
18
|
-
pid =
|
|
25
|
+
const [pid, token] = fs.readFileSync(lockPath, 'utf8').split('\n').map((line) => line.trim());
|
|
26
|
+
return { pid: Number.parseInt(pid, 10), token: token || undefined, isLegacy: Boolean(pid) && !token };
|
|
19
27
|
} catch {
|
|
20
|
-
return
|
|
28
|
+
return {};
|
|
21
29
|
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statusOf(pid) {
|
|
22
33
|
if (!Number.isInteger(pid) || pid <= 0) return 'unknown';
|
|
23
34
|
try {
|
|
24
35
|
process.kill(pid, 0);
|
|
@@ -30,42 +41,140 @@ export function lockHolderStatus(lockPath) {
|
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
// Reads the PID recorded in the lock file and reports whether that process is
|
|
45
|
+
// still running: 'alive' (signal delivered, or owned by another user), 'dead'
|
|
46
|
+
// (no such process), or 'unknown' (missing/unparseable PID).
|
|
47
|
+
export function lockHolderStatus(lockPath) {
|
|
48
|
+
return statusOf(readLock(lockPath).pid);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let refresher;
|
|
52
|
+
|
|
53
|
+
// Refreshes the held locks from a thread of its own, so a holder blocked in execFileSync still shows it is working.
|
|
54
|
+
function lockRefresher() {
|
|
55
|
+
if (!refresher) {
|
|
56
|
+
refresher = new Worker(`
|
|
57
|
+
const fs = require('node:fs');
|
|
58
|
+
const { parentPort } = require('node:worker_threads');
|
|
59
|
+
const timers = new Map();
|
|
60
|
+
parentPort.on('message', ({ hold, release, everyMs }) => {
|
|
61
|
+
clearInterval(timers.get(hold ?? release));
|
|
62
|
+
timers.delete(hold ?? release);
|
|
63
|
+
if (!hold) return;
|
|
64
|
+
timers.set(hold, setInterval(() => {
|
|
65
|
+
const now = new Date();
|
|
66
|
+
try {
|
|
67
|
+
fs.utimesSync(hold, now, now);
|
|
68
|
+
} catch {
|
|
69
|
+
// released since the last refresh
|
|
70
|
+
}
|
|
71
|
+
}, everyMs));
|
|
72
|
+
});
|
|
73
|
+
`, { eval: true, execArgv: [] });
|
|
74
|
+
refresher.on('error', (error) => logger.info(`crossbind: cannot refresh held locks (${error.message}); a waiting process may take one over.`));
|
|
75
|
+
refresher.unref();
|
|
76
|
+
}
|
|
77
|
+
return refresher;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Takes the lock, yielding each wait so the async and the sync lock share one procedure.
|
|
81
|
+
function* acquire(lockPath, token, options) {
|
|
34
82
|
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
35
83
|
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
36
84
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
37
85
|
|
|
38
86
|
let hasWarned = false;
|
|
87
|
+
let deniedSince;
|
|
88
|
+
let suspect;
|
|
39
89
|
for (;;) {
|
|
40
90
|
try {
|
|
41
91
|
const fd = fs.openSync(lockPath, 'wx');
|
|
42
|
-
fs.writeSync(fd,
|
|
92
|
+
fs.writeSync(fd, `${process.pid}\n${token}\n`);
|
|
43
93
|
fs.closeSync(fd);
|
|
44
|
-
|
|
94
|
+
lockRefresher().postMessage({ hold: lockPath, everyMs: staleMs / 6 });
|
|
95
|
+
return;
|
|
45
96
|
} catch (e) {
|
|
97
|
+
if (e.code === 'EPERM' && process.platform === 'win32') {
|
|
98
|
+
deniedSince ??= performance.now();
|
|
99
|
+
if (performance.now() - deniedSince < DELETE_PENDING_MS) {
|
|
100
|
+
yield pollMs;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
46
104
|
if (e.code !== 'EEXIST') throw e;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
105
|
+
}
|
|
106
|
+
// The file is read before its holder, so a lock another waiter took meanwhile never passes for the stale one.
|
|
107
|
+
const lock = fs.statSync(lockPath, { throwIfNoEntry: false });
|
|
108
|
+
const holder = readLock(lockPath);
|
|
109
|
+
const status = statusOf(holder.pid);
|
|
110
|
+
// A provably-dead holder (e.g. a Ctrl-C that skipped the finally cleanup) is broken at once. Any other goes once
|
|
111
|
+
// nobody refreshed it within the stale window for two refreshes in a row, so a holder waking from sleep refreshes
|
|
112
|
+
// it first; an older crossbind's lock only once its holder is gone.
|
|
113
|
+
if (lock && status === 'dead' && breakStaleLock(lockPath, lock, 'its holder is gone')) continue;
|
|
114
|
+
const age = lock ? Date.now() - lock.mtimeMs : 0;
|
|
115
|
+
const isUnrefreshed = holder.isLegacy ? status !== 'alive' && age > LEGACY_STALE_MS : age > staleMs;
|
|
116
|
+
if (lock && isUnrefreshed) {
|
|
117
|
+
if (suspect?.ino !== lock.ino || suspect.mtimeMs !== lock.mtimeMs) {
|
|
118
|
+
suspect = { ino: lock.ino, mtimeMs: lock.mtimeMs, since: performance.now() };
|
|
119
|
+
} else if (performance.now() - suspect.since >= staleMs / 3 && breakStaleLock(lockPath, lock, 'nobody refreshes it')) {
|
|
56
120
|
continue;
|
|
57
121
|
}
|
|
58
|
-
if (!hasWarned) {
|
|
59
|
-
logger.info(`crossbind: waiting for ${lockPath} (another build is working on this dependency)…`);
|
|
60
|
-
hasWarned = true;
|
|
61
|
-
}
|
|
62
|
-
await sleep(pollMs);
|
|
63
122
|
}
|
|
123
|
+
if (!hasWarned) {
|
|
124
|
+
logger.info(`crossbind: waiting for ${lockPath} (${options.heldBy ?? DEFAULT_HELD_BY})…`);
|
|
125
|
+
hasWarned = true;
|
|
126
|
+
}
|
|
127
|
+
yield pollMs;
|
|
64
128
|
}
|
|
129
|
+
}
|
|
65
130
|
|
|
131
|
+
// Waiters that all saw the stale holder break the lock one at a time, and only the file they saw: otherwise one removes
|
|
132
|
+
// the lock another has just taken. False while another waiter is breaking it.
|
|
133
|
+
function breakStaleLock(lockPath, stale, reason) {
|
|
134
|
+
const breakPath = `${lockPath}.break`;
|
|
135
|
+
try {
|
|
136
|
+
fs.closeSync(fs.openSync(breakPath, 'wx'));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (e.code !== 'EEXIST') throw e;
|
|
139
|
+
const mtimeMs = fs.statSync(breakPath, { throwIfNoEntry: false })?.mtimeMs;
|
|
140
|
+
if (mtimeMs !== undefined && Date.now() - mtimeMs > BREAK_STALE_MS) fs.rmSync(breakPath, { force: true });
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const lock = fs.statSync(lockPath, { throwIfNoEntry: false });
|
|
145
|
+
if (lock?.ino === stale.ino && lock.mtimeMs === stale.mtimeMs) {
|
|
146
|
+
logger.info(`crossbind: breaking stale lock ${lockPath} (${reason}).`);
|
|
147
|
+
fs.rmSync(lockPath, { force: true });
|
|
148
|
+
}
|
|
149
|
+
} finally {
|
|
150
|
+
fs.rmSync(breakPath, { force: true });
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A holder that stopped refreshing may have lost the lock to another process, whose lock it must leave.
|
|
156
|
+
function release(lockPath, token) {
|
|
157
|
+
lockRefresher().postMessage({ release: lockPath });
|
|
158
|
+
if (readLock(lockPath).token === token) fs.rmSync(lockPath, { force: true });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default async function withDirLock(lockPath, fn, options = {}) {
|
|
162
|
+
const token = randomUUID();
|
|
163
|
+
for (const ms of acquire(lockPath, token, options)) await sleep(ms);
|
|
66
164
|
try {
|
|
67
165
|
return await fn();
|
|
68
166
|
} finally {
|
|
69
|
-
|
|
167
|
+
release(lockPath, token);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// For a caller that holds the lock across execFileSync, where the event loop cannot run anyway.
|
|
172
|
+
export function withDirLockSync(lockPath, fn, options = {}) {
|
|
173
|
+
const token = randomUUID();
|
|
174
|
+
for (const ms of acquire(lockPath, token, options)) sleepSync(ms);
|
|
175
|
+
try {
|
|
176
|
+
return fn();
|
|
177
|
+
} finally {
|
|
178
|
+
release(lockPath, token);
|
|
70
179
|
}
|
|
71
180
|
}
|
|
@@ -4,14 +4,23 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import logger from './logger.js';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
// Upstream hosts drop a request now and then (ftp.gnu.org refused eight parallel libiconv fetches at once), so a
|
|
10
|
+
// request that failed for a passing reason is tried again after these pauses. Any other 4xx is the server's answer.
|
|
11
|
+
const RETRY_DELAYS_MS = [1000, 2000, 4000];
|
|
12
|
+
const RETRYABLE_STATUS = new Set([408, 429]);
|
|
13
|
+
|
|
14
|
+
const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
15
|
+
const retryable = (error) => Object.assign(error, { retryable: true });
|
|
16
|
+
|
|
17
|
+
export default async function downloadAndExtractFile(url, output, sha256, options = {}) {
|
|
9
18
|
const archive = path.basename(url);
|
|
10
19
|
if (fs.existsSync(`${output}/source`) && sourceCameFrom(output, archive)) {
|
|
11
20
|
return false;
|
|
12
21
|
}
|
|
13
22
|
fs.rmSync(`${output}/source`, { recursive: true, force: true });
|
|
14
|
-
const filePath = await downloadFile(url, output);
|
|
23
|
+
const filePath = await downloadFile(url, output, options);
|
|
15
24
|
verifyIntegrity(filePath, url, sha256);
|
|
16
25
|
extractArchive(filePath, url, output);
|
|
17
26
|
fs.writeFileSync(`${output}/source.archive`, archive);
|
|
@@ -86,29 +95,47 @@ export function assertHttps(target, context = target) {
|
|
|
86
95
|
|
|
87
96
|
// fetch follows redirects itself, which release downloads rely on (github and the osgeo
|
|
88
97
|
// mirrors both bounce), so the download needs no redirect library of its own.
|
|
89
|
-
export async function downloadFile(url, folder) {
|
|
98
|
+
export async function downloadFile(url, folder, { retryDelaysMs = RETRY_DELAYS_MS } = {}) {
|
|
90
99
|
mkdirSync(folder, { recursive: true });
|
|
91
100
|
const dest = `${folder}/${path.basename(url)}`;
|
|
92
101
|
if (fs.existsSync(dest)) return dest;
|
|
93
102
|
|
|
94
103
|
assertHttps(url);
|
|
104
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
105
|
+
try {
|
|
106
|
+
await fetchTo(url, dest);
|
|
107
|
+
return dest;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (!err.retryable) throw err;
|
|
110
|
+
if (attempt > retryDelaysMs.length) {
|
|
111
|
+
throw new Error(`${err.message} (after ${attempt} attempts)`, { cause: err });
|
|
112
|
+
}
|
|
113
|
+
const delay = retryDelaysMs[attempt - 1];
|
|
114
|
+
logger.info(`${err.message} Trying again in ${delay / 1000}s…`);
|
|
115
|
+
await sleep(delay);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function fetchTo(url, dest) {
|
|
95
121
|
let response;
|
|
96
122
|
try {
|
|
97
123
|
response = await fetch(url, { headers: { 'User-Agent': 'curl/8.7.1' }, redirect: 'follow' });
|
|
98
124
|
} catch (err) {
|
|
99
|
-
throw new Error(`crossbind: cannot reach ${url}: ${err.message}`, { cause: err });
|
|
125
|
+
throw retryable(new Error(`crossbind: cannot reach ${url}: ${err.message}`, { cause: err }));
|
|
100
126
|
}
|
|
101
127
|
// A redirect chain must not be able to downgrade the transport on its last hop.
|
|
102
128
|
if (response.url) assertHttps(response.url, `${url} (redirected to ${response.url})`);
|
|
103
129
|
if (!response.ok) {
|
|
104
|
-
|
|
130
|
+
await response.body?.cancel();
|
|
131
|
+
const error = new Error(`crossbind: download failed for ${url} — HTTP ${response.status}.`);
|
|
132
|
+
throw response.status >= 500 || RETRYABLE_STATUS.has(response.status) ? retryable(error) : error;
|
|
105
133
|
}
|
|
106
134
|
try {
|
|
107
135
|
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(dest));
|
|
108
136
|
} catch (err) {
|
|
109
137
|
// A half-written archive would fail its hash check later with a confusing message.
|
|
110
138
|
fs.rmSync(dest, { force: true });
|
|
111
|
-
throw new Error(`crossbind: download failed for ${url}: ${err.message}`, { cause: err });
|
|
139
|
+
throw retryable(new Error(`crossbind: download failed for ${url}: ${err.message}`, { cause: err }));
|
|
112
140
|
}
|
|
113
|
-
return dest;
|
|
114
141
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// A release build of embind passes whatever JavaScript hands an integer or enum parameter straight to
|
|
2
|
+
// the wire, where anything that is not a number becomes 0: an enum member given to an `int` parameter,
|
|
3
|
+
// a Number given to an enum parameter, a one-letter string given to a `char`. The call then runs with 0
|
|
4
|
+
// and reports no error. These rewrites convert what has one meaning and throw a TypeError for the rest,
|
|
5
|
+
// as embind's assertion builds do.
|
|
6
|
+
|
|
7
|
+
const GUARDS = [
|
|
8
|
+
{
|
|
9
|
+
registration: '_embind_register_integer',
|
|
10
|
+
from: 'toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(',
|
|
11
|
+
to: 'toWireType:(destructors,value)=>{'
|
|
12
|
+
+ 'if(typeof value=="number"||typeof value=="boolean"||typeof value=="bigint")return value;'
|
|
13
|
+
+ 'if(typeof value=="string"&&size==1&&value.length==1)return value.charCodeAt(0);'
|
|
14
|
+
+ 'if(value!==null&&typeof value=="object"&&typeof value.value=="number")return value.value;'
|
|
15
|
+
+ 'throw new TypeError(name+" takes a number, got "+(typeof value=="string"?JSON.stringify(value):value===null?"null":typeof value))'
|
|
16
|
+
+ '},readValueFromPointer:integerReadValueFromPointer(',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
registration: '_embind_register_enum',
|
|
20
|
+
from: 'toWireType:(destructors,c)=>c.value,',
|
|
21
|
+
to: 'toWireType:(destructors,c)=>{'
|
|
22
|
+
+ 'if(typeof c=="number")return c;'
|
|
23
|
+
+ 'if(c!==null&&typeof c=="object"&&typeof c.value=="number")return c.value;'
|
|
24
|
+
+ 'throw new TypeError(name+" takes a member of the enum or its number, got "+(typeof c=="string"?JSON.stringify(c):c===null?"null":typeof c))'
|
|
25
|
+
+ '},',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// The release glue spells the bigint conversion `typeof value=="number"`, the debug glue `typeof value == 'number'`.
|
|
30
|
+
const BIGINT_FROM_NUMBER = /if\s*\(\s*typeof value\s*==\s*["']number["']\s*\)\s*\{\s*value\s*=\s*BigInt\(value\)\s*;?\s*\}/g;
|
|
31
|
+
const SAFE_BIGINT_FROM_NUMBER = 'if(typeof value=="number"){if(!Number.isSafeInteger(value))throw new TypeError("a 64-bit integer parameter takes a BigInt or a safe integer Number, got "+value);value=BigInt(value)}';
|
|
32
|
+
|
|
33
|
+
// Returns the rewritten glue and whether a bigint registration kept a conversion this rewrite does not recognise.
|
|
34
|
+
export function guardBigIntArguments(glue) {
|
|
35
|
+
const text = glue.replace(BIGINT_FROM_NUMBER, SAFE_BIGINT_FROM_NUMBER);
|
|
36
|
+
return { text, missed: text.includes('_embind_register_bigint') && !text.includes('Number.isSafeInteger(value)') };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The debug glue the dev servers load spells both registrations over lines, and its integer conversion already
|
|
40
|
+
// throws for anything but a number or a boolean, so the member and character conversions go ahead of that check.
|
|
41
|
+
// Each applies inside its own registration only: the debug float conversion reads the same as the integer one.
|
|
42
|
+
const DEBUG_GUARDS = [
|
|
43
|
+
{
|
|
44
|
+
registration: '__embind_register_integer',
|
|
45
|
+
end: 'readValueFromPointer: integerReadValueFromPointer(',
|
|
46
|
+
from: /toWireType: \(destructors, value\) => \{(\s*)(?=if \(typeof value != ["']number["'])/,
|
|
47
|
+
to: 'toWireType: (destructors, value) => {$1'
|
|
48
|
+
+ 'if (typeof value == "string" && size == 1 && value.length == 1) value = value.charCodeAt(0);$1'
|
|
49
|
+
+ 'if (value !== null && typeof value == "object" && typeof value.value == "number") value = value.value;$1',
|
|
50
|
+
applied: 'value = value.charCodeAt(0);',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
registration: '__embind_register_enum',
|
|
54
|
+
end: 'readValueFromPointer: enumReadValueFromPointer(',
|
|
55
|
+
from: /toWireType: \(destructors, c\) => c\.value,/,
|
|
56
|
+
to: 'toWireType: (destructors, c) => {'
|
|
57
|
+
+ 'if (typeof c == "number") return c;'
|
|
58
|
+
+ 'if (c !== null && typeof c == "object" && typeof c.value == "number") return c.value;'
|
|
59
|
+
+ 'throw new TypeError(name + " takes a member of the enum or its number, got " + (typeof c == "string" ? JSON.stringify(c) : c === null ? "null" : typeof c))'
|
|
60
|
+
+ '},',
|
|
61
|
+
applied: 'takes a member of the enum or its number',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function guardDebugRegistration(glue, { registration, end, from, to, applied }) {
|
|
66
|
+
const start = glue.indexOf(`var ${registration} = `);
|
|
67
|
+
const stop = start < 0 ? -1 : glue.indexOf(end, start);
|
|
68
|
+
if (stop < 0) return { text: glue, missed: false };
|
|
69
|
+
const code = glue.slice(start, stop);
|
|
70
|
+
if (code.includes(applied)) return { text: glue, missed: false };
|
|
71
|
+
const rewritten = code.replace(from, to);
|
|
72
|
+
return { text: glue.slice(0, start) + rewritten + glue.slice(stop), missed: rewritten === code };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Returns the rewritten glue and the registrations whose code no longer looks the way these rewrites expect.
|
|
76
|
+
export function guardEmbindArguments(glue) {
|
|
77
|
+
const missed = GUARDS.filter(({ registration, from, to }) => glue.includes(`${registration}=`) && !glue.includes(from) && !glue.includes(to))
|
|
78
|
+
.map(({ registration }) => registration);
|
|
79
|
+
const released = GUARDS.reduce((current, { from, to }) => current.split(from).join(to), glue);
|
|
80
|
+
return DEBUG_GUARDS.reduce(({ text, missed: names }, guard) => {
|
|
81
|
+
const result = guardDebugRegistration(text, guard);
|
|
82
|
+
return { text: result.text, missed: result.missed ? [...names, guard.registration] : names };
|
|
83
|
+
}, { text: released, missed });
|
|
84
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Linked with -sDYNAMIC_EXECUTION=0, embind's craftInvokerFunction keeps argsWired, invokerFuncArgs and
|
|
2
|
+
// destructors once per bound function, shared by all its calls. A call frees its arguments from there
|
|
3
|
+
// once it returns, or once its promise settles for an async (_JSPI) function, so a call started before
|
|
4
|
+
// then overwrites them and the earlier call frees the later call's arguments instead of its own, which
|
|
5
|
+
// leak: two pending calls of one _JSPI method, or a method called again from a JavaScript callback it
|
|
6
|
+
// runs. Emscripten 6.0.9 builds it this way, as did upstream main on 2026-09-30; this rewrite declares
|
|
7
|
+
// the three inside the invoker, so each call gets its own.
|
|
8
|
+
|
|
9
|
+
// The release glue spells it on one line, the debug glue one statement per line with an argument
|
|
10
|
+
// count check first.
|
|
11
|
+
const SHARED = /var argsWired\s*=\s*new Array\(expectedArgCount\);\s*var invokerFuncArgs\s*=\s*\[\];\s*var destructors\s*=\s*\[\];\s*var invokerFn\s*=\s*function\s*\(\.\.\.args\)\s*\{(\s*(?:checkArgCount\([^;]*\);\s*)?)destructors\.length\s*=\s*0;/g;
|
|
12
|
+
const PER_CALL = 'var invokerFn=function(...args){$1var argsWired=new Array(expectedArgCount);var invokerFuncArgs=[];var destructors=[];';
|
|
13
|
+
const SEPARATED = /var invokerFn\s*=\s*function\s*\(\.\.\.args\)\s*\{\s*(?:checkArgCount\([^;]*\);\s*)?var argsWired\s*=/;
|
|
14
|
+
|
|
15
|
+
// Returns the rewritten glue and whether it still holds an invoker this rewrite does not recognise.
|
|
16
|
+
export function separateCallArguments(glue) {
|
|
17
|
+
const text = glue.replace(SHARED, PER_CALL);
|
|
18
|
+
return { text, missed: text.includes('argsWired') && !SEPARATED.test(text) };
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// JSPI suspends a call's wasm frames, but its C stack frames stay on the one stack every call of the module shares,
|
|
2
|
+
// and Emscripten 6.0.9 gives a suspended call no stack of its own: a call that resumes after a later one started
|
|
3
|
+
// returns over that one's frames, which the later one still uses. This rewrite runs the module's async (_JSPI)
|
|
4
|
+
// bindings one at a time, as Asyncify runs its async operations: a call made while another is pending enters wasm
|
|
5
|
+
// once that one settles. embind still converts the arguments when the call is made, so a wrong one throws there.
|
|
6
|
+
|
|
7
|
+
// The release glue spells it on one line, the debug glue across lines.
|
|
8
|
+
const PROMISING = /rtn\s*=\s*WebAssembly\.promising\(rtn\)/g;
|
|
9
|
+
const QUEUED = 'rtn=crossbindJspiQueue(WebAssembly.promising(rtn))';
|
|
10
|
+
const REQUIRE_FUNCTION = /var embind__requireFunction\s*=/;
|
|
11
|
+
// Declared in the module factory, so each instance queues on its own, as each has its own C stack.
|
|
12
|
+
const QUEUE = 'var crossbindJspiQueue=(()=>{var pending=null;return fn=>(...args)=>{'
|
|
13
|
+
+ 'var call=pending?pending.then(()=>fn(...args)):fn(...args);'
|
|
14
|
+
+ 'var settled=pending=call.then(()=>{},()=>{});'
|
|
15
|
+
+ 'settled.then(()=>{if(pending===settled)pending=null});'
|
|
16
|
+
+ 'return call}})();';
|
|
17
|
+
|
|
18
|
+
// Returns the rewritten glue and whether it holds a promising export this rewrite does not recognise.
|
|
19
|
+
export function queueJspiCalls(glue) {
|
|
20
|
+
const text = glue.replace(PROMISING, QUEUED);
|
|
21
|
+
if (text === glue || !REQUIRE_FUNCTION.test(text)) {
|
|
22
|
+
return { text: glue, missed: glue.includes('WebAssembly.promising(rtn') && !glue.includes(QUEUED) };
|
|
23
|
+
}
|
|
24
|
+
return { text: text.replace(REQUIRE_FUNCTION, (declaration) => `${QUEUE}${declaration}`), missed: false };
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getContentHash, getFileHash } from './hash.js';
|
|
4
|
+
import { collectRustSources } from './inputStamp.js';
|
|
5
|
+
import resolveEmbindRustRoot from './resolveEmbindRust.js';
|
|
6
|
+
|
|
7
|
+
// Every cargo staticlib bundles the embind-rs it was built from. An archive from another one links stale glue, and next
|
|
8
|
+
// to an archive built after it every embind_rs symbol is defined twice, so a prebuilt records the one it came from.
|
|
9
|
+
export const EMBIND_RS_FINGERPRINT_FILE = 'crossbind-embind-rs.fingerprint';
|
|
10
|
+
|
|
11
|
+
// Paths count relative to the crate, so an archive built from the same embind-rs elsewhere matches.
|
|
12
|
+
export function getEmbindRsFingerprint() {
|
|
13
|
+
const crate = `${resolveEmbindRustRoot()}/crate`;
|
|
14
|
+
const files = [...collectRustSources([crate]), `${crate}/Cargo.toml`];
|
|
15
|
+
return getContentHash(files.map((file) => `${path.relative(crate, file)}:${getFileHash(file)}`).sort().join('\n'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isEmbindRsFingerprintStale(prebuiltDir, fingerprint) {
|
|
19
|
+
const file = `${prebuiltDir}/${EMBIND_RS_FINGERPRINT_FILE}`;
|
|
20
|
+
return !fs.existsSync(file) || fs.readFileSync(file, { encoding: 'utf8' }) !== fingerprint;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeEmbindRsFingerprint(prebuiltDir, fingerprint) {
|
|
24
|
+
fs.mkdirSync(prebuiltDir, { recursive: true });
|
|
25
|
+
fs.writeFileSync(`${prebuiltDir}/${EMBIND_RS_FINGERPRINT_FILE}`, fingerprint);
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import getOsUserAndGroupId from './getOsUserAndGroupId.js';
|
|
5
|
+
|
|
6
|
+
function samePath(a, b) {
|
|
7
|
+
const real = (p) => {
|
|
8
|
+
try {
|
|
9
|
+
return fs.realpathSync(p);
|
|
10
|
+
} catch {
|
|
11
|
+
return path.resolve(p);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
return real(a) === real(b);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// docker exec attaches to a container the user created, so nothing else checks what it mounts. A
|
|
18
|
+
// container without a tool's cache mount keeps that cache inside itself, where the host reads paths
|
|
19
|
+
// that were never written: fail first, with the way to fix it.
|
|
20
|
+
export default function assertExecContainer({ name, mounts, workdir, hint, mountNote }) {
|
|
21
|
+
let info;
|
|
22
|
+
try {
|
|
23
|
+
// stderr is dropped: docker's own "No such container" would land before the message below.
|
|
24
|
+
info = JSON.parse(execFileSync('docker', ['container', 'inspect', name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }))[0];
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(`crossbind: RUNNER=DOCKER_EXEC needs a container named '${name}', which does not exist.\n${hint}`);
|
|
27
|
+
}
|
|
28
|
+
if (!info?.State?.Running) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`crossbind: the container '${name}' exists but is not running. Start it with \`docker start ${name}\`, or recreate it.\n${hint}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
for (const [destination, source] of mounts) {
|
|
34
|
+
const mount = (info.Mounts ?? []).find((m) => m.Destination === destination);
|
|
35
|
+
if (!mount || !samePath(mount.Source, source)) {
|
|
36
|
+
const found = mount ? ` - it mounts ${mount.Source} there` : '';
|
|
37
|
+
throw new Error(
|
|
38
|
+
`crossbind: the container '${name}' does not mount ${source} at ${destination}${found}. ${mountNote}\n${hint}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// `docker run --workdir` creates the directory; `docker exec --workdir` does not, and the tool
|
|
43
|
+
// dies with an OCI "chdir to cwd" error that names nothing the user can act on.
|
|
44
|
+
try {
|
|
45
|
+
execFileSync('docker', ['exec', '--user', getOsUserAndGroupId(), name, 'mkdir', '-p', workdir], { stdio: 'ignore' });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
throw new Error(`crossbind: could not create the working directory ${workdir} inside '${name}'.\n${hint}`, { cause: e });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import appSourceFiles from './appSources.js';
|
|
3
|
+
|
|
4
|
+
// `import * as x` and `export *` take every name the header exports.
|
|
5
|
+
export const ALL_NAMES = '*';
|
|
6
|
+
|
|
7
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
8
|
+
// The clause shapes are spelled out, so a match cannot run from one statement into the next.
|
|
9
|
+
const HEADER_IMPORT = /\b(import|export)\s+(type\s+)?((?:[A-Za-z_$][\w$]*\s*,\s*)?(?:\{[^}]*\}|\*(?:\s*as\s+[A-Za-z_$][\w$]*)?)|[A-Za-z_$][\w$]*)\s*from\s*(['"])([^'"\n]+)\4/g;
|
|
10
|
+
// `import('x.h')`, `import 'x.h'` and `require('x.h')` hand the app the whole module. A lazy import may carry a bundler
|
|
11
|
+
// comment (`/* webpackChunkName: "x" */`) or a template literal. A comment ends at its first `*/`, so a run of them
|
|
12
|
+
// matches one way only.
|
|
13
|
+
const WHOLE_MODULE = /\b(?:import\s*\(\s*(?:\/\*(?:[^*]|\*(?!\/))*\*\/\s*)*|import\s+|require\s*\(\s*)(['"`])([^'"`$\n]+)\1/g;
|
|
14
|
+
|
|
15
|
+
// A comment runs to its first `*/`, or to the end when it never closes, so each is read once.
|
|
16
|
+
function listedNames(list) {
|
|
17
|
+
return list.replace(/\/\*(?:[^*]|\*(?!\/))*(?:\*\/|$)|\/\/[^\n]*/g, '').split(',')
|
|
18
|
+
.map((entry) => entry.trim())
|
|
19
|
+
.filter((entry) => entry && !/^type\s/.test(entry))
|
|
20
|
+
.map((entry) => entry.split(/\s+as\s+/)[0])
|
|
21
|
+
.filter((name) => name !== 'default' && IDENTIFIER.test(name));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A specifier built at run time is not found.
|
|
25
|
+
export function findHeaderImports(text, headerExtensions) {
|
|
26
|
+
const isHeader = new RegExp(`\\.(${headerExtensions.join('|')})$`);
|
|
27
|
+
const listed = [...text.matchAll(HEADER_IMPORT)].flatMap(([, , type, clause, , specifier]) => {
|
|
28
|
+
if (type || !isHeader.test(specifier)) return [];
|
|
29
|
+
const open = clause.indexOf('{');
|
|
30
|
+
const close = open === -1 ? -1 : clause.indexOf('}', open);
|
|
31
|
+
const hasList = close !== -1;
|
|
32
|
+
if ((hasList ? clause.slice(0, open) + clause.slice(close + 1) : clause).includes('*')) return [{ specifier, names: ALL_NAMES }];
|
|
33
|
+
const names = listedNames(hasList ? clause.slice(open + 1, close) : '');
|
|
34
|
+
return names.length ? [{ specifier, names }] : [];
|
|
35
|
+
});
|
|
36
|
+
const whole = [...text.matchAll(WHOLE_MODULE)]
|
|
37
|
+
.filter(([, , specifier]) => isHeader.test(specifier))
|
|
38
|
+
.map(([, , specifier]) => ({ specifier, names: ALL_NAMES }));
|
|
39
|
+
return [...listed, ...whole];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const scanned = new Map();
|
|
43
|
+
|
|
44
|
+
// A source is read again only when its size or modification time changed.
|
|
45
|
+
export function findHeaderImportsIn(projectDir, headerExtensions) {
|
|
46
|
+
const extensions = headerExtensions.join(',');
|
|
47
|
+
return appSourceFiles(projectDir).flatMap((importer) => {
|
|
48
|
+
const { mtimeMs, size } = fs.statSync(importer);
|
|
49
|
+
const cached = scanned.get(importer);
|
|
50
|
+
if (cached?.mtimeMs !== mtimeMs || cached.size !== size || cached.extensions !== extensions) {
|
|
51
|
+
const imports = findHeaderImports(fs.readFileSync(importer, 'utf8'), headerExtensions);
|
|
52
|
+
scanned.set(importer, { mtimeMs, size, extensions, imports });
|
|
53
|
+
}
|
|
54
|
+
return scanned.get(importer).imports.map((found) => ({ importer, ...found }));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// What `-p host` builds: the platform whose binaries this machine runs. Node reports glibc and musl
|
|
2
|
+
// Linux alike; as the addon loader decides, a glibc process names its glibc in the report header.
|
|
3
|
+
export default function hostPlatform(platform = process.platform, readReport = processReport) {
|
|
4
|
+
if (platform !== 'linux') return platform;
|
|
5
|
+
const header = readReport()?.header;
|
|
6
|
+
return !header || header.glibcVersionRuntime ? 'linux' : 'linuxmusl';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const resolveHostPlatform = (platforms, host = hostPlatform()) => platforms.map((platform) => (platform === 'host' ? host : platform));
|
|
10
|
+
|
|
11
|
+
function processReport() {
|
|
12
|
+
if (!process.report) return undefined;
|
|
13
|
+
// Only the header is read; the network section can stall on DNS lookups.
|
|
14
|
+
const { excludeNetwork } = process.report;
|
|
15
|
+
process.report.excludeNetwork = true;
|
|
16
|
+
try {
|
|
17
|
+
return process.report.getReport();
|
|
18
|
+
} finally {
|
|
19
|
+
process.report.excludeNetwork = excludeNetwork;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/utils/inputStamp.js
CHANGED
|
@@ -21,6 +21,29 @@ export function collectInputFiles(roots, exts, extraFiles = []) {
|
|
|
21
21
|
return [...files].filter((file) => fs.existsSync(file)).sort();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// A cargo target dir holds build-script output, which changes on every build.
|
|
25
|
+
const RUST_EXCLUDED_DIRS = [...EXCLUDED_DIRS, 'target'];
|
|
26
|
+
|
|
27
|
+
// An app-local .rs file reaches its generated bridge through #[path], so the bridge text stays
|
|
28
|
+
// the same across a body edit: the sources themselves have to be stamped.
|
|
29
|
+
export function collectRustSources(roots) {
|
|
30
|
+
const files = new Set();
|
|
31
|
+
[...new Set(roots)].forEach((root) => {
|
|
32
|
+
findFiles('**/*.rs', {
|
|
33
|
+
cwd: root,
|
|
34
|
+
ignore: RUST_EXCLUDED_DIRS.map((dir) => `**/${dir}/**`),
|
|
35
|
+
}).forEach((file) => files.add(file));
|
|
36
|
+
});
|
|
37
|
+
return [...files].sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function collectRustBridgeFiles(cacheDir) {
|
|
41
|
+
return [
|
|
42
|
+
...findFiles('rust-bridges/*/{Cargo.toml,src/lib.rs}', { cwd: cacheDir }),
|
|
43
|
+
...findFiles('rust-crates/*.rs', { cwd: cacheDir }),
|
|
44
|
+
].sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
export function computeInputStamp(roots, exts, extraFiles, salt) {
|
|
25
48
|
const lines = collectInputFiles(roots, exts, extraFiles).map((file) => `${file}:${getFileHash(file)}`);
|
|
26
49
|
lines.push(salt);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The Xcode every iOS archive is built with, and the deployment target it is built for: port archives
|
|
2
|
+
// and Conan packages link into the same app, so they move together.
|
|
3
|
+
export const IOS_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
|
4
|
+
export const XCODE_TOOLCHAIN_BIN = `${IOS_DEVELOPER_DIR}/Toolchains/XcodeDefault.xctoolchain/usr/bin`;
|
|
5
|
+
// Xcode 27 refuses deployment targets below 15.0; React Native's own floor is 15.1.
|
|
6
|
+
export const IOS_DEPLOYMENT_TARGET = '15.1';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// A cmake package that ships its sources rather than a prebuilt: its own CMakeLists (at the package root, not a
|
|
4
|
+
// generated dist/prebuilt one) is compiled into the consuming build via add_subdirectory.
|
|
5
|
+
export default function isSourceCmakePackage(config) {
|
|
6
|
+
return config.export?.type === 'cmake'
|
|
7
|
+
&& config.paths.cmake !== config.paths.cliCMakeListsTxt
|
|
8
|
+
&& !config.paths.cmakeDir.endsWith('/prebuilt')
|
|
9
|
+
&& fs.existsSync(config.paths.cmake);
|
|
10
|
+
}
|