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
|
@@ -1,29 +1,60 @@
|
|
|
1
1
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import { createRequire } from 'node:module';
|
|
4
3
|
import upath from 'upath';
|
|
5
4
|
import state, { saveCache } from '../state/index.js';
|
|
5
|
+
import refreshConanDependencies from '../state/refreshConanDependencies.js';
|
|
6
|
+
import refreshBuiltDependencies from '../state/refreshBuiltDependencies.js';
|
|
7
|
+
import { conanInputsOf } from '../utils/conanDependencies.js';
|
|
6
8
|
import { getContentHash, getFileHash } from '../utils/hash.js';
|
|
7
9
|
import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
+
import fixBridgeRuntime from '../utils/bridgeRuntimeFixes.js';
|
|
11
|
+
import {
|
|
12
|
+
interfaceExtras, bridgeExtras, hasPointerRuntime, withPointerRuntimeAnchor, withoutBridgeExtras, POINTER_RUNTIME_ANCHOR,
|
|
13
|
+
} from '../utils/bridgeExtras.js';
|
|
14
|
+
import resolveNativeImport from '../integration/resolveNativeImport.js';
|
|
15
|
+
import { writeHeaderDts, writeConanImportDts } from '../utils/cppDts.js';
|
|
10
16
|
import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
|
|
11
17
|
import {
|
|
12
|
-
buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations,
|
|
13
|
-
interfaceToRetryWithoutMacros, parseMacroDump, referencedTypeHeaders,
|
|
18
|
+
buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, findSwigInlineIncludes, findSwigPreamble,
|
|
19
|
+
indexTypeDefinitions, inlineIncludes, interfaceIncludes, interfaceToRetryWithoutMacros, interfaceWithAllFunctions, parseMacroDump, referencedTypeHeaders,
|
|
20
|
+
selectSwigMacros,
|
|
14
21
|
} from '../utils/swigInterface.js';
|
|
15
22
|
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
23
|
+
import { withDirLockSync } from '../utils/dirLock.js';
|
|
24
|
+
import { imageRoleFor } from '../utils/pullDockerImage.js';
|
|
25
|
+
import { conanImportOfHeader } from '../utils/conanImport.js';
|
|
16
26
|
import run, { cxxPreprocessorFor } from './run.js';
|
|
27
|
+
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
17
28
|
|
|
18
29
|
// Part of every interface hash, so interfaces cached by an older generator are rebuilt.
|
|
19
|
-
const INTERFACE_FORMAT = 'swig-macros-
|
|
30
|
+
const INTERFACE_FORMAT = 'swig-macros-3';
|
|
20
31
|
// Part of every bridge hash: each bridge carries the SWIG fork's runtime and all bridges of a module must share it, so
|
|
21
32
|
// bridges from a fork before field bindings and constants are rebuilt.
|
|
22
|
-
const BRIDGE_FORMAT = 'swig-
|
|
33
|
+
const BRIDGE_FORMAT = 'swig-extras-1';
|
|
23
34
|
const predefinedMacros = new Map();
|
|
24
35
|
const typeDefinitions = new Map();
|
|
25
36
|
|
|
26
|
-
|
|
37
|
+
// Each process keeps the cache it loaded, so an interface or bridge another one has rebuilt since must not pass for
|
|
38
|
+
// 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
|
|
39
|
+
// comes from an older crossbind or a cache CI restores, and its cache record holds.
|
|
40
|
+
const keyOf = (file) => `${file}.key`;
|
|
41
|
+
const isMadeFrom = (file, hash) => !fs.existsSync(keyOf(file)) || fs.readFileSync(keyOf(file), 'utf8') === getContentHash(hash);
|
|
42
|
+
|
|
43
|
+
// Metro binds headers in worker processes, so two of them can generate one bridge at once. The headers a header's types
|
|
44
|
+
// come from are bound in its turn.
|
|
45
|
+
export default function createBridgeFile(headerOrModuleFilePath, target = state.targets.find((t) => t.platform === 'wasm'), options = {}) {
|
|
46
|
+
if (options.withDependencies === false) return bindHeader(headerOrModuleFilePath, target, options);
|
|
47
|
+
return withDirLockSync(`${state.config.paths.build}/bridge.lock`, () => bindHeader(headerOrModuleFilePath, target, options), {
|
|
48
|
+
pollMs: 50,
|
|
49
|
+
heldBy: 'another process is binding a header',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, wholeHeaders = [] } = {}) {
|
|
54
|
+
// A header can include a Conan package's headers that a build staged after this process loaded, or belong to a
|
|
55
|
+
// dependency built since.
|
|
56
|
+
refreshConanDependencies(state.config);
|
|
57
|
+
refreshBuiltDependencies(state.config);
|
|
27
58
|
const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
|
|
28
59
|
if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
|
|
29
60
|
fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
|
|
@@ -34,26 +65,42 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
|
|
|
34
65
|
// The header may live outside paths.native (e.g. a package-subpath import like
|
|
35
66
|
// '@scope/pkg/native/x.h'), so its own directory must reach swig's include list.
|
|
36
67
|
const sourceDir = upath.dirname(interfaceFilePath);
|
|
37
|
-
const
|
|
38
|
-
const
|
|
68
|
+
const allNames = wholeHeaders.some((file) => upath.resolve(file) === interfaceFilePath);
|
|
69
|
+
const names = bindingNames(interfaceFilePath, target, allNames);
|
|
70
|
+
if (withDependencies) warnUnimported(interfaceFilePath, names.functions);
|
|
71
|
+
const interfaceFile = createInterfaceFile(interfaceFilePath, target, sourceDir, names);
|
|
72
|
+
const { headerPath } = includeLocation(interfaceFilePath, target);
|
|
73
|
+
const inlined = findSwigInlineIncludes(interfaceFilePath, [state.config, ...state.config.allDependencies], headerPath);
|
|
74
|
+
const swigView = inlined.length ? writeSwigView(interfaceFilePath, headerPath, inlined) : null;
|
|
75
|
+
const sourceHash = [getFileHash(interfaceFilePath), ...conanInputsHash()].join('\n');
|
|
76
|
+
// The names imported by name decide what the bridge adds to SWIG's output, so another one makes another bridge.
|
|
77
|
+
const bridgeSourceHash = names.named.length ? `${sourceHash}\nnamed:${names.named.join(',')}` : sourceHash;
|
|
78
|
+
const extras = { named: names.named, headerText: () => fs.readFileSync(swigView ? upath.join(swigView.dir, headerPath) : interfaceFilePath, 'utf8') };
|
|
79
|
+
const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, sourceDir, bridgeSourceHash, swigView, extras);
|
|
39
80
|
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
40
81
|
if (bridgeFile) {
|
|
41
82
|
// Records which header this bridge came from, so directory-driven consumers
|
|
42
83
|
// (getAllBridges) can prune bridges whose header no longer exists.
|
|
43
84
|
writeIfChanged(`${bridgeFile}.source`, `${interfaceFilePath}\n`);
|
|
44
|
-
|
|
85
|
+
const dtsOptions = {
|
|
45
86
|
headerFile: interfaceFilePath,
|
|
46
87
|
exportsFile: `${bridgeFile}.exports.json`,
|
|
47
88
|
projectPath: state.config.paths.project,
|
|
48
89
|
cacheDir: state.config.paths.cache,
|
|
49
90
|
dtsMode: state.config.dts,
|
|
50
|
-
}
|
|
91
|
+
};
|
|
92
|
+
writeHeaderDts(dtsOptions);
|
|
93
|
+
if (conanImportOfHeader(interfaceFilePath, state.config.paths.cache)) {
|
|
94
|
+
dtsOptions.declarationsFile = createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView);
|
|
95
|
+
}
|
|
96
|
+
writeConanImportDts(dtsOptions);
|
|
51
97
|
}
|
|
52
98
|
if (bridgeFile && withDependencies && !moduleRegex.test(interfaceFilePath)) {
|
|
53
99
|
// A header's own bindings need the types its declarations use registered, which only their headers' bridges do.
|
|
100
|
+
// Such a bridge replaces the header's own, so a header bound whole keeps every constant here too.
|
|
54
101
|
const dependencyBridges = dependencyHeadersOf(interfaceFilePath, target).map((header) => {
|
|
55
102
|
try {
|
|
56
|
-
return createBridgeFile(header, target, { withDependencies: false });
|
|
103
|
+
return createBridgeFile(header, target, { withDependencies: false, wholeHeaders });
|
|
57
104
|
} catch (e) {
|
|
58
105
|
console.warn(`crossbind: ${upath.basename(interfaceFilePath)} uses types from ${upath.basename(header)}, whose bindings failed (${e.message})`);
|
|
59
106
|
return null;
|
|
@@ -64,7 +111,36 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
|
|
|
64
111
|
return bridgeFile;
|
|
65
112
|
}
|
|
66
113
|
|
|
67
|
-
|
|
114
|
+
// The Conan packages a header's includes may reach, for the interface and bridge caches: they are
|
|
115
|
+
// staged after a header may already be bound, and restaged in place for another version.
|
|
116
|
+
function conanInputsHash() {
|
|
117
|
+
const conan = conanInputsOf(state.config);
|
|
118
|
+
return conan ? [`conan:${getContentHash(JSON.stringify(conan))}`] : [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A header binds the constants the app imports from it, and so the free functions of a dependency's header: the project's
|
|
122
|
+
// own headers and .i modules bind every function.
|
|
123
|
+
function bindingNames(headerOrModuleFilePath, target, allNames) {
|
|
124
|
+
if (new RegExp(`.(${state.config.ext.module.join('|')})$`).test(headerOrModuleFilePath)) return { constants: [], functions: ALL_NAMES, named: [] };
|
|
125
|
+
const { names, takesModule, named } = headerUse(headerOrModuleFilePath, target);
|
|
126
|
+
if (allNames) return { constants: ALL_NAMES, functions: ALL_NAMES, named };
|
|
127
|
+
// A package's own .i says what the header binds.
|
|
128
|
+
const shipsInterface = fs.existsSync(headerOrModuleFilePath.replace(/\.[^./]+$/, '.i'));
|
|
129
|
+
const bindsEveryFunction = takesModule || shipsInterface || !includeLocation(headerOrModuleFilePath, target).isDependency;
|
|
130
|
+
return { constants: names, functions: bindsEveryFunction ? ALL_NAMES : names, named };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const warnedUnimported = new Set();
|
|
134
|
+
|
|
135
|
+
// A bundler transforms the header because something imports it, but no source the scan reads names it: a package in
|
|
136
|
+
// node_modules, say.
|
|
137
|
+
function warnUnimported(header, functions) {
|
|
138
|
+
if (functions === ALL_NAMES || functions.length || warnedUnimported.has(header)) return;
|
|
139
|
+
warnedUnimported.add(header);
|
|
140
|
+
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.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { constants, functions, named = [] }) {
|
|
68
144
|
if (!headerOrModuleFilePath) {
|
|
69
145
|
return null;
|
|
70
146
|
}
|
|
@@ -74,64 +150,70 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
|
|
|
74
150
|
const packages = [state.config, ...state.config.allDependencies];
|
|
75
151
|
const prelude = isModule ? [] : findHeaderPrelude(headerOrModuleFilePath, packages, headerPath);
|
|
76
152
|
const ignored = isModule ? [] : findIgnoredDeclarations(headerOrModuleFilePath, packages, headerPath);
|
|
153
|
+
const preamble = isModule ? [] : findSwigPreamble(headerOrModuleFilePath, packages, headerPath);
|
|
77
154
|
const completing = includeRoot ? findCompletingIncludes(headerOrModuleFilePath, includeRoot, headerPath) : [];
|
|
78
155
|
const filePathWithoutExt = headerOrModuleFilePath.match(/^(.*)\..+?$/)?.[1];
|
|
79
156
|
const interfaceFile = !isModule && filePathWithoutExt ? `${filePathWithoutExt}.i` : null;
|
|
80
|
-
const constants = isModule ? [] : importedNames(headerOrModuleFilePath, target);
|
|
81
157
|
// A prelude or ignored-declaration change in the owning package, a completed class moving to another header, an
|
|
82
|
-
// interface the package ships beside the header changing or going away,
|
|
83
|
-
// the interface as well.
|
|
158
|
+
// interface the package ships beside the header changing or going away, another imported name, or Conan packages
|
|
159
|
+
// its includes may reach being staged or moving to another version must regenerate the interface as well.
|
|
84
160
|
const fileHash = [
|
|
85
161
|
INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...prelude,
|
|
86
162
|
...(ignored.length ? [`ignored:${ignored.join(',')}`] : []), ...(completing.length ? [`completing:${completing.join(',')}`] : []),
|
|
163
|
+
...(preamble.length ? [`preamble:${preamble.join('\n')}`] : []),
|
|
87
164
|
...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
|
|
88
165
|
...(constants.length ? [`constants:${constants === ALL_NAMES ? ALL_NAMES : constants.join(',')}`] : []),
|
|
166
|
+
...(functions === ALL_NAMES ? [] : [`functions:${functions.join(',')}`]),
|
|
167
|
+
...(named.length ? [`named:${named.join(',')}`] : []),
|
|
168
|
+
...conanInputsHash(),
|
|
89
169
|
].join('\n');
|
|
90
170
|
const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
|
|
91
171
|
if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
|
|
92
|
-
&& !sharedInterface(headerOrModuleFilePath, cachedInterface)) {
|
|
172
|
+
&& isMadeFrom(cachedInterface, fileHash) && !sharedInterface(headerOrModuleFilePath, cachedInterface)) {
|
|
93
173
|
return cachedInterface;
|
|
94
174
|
}
|
|
95
175
|
|
|
96
176
|
if (isModule) {
|
|
97
177
|
const newPath = `${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`;
|
|
98
|
-
fs.copyFileSync(headerOrModuleFilePath, newPath);
|
|
99
|
-
state.cache.interfaces[headerOrModuleFilePath] = newPath;
|
|
100
|
-
state.cache.hashes[headerOrModuleFilePath] = fileHash;
|
|
101
|
-
saveCache();
|
|
102
|
-
return newPath;
|
|
178
|
+
return recordInterface(headerOrModuleFilePath, newPath, fileHash, () => fs.copyFileSync(headerOrModuleFilePath, newPath));
|
|
103
179
|
}
|
|
104
180
|
|
|
105
181
|
if (!filePathWithoutExt) return null;
|
|
106
182
|
|
|
107
183
|
if (fs.existsSync(interfaceFile)) {
|
|
108
184
|
const newPath = `${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`;
|
|
109
|
-
fs.copyFileSync(interfaceFile, newPath);
|
|
110
|
-
state.cache.interfaces[headerOrModuleFilePath] = newPath;
|
|
111
|
-
state.cache.hashes[headerOrModuleFilePath] = fileHash;
|
|
112
|
-
saveCache();
|
|
113
|
-
return newPath;
|
|
185
|
+
return recordInterface(headerOrModuleFilePath, newPath, fileHash, () => fs.copyFileSync(interfaceFile, newPath));
|
|
114
186
|
}
|
|
115
187
|
|
|
116
188
|
const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1));
|
|
117
189
|
|
|
190
|
+
const macros = collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants);
|
|
191
|
+
const extras = interfaceExtras(named, macros.table);
|
|
118
192
|
const content = buildInterfaceContent({
|
|
119
193
|
moduleName: fileName.toUpperCase(),
|
|
120
194
|
headerPath,
|
|
121
195
|
prelude,
|
|
122
196
|
completing,
|
|
123
|
-
swigMacros:
|
|
197
|
+
swigMacros: macros.lines,
|
|
124
198
|
ignored,
|
|
125
199
|
constants,
|
|
200
|
+
preamble,
|
|
201
|
+
functions: functions === ALL_NAMES ? ALL_NAMES : [...functions, ...extras.functions],
|
|
202
|
+
extras: extras.lines,
|
|
126
203
|
});
|
|
127
204
|
const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i`;
|
|
128
|
-
fs.writeFileSync(outputFilePath, content);
|
|
205
|
+
return recordInterface(headerOrModuleFilePath, outputFilePath, fileHash, () => fs.writeFileSync(outputFilePath, content));
|
|
206
|
+
}
|
|
129
207
|
|
|
130
|
-
|
|
131
|
-
|
|
208
|
+
// The key is blank while the file is written, so a process that dies in between leaves no file passing for another hash.
|
|
209
|
+
function recordInterface(headerFile, interfaceFile, fileHash, write) {
|
|
210
|
+
fs.writeFileSync(keyOf(interfaceFile), '');
|
|
211
|
+
write();
|
|
212
|
+
fs.writeFileSync(keyOf(interfaceFile), getContentHash(fileHash));
|
|
213
|
+
state.cache.interfaces[headerFile] = interfaceFile;
|
|
214
|
+
state.cache.hashes[headerFile] = fileHash;
|
|
132
215
|
saveCache();
|
|
133
|
-
|
|
134
|
-
return outputFilePath;
|
|
216
|
+
return interfaceFile;
|
|
135
217
|
}
|
|
136
218
|
|
|
137
219
|
const interfacePath = (name) => `${state.config.paths.build}/interface/${name}.i`;
|
|
@@ -157,10 +239,11 @@ function includeLocation(headerFile, target) {
|
|
|
157
239
|
const dependencyRoots = (state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends || [])
|
|
158
240
|
.filter((d) => d.startsWith(state.config.paths.base));
|
|
159
241
|
const match = dependencyRoots.map((p) => headerFile.match(new RegExp(`^(${escapeRegExp(p)}/.*?/include)/(.*?)$`, 'i'))).find(Boolean);
|
|
160
|
-
return match ? { includeRoot: match[1], headerPath: match[2] } : { includeRoot: null, headerPath: headerFile.split('/').at(-1) };
|
|
242
|
+
return match ? { includeRoot: match[1], headerPath: match[2], isDependency: true } : { includeRoot: null, headerPath: headerFile.split('/').at(-1) };
|
|
161
243
|
}
|
|
162
244
|
|
|
163
|
-
// The headers under an include root are indexed once per build.
|
|
245
|
+
// The headers under an include root are indexed once per build. A copy of a header is that header, and the copy nearest
|
|
246
|
+
// the root stands for it (libpng installs its headers in include/ and include/libpng16/).
|
|
164
247
|
function definitionsUnder(includeRoot) {
|
|
165
248
|
if (!typeDefinitions.has(includeRoot)) {
|
|
166
249
|
const extensions = new RegExp(`\\.(${state.config.ext.header.join('|')})$`, 'i');
|
|
@@ -169,22 +252,35 @@ function definitionsUnder(includeRoot) {
|
|
|
169
252
|
.map((entry) => upath.relative(includeRoot, upath.join(entry.parentPath, entry.name)))
|
|
170
253
|
.sort()
|
|
171
254
|
.map((path) => ({ path, text: fs.readFileSync(upath.join(includeRoot, path), 'utf8') }));
|
|
172
|
-
|
|
255
|
+
const depth = (path) => path.split('/').length;
|
|
256
|
+
const nearest = new Map();
|
|
257
|
+
files.forEach(({ path, text }) => {
|
|
258
|
+
if (!nearest.has(text) || depth(path) < depth(nearest.get(text))) nearest.set(text, path);
|
|
259
|
+
});
|
|
260
|
+
typeDefinitions.set(includeRoot, {
|
|
261
|
+
definitions: indexTypeDefinitions(files.filter(({ path, text }) => nearest.get(text) === path)),
|
|
262
|
+
copyOf: new Map(files.map(({ path, text }) => [path, nearest.get(text)])),
|
|
263
|
+
});
|
|
173
264
|
}
|
|
174
265
|
return typeDefinitions.get(includeRoot);
|
|
175
266
|
}
|
|
176
267
|
|
|
268
|
+
function lookupContext(includeRoot, headerPath, headerText) {
|
|
269
|
+
const { definitions, copyOf } = definitionsUnder(includeRoot);
|
|
270
|
+
return { headerText, headerPath: copyOf.get(headerPath) ?? headerPath, definitions };
|
|
271
|
+
}
|
|
272
|
+
|
|
177
273
|
function findCompletingIncludes(headerFile, includeRoot, headerPath) {
|
|
178
274
|
const headerText = fs.readFileSync(headerFile, 'utf8');
|
|
179
275
|
if (!headerText.includes('unique_ptr')) return [];
|
|
180
|
-
return completingIncludes(
|
|
276
|
+
return completingIncludes(lookupContext(includeRoot, headerPath, headerText));
|
|
181
277
|
}
|
|
182
278
|
|
|
183
279
|
function dependencyHeadersOf(headerFile, target) {
|
|
184
280
|
const { includeRoot, headerPath } = includeLocation(headerFile, target);
|
|
185
281
|
if (!includeRoot) return [];
|
|
186
282
|
const headerText = fs.readFileSync(headerFile, 'utf8');
|
|
187
|
-
return referencedTypeHeaders(
|
|
283
|
+
return referencedTypeHeaders(lookupContext(includeRoot, headerPath, headerText))
|
|
188
284
|
.map((header) => upath.join(includeRoot, header));
|
|
189
285
|
}
|
|
190
286
|
|
|
@@ -204,29 +300,33 @@ function realPath(file) {
|
|
|
204
300
|
}
|
|
205
301
|
}
|
|
206
302
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
} catch (e) {
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// The names the app's own sources import from this header, or every name for `import * as`.
|
|
219
|
-
function importedNames(headerFile, target) {
|
|
303
|
+
// The names the app's own sources import from this header, or every name for `import * as` and the other whole-module
|
|
304
|
+
// imports. The app also takes the module, and reaches its functions by name, when it imports AllSymbols or when its only
|
|
305
|
+
// imports of the header take initNative alone.
|
|
306
|
+
// `named` keeps the names imported by name even when another import takes the whole module: macros, variadic functions
|
|
307
|
+
// and mutable globals bind only for those.
|
|
308
|
+
function headerUse(headerFile, target) {
|
|
220
309
|
const projectDir = state.config.paths.project;
|
|
221
|
-
if (!projectDir || !fs.existsSync(projectDir)) return [];
|
|
310
|
+
if (!projectDir || !fs.existsSync(projectDir)) return { names: [], takesModule: false, named: [] };
|
|
222
311
|
const header = targetNeutral(realPath(headerFile));
|
|
223
312
|
const names = new Set();
|
|
313
|
+
let isImported = false;
|
|
314
|
+
let takesAllSymbols = false;
|
|
315
|
+
let takesWhole = false;
|
|
224
316
|
for (const { importer, specifier, names: imported } of findHeaderImportsIn(projectDir, state.config.ext.header)) {
|
|
225
|
-
if (targetNeutral(realPath(
|
|
226
|
-
if (imported === ALL_NAMES)
|
|
227
|
-
|
|
317
|
+
if (targetNeutral(realPath(resolveNativeImport(specifier, importer, target))) !== header) continue;
|
|
318
|
+
if (imported === ALL_NAMES) {
|
|
319
|
+
takesWhole = true;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
isImported = true;
|
|
323
|
+
const listed = imported.filter((name) => C_IDENTIFIER.test(name));
|
|
324
|
+
takesAllSymbols ||= listed.includes('AllSymbols');
|
|
325
|
+
listed.filter((name) => !PROXY_NAMES.has(name)).forEach((name) => names.add(name));
|
|
228
326
|
}
|
|
229
|
-
|
|
327
|
+
const named = [...names].sort();
|
|
328
|
+
if (takesWhole) return { names: ALL_NAMES, takesModule: true, named };
|
|
329
|
+
return { names: named, takesModule: takesAllSymbols || (isImported && names.size === 0), named };
|
|
230
330
|
}
|
|
231
331
|
|
|
232
332
|
// A header that does not preprocess on its own, or a host without the image's compiler, keeps the plain interface.
|
|
@@ -237,10 +337,13 @@ function collectSwigMacros(headerFile, includes, name, target, sourceDir, consta
|
|
|
237
337
|
predefinedMacros.set(target.path, dumpMacros(`${interfaceDir}/predefined-${target.path}.macros.h`, [], [], target));
|
|
238
338
|
}
|
|
239
339
|
const macros = dumpMacros(`${interfaceDir}/${name}.macros.h`, includes, swigIncludePath(target, sourceDir), target, [RELEASE_DEFINE]);
|
|
240
|
-
return
|
|
340
|
+
return {
|
|
341
|
+
lines: selectSwigMacros({ headerText: fs.readFileSync(headerFile, 'utf8'), macros, predefined: predefinedMacros.get(target.path), constants }),
|
|
342
|
+
table: macros,
|
|
343
|
+
};
|
|
241
344
|
} catch (e) {
|
|
242
345
|
console.warn(`crossbind: SWIG reads ${upath.basename(headerFile)} without the macros of its includes (${e.message})`);
|
|
243
|
-
return [];
|
|
346
|
+
return { lines: [], table: new Map() };
|
|
244
347
|
}
|
|
245
348
|
}
|
|
246
349
|
|
|
@@ -249,14 +352,20 @@ function collectSwigMacros(headerFile, includes, name, target, sourceDir, consta
|
|
|
249
352
|
// it, so NDEBUG counts as a macro SWIG needs rather than a compiler predefine.
|
|
250
353
|
const RELEASE_DEFINE = '-DNDEBUG';
|
|
251
354
|
|
|
355
|
+
// One bridge serves every desktop target, so SWIG and the macro table it reads come from the web image's em++,
|
|
356
|
+
// which defines no operating system; an android bridge is read with the NDK it compiles with. The headers
|
|
357
|
+
// still come from the target's own packages.
|
|
358
|
+
const toolTargetFor = (target) => (imageRoleFor(target) === 'android' ? target : { ...target, platform: 'wasm' });
|
|
359
|
+
|
|
252
360
|
function dumpMacros(outputFile, includes, includePath, target, defines = []) {
|
|
253
|
-
|
|
361
|
+
const tool = toolTargetFor(target);
|
|
362
|
+
run(cxxPreprocessorFor(tool), [
|
|
254
363
|
'-x', 'c++', '-std=c++17', '-dM', '-E', ...defines,
|
|
255
364
|
...includePath,
|
|
256
365
|
...includes.flatMap((header) => ['-include', header]),
|
|
257
366
|
'-o', outputFile,
|
|
258
367
|
'/dev/null',
|
|
259
|
-
], null,
|
|
368
|
+
], null, tool);
|
|
260
369
|
return parseMacroDump(fs.readFileSync(outputFile, 'utf8'));
|
|
261
370
|
}
|
|
262
371
|
|
|
@@ -267,6 +376,9 @@ function swigIncludePath(target, sourceDir) {
|
|
|
267
376
|
...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/swig`),
|
|
268
377
|
...state.config.paths.header,
|
|
269
378
|
...allHeaders,
|
|
379
|
+
// A package that ships its sources has no prebuilt include directory: the app reaches its headers through
|
|
380
|
+
// the package's own CMakeLists.
|
|
381
|
+
...state.config.allDependencies.filter(isSourceCmakePackage).flatMap((d) => d.paths.header),
|
|
270
382
|
...(sourceDir ? [sourceDir] : []),
|
|
271
383
|
].filter((path) => !!path.toString()).map((path) => `-I${path}`);
|
|
272
384
|
return [...new Set(includePath)];
|
|
@@ -278,7 +390,7 @@ function swigIncludePath(target, sourceDir) {
|
|
|
278
390
|
// next time they are picked up.
|
|
279
391
|
function applyAsyncGuard(bridgeFilePath) {
|
|
280
392
|
const bridgeText = fs.readFileSync(bridgeFilePath, { encoding: 'utf8' });
|
|
281
|
-
const guarded = guardAsyncBindings(bridgeText);
|
|
393
|
+
const guarded = guardAsyncBindings(fixBridgeRuntime(bridgeText));
|
|
282
394
|
if (guarded !== bridgeText) {
|
|
283
395
|
fs.writeFileSync(bridgeFilePath, guarded);
|
|
284
396
|
}
|
|
@@ -294,31 +406,78 @@ function reportSkippedBindings(bridgeFilePath) {
|
|
|
294
406
|
});
|
|
295
407
|
}
|
|
296
408
|
|
|
409
|
+
// SWIG skips function-like macros, variadic functions and mutable globals: the ones the app imports bind here, after its
|
|
410
|
+
// output. Their C++ uses SWIG's pointer runtime, which SWIG writes only for a pointer binding, so a bridge without one
|
|
411
|
+
// is generated again with a binding that has one.
|
|
412
|
+
function addBridgeExtras(bridgeFilePath, interfaceFilePath, named, headerText, swig) {
|
|
413
|
+
const warningsFile = `${bridgeFilePath}.warnings`;
|
|
414
|
+
const readWarnings = () => (fs.existsSync(warningsFile) ? fs.readFileSync(warningsFile, 'utf8').split('\n').filter(Boolean) : []);
|
|
415
|
+
const interfaceText = fs.readFileSync(interfaceFilePath, 'utf8');
|
|
416
|
+
const module = interfaceText.match(/^%module (\w+)$/m)?.[1];
|
|
417
|
+
let extras = bridgeExtras({ interfaceText, named, bridgeText: fs.readFileSync(bridgeFilePath, 'utf8'), warnings: readWarnings(), headerText, module });
|
|
418
|
+
if (!extras.code && !extras.notes.length) return;
|
|
419
|
+
if (extras.code && !hasPointerRuntime(fs.readFileSync(bridgeFilePath, 'utf8'))) {
|
|
420
|
+
const anchored = withPointerRuntimeAnchor(interfaceText);
|
|
421
|
+
if (anchored !== interfaceText) {
|
|
422
|
+
fs.writeFileSync(interfaceFilePath, anchored);
|
|
423
|
+
swig();
|
|
424
|
+
applyAsyncGuard(bridgeFilePath);
|
|
425
|
+
}
|
|
426
|
+
if (!hasPointerRuntime(fs.readFileSync(bridgeFilePath, 'utf8'))) {
|
|
427
|
+
extras = {
|
|
428
|
+
code: '', exports: [], answered: new Set(),
|
|
429
|
+
notes: [...extras.notes, `${extras.exports.join(', ')}: these need the pointer runtime, which SWIG left out of this bridge; skipped.`],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (extras.code) fs.appendFileSync(bridgeFilePath, `\n${extras.code}`);
|
|
434
|
+
const exportsFile = `${bridgeFilePath}.exports.json`;
|
|
435
|
+
const exported = JSON.parse(fs.readFileSync(exportsFile, 'utf8')).filter((name) => name !== POINTER_RUNTIME_ANCHOR);
|
|
436
|
+
fs.writeFileSync(exportsFile, `${JSON.stringify([...new Set([...exported, ...extras.exports])])}\n`);
|
|
437
|
+
fs.writeFileSync(warningsFile, [...readWarnings().filter((line) => !extras.answered.has(line)), ...extras.notes].map((line) => `${line}\n`).join(''));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// SWIG looks in its working directory and the interface's before its include path, and neither holds the header, so
|
|
441
|
+
// it reads this copy, put first on that path: the includes its package lists are inlined there, while the compiler
|
|
442
|
+
// still includes the real header.
|
|
443
|
+
function writeSwigView(headerFile, headerPath, names) {
|
|
444
|
+
const dir = `${state.config.paths.build}/swigview`;
|
|
445
|
+
const viewFile = upath.join(dir, headerPath);
|
|
446
|
+
fs.mkdirSync(upath.dirname(viewFile), { recursive: true });
|
|
447
|
+
const readInclude = (name) => fs.readFileSync(upath.join(upath.dirname(headerFile), name), 'utf8');
|
|
448
|
+
writeIfChanged(viewFile, inlineIncludes(fs.readFileSync(headerFile, 'utf8'), names, readInclude));
|
|
449
|
+
return { dir, hash: getFileHash(viewFile) };
|
|
450
|
+
}
|
|
451
|
+
|
|
297
452
|
// The interface text only names the header, so its hash alone kept a bridge across header edits: the
|
|
298
|
-
// header's own hash
|
|
299
|
-
function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null, sourceHash = '') {
|
|
453
|
+
// header's own hash, and the inlined copy's when SWIG reads one, are part of the key.
|
|
454
|
+
function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null, sourceHash = '', swigView = null, extras = { named: [], headerText: () => '' }) {
|
|
300
455
|
if (!interfaceFilePath) {
|
|
301
456
|
return null;
|
|
302
457
|
}
|
|
303
458
|
|
|
304
|
-
const bridgeHash = () =>
|
|
459
|
+
const bridgeHash = () => [
|
|
460
|
+
BRIDGE_FORMAT, getFileHash(interfaceFilePath), sourceHash, ...(swigView ? [`view:${swigView.hash}`] : []),
|
|
461
|
+
].join('\n');
|
|
305
462
|
const fileHash = bridgeHash();
|
|
306
463
|
const cachedBridge = state.cache.bridges[interfaceFilePath];
|
|
307
|
-
if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge)) {
|
|
464
|
+
if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge) && isMadeFrom(cachedBridge, fileHash)) {
|
|
308
465
|
applyAsyncGuard(cachedBridge);
|
|
309
466
|
return cachedBridge;
|
|
310
467
|
}
|
|
311
468
|
|
|
312
469
|
const bridgeFilePath = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
|
|
470
|
+
fs.writeFileSync(keyOf(bridgeFilePath), '');
|
|
313
471
|
// #error lines guard branches SWIG evaluates without the compiler's predefined macros, so they only warn.
|
|
314
472
|
const swig = () => run('swig', [
|
|
315
473
|
'-c++',
|
|
316
474
|
'-embind',
|
|
317
475
|
'-cpperraswarn',
|
|
318
476
|
'-o', bridgeFilePath,
|
|
477
|
+
...(swigView ? [`-I${swigView.dir}`] : []),
|
|
319
478
|
...swigIncludePath(target, sourceDir),
|
|
320
479
|
interfaceFilePath,
|
|
321
|
-
], null, target);
|
|
480
|
+
], null, toolTargetFor(target));
|
|
322
481
|
try {
|
|
323
482
|
swig();
|
|
324
483
|
} catch (e) {
|
|
@@ -337,11 +496,31 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
|
|
|
337
496
|
}
|
|
338
497
|
}
|
|
339
498
|
applyAsyncGuard(bridgeFilePath);
|
|
499
|
+
if (extras.named.length) addBridgeExtras(bridgeFilePath, interfaceFilePath, extras.named, extras.headerText(), swig);
|
|
340
500
|
reportSkippedBindings(bridgeFilePath);
|
|
341
501
|
|
|
502
|
+
const madeFrom = bridgeHash();
|
|
503
|
+
fs.writeFileSync(keyOf(bridgeFilePath), getContentHash(madeFrom));
|
|
342
504
|
state.cache.bridges[interfaceFilePath] = bridgeFilePath;
|
|
343
|
-
state.cache.hashes[interfaceFilePath] =
|
|
505
|
+
state.cache.hashes[interfaceFilePath] = madeFrom;
|
|
344
506
|
saveCache();
|
|
345
507
|
|
|
346
508
|
return state.cache.bridges[interfaceFilePath];
|
|
347
509
|
}
|
|
510
|
+
|
|
511
|
+
// Generate a full export catalog for the editor, without compiling or linking these extra functions.
|
|
512
|
+
function createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView) {
|
|
513
|
+
const dir = `${state.config.paths.build}/declarations`;
|
|
514
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
515
|
+
const file = `${dir}/${upath.basename(interfaceFile)}`;
|
|
516
|
+
writeIfChanged(file, interfaceWithAllFunctions(withoutBridgeExtras(fs.readFileSync(interfaceFile, 'utf8'))));
|
|
517
|
+
const bridge = `${file}.cpp`;
|
|
518
|
+
const hash = getContentHash([BRIDGE_FORMAT, getFileHash(file), sourceHash, swigView?.hash ?? ''].join('\n'));
|
|
519
|
+
if (!fs.existsSync(`${bridge}.exports.json`) || !fs.existsSync(keyOf(bridge)) || fs.readFileSync(keyOf(bridge), 'utf8') !== hash) {
|
|
520
|
+
fs.writeFileSync(keyOf(bridge), '');
|
|
521
|
+
run('swig', ['-c++', '-embind', '-cpperraswarn', '-o', bridge,
|
|
522
|
+
...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], null, toolTargetFor(target));
|
|
523
|
+
fs.writeFileSync(keyOf(bridge), hash);
|
|
524
|
+
}
|
|
525
|
+
return `${bridge}.exports.json`;
|
|
526
|
+
}
|