crossbind 2.0.0-beta.62 → 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 +2 -2
- package/package.json +6 -1
- package/src/actions/buildDependencies.js +15 -2
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildLib.js +25 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +48 -7
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +0 -4
- package/src/actions/buildWasm.js +49 -29
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +64 -0
- package/src/actions/createInterface.js +247 -68
- package/src/actions/createLib.js +57 -41
- package/src/actions/getData.js +4 -1
- package/src/actions/getLinkInputs.js +12 -5
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +143 -19
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +63 -66
- package/src/actions/runnerCommands.js +421 -0
- package/src/actions/writeRuntimeEntry.js +69 -0
- 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 -60
- package/src/assets/js-runtime/core.js +11 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +19 -19
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +257 -50
- package/src/index.js +3 -0
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +46 -10
- package/src/integration/getDependFilePath.js +9 -0
- 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 +17 -9
- package/src/state/loadConfig.js +33 -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 +15 -1
- package/src/utils/appSources.js +97 -6
- 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/cargoTarget.js +21 -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 +58 -15
- 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 +41 -2
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +16 -5
- package/src/utils/hostPlatform.js +21 -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/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 +3 -1
- 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/runCargo.js +48 -51
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +9 -4
- 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 +69 -4
- package/src/utils/systemKeys.js +35 -3
- package/src/utils/targets.js +49 -4
- 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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import upath from 'upath';
|
|
4
|
+
import loadJson from './loadJson.js';
|
|
5
|
+
import writeIfChanged from './writeIfChanged.js';
|
|
6
|
+
import makeTreeWritable from './makeTreeWritable.js';
|
|
7
|
+
import logger from './logger.js';
|
|
8
|
+
import { libNameOf } from './linkLayout.js';
|
|
9
|
+
import { CONAN_NAME, conanPackageDir } from './conanImport.js';
|
|
10
|
+
import { CONAN_VERSION } from './conanDependencies.js';
|
|
11
|
+
|
|
12
|
+
const MANIFESTS = 'manifests';
|
|
13
|
+
const LIB_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.+-]*$/;
|
|
14
|
+
// A macro name with a plain value, if any (a path or a list too): one compiler argument, in a list CMake
|
|
15
|
+
// splits on ';'.
|
|
16
|
+
const DEFINE = /^[A-Za-z_]\w*(=[\w.+/,:-]*)?$/;
|
|
17
|
+
// What may follow name/version in a reference: a user/channel and a recipe revision.
|
|
18
|
+
const REFERENCE_TAIL = /^(@[A-Za-z0-9_][A-Za-z0-9_.+-]*\/[A-Za-z0-9_][A-Za-z0-9_.+-]*)?(#[0-9a-f]+)?$/;
|
|
19
|
+
const URL = /^https?:\/\/[^\s"'<>`\\]+$/;
|
|
20
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
21
|
+
const MAX_TEXT = 1000;
|
|
22
|
+
const TARGET_PATH = /^[A-Za-z0-9_-]+$/;
|
|
23
|
+
// The prefix of the variables a package's CMakeLists names each target's libraries by.
|
|
24
|
+
const HOST_LIBS = 'CROSSBIND_CONAN_LIBS_';
|
|
25
|
+
|
|
26
|
+
// The CMake target of a package; Conan names may carry '.', '+' and '-'.
|
|
27
|
+
export const conanTargetName = (name) => `conan_${name.replace(/[^A-Za-z0-9_]/g, '_')}`;
|
|
28
|
+
|
|
29
|
+
const matches = (pattern, value) => typeof value === 'string' && pattern.test(value);
|
|
30
|
+
const isListOf = (pattern, values) => Array.isArray(values) && values.every((value) => matches(pattern, value));
|
|
31
|
+
const shown = (value) => `${JSON.stringify(value)}`.slice(0, 200);
|
|
32
|
+
// C0 and C1 controls, which carry terminal escape sequences.
|
|
33
|
+
export const isControlCharacter = (char) => char < ' ' || (char >= '\u007f' && char <= '\u009f');
|
|
34
|
+
// Recipe text ends up in notices and terminals: one line, no control characters.
|
|
35
|
+
const textOf = (value) => (typeof value === 'string' && value.length <= MAX_TEXT && ![...value].some(isControlCharacter) ? value : null);
|
|
36
|
+
const urlOf = (value) => (matches(URL, value) && value.length <= MAX_TEXT ? value : null);
|
|
37
|
+
|
|
38
|
+
// A list means every license applies; an item that offers a choice stays one term of it.
|
|
39
|
+
function licenseOf(license) {
|
|
40
|
+
const parts = Array.isArray(license) ? license.map(textOf) : [textOf(license)];
|
|
41
|
+
if (parts.length === 0 || !parts.every(Boolean)) return null;
|
|
42
|
+
return parts.length === 1 ? parts[0] : parts.map((part) => (part.includes(' OR ') ? `(${part})` : part)).join(' AND ');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The fields of a package that crossbind keeps. Conan's JSON comes from recipe code and these fields
|
|
46
|
+
// end up in paths, CMake files and notices, so they are checked where they come in and again where
|
|
47
|
+
// the manifest is read back.
|
|
48
|
+
export function conanPackageEntry(entry) {
|
|
49
|
+
const fail = (field) => {
|
|
50
|
+
throw new Error(`crossbind: the Conan package ${shown(entry.ref ?? entry.name)} has an invalid ${field}: ${shown(entry[field])}.`);
|
|
51
|
+
};
|
|
52
|
+
if (!matches(CONAN_NAME, entry.name)) fail('name');
|
|
53
|
+
if (!matches(CONAN_VERSION, entry.version)) fail('version');
|
|
54
|
+
const reference = `${entry.name}/${entry.version}`;
|
|
55
|
+
if (typeof entry.ref !== 'string' || !entry.ref.startsWith(reference) || !REFERENCE_TAIL.test(entry.ref.slice(reference.length))) fail('ref');
|
|
56
|
+
if (!isListOf(LIB_NAME, entry.libs)) fail('libs');
|
|
57
|
+
if (!isListOf(CONAN_NAME, entry.requires)) fail('requires');
|
|
58
|
+
// They become linker and compiler arguments. A manifest an older crossbind wrote has none of them.
|
|
59
|
+
const systemLibs = entry.systemLibs ?? [];
|
|
60
|
+
const frameworks = entry.frameworks ?? [];
|
|
61
|
+
const defines = entry.defines ?? [];
|
|
62
|
+
if (!isListOf(LIB_NAME, systemLibs)) fail('systemLibs');
|
|
63
|
+
if (!isListOf(LIB_NAME, frameworks)) fail('frameworks');
|
|
64
|
+
if (!isListOf(DEFINE, defines)) fail('defines');
|
|
65
|
+
const url = urlOf(entry.source?.url);
|
|
66
|
+
return {
|
|
67
|
+
name: entry.name,
|
|
68
|
+
version: entry.version,
|
|
69
|
+
ref: entry.ref,
|
|
70
|
+
license: licenseOf(entry.license),
|
|
71
|
+
homepage: urlOf(entry.homepage),
|
|
72
|
+
source: url ? { url, sha256: matches(SHA256, entry.source.sha256) ? entry.source.sha256 : null } : null,
|
|
73
|
+
libs: [...entry.libs],
|
|
74
|
+
requires: [...entry.requires],
|
|
75
|
+
systemLibs: [...systemLibs],
|
|
76
|
+
frameworks: [...frameworks],
|
|
77
|
+
defines: [...defines],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sourceOf(entry) {
|
|
82
|
+
const source = Array.isArray(entry) ? entry[0] : entry;
|
|
83
|
+
if (!source?.url) return null;
|
|
84
|
+
return { url: Array.isArray(source.url) ? source.url[0] : source.url, sha256: source.sha256 ?? null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A define crossbind cannot pass as one plain argument was left out before crossbind took defines at all,
|
|
88
|
+
// so it stays out, with a notice, instead of stopping the build.
|
|
89
|
+
function plainDefinesOf(node, defines) {
|
|
90
|
+
return defines.filter((define) => {
|
|
91
|
+
if (matches(DEFINE, define)) return true;
|
|
92
|
+
logger.info(`crossbind: ${shown(node.ref)} asks the code that uses it to compile with ${shown(define)}, which crossbind leaves out: a define passes as a plain name with a plain value.`);
|
|
93
|
+
return false;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function packageOf(node, requires) {
|
|
98
|
+
const components = Object.values(node.cpp_info ?? {});
|
|
99
|
+
const collect = (key) => [...new Set(components.flatMap((component) => component?.[key] ?? []))];
|
|
100
|
+
const folders = (key) => {
|
|
101
|
+
const values = collect(key);
|
|
102
|
+
if (!values.every((value) => typeof value === 'string')) {
|
|
103
|
+
throw new Error(`crossbind: the Conan package ${shown(node.ref)} has invalid ${key}.`);
|
|
104
|
+
}
|
|
105
|
+
return values;
|
|
106
|
+
};
|
|
107
|
+
if (typeof node.package_folder !== 'string') {
|
|
108
|
+
throw new Error(`crossbind: conan reported the package ${shown(node.ref)} without a package folder.`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...conanPackageEntry({
|
|
112
|
+
name: node.name,
|
|
113
|
+
version: node.version,
|
|
114
|
+
ref: node.ref,
|
|
115
|
+
license: node.license,
|
|
116
|
+
homepage: node.homepage,
|
|
117
|
+
source: sourceOf(node.conandata?.sources?.[node.version]),
|
|
118
|
+
libs: collect('libs'),
|
|
119
|
+
requires,
|
|
120
|
+
systemLibs: collect('system_libs'),
|
|
121
|
+
frameworks: collect('frameworks'),
|
|
122
|
+
defines: plainDefinesOf(node, collect('defines')),
|
|
123
|
+
}),
|
|
124
|
+
packageFolder: node.package_folder,
|
|
125
|
+
includedirs: folders('includedirs'),
|
|
126
|
+
libdirs: folders('libdirs'),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// One entry per package name, each before the packages it requires: the order a static link takes
|
|
131
|
+
// them in.
|
|
132
|
+
function linkOrder(packages) {
|
|
133
|
+
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
|
|
134
|
+
const order = [];
|
|
135
|
+
const visited = new Set();
|
|
136
|
+
const visit = (name) => {
|
|
137
|
+
if (visited.has(name) || !byName.has(name)) return;
|
|
138
|
+
visited.add(name);
|
|
139
|
+
byName.get(name).requires.forEach(visit);
|
|
140
|
+
order.push(byName.get(name));
|
|
141
|
+
};
|
|
142
|
+
byName.forEach((pkg, name) => visit(name));
|
|
143
|
+
return order.reverse();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Host packages of a `conan install --format=json` graph in link order. Left out are the consumer,
|
|
147
|
+
// build tools, test requirements and packages conan skipped because the consumer needs nothing of them.
|
|
148
|
+
export function conanPackagesOf(graph) {
|
|
149
|
+
const nodes = graph?.nodes;
|
|
150
|
+
if (!nodes || typeof nodes !== 'object' || !graph.root) {
|
|
151
|
+
throw new Error('crossbind: conan install printed a graph without nodes or a root.');
|
|
152
|
+
}
|
|
153
|
+
const [root] = Object.keys(graph.root);
|
|
154
|
+
const isPackage = (id) => id !== root && nodes[id]?.context === 'host' && !nodes[id].test && nodes[id].binary !== 'Skip';
|
|
155
|
+
const ids = Object.keys(nodes).filter(isPackage);
|
|
156
|
+
const requiresOf = (id) => Object.entries(nodes[id].dependencies ?? {})
|
|
157
|
+
.filter(([dependency, edge]) => !edge?.build && !edge?.test && ids.includes(dependency))
|
|
158
|
+
.map(([dependency]) => nodes[dependency].name);
|
|
159
|
+
return linkOrder(ids.map((id) => packageOf(nodes[id], requiresOf(id))));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The real path of a file conan reported, or null when there is none; refused when it resolves
|
|
163
|
+
// outside `root`.
|
|
164
|
+
function realPathWithin(file, root, pkg) {
|
|
165
|
+
if (!fs.existsSync(file)) return null;
|
|
166
|
+
const real = fs.realpathSync(file);
|
|
167
|
+
const relative = path.relative(root, real);
|
|
168
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
169
|
+
throw new Error(`crossbind: the Conan package ${pkg.ref} reports ${file}, which resolves outside ${root}.`);
|
|
170
|
+
}
|
|
171
|
+
return real;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Packages link files to each other (libpng.a -> libpng16.a), so links are followed, but only to
|
|
175
|
+
// directories and regular files inside the package, and into each linked directory once: a link out
|
|
176
|
+
// of the package could point the copy at any file the host can read, and links back up the tree
|
|
177
|
+
// would copy it without end.
|
|
178
|
+
function copyTree(from, to, packageRoot, pkg) {
|
|
179
|
+
const linked = new Set([from]);
|
|
180
|
+
fs.cpSync(from, to, {
|
|
181
|
+
recursive: true,
|
|
182
|
+
dereference: true,
|
|
183
|
+
filter: (source) => {
|
|
184
|
+
const entry = fs.lstatSync(source);
|
|
185
|
+
if (!entry.isSymbolicLink()) return entry.isDirectory() || entry.isFile();
|
|
186
|
+
const real = realPathWithin(source, packageRoot, pkg);
|
|
187
|
+
const target = real && fs.statSync(real);
|
|
188
|
+
if (!target?.isDirectory()) return Boolean(target?.isFile());
|
|
189
|
+
if (linked.has(real)) return false;
|
|
190
|
+
linked.add(real);
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
makeTreeWritable(to);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The package's own folder: no link, and below the store's own layout, so it cannot take in the
|
|
198
|
+
// store or another package.
|
|
199
|
+
function packageRootOf(pkg, folder, store) {
|
|
200
|
+
const real = realPathWithin(folder, store, pkg);
|
|
201
|
+
if (!real || fs.lstatSync(folder).isSymbolicLink() || path.relative(store, real).split(path.sep).length < 2) {
|
|
202
|
+
throw new Error(`crossbind: the Conan package ${pkg.ref} has no package folder crossbind can use at ${pkg.packageFolder}.`);
|
|
203
|
+
}
|
|
204
|
+
return real;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A package lands where a published port keeps its prebuilt (<dir>/dist/prebuilt/<target>/{include,lib}),
|
|
208
|
+
// so the dependency machinery links it and SWIG treats its headers as one across targets. Everything
|
|
209
|
+
// is read from inside the package's own folder in the store.
|
|
210
|
+
export function stageConanPackage(pkg, {
|
|
211
|
+
stageDir, targetPath, toHost, store, distCmake,
|
|
212
|
+
}) {
|
|
213
|
+
const packageRoot = packageRootOf(pkg, toHost(pkg.packageFolder), fs.realpathSync(store));
|
|
214
|
+
const reported = (folder) => realPathWithin(toHost(upath.isAbsolute(folder) ? folder : upath.join(pkg.packageFolder, folder)), packageRoot, pkg);
|
|
215
|
+
const dir = conanPackageDir(stageDir, pkg.name);
|
|
216
|
+
const prebuilt = upath.join(dir, 'dist', 'prebuilt');
|
|
217
|
+
const prefix = upath.join(prebuilt, targetPath);
|
|
218
|
+
|
|
219
|
+
fs.rmSync(prefix, { recursive: true, force: true });
|
|
220
|
+
fs.mkdirSync(upath.join(prefix, 'include'), { recursive: true });
|
|
221
|
+
fs.mkdirSync(upath.join(prefix, 'lib'), { recursive: true });
|
|
222
|
+
pkg.includedirs.map(reported).filter(Boolean).forEach((folder) => copyTree(folder, upath.join(prefix, 'include'), packageRoot, pkg));
|
|
223
|
+
const libdirs = pkg.libdirs.map(reported).filter(Boolean);
|
|
224
|
+
pkg.libs.forEach((lib) => {
|
|
225
|
+
const archive = libdirs.map((folder) => realPathWithin(path.join(folder, `lib${lib}.a`), packageRoot, pkg))
|
|
226
|
+
.find((file) => file && fs.statSync(file).isFile());
|
|
227
|
+
if (!archive) {
|
|
228
|
+
throw new Error(`crossbind: the Conan package ${pkg.ref} declares the library ${lib}, but its package has no lib${lib}.a.`);
|
|
229
|
+
}
|
|
230
|
+
fs.copyFileSync(archive, upath.join(prefix, 'lib', `lib${lib}.a`));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const licenses = upath.join(dir, 'licenses');
|
|
234
|
+
fs.rmSync(licenses, { recursive: true, force: true });
|
|
235
|
+
const shipped = realPathWithin(path.join(packageRoot, 'licenses'), packageRoot, pkg);
|
|
236
|
+
if (shipped && fs.statSync(shipped).isDirectory()) copyTree(shipped, licenses, packageRoot, pkg);
|
|
237
|
+
writeIfChanged(upath.join(dir, 'package.json'), `${JSON.stringify({
|
|
238
|
+
name: `conan:${pkg.name}`, version: pkg.version, nativeVersion: pkg.version, license: pkg.license, private: true,
|
|
239
|
+
}, null, 4)}\n`);
|
|
240
|
+
|
|
241
|
+
const hosts = fs.readdirSync(prebuilt, { withFileTypes: true })
|
|
242
|
+
.filter((entry) => entry.isDirectory() && TARGET_PATH.test(entry.name)).map((entry) => entry.name).sort();
|
|
243
|
+
// A recipe can name its libraries otherwise on another platform (libpng is png16 on Windows), so each
|
|
244
|
+
// target links the archives staged for it.
|
|
245
|
+
const archivesOf = (host) => {
|
|
246
|
+
const libdir = upath.join(prebuilt, host, 'lib');
|
|
247
|
+
if (!fs.existsSync(libdir)) return [];
|
|
248
|
+
return fs.readdirSync(libdir).filter((file) => file.endsWith('.a')).map(libNameOf).filter((name) => matches(LIB_NAME, name)).sort();
|
|
249
|
+
};
|
|
250
|
+
writeIfChanged(upath.join(prebuilt, 'CMakeLists.txt'), [
|
|
251
|
+
...hosts.map((host) => `set(${HOST_LIBS}${host} "${archivesOf(host).join(';')}")`),
|
|
252
|
+
distCmake
|
|
253
|
+
.replace('___PROJECT_NAME___', () => conanTargetName(pkg.name))
|
|
254
|
+
.replace('___PROJECT_HOST___', () => hosts.join(';'))
|
|
255
|
+
.replace('___PROJECT_LIBS___', () => `\${${HOST_LIBS}\${PROJECT_TARGET_HOST}}`)
|
|
256
|
+
.replace('___PROJECT_WHOLE_ARCHIVE___', () => ''),
|
|
257
|
+
].join('\n'));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const manifestsOf = (stageDir) => upath.join(stageDir, MANIFESTS);
|
|
261
|
+
const manifestFileOf = (stageDir, targetPath) => upath.join(manifestsOf(stageDir), `${targetPath}.json`);
|
|
262
|
+
|
|
263
|
+
// One manifest per target: a recipe can require other packages on another platform.
|
|
264
|
+
export function writeConanManifest(stageDir, targetPath, manifest) {
|
|
265
|
+
writeIfChanged(manifestFileOf(stageDir, targetPath), `${JSON.stringify(manifest, null, 4)}\n`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The packages staged for one target, or for every target staged so far, in link order, and each
|
|
269
|
+
// target's own list. A manifest staged for other conanDependencies describes packages the config no
|
|
270
|
+
// longer asks for.
|
|
271
|
+
export function readConanManifest(stageDir, key, targetPath = null) {
|
|
272
|
+
const dir = manifestsOf(stageDir);
|
|
273
|
+
let files = [];
|
|
274
|
+
if (targetPath) files = [manifestFileOf(stageDir, targetPath)];
|
|
275
|
+
else if (fs.existsSync(dir)) files = fs.readdirSync(dir).filter((name) => name.endsWith('.json')).sort().map((name) => upath.join(dir, name));
|
|
276
|
+
const manifests = files.map((file) => ({ target: upath.basename(file, '.json'), manifest: loadJson(file) }))
|
|
277
|
+
.filter(({ manifest }) => manifest?.key === key);
|
|
278
|
+
if (manifests.length === 0) return null;
|
|
279
|
+
try {
|
|
280
|
+
const targets = Object.fromEntries(manifests.map(({ target, manifest }) => [target, manifest.packages.map(conanPackageEntry)]));
|
|
281
|
+
return { key, packages: linkOrder(Object.values(targets).flat()), targets };
|
|
282
|
+
} catch (e) {
|
|
283
|
+
throw new Error(`crossbind: ${dir} lists a package crossbind will not use - delete ${stageDir} and build again. ${e.message}`, { cause: e });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Changes whenever a build stages packages, so a process that attached before can tell.
|
|
288
|
+
export function conanManifestsStamp(stageDir) {
|
|
289
|
+
const dir = manifestsOf(stageDir);
|
|
290
|
+
if (!fs.existsSync(dir)) return '';
|
|
291
|
+
return fs.readdirSync(dir).sort().map((name) => `${name}@${fs.statSync(upath.join(dir, name)).mtimeMs}`).join('|');
|
|
292
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import upath from 'upath';
|
|
4
|
+
|
|
5
|
+
const NODE_MODULES = 'node_modules/';
|
|
6
|
+
|
|
7
|
+
// A copyToDist source names another package's file as node_modules/<package>/<file>. npm may hoist that package
|
|
8
|
+
// above the one being built, so a path missing beside the package resolves the way Node finds the package.
|
|
9
|
+
export default function copyToDistSource(projectDir, key) {
|
|
10
|
+
const local = upath.join(projectDir, key);
|
|
11
|
+
if (fs.existsSync(local) || !key.startsWith(NODE_MODULES)) return local;
|
|
12
|
+
return upath.normalize(createRequire(upath.join(projectDir, 'package.json')).resolve(key.slice(NODE_MODULES.length)));
|
|
13
|
+
}
|
package/src/utils/cppDts.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { stripComments } from './swigInterface.js';
|
|
4
4
|
import writeIfChanged from './writeIfChanged.js';
|
|
5
|
+
import { conanImportOfHeader, conanStageDir } from './conanImport.js';
|
|
5
6
|
|
|
6
7
|
// Best-effort .d.ts for `.h` imports, the C++ analog of the Rust emitDts: parse the
|
|
7
8
|
// binding-rules surface (classes, public methods, primitives/string/shared_ptr), skip
|
|
@@ -34,7 +35,7 @@ function tsType(raw, classNames, { isReturn = false } = {}) {
|
|
|
34
35
|
if (t === 'void') return 'void';
|
|
35
36
|
if (t === 'bool') return 'boolean';
|
|
36
37
|
if (NUMBER_TYPES.has(t)) return 'number';
|
|
37
|
-
if (t === 'std::string') return 'string';
|
|
38
|
+
if (t === 'std::string' || t === 'std::u16string') return 'string';
|
|
38
39
|
if (classNames.has(t)) return t;
|
|
39
40
|
return null;
|
|
40
41
|
}
|
|
@@ -133,8 +134,12 @@ function typedefNamesAfter(text, from) {
|
|
|
133
134
|
return tail ? tail[1].split(',').map((part) => part.trim()).filter((part) => /^[A-Za-z_]\w*$/.test(part)) : [];
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// A brace inside a string or character literal is text; the model needs no literal's contents.
|
|
138
|
+
const LITERAL = /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g;
|
|
139
|
+
const blankLiterals = (text) => text.replace(LITERAL, (literal) => `${literal[0]}${' '.repeat(literal.length - 2)}${literal[0]}`);
|
|
140
|
+
|
|
136
141
|
export function parseCppSurface(source, log = console.log) {
|
|
137
|
-
const clean = stripComments(source).replace(/^[ \t]*#[^\n]*$/gm, ' ');
|
|
142
|
+
const clean = blankLiterals(stripComments(source)).replace(/^[ \t]*#[^\n]*$/gm, ' ');
|
|
138
143
|
|
|
139
144
|
const classes = [];
|
|
140
145
|
const classNames = new Set();
|
|
@@ -154,13 +159,18 @@ export function parseCppSurface(source, log = console.log) {
|
|
|
154
159
|
for (const cls of found) {
|
|
155
160
|
let access = cls.kind === 'class' ? 'private' : 'public';
|
|
156
161
|
let ctor = null;
|
|
162
|
+
let declaresCtor = false;
|
|
163
|
+
let isAbstract = false;
|
|
157
164
|
const methods = [];
|
|
158
165
|
const fields = [];
|
|
159
166
|
for (let statement of bodyStatements(cls.body)) {
|
|
160
167
|
const sections = statement.match(/\b(public|private|protected)\s*:\s*([\s\S]*)$/);
|
|
161
168
|
if (sections) { access = sections[1]; statement = sections[2].trim(); }
|
|
169
|
+
if (new RegExp(`^(?:explicit\\s+)?${cls.name}\\s*\\(`).test(statement)) declaresCtor = true;
|
|
170
|
+
if (/\)\s*(?:const\s*)?(?:override\s*)?=\s*0$/.test(statement)) isAbstract = true;
|
|
162
171
|
if (!statement || access !== 'public') continue;
|
|
163
|
-
|
|
172
|
+
const isDeleted = /=\s*delete$/.test(statement);
|
|
173
|
+
statement = statement.replace(/\s*=\s*(?:default|delete)$/, '').replace(/\)\s*:\s*[\s\S]*$/, ')').replace(/\)\s*const$/, ')').trim();
|
|
164
174
|
|
|
165
175
|
const sig = statement.match(/^(static\s+)?(?:explicit\s+)?([\w:<>,\s*&]*?)\s*\b([A-Za-z_]\w*)\s*\(([\s\S]*)\)$/);
|
|
166
176
|
if (!sig) {
|
|
@@ -172,12 +182,17 @@ export function parseCppSurface(source, log = console.log) {
|
|
|
172
182
|
const [, staticKw, retRaw, name, argsRaw] = sig;
|
|
173
183
|
const args = parseArgs(argsRaw, classNames);
|
|
174
184
|
if (args.includes(null)) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported parameter type)`); continue; }
|
|
175
|
-
if (name === cls.name && retRaw.trim() === '') {
|
|
185
|
+
if (name === cls.name && retRaw.trim() === '') {
|
|
186
|
+
if (!isDeleted) ctor = { args };
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
176
189
|
if (name.startsWith('~')) continue;
|
|
177
190
|
const ret = tsType(retRaw, classNames, { isReturn: true });
|
|
178
191
|
if (ret === null) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported return type '${retRaw.trim()}')`); continue; }
|
|
179
192
|
methods.push({ name, isStatic: Boolean(staticKw), args, ret });
|
|
180
193
|
}
|
|
194
|
+
// The bridge binds the implicit default constructor of a class that declares none, unless it is abstract.
|
|
195
|
+
if (!declaresCtor && !isAbstract) ctor = { args: [] };
|
|
181
196
|
classes.push({ name: cls.name, aliases: cls.aliases, ctor, methods, fields });
|
|
182
197
|
}
|
|
183
198
|
return { classes };
|
|
@@ -221,20 +236,48 @@ export function emitCppDts(model, exportNames, mode = 'sync') {
|
|
|
221
236
|
return out.join('\n');
|
|
222
237
|
}
|
|
223
238
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, dtsMode = 'sync', log = () => {} }) {
|
|
227
|
-
if (!fs.existsSync(exportsFile)) return;
|
|
228
|
-
const relative = path.relative(projectPath, headerFile);
|
|
229
|
-
if (relative.startsWith('..')) return;
|
|
230
|
-
let exportNames;
|
|
239
|
+
function readExportNames(exportsFile, log) {
|
|
240
|
+
if (!fs.existsSync(exportsFile)) return null;
|
|
231
241
|
try {
|
|
232
|
-
exportNames = JSON.parse(fs.readFileSync(exportsFile, 'utf8'));
|
|
242
|
+
const exportNames = JSON.parse(fs.readFileSync(exportsFile, 'utf8'));
|
|
243
|
+
return Array.isArray(exportNames) ? exportNames : null;
|
|
233
244
|
} catch (e) {
|
|
234
245
|
log(`crossbind: dts: unreadable exports file ${exportsFile} (${e.message})`);
|
|
235
|
-
return;
|
|
246
|
+
return null;
|
|
236
247
|
}
|
|
237
|
-
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function declarationsOf(headerFile, exportNames, dtsMode, log) {
|
|
238
251
|
const model = headerFile.endsWith('.i') ? { classes: [] } : parseCppSurface(fs.readFileSync(headerFile, 'utf8'), log);
|
|
239
|
-
|
|
252
|
+
return emitCppDts(model, exportNames, dtsMode);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Mirror the declaration under <cache>/types/<project-relative>.d.ts - never next to the
|
|
256
|
+
// user's header. Package headers are skipped: their types ship with the package itself, and a
|
|
257
|
+
// header staged from Conan is typed by its conan: module instead.
|
|
258
|
+
export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, dtsMode = 'sync', log = () => {} }) {
|
|
259
|
+
const relative = path.relative(projectPath, headerFile);
|
|
260
|
+
if (relative.startsWith('..') || headerFile.startsWith(`${conanStageDir(cacheDir)}/`)) return;
|
|
261
|
+
const exportNames = readExportNames(exportsFile, log);
|
|
262
|
+
if (!exportNames) return;
|
|
263
|
+
writeIfChanged(`${cacheDir}/types/${relative}.d.ts`, declarationsOf(headerFile, exportNames, dtsMode, log));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// A conan: specifier names no file the editor could map, so it is typed by an ambient module that
|
|
267
|
+
// @crossbind/typescript-config includes. `declare` is an error inside one (TS1038).
|
|
268
|
+
export function writeConanImportDts({ headerFile, exportsFile, declarationsFile = exportsFile, cacheDir, dtsMode = 'sync', log = () => {} }) {
|
|
269
|
+
const conanImport = conanImportOfHeader(headerFile, cacheDir);
|
|
270
|
+
const declared = conanImport && readExportNames(declarationsFile, log);
|
|
271
|
+
if (!declared) return;
|
|
272
|
+
// The bridge also binds macros and variadic functions the app imports, which the whole header's SWIG run skips.
|
|
273
|
+
const exportNames = [...new Set([...declared, ...(readExportNames(exportsFile, log) ?? [])])];
|
|
274
|
+
const body = declarationsOf(headerFile, exportNames, dtsMode, log)
|
|
275
|
+
.replaceAll('export declare ', 'export ')
|
|
276
|
+
.split('\n')
|
|
277
|
+
.map((line) => (line ? ` ${line}` : line))
|
|
278
|
+
.join('\n');
|
|
279
|
+
writeIfChanged(
|
|
280
|
+
`${conanStageDir(cacheDir)}/types/${conanImport.name}/${conanImport.header}.d.ts`,
|
|
281
|
+
`declare module 'conan:${conanImport.name}/${conanImport.header}' {\n${body}\n}\n`,
|
|
282
|
+
);
|
|
240
283
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The tools xcode-select picks, behind /usr/bin's shims, which the Command Line Tools alone also provide,
|
|
2
|
+
// and the macOS every macOS archive is built for: port archives and Conan packages link into the same
|
|
3
|
+
// addon, so they move together.
|
|
4
|
+
export const DARWIN_TOOLS_BIN = '/usr/bin';
|
|
5
|
+
export const DARWIN_CC = `${DARWIN_TOOLS_BIN}/clang`;
|
|
6
|
+
export const DARWIN_CXX = `${DARWIN_TOOLS_BIN}/clang++`;
|
|
7
|
+
// Node 22, the oldest supported line, needs macOS 11.
|
|
8
|
+
export const DARWIN_DEPLOYMENT_TARGET = '11.0';
|
|
9
|
+
// Homebrew and MacPorts packages exist on the build machine only: an archive compiled against one fails
|
|
10
|
+
// to link, or to load, anywhere else.
|
|
11
|
+
export const DARWIN_HOST_PACKAGE_PREFIXES = ['/opt/homebrew', '/usr/local', '/opt/local'];
|
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
|
}
|