crossbind 2.0.0-beta.74 → 2.0.0-beta.76
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/package.json +3 -3
- package/src/actions/androidGradle.js +47 -4
- package/src/actions/buildApp.js +4 -3
- package/src/actions/buildNode.js +2 -10
- package/src/actions/buildWasm.js +20 -1
- package/src/actions/createInterface.js +266 -38
- package/src/actions/createLib.js +4 -1
- package/src/actions/getCmakeParameters.js +0 -1
- package/src/actions/licenses.js +65 -19
- package/src/actions/nativeNotices.js +43 -0
- package/src/actions/run.js +72 -6
- package/src/actions/wasmNotices.js +32 -0
- package/src/app/index.js +1 -1
- package/src/assets/js-runtime/adapters/worker-comlink.js +11 -0
- package/src/assets/toolchain-digests.json +20 -20
- package/src/integration/getDependFilePath.js +8 -1
- package/src/state/loadConfig.js +1 -1
- package/src/utils/androidToolchain.js +4 -0
- package/src/utils/bridgeRuntimeFixes.js +24 -11
- package/src/utils/embindPointerOwners.js +42 -0
- package/src/utils/sourceFingerprint.js +5 -1
- package/src/utils/swigInterface.js +22 -7
- package/src/utils/toolchainNotices.js +20 -0
- package/src/utils/typeClosure.js +170 -0
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
interfaceExtras, bridgeExtras, hasPointerRuntime, withPointerRuntimeAnchor, withoutBridgeExtras, POINTER_RUNTIME_ANCHOR,
|
|
13
13
|
} from '../utils/bridgeExtras.js';
|
|
14
14
|
import resolveNativeImport from '../integration/resolveNativeImport.js';
|
|
15
|
+
import getDependFilePath from '../integration/getDependFilePath.js';
|
|
16
|
+
import { boundHeaderSpecifiers } from '../utils/boundHeaders.js';
|
|
15
17
|
import { writeHeaderDts, writeSourceImportDts } from '../utils/cppDts.js';
|
|
16
18
|
import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
|
|
17
19
|
import {
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
indexTypeDefinitions, inlineIncludes, interfaceIncludes, interfaceToRetryWithoutMacros, interfaceWithAllFunctions, parseMacroDump, referencedTypeHeaders,
|
|
20
22
|
selectSwigMacros,
|
|
21
23
|
} from '../utils/swigInterface.js';
|
|
24
|
+
import { readSwigTree, typeClosure } from '../utils/typeClosure.js';
|
|
22
25
|
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
23
26
|
import { withDirLockSync } from '../utils/dirLock.js';
|
|
24
27
|
import { sourceImportOfHeader } from '../utils/packageSourceImport.js';
|
|
@@ -29,12 +32,18 @@ import {
|
|
|
29
32
|
} from '../utils/bridgeTools.js';
|
|
30
33
|
|
|
31
34
|
// Part of every interface hash, so interfaces cached by an older generator are rebuilt.
|
|
32
|
-
const INTERFACE_FORMAT = 'swig-macros-
|
|
35
|
+
const INTERFACE_FORMAT = 'swig-macros-5';
|
|
33
36
|
// Part of every bridge hash: each bridge carries the SWIG fork's runtime and all bridges of a module must share it, so
|
|
34
37
|
// bridges from a fork before field bindings and constants are rebuilt.
|
|
35
38
|
const BRIDGE_FORMAT = 'swig-extras-2';
|
|
39
|
+
// Part of the key of SWIG's parse of a header, which the bindings plan reads.
|
|
40
|
+
const TREE_FORMAT = 'swig-xml-1';
|
|
36
41
|
const predefinedMacros = new Map();
|
|
37
42
|
const typeDefinitions = new Map();
|
|
43
|
+
const swigTrees = new Map();
|
|
44
|
+
const plans = new Map();
|
|
45
|
+
const importedHeaderSets = new Map();
|
|
46
|
+
const macroDumps = new Map();
|
|
38
47
|
|
|
39
48
|
// Each process keeps the cache it loaded, so an interface or bridge another one has rebuilt since must not pass for
|
|
40
49
|
// this one's: beside each file is the hash of what it was made from, blank while the file is written. A file without one
|
|
@@ -70,7 +79,11 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
|
|
|
70
79
|
// '@scope/pkg/native/x.h'), so its own directory must reach swig's include list.
|
|
71
80
|
const sourceDir = upath.dirname(interfaceFilePath);
|
|
72
81
|
const allNames = wholeHeaders.some((file) => upath.resolve(file) === interfaceFilePath);
|
|
82
|
+
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
83
|
+
const isModule = moduleRegex.test(interfaceFilePath);
|
|
73
84
|
const names = bindingNames(interfaceFilePath, target, allNames);
|
|
85
|
+
const plan = isModule ? null : bindingPlan(target, wholeHeaders, interfaceFilePath, names);
|
|
86
|
+
names.classes = plan ? plan.classesOf(interfaceFilePath) : ALL_NAMES;
|
|
74
87
|
if (withDependencies) warnUnimported(interfaceFilePath, names.functions);
|
|
75
88
|
const interfaceFile = createInterfaceFile(interfaceFilePath, target, sourceDir, names);
|
|
76
89
|
const { headerPath } = includeLocation(interfaceFilePath, target);
|
|
@@ -81,8 +94,8 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
|
|
|
81
94
|
const bridgeSourceHash = names.named.length ? `${sourceHash}\nnamed:${names.named.join(',')}` : sourceHash;
|
|
82
95
|
const extras = { named: names.named, headerText: () => fs.readFileSync(swigView ? upath.join(swigView.dir, headerPath) : interfaceFilePath, 'utf8') };
|
|
83
96
|
const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, sourceDir, bridgeSourceHash, swigView, extras);
|
|
84
|
-
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
85
97
|
if (bridgeFile) {
|
|
98
|
+
if (plan) warnUnboundClasses(bridgeFile, names.classes, plan);
|
|
86
99
|
// Records which header this bridge came from, so directory-driven consumers
|
|
87
100
|
// (getAllBridges) can prune bridges whose header no longer exists.
|
|
88
101
|
writeIfChanged(`${bridgeFile}.source`, `${interfaceFilePath}\n`);
|
|
@@ -100,10 +113,10 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
|
|
|
100
113
|
}
|
|
101
114
|
writeSourceImportDts(dtsOptions);
|
|
102
115
|
}
|
|
103
|
-
if (bridgeFile && withDependencies && !
|
|
104
|
-
// A header's own bindings need the types
|
|
105
|
-
//
|
|
106
|
-
const dependencyBridges =
|
|
116
|
+
if (bridgeFile && withDependencies && !isModule) {
|
|
117
|
+
// A header's own bindings need the types they use registered, which only their headers' bridges do. Such a bridge
|
|
118
|
+
// replaces the header's own, so a header bound whole keeps every constant here too.
|
|
119
|
+
const dependencyBridges = plan.dependenciesOf(interfaceFilePath).map((header) => {
|
|
107
120
|
try {
|
|
108
121
|
return createBridgeFile(header, target, { withDependencies: false, wholeHeaders });
|
|
109
122
|
} catch (e) {
|
|
@@ -145,10 +158,15 @@ function warnUnimported(header, functions) {
|
|
|
145
158
|
console.warn(`crossbind: ${upath.basename(header)} binds none of its functions, since no source of the app imports a name from it. Import the functions you call by name, or the whole header with import * as.`);
|
|
146
159
|
}
|
|
147
160
|
|
|
148
|
-
|
|
161
|
+
// `tree` writes the interface SWIG's parse for the bindings plan reads, beside the bridge's own and cached apart from it.
|
|
162
|
+
function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, {
|
|
163
|
+
constants, functions, classes = ALL_NAMES, named = [],
|
|
164
|
+
}, { tree = false } = {}) {
|
|
149
165
|
if (!headerOrModuleFilePath) {
|
|
150
166
|
return null;
|
|
151
167
|
}
|
|
168
|
+
const cacheKey = tree ? `${headerOrModuleFilePath}#tree` : headerOrModuleFilePath;
|
|
169
|
+
const outputName = (name) => (tree ? name.replace(/\.i$/, '.tree.i') : name);
|
|
152
170
|
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
153
171
|
const isModule = moduleRegex.test(headerOrModuleFilePath);
|
|
154
172
|
const { includeRoot, headerPath } = isModule ? {} : includeLocation(headerOrModuleFilePath, target);
|
|
@@ -170,28 +188,30 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
|
|
|
170
188
|
...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
|
|
171
189
|
...(constants.length ? [`constants:${constants === ALL_NAMES ? ALL_NAMES : constants.join(',')}`] : []),
|
|
172
190
|
...(functions === ALL_NAMES ? [] : [`functions:${functions.join(',')}`]),
|
|
191
|
+
...(classes === ALL_NAMES ? [] : [`classes:${classes.join(',')}`]),
|
|
173
192
|
...(named.length ? [`named:${named.join(',')}`] : []),
|
|
193
|
+
...(tree ? ['tree'] : []),
|
|
174
194
|
...packageInputsHash(),
|
|
175
195
|
].join('\n');
|
|
176
|
-
const cachedInterface = state.cache.interfaces[
|
|
177
|
-
if (state.cache.hashes[
|
|
178
|
-
&& isMadeFrom(cachedInterface, fileHash) && !sharedInterface(
|
|
196
|
+
const cachedInterface = state.cache.interfaces[cacheKey];
|
|
197
|
+
if (state.cache.hashes[cacheKey] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
|
|
198
|
+
&& isMadeFrom(cachedInterface, fileHash) && !sharedInterface(cacheKey, cachedInterface)) {
|
|
179
199
|
return cachedInterface;
|
|
180
200
|
}
|
|
181
201
|
|
|
182
202
|
if (isModule) {
|
|
183
|
-
const newPath = `${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}
|
|
184
|
-
return recordInterface(
|
|
203
|
+
const newPath = outputName(`${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`);
|
|
204
|
+
return recordInterface(cacheKey, newPath, fileHash, () => fs.copyFileSync(headerOrModuleFilePath, newPath));
|
|
185
205
|
}
|
|
186
206
|
|
|
187
207
|
if (!filePathWithoutExt) return null;
|
|
188
208
|
|
|
189
209
|
if (fs.existsSync(interfaceFile)) {
|
|
190
|
-
const newPath = `${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}
|
|
191
|
-
return recordInterface(
|
|
210
|
+
const newPath = outputName(`${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`);
|
|
211
|
+
return recordInterface(cacheKey, newPath, fileHash, () => fs.copyFileSync(interfaceFile, newPath));
|
|
192
212
|
}
|
|
193
213
|
|
|
194
|
-
const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1));
|
|
214
|
+
const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1), (name) => outputName(interfacePath(name)), cacheKey);
|
|
195
215
|
|
|
196
216
|
const macros = collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants);
|
|
197
217
|
const extras = interfaceExtras(named, macros.table, fs.readFileSync(headerOrModuleFilePath, 'utf8'));
|
|
@@ -205,10 +225,11 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
|
|
|
205
225
|
constants,
|
|
206
226
|
preamble,
|
|
207
227
|
functions: functions === ALL_NAMES ? ALL_NAMES : [...functions, ...extras.functions],
|
|
228
|
+
classes,
|
|
208
229
|
extras: extras.lines,
|
|
209
230
|
});
|
|
210
|
-
const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i
|
|
211
|
-
return recordInterface(
|
|
231
|
+
const outputFilePath = outputName(`${state.config.paths.build}/interface/${fileName}.i`);
|
|
232
|
+
return recordInterface(cacheKey, outputFilePath, fileHash, () => fs.writeFileSync(outputFilePath, content));
|
|
212
233
|
}
|
|
213
234
|
|
|
214
235
|
// The key is blank while the file is written, so a process that dies in between leaves no file passing for another hash.
|
|
@@ -230,9 +251,10 @@ const sharedInterface = (headerFile, interfaceFile) => Object.entries(state.cach
|
|
|
230
251
|
|
|
231
252
|
// An interface is named after its header's file name. A second header with that name (tiffio.h and tiffio.hxx, or one
|
|
232
253
|
// name in two directories) takes its extension, then a hash of its path, so neither replaces the other's bridge.
|
|
233
|
-
|
|
254
|
+
// SWIG's parse for the bindings plan names its interface the same way, checked against the other parses'.
|
|
255
|
+
function interfaceName(headerFile, base, pathOf = interfacePath, identity = headerFile) {
|
|
234
256
|
const candidates = [base, `${base}_${upath.extname(headerFile).slice(1)}`, `${base}_${getContentHash(headerFile).slice(0, 8)}`];
|
|
235
|
-
return candidates.find((name) => !sharedInterface(
|
|
257
|
+
return candidates.find((name) => !sharedInterface(identity, pathOf(name))) ?? candidates.at(-1);
|
|
236
258
|
}
|
|
237
259
|
|
|
238
260
|
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -263,14 +285,31 @@ function definitionsUnder(includeRoot) {
|
|
|
263
285
|
files.forEach(({ path, text }) => {
|
|
264
286
|
if (!nearest.has(text) || depth(path) < depth(nearest.get(text))) nearest.set(text, path);
|
|
265
287
|
});
|
|
288
|
+
const originals = files.filter(({ path, text }) => nearest.get(text) === path);
|
|
266
289
|
typeDefinitions.set(includeRoot, {
|
|
267
|
-
definitions: indexTypeDefinitions(
|
|
290
|
+
definitions: indexTypeDefinitions(originals),
|
|
291
|
+
aliases: indexAliases(originals),
|
|
268
292
|
copyOf: new Map(files.map(({ path, text }) => [path, nearest.get(text)])),
|
|
269
293
|
});
|
|
270
294
|
}
|
|
271
295
|
return typeDefinitions.get(includeRoot);
|
|
272
296
|
}
|
|
273
297
|
|
|
298
|
+
// The names plain typedefs and alias declarations give (typedef T Name; typedef R (*Name)(...); using Name = T;), each
|
|
299
|
+
// mapped to the first header giving it, so the bindings plan follows a type through the header that names it.
|
|
300
|
+
const ALIAS = /\btypedef\b[^;{}]*?(?:\(\s*\*\s*([A-Za-z_]\w*)\s*\)[^;{}]*|\b([A-Za-z_]\w*)\s*(?:\[[^\]]*\]\s*)*);|\busing\s+([A-Za-z_]\w*)\s*=/g;
|
|
301
|
+
|
|
302
|
+
function indexAliases(files) {
|
|
303
|
+
const aliases = new Map();
|
|
304
|
+
for (const { path, text } of files) {
|
|
305
|
+
for (const [, pointer, plain, using] of text.matchAll(ALIAS)) {
|
|
306
|
+
const name = pointer ?? plain ?? using;
|
|
307
|
+
if (name && !aliases.has(name)) aliases.set(name, path);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return aliases;
|
|
311
|
+
}
|
|
312
|
+
|
|
274
313
|
function lookupContext(includeRoot, headerPath, headerText) {
|
|
275
314
|
const { definitions, copyOf } = definitionsUnder(includeRoot);
|
|
276
315
|
return { headerText, headerPath: copyOf.get(headerPath) ?? headerPath, definitions };
|
|
@@ -290,6 +329,187 @@ function dependencyHeadersOf(headerFile, target) {
|
|
|
290
329
|
.map((header) => upath.join(includeRoot, header));
|
|
291
330
|
}
|
|
292
331
|
|
|
332
|
+
// The header under a header's include root that defines a type, or names one with a typedef or alias, for the plan.
|
|
333
|
+
function definingHeaderOf(headerFile, name, target) {
|
|
334
|
+
const { includeRoot } = includeLocation(headerFile, target);
|
|
335
|
+
if (!includeRoot) return null;
|
|
336
|
+
const { definitions, aliases } = definitionsUnder(includeRoot);
|
|
337
|
+
const found = definitions.get(name) ?? aliases.get(name) ?? aliases.get(name.split('::').at(-1));
|
|
338
|
+
return found ? upath.join(includeRoot, found) : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// SWIG's own parse of a header, which the bindings plan reads (see typeClosure.js): an interface with every function and
|
|
342
|
+
// class of the header, written beside the bridge's own, through SWIG's XML module, made again when what it reads changes.
|
|
343
|
+
function swigTreeOf(headerFile, target) {
|
|
344
|
+
const file = upath.resolve(headerFile);
|
|
345
|
+
const sourceDir = upath.dirname(file);
|
|
346
|
+
const { headerPath } = includeLocation(file, target);
|
|
347
|
+
const inlined = findSwigInlineIncludes(file, [state.config, ...state.config.allDependencies], headerPath);
|
|
348
|
+
const swigView = inlined.length ? writeSwigView(file, headerPath, inlined) : null;
|
|
349
|
+
const interfaceFile = createInterfaceFile(file, target, sourceDir, { constants: [], functions: ALL_NAMES, named: [] }, { tree: true });
|
|
350
|
+
if (!interfaceFile) return null;
|
|
351
|
+
const treeFile = interfaceFile.replace(/\.i$/, '.xml');
|
|
352
|
+
const treeHash = getContentHash([
|
|
353
|
+
TREE_FORMAT, getFileHash(interfaceFile), getFileHash(file), ...(swigView ? [`view:${swigView.hash}`] : []),
|
|
354
|
+
].join('\n'));
|
|
355
|
+
// A parse that failed is kept too, so the plans of one process ask SWIG once for it.
|
|
356
|
+
const key = `${treeFile}\n${treeHash}`;
|
|
357
|
+
if (!swigTrees.has(key)) {
|
|
358
|
+
try {
|
|
359
|
+
if (!fs.existsSync(treeFile) || !fs.existsSync(keyOf(treeFile)) || fs.readFileSync(keyOf(treeFile), 'utf8') !== treeHash) {
|
|
360
|
+
fs.writeFileSync(keyOf(treeFile), '');
|
|
361
|
+
withMacroRetry(interfaceFile, () => runSwigFor([
|
|
362
|
+
'-c++', '-xml', '-cpperraswarn', '-o', treeFile,
|
|
363
|
+
...(swigView ? [`-I${swigView.dir}`] : []),
|
|
364
|
+
...swigIncludePath(target, sourceDir),
|
|
365
|
+
interfaceFile,
|
|
366
|
+
], target));
|
|
367
|
+
fs.writeFileSync(keyOf(treeFile), treeHash);
|
|
368
|
+
}
|
|
369
|
+
swigTrees.set(key, readSwigTree(fs.readFileSync(treeFile, 'utf8')));
|
|
370
|
+
} catch (e) {
|
|
371
|
+
swigTrees.set(key, e);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const tree = swigTrees.get(key);
|
|
375
|
+
if (tree instanceof Error) throw tree;
|
|
376
|
+
return tree;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// A header by what it is under every path the build or a bundler reaches it through.
|
|
380
|
+
const headerKey = (file) => targetNeutral(realPath(file));
|
|
381
|
+
|
|
382
|
+
// Each header the app's sources import, with the names its bindings start from: ALL_NAMES for one bound whole. Every
|
|
383
|
+
// header a build binds asks, so the answer is kept until the app's imports change, or where a header counts as a
|
|
384
|
+
// dependency's does (a dependency built after the process loaded).
|
|
385
|
+
function importedHeaders(target) {
|
|
386
|
+
const projectDir = state.config.paths.project;
|
|
387
|
+
if (!projectDir || !fs.existsSync(projectDir)) return new Map();
|
|
388
|
+
const imports = findHeaderImportsIn(projectDir, state.config.ext.header);
|
|
389
|
+
const dependencyRoots = state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends ?? [];
|
|
390
|
+
const key = JSON.stringify([target.path, imports, state.config.paths.header, dependencyRoots]);
|
|
391
|
+
if (!importedHeaderSets.has(key)) importedHeaderSets.set(key, resolveImportedHeaders(imports, target));
|
|
392
|
+
return new Map(importedHeaderSets.get(key));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function resolveImportedHeaders(imports, target) {
|
|
396
|
+
const headers = new Map();
|
|
397
|
+
for (const { importer, specifier } of imports) {
|
|
398
|
+
const resolved = resolveNativeImport(specifier, importer, target);
|
|
399
|
+
if (!resolved || !fs.existsSync(resolved) || headers.has(headerKey(resolved))) continue;
|
|
400
|
+
const file = upath.resolve(resolved);
|
|
401
|
+
const names = bindingNames(file, target, false);
|
|
402
|
+
headers.set(headerKey(file), { file, start: names.functions === ALL_NAMES ? ALL_NAMES : names.constants });
|
|
403
|
+
}
|
|
404
|
+
return headers;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Which classes each bridge registers, and which other bridges each header needs: the closures of what the app takes
|
|
408
|
+
// from every header it binds (see typeClosure.js). One bridge per header registers a class for every header using it,
|
|
409
|
+
// so its classes are the union of those closures. A header SWIG cannot parse keeps the dependencies found in its text,
|
|
410
|
+
// whose bridges bind every class, as all headers did before.
|
|
411
|
+
function bindingPlan(target, wholeHeaders, header, names) {
|
|
412
|
+
const headers = importedHeaders(target);
|
|
413
|
+
[...configBoundHeaders(target), ...wholeHeaders].forEach((file) => headers.set(headerKey(file), { file: upath.resolve(file), start: ALL_NAMES }));
|
|
414
|
+
// A header no source imports from reaches nothing, so it leaves the plan as it is.
|
|
415
|
+
const start = names.functions === ALL_NAMES ? ALL_NAMES : names.constants;
|
|
416
|
+
if (!headers.has(headerKey(header)) && (start === ALL_NAMES || start.length)) headers.set(headerKey(header), { file: header, start });
|
|
417
|
+
const key = JSON.stringify([target.path, ...[...headers].sort(([a], [b]) => (a < b ? -1 : 1)).map(([file, entry]) => [file, entry.start])]);
|
|
418
|
+
// A dev server binds again after an edit, which can move what a header's names reach.
|
|
419
|
+
const cached = plans.get(key);
|
|
420
|
+
if (cached?.stamps.every(([file, stamp]) => fileStamp(file) === stamp)) return cached.plan;
|
|
421
|
+
const made = makePlan(headers, target);
|
|
422
|
+
plans.set(key, made);
|
|
423
|
+
return made.plan;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const fileStamp = (file) => {
|
|
427
|
+
try {
|
|
428
|
+
const { mtimeMs, size } = fs.statSync(file);
|
|
429
|
+
return `${mtimeMs}:${size}`;
|
|
430
|
+
} catch (e) {
|
|
431
|
+
return 'missing';
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// The dependency headers the config binds whole (export.bindings.headers) that this target's packages ship, which every
|
|
436
|
+
// caller binding a header has to see alike.
|
|
437
|
+
function configBoundHeaders(target) {
|
|
438
|
+
return boundHeaderSpecifiers(state.config).map((specifier) => {
|
|
439
|
+
try {
|
|
440
|
+
return getDependFilePath(specifier, target);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}).filter((file) => file && fs.existsSync(file));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function makePlan(headers, target) {
|
|
448
|
+
const closures = new Map();
|
|
449
|
+
const parsedNot = new Set();
|
|
450
|
+
const read = new Set();
|
|
451
|
+
for (const [key, { file, start }] of headers) {
|
|
452
|
+
try {
|
|
453
|
+
const reached = typeClosure(file, start, {
|
|
454
|
+
treeOf: (header) => {
|
|
455
|
+
read.add(header);
|
|
456
|
+
return swigTreeOf(header, target);
|
|
457
|
+
},
|
|
458
|
+
definingHeader: (name) => definingHeaderOf(file, name, target),
|
|
459
|
+
});
|
|
460
|
+
closures.set(key, [...reached].map(([name, { kind, header, bound }]) => ({ name, kind, header, bound, key: headerKey(header) })));
|
|
461
|
+
} catch (e) {
|
|
462
|
+
console.warn(`crossbind: SWIG cannot parse ${upath.basename(file)} for the classes its bindings need (${e.message}); the headers its types come from bind every class`);
|
|
463
|
+
closures.set(key, null);
|
|
464
|
+
dependencyHeadersOf(file, target).forEach((dependency) => parsedNot.add(headerKey(dependency)));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const plan = {
|
|
468
|
+
classesOf(file) {
|
|
469
|
+
const key = headerKey(file);
|
|
470
|
+
if (headers.get(key)?.start === ALL_NAMES || closures.get(key) === null || parsedNot.has(key)) return ALL_NAMES;
|
|
471
|
+
const classes = new Set();
|
|
472
|
+
for (const closure of closures.values()) {
|
|
473
|
+
closure?.forEach((type) => {
|
|
474
|
+
if (type.kind === 'class' && type.key === key) classes.add(type.name);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return [...classes].sort();
|
|
478
|
+
},
|
|
479
|
+
dependenciesOf(file) {
|
|
480
|
+
const key = headerKey(file);
|
|
481
|
+
const closure = closures.get(key);
|
|
482
|
+
if (closure === null) return dependencyHeadersOf(file, target);
|
|
483
|
+
const dependencies = new Map();
|
|
484
|
+
closure?.forEach((type) => {
|
|
485
|
+
if (type.key !== key && !dependencies.has(type.key)) dependencies.set(type.key, type.header);
|
|
486
|
+
});
|
|
487
|
+
return [...dependencies.values()];
|
|
488
|
+
},
|
|
489
|
+
// The name SWIG binds a class under, which a C struct takes from its typedef.
|
|
490
|
+
boundName(name) {
|
|
491
|
+
for (const closure of closures.values()) {
|
|
492
|
+
const type = closure?.find((reached) => reached.name === name && reached.bound);
|
|
493
|
+
if (type) return type.bound;
|
|
494
|
+
}
|
|
495
|
+
return name.split('::').at(-1);
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
// The headers the plan read, each as it was, so a later edit of one makes the plan again.
|
|
499
|
+
return { plan, stamps: [...read].map((file) => [file, fileStamp(file)]) };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// A class the plan asks for that SWIG left out of the bridge fails each binding that passes or returns it, when called.
|
|
503
|
+
function warnUnboundClasses(bridgeFile, classes, plan) {
|
|
504
|
+
const exportsFile = `${bridgeFile}.exports.json`;
|
|
505
|
+
if (classes === ALL_NAMES || !classes.length || !fs.existsSync(exportsFile)) return;
|
|
506
|
+
const exported = new Set(JSON.parse(fs.readFileSync(exportsFile, 'utf8')));
|
|
507
|
+
const missing = classes.filter((name) => !exported.has(plan.boundName(name)));
|
|
508
|
+
if (missing.length) {
|
|
509
|
+
console.warn(`crossbind: ${upath.basename(bridgeFile)} does not bind ${missing.join(', ')}, which the names the app imports use; a call passing or returning one fails`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
293
513
|
// The proxy module's own exports, never names of the header.
|
|
294
514
|
const PROXY_NAMES = new Set(['initNative', 'AllSymbols']);
|
|
295
515
|
const C_IDENTIFIER = /^[A-Za-z_]\w*$/;
|
|
@@ -342,7 +562,11 @@ function collectSwigMacros(headerFile, includes, name, target, sourceDir, consta
|
|
|
342
562
|
if (!predefinedMacros.has(target.path)) {
|
|
343
563
|
predefinedMacros.set(target.path, dumpMacros(`${interfaceDir}/predefined-${target.path}.macros.h`, [], [], target));
|
|
344
564
|
}
|
|
345
|
-
|
|
565
|
+
// The bridge's interface and SWIG's parse for the bindings plan read the same header with the same includes.
|
|
566
|
+
const includePath = swigIncludePath(target, sourceDir);
|
|
567
|
+
const dumpKey = JSON.stringify([name, includes, includePath, target.path, getFileHash(headerFile)]);
|
|
568
|
+
if (!macroDumps.has(dumpKey)) macroDumps.set(dumpKey, dumpMacros(`${interfaceDir}/${name}.macros.h`, includes, includePath, target, [RELEASE_DEFINE]));
|
|
569
|
+
const macros = macroDumps.get(dumpKey);
|
|
346
570
|
return {
|
|
347
571
|
lines: selectSwigMacros({ headerText: fs.readFileSync(headerFile, 'utf8'), macros, predefined: predefinedMacros.get(target.path), constants }),
|
|
348
572
|
table: macros,
|
|
@@ -447,6 +671,26 @@ function addBridgeExtras(bridgeFilePath, interfaceFilePath, named, headerText, s
|
|
|
447
671
|
fs.writeFileSync(warningsFile, [...readWarnings().filter((line) => !extras.answered.has(line)), ...extras.notes].map((line) => `${line}\n`).join(''));
|
|
448
672
|
}
|
|
449
673
|
|
|
674
|
+
// Known macros can expose declarations SWIG cannot parse and used to skip; without them it parses as before.
|
|
675
|
+
function withMacroRetry(interfaceFilePath, swig) {
|
|
676
|
+
try {
|
|
677
|
+
swig();
|
|
678
|
+
} catch (e) {
|
|
679
|
+
const content = fs.readFileSync(interfaceFilePath, 'utf8');
|
|
680
|
+
const plain = interfaceToRetryWithoutMacros(content, e);
|
|
681
|
+
if (!plain) throw e;
|
|
682
|
+
console.warn(`crossbind: SWIG cannot parse ${upath.basename(interfaceFilePath)} with the macros of its includes; generating it without them`);
|
|
683
|
+
fs.writeFileSync(interfaceFilePath, plain);
|
|
684
|
+
try {
|
|
685
|
+
swig();
|
|
686
|
+
} catch (retryError) {
|
|
687
|
+
// The cached interface keeps its macros, so the next build tries them again.
|
|
688
|
+
fs.writeFileSync(interfaceFilePath, content);
|
|
689
|
+
throw retryError;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
450
694
|
// SWIG looks in its working directory and the interface's before its include path, and neither holds the header, so
|
|
451
695
|
// it reads this copy, put first on that path: the includes its package lists are inlined there, while the compiler
|
|
452
696
|
// still includes the real header.
|
|
@@ -488,23 +732,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
|
|
|
488
732
|
...swigIncludePath(target, sourceDir),
|
|
489
733
|
interfaceFilePath,
|
|
490
734
|
], target);
|
|
491
|
-
|
|
492
|
-
swig();
|
|
493
|
-
} catch (e) {
|
|
494
|
-
// Known macros can expose declarations SWIG cannot parse and used to skip; without them it parses as before.
|
|
495
|
-
const content = fs.readFileSync(interfaceFilePath, 'utf8');
|
|
496
|
-
const plain = interfaceToRetryWithoutMacros(content, e);
|
|
497
|
-
if (!plain) throw e;
|
|
498
|
-
console.warn(`crossbind: SWIG cannot parse ${upath.basename(interfaceFilePath)} with the macros of its includes; generating it without them`);
|
|
499
|
-
fs.writeFileSync(interfaceFilePath, plain);
|
|
500
|
-
try {
|
|
501
|
-
swig();
|
|
502
|
-
} catch (retryError) {
|
|
503
|
-
// The cached interface keeps its macros, so the next build tries them again.
|
|
504
|
-
fs.writeFileSync(interfaceFilePath, content);
|
|
505
|
-
throw retryError;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
735
|
+
withMacroRetry(interfaceFilePath, swig);
|
|
508
736
|
applyAsyncGuard(bridgeFilePath);
|
|
509
737
|
if (extras.named.length) addBridgeExtras(bridgeFilePath, interfaceFilePath, extras.named, extras.headerText(), swig);
|
|
510
738
|
reportSkippedBindings(bridgeFilePath);
|
package/src/actions/createLib.js
CHANGED
|
@@ -19,6 +19,7 @@ import { targetArchiveFlags } from '../utils/archiveFlags.js';
|
|
|
19
19
|
import { packageSourceInputsOf } from '../utils/packageSources.js';
|
|
20
20
|
import { nativeSourceStampChanged, writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
|
|
21
21
|
import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
|
|
22
|
+
import { ANDROID_STL } from '../utils/androidToolchain.js';
|
|
22
23
|
|
|
23
24
|
const cpuCount = Math.max(1, os.cpus().length - 1);
|
|
24
25
|
const sharedPlatforms = ['android'];
|
|
@@ -72,7 +73,9 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
72
73
|
const appArchives = target.platform === 'android' && target.runtimeEnv === 'app' ? 'archives' : null;
|
|
73
74
|
// An app's own code compiles against <crossbind/app.h>, with the options of an app.
|
|
74
75
|
const appApi = compilesAsApp(target) ? [getFilesFingerprint([appApiHeader()]), ...appCompileOptions(target)] : null;
|
|
75
|
-
|
|
76
|
+
// An Android library built while it linked libc++ in links again against the shared runtime.
|
|
77
|
+
const androidRuntime = target.platform === 'android' ? ANDROID_STL : null;
|
|
78
|
+
const extraKeys = [httpHeader, appArchives, appApi, androidRuntime].filter(Boolean);
|
|
76
79
|
const flagsFingerprint = getContentHash(JSON.stringify(extraKeys.length ? [keyInputs, ...extraKeys] : keyInputs));
|
|
77
80
|
const flagsChanged = !fs.existsSync(flagsFingerprintFile)
|
|
78
81
|
|| fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
|
|
@@ -61,7 +61,6 @@ export default function getCmakeParameters(target, options = {}) {
|
|
|
61
61
|
`-DBUILD_TYPE=${buildType}`,
|
|
62
62
|
`-DBUILD_${otherBuildType}_LIBS=OFF`,
|
|
63
63
|
`-DCOMPILE_OPTIONS=${cmakeCompileOptions.join(';')}`,
|
|
64
|
-
// '-DCMAKE_CXX_COMPILER_LAUNCHER=ccache',
|
|
65
64
|
...(options.buildSource !== false ? ['-DBUILD_SOURCE=TRUE'] : []),
|
|
66
65
|
]);
|
|
67
66
|
|