crossbind 2.0.0-beta.64 → 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/package.json +1 -1
- package/src/actions/androidDevice.js +77 -0
- package/src/actions/androidGradle.js +121 -0
- package/src/actions/buildApp.js +434 -0
- package/src/actions/buildDependencies.js +4 -3
- package/src/actions/buildExternal.js +3 -3
- package/src/actions/buildLib.js +1 -1
- package/src/actions/buildNode.js +15 -10
- package/src/actions/createInterface.js +1 -2
- package/src/actions/createLib.js +4 -1
- package/src/actions/getCmakeParameters.js +2 -1
- package/src/actions/getDependLibs.js +8 -2
- 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/target.js +2 -1
- package/src/actions/xcodeProject.js +152 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +8 -1
- package/src/assets/toolchain-digests.json +24 -18
- package/src/bin.js +75 -32
- package/src/index.js +6 -0
- package/src/integration/getCrossbindScript.js +15 -1
- package/src/state/index.js +5 -1
- package/src/state/loadConfig.js +2 -0
- package/src/utils/appRustCrates.js +3 -1
- package/src/utils/downloadAndExtractFile.js +42 -14
- package/src/utils/embindRsFingerprint.js +10 -3
- package/src/utils/hermes.js +40 -0
- package/src/utils/plist.js +42 -0
- package/src/utils/pullDockerImage.js +9 -3
- package/src/utils/replaceBasePathForDocker.js +8 -0
- package/src/utils/resolveAppHost.js +21 -0
- package/src/utils/selectRunner.js +1 -1
- package/src/utils/systemKeys.js +4 -0
- package/src/utils/targets.js +19 -3
- package/vitest.config.js +5 -0
|
@@ -83,12 +83,13 @@ async function buildMissingCargoDependencies(buildTargets, targetParams) {
|
|
|
83
83
|
const bridgeTarget = buildTargets.some(isWasm) ? state.targets.find(isWasm) : undefined;
|
|
84
84
|
const extraTarget = bridgeTarget && !buildTargets.some((target) => target.path === bridgeTarget.path) ? bridgeTarget : undefined;
|
|
85
85
|
const targets = extraTarget ? [...buildTargets, extraTarget] : buildTargets;
|
|
86
|
-
// A prebuilt from another embind-rs
|
|
87
|
-
|
|
86
|
+
// A prebuilt from another embind-rs, or for an image's target from another rustc, counts as missing: linked, it
|
|
87
|
+
// carries stale glue or clashes with the others.
|
|
88
88
|
const prebuiltOf = (dep, target) => [target.path, target.releasePath]
|
|
89
89
|
.map((targetPath) => `${dep.paths.output}/prebuilt/${targetPath}`)
|
|
90
90
|
.find((dir) => fs.existsSync(dir)) ?? `${dep.paths.output}/prebuilt/${target.path}`;
|
|
91
|
-
const isBuilt = (dep, target) => dep.functions.isEnabled(target)
|
|
91
|
+
const isBuilt = (dep, target) => dep.functions.isEnabled(target)
|
|
92
|
+
&& !isEmbindRsFingerprintStale(prebuiltOf(dep, target), getEmbindRsFingerprint(target));
|
|
92
93
|
const missing = cargoDependencies.filter((dep) => targets.some((target) => !isBuilt(dep, target)));
|
|
93
94
|
|
|
94
95
|
for (const dep of missing) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import replace from 'replace';
|
|
3
|
-
import downloadAndExtractFile from '../utils/downloadAndExtractFile.js';
|
|
3
|
+
import downloadAndExtractFile, { sourcesOf } from '../utils/downloadAndExtractFile.js';
|
|
4
4
|
import copyToDistSource from '../utils/copyToDistSource.js';
|
|
5
5
|
import { getBuildTargets } from './target.js';
|
|
6
6
|
import state from '../state/index.js';
|
|
@@ -14,8 +14,8 @@ export default async function buildExternal(targetParams, options = {}) {
|
|
|
14
14
|
throw new Error(`crossbind: "${state.config.general?.name}" has a crossbind.build recipe but no "nativeVersion" in package.json — cannot build from source.`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const {
|
|
18
|
-
const isNewlyCreated = await downloadAndExtractFile(
|
|
17
|
+
const { sha256, replaceList, copyToSource, copyToDist } = state.config.build;
|
|
18
|
+
const isNewlyCreated = await downloadAndExtractFile(sourcesOf(state.config.build, version), state.config.paths.build, sha256);
|
|
19
19
|
const sourcePath = `${state.config.paths.build}/source`;
|
|
20
20
|
if (isNewlyCreated && replaceList) {
|
|
21
21
|
replaceList.forEach(({ regex, replacement, paths }) => {
|
package/src/actions/buildLib.js
CHANGED
|
@@ -54,7 +54,7 @@ export default function buildLib(targetParams, options = {}) {
|
|
|
54
54
|
fs.copyFileSync(modulePath, `${state.config.paths.output}/prebuilt/${target.path}/swig/${fileName}`);
|
|
55
55
|
});
|
|
56
56
|
writeSourceFingerprint(libdir, sourceFingerprint);
|
|
57
|
-
if (isCargo) writeEmbindRsFingerprint(libdir, getEmbindRsFingerprint());
|
|
57
|
+
if (isCargo) writeEmbindRsFingerprint(libdir, getEmbindRsFingerprint(target));
|
|
58
58
|
isChanged = true;
|
|
59
59
|
} else {
|
|
60
60
|
logger.cachedStep(target, 'lib');
|
package/src/actions/buildNode.js
CHANGED
|
@@ -24,7 +24,7 @@ import toolchainNoticesDir from '../utils/toolchainNotices.js';
|
|
|
24
24
|
const cpuCount = Math.max(1, os.cpus().length - 1);
|
|
25
25
|
const WINDOWS_IMAGE_NOTICES = '/opt/licenses/llvm-mingw';
|
|
26
26
|
|
|
27
|
-
function filesUnder(dirs) {
|
|
27
|
+
export function filesUnder(dirs) {
|
|
28
28
|
return dirs.flatMap((dir) => (fs.existsSync(dir)
|
|
29
29
|
? fs.readdirSync(dir, { recursive: true })
|
|
30
30
|
.map((file) => `${dir}/${file}`)
|
|
@@ -32,18 +32,21 @@ function filesUnder(dirs) {
|
|
|
32
32
|
: []));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// The addon's link, and an app host's, whose bridges come from an archive of their own.
|
|
36
|
+
export function linkInputs(target, { bridge } = {}) {
|
|
37
|
+
const isMachO = ['darwin', 'ios'].includes(target.platform);
|
|
36
38
|
const {
|
|
37
39
|
libs, appRustLibs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
|
|
38
40
|
} = getLinkInputs(target, {
|
|
39
41
|
// The linker loads the archive member that defines a -u symbol; Mach-O prefixes C names with _.
|
|
40
|
-
keepFlag: (name) => `-Wl,-u,${
|
|
42
|
+
keepFlag: (name) => `-Wl,-u,${isMachO ? '_' : ''}crossbind_keep_${name}`,
|
|
43
|
+
bridge,
|
|
41
44
|
});
|
|
42
45
|
// The app's Rust goes first: what a package's kept member needs from std and rustc's allocator shims is then
|
|
43
46
|
// already the app's, and the package's own copies stay in its archive instead of defining them twice.
|
|
44
47
|
const ordered = [...appRustLibs, ...libs.filter((lib) => !appRustLibs.includes(lib))];
|
|
45
48
|
const linkArgs = [
|
|
46
|
-
...buildLinkLibArgs(ordered, { wholeArchiveAll, wholeArchiveNames, forceLoad:
|
|
49
|
+
...buildLinkLibArgs(ordered, { wholeArchiveAll, wholeArchiveNames, forceLoad: isMachO }),
|
|
47
50
|
...rustKeepFlags,
|
|
48
51
|
// The system libraries the archives need, declared by the packages that bring them in.
|
|
49
52
|
...(getData('binary', target)?.addonFlags ?? []),
|
|
@@ -116,6 +119,13 @@ export function bundleNodeLoader(target) {
|
|
|
116
119
|
return bundleLoader(target, resolveEmbindNapiRoot(), nodeLoaderConfig(target));
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
// The mingw-w64 runtime and winpthreads in a Windows addon or app ask for their notices in binary
|
|
123
|
+
// distributions. The windows image carries them; a host toolchain may not.
|
|
124
|
+
export function copyWindowsNotices(target) {
|
|
125
|
+
const notices = toolchainNoticesDir('win32');
|
|
126
|
+
run(null, ['sh', '-c', `if [ -d ${WINDOWS_IMAGE_NOTICES} ]; then mkdir -p ${notices} && cp ${WINDOWS_IMAGE_NOTICES}/* ${notices}/; fi`], null, target);
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
// Links one Node-API addon from the project's archives and bundles the loader that picks the
|
|
120
130
|
// addon matching the running process.
|
|
121
131
|
export default async function buildNode(target, options = {}) {
|
|
@@ -170,12 +180,7 @@ export default async function buildNode(target, options = {}) {
|
|
|
170
180
|
`-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
|
|
171
181
|
], platformPrefix, target);
|
|
172
182
|
run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target, { exclusive: true });
|
|
173
|
-
if (target.platform === 'win32')
|
|
174
|
-
// The mingw-w64 runtime and winpthreads in the addon ask for their notices in binary
|
|
175
|
-
// distributions. The windows image carries them; a host toolchain may not.
|
|
176
|
-
const notices = toolchainNoticesDir('win32');
|
|
177
|
-
run(null, ['sh', '-c', `if [ -d ${WINDOWS_IMAGE_NOTICES} ]; then mkdir -p ${notices} && cp ${WINDOWS_IMAGE_NOTICES}/* ${notices}/; fi`], null, target);
|
|
178
|
-
}
|
|
183
|
+
if (target.platform === 'win32') copyWindowsNotices(target);
|
|
179
184
|
fs.copyFileSync(`${build}/${platformPrefix}/${target.path}/${target.addonName}`, `${build}/${target.addonName}`);
|
|
180
185
|
logger.doneStep(target, 'addon');
|
|
181
186
|
|
|
@@ -21,7 +21,6 @@ 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';
|
|
@@ -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
|
@@ -63,7 +63,10 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
63
63
|
const httpHeader = target.platform === 'wasi' && !state.config.build.withBuildConfig
|
|
64
64
|
? getFilesFingerprint([wasiHttpHeader(state.config.paths.cli)])
|
|
65
65
|
: null;
|
|
66
|
-
|
|
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));
|
|
67
70
|
const flagsChanged = !fs.existsSync(flagsFingerprintFile)
|
|
68
71
|
|| fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
|
|
69
72
|
|
|
@@ -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 || [])];
|
|
@@ -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
|
});
|
|
@@ -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
|
+
}
|
package/src/actions/licenses.js
CHANGED
|
@@ -8,6 +8,11 @@ import familyManifestOf from '../utils/familyManifest.js';
|
|
|
8
8
|
import { wasiToolchainIdentity } from '../utils/provenance.js';
|
|
9
9
|
import { resolveWasiSdkPath } from '../utils/wasiToolchain.js';
|
|
10
10
|
import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from '../utils/resolveEmbindNapi.js';
|
|
11
|
+
import resolveAppHostRoot, { resolveAppEmbindJsiRoot } from '../utils/resolveAppHost.js';
|
|
12
|
+
import {
|
|
13
|
+
HERMES, HERMES_ANDROID, HERMES_SOURCE, hermesUrl,
|
|
14
|
+
} from '../utils/hermes.js';
|
|
15
|
+
import { ANDROIDX_WEBKIT } from './gradleProject.js';
|
|
11
16
|
import { shippedBundledLicense } from '../utils/bundledLicenses.js';
|
|
12
17
|
import toolchainNoticesDir from '../utils/toolchainNotices.js';
|
|
13
18
|
|
|
@@ -103,7 +108,7 @@ function wasiToolchainRows() {
|
|
|
103
108
|
];
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
// The notices a build copied out of its toolchain image (
|
|
111
|
+
// The notices a build copied out of its toolchain image (a Windows addon's or app's does).
|
|
107
112
|
function toolchainNoticeTexts(platform) {
|
|
108
113
|
const build = state.config?.paths?.build;
|
|
109
114
|
const dir = build ? path.join(build, toolchainNoticesDir(platform)) : null;
|
|
@@ -130,33 +135,32 @@ function addonToolchainRows(platform) {
|
|
|
130
135
|
};
|
|
131
136
|
const llvmText = 'license text: https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT';
|
|
132
137
|
if (platform === 'linux' || platform === 'linuxmusl') {
|
|
133
|
-
return [{ ...llvmRuntimes, licenseNotes: `libc++ and libc++abi of the linux toolchain image, statically linked into the
|
|
138
|
+
return [{ ...llvmRuntimes, licenseNotes: `libc++ and libc++abi of the linux toolchain image, statically linked into the binary; ${llvmText}` }];
|
|
134
139
|
}
|
|
135
140
|
const mingwLicense = 'LicenseRef-MinGW-w64-runtime';
|
|
136
141
|
return [
|
|
137
|
-
{ ...llvmRuntimes, licenseNotes: `libc++, libc++abi, libunwind and compiler-rt of the windows toolchain image (llvm-mingw), statically linked into the
|
|
142
|
+
{ ...llvmRuntimes, licenseNotes: `libc++, libc++abi, libunwind and compiler-rt of the windows toolchain image (llvm-mingw), statically linked into the binary; ${llvmText}` },
|
|
138
143
|
{
|
|
139
144
|
...shared,
|
|
140
145
|
name: 'mingw-w64-runtime',
|
|
141
146
|
license: mingwLicense,
|
|
142
147
|
licenseDeclared: mingwLicense,
|
|
143
148
|
sourceUrl: 'https://github.com/mingw-w64/mingw-w64',
|
|
144
|
-
licenseNotes: 'the mingw-w64 startup code and winpthreads, statically linked into the
|
|
149
|
+
licenseNotes: 'the mingw-w64 startup code and winpthreads, statically linked into the binary; a binary distribution includes their notices: https://github.com/mingw-w64/mingw-w64/blob/master/COPYING.MinGW-w64-runtime/COPYING.MinGW-w64-runtime.txt and https://github.com/mingw-w64/mingw-w64/blob/master/mingw-w64-libraries/winpthreads/COPYING (the windows image carries both in /opt/licenses/llvm-mingw)',
|
|
145
150
|
licenseText: toolchainNoticeTexts('win32'),
|
|
146
151
|
},
|
|
147
152
|
];
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
const versionOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
|
|
156
|
+
const textOf = (file) => fs.readFileSync(file, 'utf8').trim();
|
|
157
|
+
|
|
158
|
+
// crossbind's runtime, which an addon and an app host compile in: embind-jsi, which adapts Emscripten's embind.
|
|
159
|
+
function runtimeRows(jsiRoot, notes) {
|
|
155
160
|
const shared = {
|
|
156
161
|
nativeVersion: null, sha256: null, licenseSelected: null, isCopyleft: false,
|
|
157
162
|
};
|
|
158
|
-
|
|
159
|
-
const rows = [
|
|
163
|
+
return [
|
|
160
164
|
{
|
|
161
165
|
...shared,
|
|
162
166
|
name: 'crossbind',
|
|
@@ -165,7 +169,7 @@ function nodeRuntimeRows({ isAddon }) {
|
|
|
165
169
|
license: 'MIT',
|
|
166
170
|
licenseDeclared: 'MIT',
|
|
167
171
|
sourceUrl: 'https://github.com/crossbind/crossbind',
|
|
168
|
-
licenseNotes:
|
|
172
|
+
licenseNotes: notes,
|
|
169
173
|
licenseText: textOf(path.join(CROSSBIND_ROOT, 'LICENSE')),
|
|
170
174
|
},
|
|
171
175
|
{
|
|
@@ -180,10 +184,19 @@ function nodeRuntimeRows({ isAddon }) {
|
|
|
180
184
|
licenseText: textOf(path.join(jsiRoot, 'LICENSE')),
|
|
181
185
|
},
|
|
182
186
|
];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// A Node-API addon compiles in crossbind's runtime and node-api-jsi. The loader bundles the JavaScript half of the
|
|
190
|
+
// same runtime.
|
|
191
|
+
function nodeRuntimeRows({ isAddon }) {
|
|
192
|
+
const rows = runtimeRows(resolveEmbindJsiRoot(), 'the embind-jsi and embind-napi runtime and the bindings crossbind generates');
|
|
183
193
|
if (!isAddon) return rows;
|
|
184
194
|
const napiRoot = resolveEmbindNapiRoot();
|
|
185
195
|
return [...rows, {
|
|
186
|
-
|
|
196
|
+
nativeVersion: null,
|
|
197
|
+
sha256: null,
|
|
198
|
+
licenseSelected: null,
|
|
199
|
+
isCopyleft: false,
|
|
187
200
|
name: 'node-api-jsi',
|
|
188
201
|
npmName: '@crossbind/core-embind-napi',
|
|
189
202
|
version: versionOf(napiRoot),
|
|
@@ -195,6 +208,137 @@ function nodeRuntimeRows({ isAddon }) {
|
|
|
195
208
|
}];
|
|
196
209
|
}
|
|
197
210
|
|
|
211
|
+
// What an app's host carries beside crossbind's runtime, pinned as the third_party READMEs of @crossbind/core-app-host
|
|
212
|
+
// record: React Native's Hermes, JSI's jsi.cpp from the same release, webview/webview, and on Windows the WebView2 SDK's
|
|
213
|
+
// headers.
|
|
214
|
+
const WEBVIEW = { version: 'cbbdee44afff22867de9fd88a9fc8350d9bdd399', sha256: '10e972a2327b5681474f4aa4499e505eaa4ff659aa995380386f08fd6fc1b763' };
|
|
215
|
+
const WEBVIEW2_SDK = { version: '1.0.4258.31', sha256: '56f7f4b8bf9aee4b8efefbbdd4f67d5f74ebd1b100ed0806da71bf76af481aa9' };
|
|
216
|
+
const JSI_SHA256 = 'cb6274a22abc93ecc970066539815e716d3e47716e8f93a9971cf4eb5918ffc1';
|
|
217
|
+
// The fbjni the Hermes release for Android depends on.
|
|
218
|
+
const FBJNI_VERSION = '0.7.0';
|
|
219
|
+
|
|
220
|
+
const HERMES_BUILDS = {
|
|
221
|
+
linux: {
|
|
222
|
+
sha256: HERMES_SOURCE.sha256,
|
|
223
|
+
sourceUrl: HERMES_SOURCE.url,
|
|
224
|
+
licenseNotes: "Hermes built from its release's source in the linux toolchain image, statically linked into the host",
|
|
225
|
+
},
|
|
226
|
+
win32: {
|
|
227
|
+
sha256: HERMES_SOURCE.sha256,
|
|
228
|
+
sourceUrl: HERMES_SOURCE.url,
|
|
229
|
+
licenseNotes: "Hermes built from its release's source in the windows toolchain image (llvm-mingw), statically linked into the host",
|
|
230
|
+
},
|
|
231
|
+
android: {
|
|
232
|
+
sha256: HERMES_ANDROID.sha256,
|
|
233
|
+
sourceUrl: HERMES_ANDROID.url,
|
|
234
|
+
licenseNotes: "React Native's Hermes for Android, whose libraries the app's Gradle build takes from Maven Central",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// What an Android app's Gradle build adds from Maven, and the C++ runtime the app carries beside its host.
|
|
239
|
+
function androidAppRows() {
|
|
240
|
+
const shared = {
|
|
241
|
+
npmName: null, sha256: null, licenseSelected: null, licenseText: null, isCopyleft: false,
|
|
242
|
+
};
|
|
243
|
+
const apache = { ...shared, license: 'Apache-2.0', licenseDeclared: 'Apache-2.0' };
|
|
244
|
+
const llvmLicense = 'Apache-2.0 WITH LLVM-exception';
|
|
245
|
+
return [
|
|
246
|
+
{
|
|
247
|
+
...apache,
|
|
248
|
+
name: 'fbjni',
|
|
249
|
+
version: FBJNI_VERSION,
|
|
250
|
+
nativeVersion: FBJNI_VERSION,
|
|
251
|
+
sourceUrl: 'https://github.com/facebookincubator/fbjni',
|
|
252
|
+
licenseNotes: 'libfbjni.so, which Hermes loads and Gradle takes with it; license text: https://github.com/facebookincubator/fbjni/blob/main/LICENSE',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
...apache,
|
|
256
|
+
name: 'androidx-webkit',
|
|
257
|
+
version: ANDROIDX_WEBKIT,
|
|
258
|
+
nativeVersion: ANDROIDX_WEBKIT,
|
|
259
|
+
sourceUrl: 'https://developer.android.com/jetpack/androidx/releases/webkit',
|
|
260
|
+
licenseNotes: "androidx.webkit, the WebView's message ports and start scripts, from Google's Maven repository; license text: https://www.apache.org/licenses/LICENSE-2.0.txt",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
...shared,
|
|
264
|
+
name: 'llvm-runtimes',
|
|
265
|
+
version: null,
|
|
266
|
+
nativeVersion: null,
|
|
267
|
+
license: llvmLicense,
|
|
268
|
+
licenseDeclared: llvmLicense,
|
|
269
|
+
sourceUrl: 'https://github.com/llvm/llvm-project',
|
|
270
|
+
licenseNotes: "libc++ of the android image's NDK, which the app carries as libc++_shared.so; license text: https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT",
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// The WebView2 SDK headers a Windows host compiles against. webview/webview loads the WebView2 Runtime itself, without
|
|
276
|
+
// the SDK's loader.
|
|
277
|
+
function windowsAppRows(hostRoot) {
|
|
278
|
+
const license = 'BSD-3-Clause';
|
|
279
|
+
return [{
|
|
280
|
+
npmName: '@crossbind/core-app-host',
|
|
281
|
+
name: 'webview2-sdk',
|
|
282
|
+
version: WEBVIEW2_SDK.version,
|
|
283
|
+
nativeVersion: WEBVIEW2_SDK.version,
|
|
284
|
+
sha256: WEBVIEW2_SDK.sha256,
|
|
285
|
+
license,
|
|
286
|
+
licenseDeclared: license,
|
|
287
|
+
licenseSelected: null,
|
|
288
|
+
isCopyleft: false,
|
|
289
|
+
sourceUrl: `https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/${WEBVIEW2_SDK.version}/microsoft.web.webview2.${WEBVIEW2_SDK.version}.nupkg`,
|
|
290
|
+
licenseNotes: "the WebView2 SDK's headers from the windows toolchain image, whose interface IDs the host compiles in",
|
|
291
|
+
licenseText: textOf(path.join(hostRoot, 'third_party', 'webview2', 'LICENSE')),
|
|
292
|
+
}];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function appRuntimeRows(platform) {
|
|
296
|
+
const hostRoot = resolveAppHostRoot();
|
|
297
|
+
const hermesLicense = textOf(path.join(hostRoot, 'third_party', 'jsi', 'LICENSE'));
|
|
298
|
+
const mit = {
|
|
299
|
+
npmName: '@crossbind/core-app-host', license: 'MIT', licenseDeclared: 'MIT', licenseSelected: null, isCopyleft: false,
|
|
300
|
+
};
|
|
301
|
+
const hermes = HERMES_BUILDS[platform] ?? {
|
|
302
|
+
sha256: HERMES.sha256,
|
|
303
|
+
sourceUrl: hermesUrl(HERMES.version),
|
|
304
|
+
licenseNotes: "React Native's Hermes for Apple platforms, which the app carries as hermesvm.framework",
|
|
305
|
+
};
|
|
306
|
+
return [
|
|
307
|
+
...runtimeRows(resolveAppEmbindJsiRoot(), 'the embind-jsi runtime, the app host and the bindings crossbind generates'),
|
|
308
|
+
{
|
|
309
|
+
...mit,
|
|
310
|
+
...hermes,
|
|
311
|
+
name: 'hermes',
|
|
312
|
+
version: HERMES.version,
|
|
313
|
+
nativeVersion: HERMES.version,
|
|
314
|
+
licenseText: hermesLicense,
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
...mit,
|
|
318
|
+
name: 'jsi',
|
|
319
|
+
version: HERMES.version,
|
|
320
|
+
nativeVersion: HERMES.version,
|
|
321
|
+
sha256: JSI_SHA256,
|
|
322
|
+
sourceUrl: 'https://github.com/facebook/hermes',
|
|
323
|
+
licenseNotes: "JSI's jsi.cpp from the same Hermes release, compiled into the host",
|
|
324
|
+
licenseText: hermesLicense,
|
|
325
|
+
},
|
|
326
|
+
...(platform === 'android' ? androidAppRows() : []),
|
|
327
|
+
...(platform === 'win32' ? windowsAppRows(hostRoot) : []),
|
|
328
|
+
// A mobile app makes its window with UIKit or an Android activity, without webview/webview.
|
|
329
|
+
...(['ios', 'android'].includes(platform) ? [] : [{
|
|
330
|
+
...mit,
|
|
331
|
+
name: 'webview',
|
|
332
|
+
version: WEBVIEW.version,
|
|
333
|
+
nativeVersion: WEBVIEW.version,
|
|
334
|
+
sha256: WEBVIEW.sha256,
|
|
335
|
+
sourceUrl: 'https://github.com/webview/webview',
|
|
336
|
+
licenseNotes: 'webview/webview, with the local patch @crossbind/core-app-host lists',
|
|
337
|
+
licenseText: textOf(path.join(hostRoot, 'third_party', 'webview', 'LICENSE')),
|
|
338
|
+
}]),
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
|
|
198
342
|
async function buildRow(node) {
|
|
199
343
|
const recipe = await loadJs(node.paths.project, 'crossbind.build');
|
|
200
344
|
const manifest = node.package || null;
|
|
@@ -284,7 +428,8 @@ function assertConanStaged() {
|
|
|
284
428
|
// platform (optional, e.g. 'wasi') additionally includes what that platform's
|
|
285
429
|
// artifact statically links beyond the package graph: recipe-declared vendored
|
|
286
430
|
// copies and the toolchain runtime. runtimeEnv 'node' adds the crossbind runtime an
|
|
287
|
-
// addon of that platform compiles in, or without a platform the one its loader bundles
|
|
431
|
+
// addon of that platform compiles in, or without a platform the one its loader bundles;
|
|
432
|
+
// runtimeEnv 'app' adds what an app's host carries.
|
|
288
433
|
export default async function collectLicenseRows(platform = null, { runtimeEnv = null } = {}) {
|
|
289
434
|
assertConanStaged();
|
|
290
435
|
// The root package is a component too when it is a port: leaf -wasi packages have no deps but ship
|
|
@@ -310,5 +455,6 @@ export default async function collectLicenseRows(platform = null, { runtimeEnv =
|
|
|
310
455
|
if (platform === 'wasi') rows.push(...wasiToolchainRows());
|
|
311
456
|
if (['linux', 'linuxmusl', 'win32'].includes(platform)) rows.push(...addonToolchainRows(platform));
|
|
312
457
|
if (runtimeEnv === 'node') rows.push(...nodeRuntimeRows({ isAddon: platform !== null }));
|
|
458
|
+
if (runtimeEnv === 'app') rows.push(...appRuntimeRows(platform));
|
|
313
459
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
314
460
|
}
|