crossbind 2.0.0-beta.63 → 2.0.0-beta.65
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/AGENTS.md +2 -1
- package/package.json +2 -2
- package/src/actions/androidDevice.js +77 -0
- package/src/actions/androidGradle.js +121 -0
- package/src/actions/buildApp.js +434 -0
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildDependencies.js +4 -3
- package/src/actions/buildExternal.js +3 -3
- package/src/actions/buildJs.js +2 -9
- package/src/actions/buildLib.js +1 -1
- package/src/actions/buildNode.js +19 -16
- package/src/actions/buildWasiCommand.js +10 -5
- package/src/actions/buildWasm.js +24 -3
- package/src/actions/createImportedBridges.js +7 -0
- package/src/actions/createInterface.js +4 -5
- package/src/actions/createLib.js +17 -2
- package/src/actions/getCmakeParameters.js +2 -1
- package/src/actions/getData.js +2 -1
- package/src/actions/getDependLibs.js +8 -2
- package/src/actions/getEnv.js +7 -0
- package/src/actions/getLinkInputs.js +10 -7
- package/src/actions/gradleProject.js +110 -0
- package/src/actions/iosSimulator.js +63 -0
- package/src/actions/licenses.js +160 -14
- package/src/actions/nativeBridges.js +35 -0
- package/src/actions/run.js +3 -3
- package/src/actions/runnerCommands.js +16 -4
- package/src/actions/target.js +2 -1
- package/src/actions/writeRuntimeEntry.js +18 -7
- package/src/actions/xcodeProject.js +152 -0
- package/src/assets/cpp-runtime/node.cpp +18 -1
- package/src/assets/js-runtime/adapters/worker-comlink.js +58 -0
- package/src/assets/js-runtime/core.js +5 -4
- package/src/assets/toolchain-digests.json +25 -19
- package/src/assets/wasi-runtime/http/README.md +17 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
- package/src/assets/wasi-runtime/http/handler.cpp +298 -0
- package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
- package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
- package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
- package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
- package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
- package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
- package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
- package/src/bin.js +102 -209
- package/src/cli.js +14 -0
- package/src/index.js +6 -0
- package/src/integration/getCrossbindScript.js +19 -4
- package/src/program.js +169 -0
- package/src/state/index.js +8 -3
- package/src/state/loadConfig.js +6 -11
- package/src/utils/appRustCrates.js +7 -1
- package/src/utils/bridgeExtras.js +74 -6
- package/src/utils/cargoLock.js +19 -0
- package/src/utils/cppDts.js +2 -0
- package/src/utils/downloadAndExtractFile.js +42 -14
- package/src/utils/embindObjectLifetime.js +35 -0
- package/src/utils/embindRsFingerprint.js +10 -3
- package/src/utils/hermes.js +40 -0
- package/src/utils/packageLicense.js +4 -2
- package/src/utils/plist.js +42 -0
- package/src/utils/pullDockerImage.js +9 -3
- package/src/utils/replaceBasePathForDocker.js +8 -0
- package/src/utils/resetCMakeCacheForSource.js +26 -0
- package/src/utils/resolveAppHost.js +21 -0
- package/src/utils/rustBridgeGen.js +13 -4
- package/src/utils/rustMt.js +5 -0
- package/src/utils/rustSysroot.js +7 -5
- package/src/utils/selectRunner.js +1 -1
- package/src/utils/swigInterface.js +5 -4
- package/src/utils/systemConfig.js +13 -0
- package/src/utils/systemKeys.js +4 -0
- package/src/utils/targets.js +30 -3
- package/src/utils/wasiHttp.js +50 -0
- package/vitest.config.js +5 -0
|
@@ -7,14 +7,18 @@ import logger from '../utils/logger.js';
|
|
|
7
7
|
import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
|
|
8
8
|
import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
9
9
|
import { wasiCxxFlags, WASI_LINK_LIBS } from '../utils/wasiToolchain.js';
|
|
10
|
+
import { stageWasiHttpLink, wasiHttpFiles } from '../utils/wasiHttp.js';
|
|
10
11
|
|
|
11
12
|
// Links a platform:'wasi' target into one command module: main() is the entry, deps are DCE'd, data lands as a real dir for --dir preopens.
|
|
13
|
+
// An http target links into a wasi:http/service component instead, whose entry is crossbind's handler export.
|
|
12
14
|
export default async function buildWasiCommand(target, options = {}) {
|
|
13
15
|
const isProd = target.buildType === 'release';
|
|
14
16
|
const buildType = isProd ? 'Release' : 'Debug';
|
|
17
|
+
const isHttp = target.runtimeEnv === 'http';
|
|
18
|
+
const step = isHttp ? 'wasi http' : 'wasi command';
|
|
15
19
|
|
|
16
20
|
if (state.config.export?.type === 'cargo') {
|
|
17
|
-
logger.info(`[${target.path}]
|
|
21
|
+
logger.info(`[${target.path}] ${step} skipped (cargo packages have no cargo wasi triple)`);
|
|
18
22
|
return false;
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -52,17 +56,17 @@ export default async function buildWasiCommand(target, options = {}) {
|
|
|
52
56
|
return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
|
|
53
57
|
}),
|
|
54
58
|
data: getData('data', target),
|
|
55
|
-
runtime: getFilesFingerprint([stubs]),
|
|
59
|
+
runtime: getFilesFingerprint([stubs, ...(isHttp ? wasiHttpFiles(state.config.paths.cli) : [])]),
|
|
56
60
|
}));
|
|
57
61
|
const linkChanged = !fs.existsSync(linkFingerprintFile)
|
|
58
62
|
|| fs.readFileSync(linkFingerprintFile, { encoding: 'utf8' }) !== linkFingerprint;
|
|
59
63
|
|
|
60
64
|
if (!options.force && !linkChanged && fs.existsSync(`${state.config.paths.build}/${target.wasmName}`)) {
|
|
61
|
-
logger.cachedStep(target,
|
|
65
|
+
logger.cachedStep(target, step);
|
|
62
66
|
return false;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
logger.startStep(target,
|
|
69
|
+
logger.startStep(target, step);
|
|
66
70
|
const t0 = performance.now();
|
|
67
71
|
// stubs.c lives outside the docker mount (paths.cli); stage it next to the artifact.
|
|
68
72
|
const stagedStubs = `${state.config.paths.build}/crossbind-wasi-stubs.c`;
|
|
@@ -71,6 +75,7 @@ export default async function buildWasiCommand(target, options = {}) {
|
|
|
71
75
|
run(null, [
|
|
72
76
|
'wasi-clang++',
|
|
73
77
|
stagedStubs,
|
|
78
|
+
...(isHttp ? stageWasiHttpLink(state.config.paths.cli, `${state.config.paths.build}/crossbind-wasi-http`) : []),
|
|
74
79
|
...wasiCxxFlags(),
|
|
75
80
|
...(isProd ? ['-O3'] : ['-O0']),
|
|
76
81
|
...wasiFlags,
|
|
@@ -79,7 +84,7 @@ export default async function buildWasiCommand(target, options = {}) {
|
|
|
79
84
|
'-o', `${state.config.paths.build}/${target.wasmName}`,
|
|
80
85
|
], null, target);
|
|
81
86
|
const ms = Math.round(performance.now() - t0);
|
|
82
|
-
logger.doneStep(target,
|
|
87
|
+
logger.doneStep(target, step, ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`);
|
|
83
88
|
|
|
84
89
|
// Dep data lands as a real folder next to the artifact for --dir preopens.
|
|
85
90
|
Object.entries(getData('data', target)).forEach(([key, value]) => {
|
package/src/actions/buildWasm.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import run from './run.js';
|
|
5
5
|
import getLinkInputs from './getLinkInputs.js';
|
|
6
6
|
import getData from './getData.js';
|
|
7
|
+
import getEnv from './getEnv.js';
|
|
7
8
|
import buildJs from './buildJs.js';
|
|
8
9
|
import triggerExtensions from './extensions.js';
|
|
9
10
|
import state from '../state/index.js';
|
|
@@ -14,9 +15,10 @@ import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
|
14
15
|
import { guardBigIntArguments, guardEmbindArguments } from '../utils/embindArgumentGuards.js';
|
|
15
16
|
import { separateCallArguments } from '../utils/embindCallArguments.js';
|
|
16
17
|
import { queueJspiCalls } from '../utils/embindJspiQueue.js';
|
|
18
|
+
import { releaseOwnedHandles } from '../utils/embindObjectLifetime.js';
|
|
17
19
|
import { spawnPthreadsFromMainScript } from '../utils/pthreadMainScript.js';
|
|
18
20
|
|
|
19
|
-
const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/pthreadMainScript.js']
|
|
21
|
+
const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/embindObjectLifetime.js', '../utils/pthreadMainScript.js']
|
|
20
22
|
.map((file) => fileURLToPath(new URL(file, import.meta.url)));
|
|
21
23
|
|
|
22
24
|
// embind's bigint converter turns any Number into a BigInt, so 2^53+1 silently becomes 2^53.
|
|
@@ -64,6 +66,17 @@ function queueJspiEmbindCalls(target) {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
// Objects JavaScript owns outside a smart pointer were freed only by .delete() (utils/embindObjectLifetime.js);
|
|
70
|
+
// each runtimeEnv links its own glue, so each one calls this after its link.
|
|
71
|
+
function releaseOwnedObjects(target) {
|
|
72
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
73
|
+
const { text, missed } = releaseOwnedHandles(fs.readFileSync(gluePath, 'utf8'));
|
|
74
|
+
fs.writeFileSync(gluePath, text);
|
|
75
|
+
if (missed) {
|
|
76
|
+
logger.error('embind object lifetime rewrite missed (emscripten glue format changed?): returned values and constructed vectors are freed only by .delete()');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
// A direct-mode mt init in a browser spawns its pthreads from "/undefined" (utils/pthreadMainScript.js).
|
|
68
81
|
function pointPthreadSpawn(target) {
|
|
69
82
|
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
@@ -173,6 +186,8 @@ export default async function buildWasm(target, options = {}) {
|
|
|
173
186
|
return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
|
|
174
187
|
}),
|
|
175
188
|
data: getData('data', target),
|
|
189
|
+
// The JS the link step writes carries the env.
|
|
190
|
+
env: getEnv(target),
|
|
176
191
|
runtime: getFilesFingerprint(runtimeAssets),
|
|
177
192
|
rustSources: getFilesFingerprint(rustSources),
|
|
178
193
|
}));
|
|
@@ -233,6 +248,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
233
248
|
guardArgumentConversions(target);
|
|
234
249
|
separateEmbindCalls(target);
|
|
235
250
|
queueJspiEmbindCalls(target);
|
|
251
|
+
releaseOwnedObjects(target);
|
|
236
252
|
await buildJs(target);
|
|
237
253
|
// fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
|
|
238
254
|
// fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
|
|
@@ -257,12 +273,13 @@ export default async function buildWasm(target, options = {}) {
|
|
|
257
273
|
// it any linked Rust package archive fails with undefined crossbind_* symbols.
|
|
258
274
|
...linkLibs, ...rustSources,
|
|
259
275
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
260
|
-
'-s', 'RESERVED_FUNCTION_POINTERS=200',
|
|
276
|
+
'-s', 'RESERVED_FUNCTION_POINTERS=200',
|
|
261
277
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
262
278
|
// See the GROWABLE_ARRAYBUFFERS note in the browser block (Firefox/WebKit TextDecoder).
|
|
263
279
|
'-s', 'GROWABLE_ARRAYBUFFERS=0',
|
|
280
|
+
'-s', 'FORCE_FILESYSTEM=1',
|
|
264
281
|
'-s', 'ENVIRONMENT=web',
|
|
265
|
-
'-s', 'EXPORTED_RUNTIME_METHODS=["ENV", "getExceptionMessage"]',
|
|
282
|
+
'-s', 'EXPORTED_RUNTIME_METHODS=["FS", "ENV", "getExceptionMessage"]',
|
|
266
283
|
'-fwasm-exceptions',
|
|
267
284
|
'-sEXPORT_EXCEPTION_HANDLING_HELPERS',
|
|
268
285
|
'-o', `${state.config.paths.build}/${target.rawJsName}`,
|
|
@@ -275,6 +292,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
275
292
|
guardArgumentConversions(target);
|
|
276
293
|
separateEmbindCalls(target);
|
|
277
294
|
queueJspiEmbindCalls(target);
|
|
295
|
+
releaseOwnedObjects(target);
|
|
278
296
|
await buildJs(target);
|
|
279
297
|
logger.doneStep(target, 'js');
|
|
280
298
|
}
|
|
@@ -292,6 +310,8 @@ export default async function buildWasm(target, options = {}) {
|
|
|
292
310
|
// '-s', 'FETCH', '-sJSPI', '-pthread', '-sPTHREAD_POOL_SIZE=5',
|
|
293
311
|
'-s', 'FORCE_FILESYSTEM=1',
|
|
294
312
|
...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/node.cpp`,
|
|
313
|
+
// node.cpp answers fsync and fdatasync for host files.
|
|
314
|
+
'-Wl,--wrap=__wasi_fd_sync', '-Wl,--wrap=__syscall_fdatasync',
|
|
295
315
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
296
316
|
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'DISABLE_EXCEPTION_CATCHING=0', '-s', 'FORCE_FILESYSTEM=1',
|
|
297
317
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
@@ -311,6 +331,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
311
331
|
guardArgumentConversions(target);
|
|
312
332
|
separateEmbindCalls(target);
|
|
313
333
|
queueJspiEmbindCalls(target);
|
|
334
|
+
releaseOwnedObjects(target);
|
|
314
335
|
await buildJs(target);
|
|
315
336
|
if (emccFlags.includes('FETCH')) {
|
|
316
337
|
fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
|
|
@@ -34,6 +34,13 @@ function resolveImport(specifier, importer, targets) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Whether a binary binds what the app's own sources import. A Node binary does: its module hooks resolve those
|
|
38
|
+
// imports. A browser binary only with --bind-imports: without a bundler a page cannot resolve a header import
|
|
39
|
+
// unless it maps the specifiers itself, as the playground's sandbox does. Nothing on an edge runtime resolves one.
|
|
40
|
+
export function bindsAppImports(target, { bindImports = false } = {}) {
|
|
41
|
+
return target.runtimeEnv === 'node' || (bindImports && target.runtimeEnv === 'browser');
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
// The native files the app's own sources import, by real path, each under the first target whose packages ship it
|
|
38
45
|
// and with the bare specifiers that name it, which Node cannot resolve on its own.
|
|
39
46
|
export function findAppNativeImports(targets) {
|
|
@@ -21,16 +21,15 @@ import {
|
|
|
21
21
|
} from '../utils/swigInterface.js';
|
|
22
22
|
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
23
23
|
import { withDirLockSync } from '../utils/dirLock.js';
|
|
24
|
-
import { imageRoleFor } from '../utils/pullDockerImage.js';
|
|
25
24
|
import { conanImportOfHeader } from '../utils/conanImport.js';
|
|
26
25
|
import run, { cxxPreprocessorFor } from './run.js';
|
|
27
26
|
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
28
27
|
|
|
29
28
|
// Part of every interface hash, so interfaces cached by an older generator are rebuilt.
|
|
30
|
-
const INTERFACE_FORMAT = 'swig-macros-
|
|
29
|
+
const INTERFACE_FORMAT = 'swig-macros-4';
|
|
31
30
|
// Part of every bridge hash: each bridge carries the SWIG fork's runtime and all bridges of a module must share it, so
|
|
32
31
|
// bridges from a fork before field bindings and constants are rebuilt.
|
|
33
|
-
const BRIDGE_FORMAT = 'swig-extras-
|
|
32
|
+
const BRIDGE_FORMAT = 'swig-extras-2';
|
|
34
33
|
const predefinedMacros = new Map();
|
|
35
34
|
const typeDefinitions = new Map();
|
|
36
35
|
|
|
@@ -188,7 +187,7 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
|
|
|
188
187
|
const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1));
|
|
189
188
|
|
|
190
189
|
const macros = collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants);
|
|
191
|
-
const extras = interfaceExtras(named, macros.table);
|
|
190
|
+
const extras = interfaceExtras(named, macros.table, fs.readFileSync(headerOrModuleFilePath, 'utf8'));
|
|
192
191
|
const content = buildInterfaceContent({
|
|
193
192
|
moduleName: fileName.toUpperCase(),
|
|
194
193
|
headerPath,
|
|
@@ -355,7 +354,7 @@ const RELEASE_DEFINE = '-DNDEBUG';
|
|
|
355
354
|
// One bridge serves every desktop target, so SWIG and the macro table it reads come from the web image's em++,
|
|
356
355
|
// which defines no operating system; an android bridge is read with the NDK it compiles with. The headers
|
|
357
356
|
// still come from the target's own packages.
|
|
358
|
-
const toolTargetFor = (target) => (
|
|
357
|
+
const toolTargetFor = (target) => (target?.platform === 'android' ? target : { ...target, platform: 'wasm' });
|
|
359
358
|
|
|
360
359
|
function dumpMacros(outputFile, includes, includePath, target, defines = []) {
|
|
361
360
|
const tool = toolTargetFor(target);
|
package/src/actions/createLib.js
CHANGED
|
@@ -10,11 +10,13 @@ import triggerExtensions from './extensions.js';
|
|
|
10
10
|
import state from '../state/index.js';
|
|
11
11
|
import logger from '../utils/logger.js';
|
|
12
12
|
import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
|
|
13
|
+
import { stageWasiHttpInclude, wasiHttpHeader } from '../utils/wasiHttp.js';
|
|
13
14
|
import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
|
|
14
15
|
import { withDependencyBridges } from '../utils/dependencyBridges.js';
|
|
15
16
|
import { targetArchiveFlags } from '../utils/archiveFlags.js';
|
|
16
17
|
import { conanInputsOf } from '../utils/conanDependencies.js';
|
|
17
18
|
import { nativeSourceStampChanged, writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
|
|
19
|
+
import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
|
|
18
20
|
|
|
19
21
|
const cpuCount = Math.max(1, os.cpus().length - 1);
|
|
20
22
|
const sharedPlatforms = ['android'];
|
|
@@ -56,7 +58,15 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
56
58
|
const conanInputs = conanInputsOf(state.config);
|
|
57
59
|
const compileInputs = compileOptions.length ? { emccFlags: configEmccFlags, compileOptions } : configEmccFlags;
|
|
58
60
|
const flagsInputs = sourceReplacements.length ? { compileInputs, sourceReplacements } : compileInputs;
|
|
59
|
-
const
|
|
61
|
+
const keyInputs = conanInputs ? [flagsInputs, conanInputs] : flagsInputs;
|
|
62
|
+
// An app's wasi source compiles against <crossbind/http.h>, so a new one misses this cache too.
|
|
63
|
+
const httpHeader = target.platform === 'wasi' && !state.config.build.withBuildConfig
|
|
64
|
+
? getFilesFingerprint([wasiHttpHeader(state.config.paths.cli)])
|
|
65
|
+
: null;
|
|
66
|
+
// An Android app links the project's archives from the build where React Native's shared library comes from.
|
|
67
|
+
const appArchives = target.platform === 'android' && target.runtimeEnv === 'app' ? 'archives' : null;
|
|
68
|
+
const extraKeys = [httpHeader, appArchives].filter(Boolean);
|
|
69
|
+
const flagsFingerprint = getContentHash(JSON.stringify(extraKeys.length ? [keyInputs, ...extraKeys] : keyInputs));
|
|
60
70
|
const flagsChanged = !fs.existsSync(flagsFingerprintFile)
|
|
61
71
|
|| fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
|
|
62
72
|
|
|
@@ -142,7 +152,11 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
142
152
|
buildEnv.params.push('-e', e);
|
|
143
153
|
});
|
|
144
154
|
} else {
|
|
145
|
-
|
|
155
|
+
// The source of an http build includes <crossbind/http.h>.
|
|
156
|
+
const headerDirs = target.platform === 'wasi'
|
|
157
|
+
? [...(options.headerDirs ?? []), stageWasiHttpInclude(state.config.paths.cli, `${buildPath}/crossbind-wasi-http`)]
|
|
158
|
+
: options.headerDirs;
|
|
159
|
+
buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
|
|
146
160
|
|
|
147
161
|
triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
|
|
148
162
|
|
|
@@ -229,6 +243,7 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
229
243
|
stageWasiStubs();
|
|
230
244
|
buildParams.push(`-DCMAKE_EXE_LINKER_FLAGS=${[...wasiCxxFlags(), wasiStubObj, ...WASI_LINK_LIBS].join(' ')}`);
|
|
231
245
|
}
|
|
246
|
+
resetCMakeCacheForSource(buildPath, cmakeDir, target);
|
|
232
247
|
run(null, [
|
|
233
248
|
target.platform === 'ios' && state.config.build?.useIOSCMake ? 'ios-cmake' : 'cmake', cmakeDir,
|
|
234
249
|
`-DCMAKE_BUILD_TYPE=${buildType}`,
|
|
@@ -32,8 +32,9 @@ export default function getCmakeParameters(target, options = {}) {
|
|
|
32
32
|
...(options.headerDirs || []),
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
+
// React Native loads a library on Android as a shared one; an app's host links the project's archives into itself.
|
|
35
36
|
const sharedPlatforms = ['android'];
|
|
36
|
-
const buildType = sharedPlatforms.includes(target.platform) ? 'SHARED' : 'STATIC';
|
|
37
|
+
const buildType = sharedPlatforms.includes(target.platform) && target.runtimeEnv !== 'app' ? 'SHARED' : 'STATIC';
|
|
37
38
|
const otherBuildType = buildType === 'STATIC' ? 'SHARED' : 'STATIC';
|
|
38
39
|
|
|
39
40
|
const cmakeCompileOptions = [...new Set(getData('cmake', target)?.compileOptions || [])];
|
package/src/actions/getData.js
CHANGED
|
@@ -30,7 +30,8 @@ function getRecursiveData(obj, config, dependency, field, target) {
|
|
|
30
30
|
obj[dKey] = [...(obj[dKey] || []), ...value];
|
|
31
31
|
} else if (typeof value === 'object') {
|
|
32
32
|
obj[dKey] = { ...(obj[dKey] || {}), ...value };
|
|
33
|
-
} else {
|
|
33
|
+
} else if (!Object.hasOwn(obj, dKey)) {
|
|
34
|
+
// The app comes before its dependencies and a package before its own, so the nearest value stays.
|
|
34
35
|
obj[dKey] = value;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -11,9 +11,15 @@ export default function getDependLibs(target) {
|
|
|
11
11
|
const ignoreLibNames = getFilteredTargetSpec(d?.targetSpecs, target).map(s => s.ignoreLibName).flat();
|
|
12
12
|
d.export.libName.forEach((fileName) => {
|
|
13
13
|
if (ignoreLibNames?.includes(fileName)) return;
|
|
14
|
-
|
|
14
|
+
// On Android a package ships the shared library React Native packages, which an app carries beside
|
|
15
|
+
// its host.
|
|
16
|
+
const extensions = target.platform === 'android' ? ['a', 'so'] : ['a'];
|
|
17
|
+
const libsIn = (targetPath) => extensions
|
|
18
|
+
.map((extension) => findFiles(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${fileName}.${extension}`, { cwd: d.paths.project }))
|
|
19
|
+
.find((found) => found.length > 0) ?? [];
|
|
20
|
+
let libPaths = libsIn(target.path);
|
|
15
21
|
if (libPaths.length === 0) {
|
|
16
|
-
libPaths =
|
|
22
|
+
libPaths = libsIn(target.releasePath);
|
|
17
23
|
}
|
|
18
24
|
dependLibs.push(...libPaths);
|
|
19
25
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import state from '../state/index.js';
|
|
2
|
+
import getData from './getData.js';
|
|
3
|
+
|
|
4
|
+
export default function getEnv(target) {
|
|
5
|
+
return Object.fromEntries(Object.entries(getData('env', target))
|
|
6
|
+
.map(([key, value]) => [key, typeof value === 'function' ? value(state, target) : value]));
|
|
7
|
+
}
|
|
@@ -3,20 +3,23 @@ import getDependLibs from './getDependLibs.js';
|
|
|
3
3
|
import state from '../state/index.js';
|
|
4
4
|
import buildAppRustCrates from '../utils/appRustCrates.js';
|
|
5
5
|
|
|
6
|
-
// The archives of a final link (wasm, Node-API addon or
|
|
6
|
+
// The archives of a final link (wasm, Node-API addon, native executable or app host) and which of them must
|
|
7
7
|
// survive whole. keepFlag spells "keep this symbol" for the target's linker; a native executable
|
|
8
|
-
// binds nothing, so it links no bridge.
|
|
9
|
-
export default function getLinkInputs(target, { keepFlag, withBridge = true }) {
|
|
8
|
+
// binds nothing, so it links no bridge. An app host's bridges compile against another JSI, in an archive of their own.
|
|
9
|
+
export default function getLinkInputs(target, { keepFlag, withBridge = true, bridge = 'Bridge' }) {
|
|
10
10
|
const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
|
|
11
11
|
const { build, output, cache } = state.config.paths;
|
|
12
12
|
const { name } = state.config.general;
|
|
13
13
|
|
|
14
14
|
// buildLib's cache is keyed on paths.output/prebuilt; after a cache clean the
|
|
15
15
|
// build-dir copy can be gone while the output artifact is still valid — link it then.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`${
|
|
16
|
+
// An Xcode build (iOS) leaves its archive in the install tree alone.
|
|
17
|
+
const builtLib = (prefix) => [
|
|
18
|
+
`${build}/${prefix}-${buildType}/${target.path}/lib${name}.a`,
|
|
19
|
+
`${build}/${prefix}-${buildType}/prebuilt/${target.path}/lib/lib${name}.a`,
|
|
19
20
|
];
|
|
21
|
+
const sourceLibCandidates = [...builtLib('Source'), `${output}/prebuilt/${target.path}/lib/lib${name}.a`];
|
|
22
|
+
const bridgeLibCandidates = builtLib(bridge);
|
|
20
23
|
// App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
|
|
21
24
|
// single fully-loaded Rust archive of the link (see the libstd rule below).
|
|
22
25
|
const appRustLibs = buildAppRustCrates(target, cache, state.config.cargoDependencies ?? {});
|
|
@@ -24,7 +27,7 @@ export default function getLinkInputs(target, { keepFlag, withBridge = true }) {
|
|
|
24
27
|
...getDependLibs(target),
|
|
25
28
|
...appRustLibs,
|
|
26
29
|
sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
|
|
27
|
-
...(withBridge ? [
|
|
30
|
+
...(withBridge ? [bridgeLibCandidates.find((lib) => fs.existsSync(lib)) ?? bridgeLibCandidates[0]] : []),
|
|
28
31
|
];
|
|
29
32
|
|
|
30
33
|
// By default only the Bridge archive is kept whole (see linkLayout.js) so the linker
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import state from '../state/index.js';
|
|
4
|
+
import { ANDROID_API_LEVEL } from '../utils/androidToolchain.js';
|
|
5
|
+
import { HERMES } from '../utils/hermes.js';
|
|
6
|
+
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
7
|
+
import { SIGNING } from './androidGradle.js';
|
|
8
|
+
|
|
9
|
+
// The ABIs an Android app builds for: phones and Apple silicon's emulators, and the emulators of x86_64 machines.
|
|
10
|
+
export const ANDROID_ABIS = ['arm64-v8a', 'x86_64'];
|
|
11
|
+
// The Android release the app is compiled against and declares it was tested on.
|
|
12
|
+
const TARGET_SDK = 36;
|
|
13
|
+
export const ANDROIDX_WEBKIT = '1.17.1';
|
|
14
|
+
|
|
15
|
+
const groovyString = (text) => `'${text.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
16
|
+
|
|
17
|
+
const buildGradle = ({ id, label, version, abis }) => `// crossbind writes this project anew on every build: the module comes prebuilt in src/main/jniLibs/, and the app's
|
|
18
|
+
// files are in src/main/assets/app/.
|
|
19
|
+
plugins {
|
|
20
|
+
id 'com.android.application'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
android {
|
|
24
|
+
namespace 'dev.crossbind.app'
|
|
25
|
+
compileSdk ${TARGET_SDK}
|
|
26
|
+
|
|
27
|
+
defaultConfig {
|
|
28
|
+
applicationId ${groovyString(id)}
|
|
29
|
+
// The module's libraries are built against this API level.
|
|
30
|
+
minSdk ${ANDROID_API_LEVEL}
|
|
31
|
+
targetSdk ${TARGET_SDK}
|
|
32
|
+
versionCode ${version.code}
|
|
33
|
+
versionName ${groovyString(version.name)}
|
|
34
|
+
manifestPlaceholders = [appName: ${groovyString(label)}]
|
|
35
|
+
ndk {
|
|
36
|
+
abiFilters ${abis.map(groovyString).join(', ')}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A release is signed with the upload key crossbind's environment names, else with the debug key.
|
|
41
|
+
signingConfigs {
|
|
42
|
+
release {
|
|
43
|
+
def keystore = System.getenv('${SIGNING.keystore}')
|
|
44
|
+
if (keystore) {
|
|
45
|
+
storeFile file(keystore)
|
|
46
|
+
storePassword System.getenv('${SIGNING.password}')
|
|
47
|
+
keyAlias System.getenv('${SIGNING.alias}')
|
|
48
|
+
keyPassword System.getenv('${SIGNING.keyPassword}') ?: System.getenv('${SIGNING.password}')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
buildTypes {
|
|
54
|
+
release {
|
|
55
|
+
signingConfig System.getenv('${SIGNING.keystore}') ? signingConfigs.release : signingConfigs.debug
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Release builds skip lint's vital checks, whose tools crossbind's android-app image leaves out: the module's code is
|
|
60
|
+
// crossbind's own.
|
|
61
|
+
lint {
|
|
62
|
+
checkReleaseBuilds false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
packaging {
|
|
66
|
+
jniLibs {
|
|
67
|
+
// Hermes brings fbjni, and with it an older libc++_shared.so; the module's is the NDK's it was built with.
|
|
68
|
+
pickFirsts += ['**/libc++_shared.so']
|
|
69
|
+
// The libraries come stripped, and stripping them again would take an NDK of the plugin's own.
|
|
70
|
+
keepDebugSymbols += ['**/*.so']
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The page keeps all its files, an _app directory or a .well-known one among them, which Android leaves out by default.
|
|
75
|
+
androidResources {
|
|
76
|
+
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
compileOptions {
|
|
80
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
81
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
dependencies {
|
|
86
|
+
// Hermes' release build in every variant: it is the one the host links against.
|
|
87
|
+
implementation('com.facebook.hermes:hermes-android:${HERMES.version}') {
|
|
88
|
+
attributes {
|
|
89
|
+
attribute(com.android.build.api.attributes.BuildTypeAttr.ATTRIBUTE, objects.named(com.android.build.api.attributes.BuildTypeAttr, 'release'))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
implementation 'androidx.webkit:webkit:${ANDROIDX_WEBKIT}'
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
// An Android app as a Gradle project, written anew from the parts its ABI targets built: each brings its libraries,
|
|
97
|
+
// the first also the app's files. Gradle adds Hermes and androidx.webkit from Maven, and makes the APK or the bundle.
|
|
98
|
+
export default function writeGradleProject(to, { parts, id, version }) {
|
|
99
|
+
const { name } = state.config.general;
|
|
100
|
+
if (parts.length === 0) throw new Error(`crossbind: no part of the Android app ${name} is built, so there is no Gradle project to write.`);
|
|
101
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
102
|
+
fs.cpSync(path.join(resolveAppHostRoot(), 'android'), to, { recursive: true });
|
|
103
|
+
const main = path.join(to, 'app', 'src', 'main');
|
|
104
|
+
parts.forEach(({ arch, dir }) => fs.cpSync(path.join(dir, 'lib'), path.join(main, 'jniLibs', arch), { recursive: true }));
|
|
105
|
+
fs.cpSync(path.join(parts[0].dir, 'app'), path.join(main, 'assets', 'app'), { recursive: true });
|
|
106
|
+
fs.writeFileSync(path.join(to, 'app', 'build.gradle'), buildGradle({
|
|
107
|
+
id, label: name, version, abis: parts.map(({ arch }) => arch),
|
|
108
|
+
}));
|
|
109
|
+
return to;
|
|
110
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { EXEC_MAX_BUFFER } from './run.js';
|
|
3
|
+
import state from '../state/index.js';
|
|
4
|
+
import logger from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
let hasToldNoWindow = false;
|
|
7
|
+
|
|
8
|
+
function tool(program, args) {
|
|
9
|
+
const result = spawnSync(program, args, { encoding: 'utf8', maxBuffer: EXEC_MAX_BUFFER });
|
|
10
|
+
if (result.status !== 0) {
|
|
11
|
+
throw new Error(`crossbind: ${program} ${args.join(' ')} failed: ${`${result.stdout ?? ''}${result.stderr ?? ''}`.trim() || result.error?.message}`);
|
|
12
|
+
}
|
|
13
|
+
return result.stdout;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Newer iOS first: 27.0 before 18.5.
|
|
17
|
+
function byNewerIos(a, b) {
|
|
18
|
+
const index = a.version.findIndex((part, i) => part !== b.version[i]);
|
|
19
|
+
return index === -1 ? 0 : (b.version[index] ?? 0) - a.version[index];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The Simulator an app opens in: one already booted, or else an iPhone of the newest iOS, booted for it.
|
|
23
|
+
function simulator() {
|
|
24
|
+
const { devices } = JSON.parse(tool('xcrun', ['simctl', 'list', 'devices', 'available', '--json']));
|
|
25
|
+
const all = Object.entries(devices)
|
|
26
|
+
.filter(([runtime]) => runtime.includes('.SimRuntime.iOS-'))
|
|
27
|
+
.flatMap(([runtime, list]) => list.map((device) => ({ ...device, version: runtime.split('.SimRuntime.iOS-')[1].split('-').map(Number) })));
|
|
28
|
+
const booted = all.find((device) => device.state === 'Booted');
|
|
29
|
+
if (booted) return booted.udid;
|
|
30
|
+
const [newest] = all.filter((device) => device.name.startsWith('iPhone')).sort(byNewerIos);
|
|
31
|
+
if (!newest) throw new Error('crossbind: no iPhone Simulator is installed. Add the iOS platform in Xcode, under Settings > Components.');
|
|
32
|
+
tool('xcrun', ['simctl', 'boot', newest.udid]);
|
|
33
|
+
tool('xcrun', ['simctl', 'bootstatus', newest.udid, '-b']);
|
|
34
|
+
return newest.udid;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Builds an iOS app's Xcode project for the Simulator, and returns the app.
|
|
38
|
+
export function buildForSimulator(project, configuration = 'Release') {
|
|
39
|
+
const { name } = state.config.general;
|
|
40
|
+
const products = `${state.config.paths.build}/xcode`;
|
|
41
|
+
tool('xcodebuild', ['-project', `${project}/${name}.xcodeproj`, '-configuration', configuration, '-sdk', 'iphonesimulator',
|
|
42
|
+
`SYMROOT=${products}`, '-quiet', 'build']);
|
|
43
|
+
return `${products}/${configuration}-iphonesimulator/${name}.app`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Opens a Simulator build of an iOS app, with `env` in its environment. The process returned carries the app's output
|
|
47
|
+
// and ends with the app; close() ends the app.
|
|
48
|
+
export default function openInSimulator(app, { env = {}, show = false, stdio = 'pipe' } = {}) {
|
|
49
|
+
const device = simulator();
|
|
50
|
+
// Simulator.app only shows the device, which runs the app without it as well.
|
|
51
|
+
if (show && spawnSync('open', ['-b', 'com.apple.iphonesimulator']).status !== 0 && !hasToldNoWindow) {
|
|
52
|
+
hasToldNoWindow = true;
|
|
53
|
+
logger.info('crossbind: Simulator.app is not installed, so the Simulator shows no window; the app runs in it all the same.');
|
|
54
|
+
}
|
|
55
|
+
tool('xcrun', ['simctl', 'install', device, app]);
|
|
56
|
+
const bundleId = tool('plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', `${app}/Info.plist`]).trim();
|
|
57
|
+
const childEnv = { ...process.env };
|
|
58
|
+
Object.entries(env).forEach(([key, value]) => {
|
|
59
|
+
childEnv[`SIMCTL_CHILD_${key}`] = value;
|
|
60
|
+
});
|
|
61
|
+
const child = spawn('xcrun', ['simctl', 'launch', '--console-pty', '--terminate-running-process', device, bundleId], { env: childEnv, stdio });
|
|
62
|
+
return { child, close: () => spawnSync('xcrun', ['simctl', 'terminate', device, bundleId]) };
|
|
63
|
+
}
|