crossbind 2.0.0-beta.64 → 2.0.0-beta.66

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.
Files changed (49) hide show
  1. package/package.json +6 -1
  2. package/src/actions/androidDevice.js +77 -0
  3. package/src/actions/androidGradle.js +121 -0
  4. package/src/actions/appInstaller.js +174 -0
  5. package/src/actions/appUpdate.js +68 -0
  6. package/src/actions/appleSigning.js +96 -0
  7. package/src/actions/buildApp.js +509 -0
  8. package/src/actions/buildDependencies.js +4 -3
  9. package/src/actions/buildExternal.js +3 -3
  10. package/src/actions/buildLib.js +1 -1
  11. package/src/actions/buildNode.js +15 -10
  12. package/src/actions/createInterface.js +1 -2
  13. package/src/actions/createLib.js +13 -5
  14. package/src/actions/getCmakeParameters.js +6 -2
  15. package/src/actions/getDependLibs.js +8 -2
  16. package/src/actions/getLinkInputs.js +10 -7
  17. package/src/actions/gradleProject.js +171 -0
  18. package/src/actions/iosSimulator.js +63 -0
  19. package/src/actions/licenses.js +189 -14
  20. package/src/actions/nativeBridges.js +35 -0
  21. package/src/actions/run.js +3 -3
  22. package/src/actions/target.js +2 -1
  23. package/src/actions/windowsSigning.js +96 -0
  24. package/src/actions/xcodeProject.js +186 -0
  25. package/src/app/index.d.ts +129 -0
  26. package/src/app/index.js +184 -0
  27. package/src/assets/js-runtime/adapters/worker-comlink.js +8 -1
  28. package/src/assets/toolchain-digests.json +24 -18
  29. package/src/bin.js +84 -32
  30. package/src/index.js +7 -0
  31. package/src/integration/getCrossbindScript.js +15 -1
  32. package/src/state/index.js +5 -1
  33. package/src/state/loadConfig.js +2 -0
  34. package/src/utils/appApiHeader.js +29 -0
  35. package/src/utils/appIcon.js +174 -0
  36. package/src/utils/appOptions.js +284 -0
  37. package/src/utils/appRustCrates.js +3 -1
  38. package/src/utils/downloadAndExtractFile.js +42 -14
  39. package/src/utils/embindRsFingerprint.js +10 -3
  40. package/src/utils/hermes.js +40 -0
  41. package/src/utils/plist.js +42 -0
  42. package/src/utils/pullDockerImage.js +9 -3
  43. package/src/utils/replaceBasePathForDocker.js +8 -0
  44. package/src/utils/resolveAppHost.js +21 -0
  45. package/src/utils/selectRunner.js +1 -1
  46. package/src/utils/systemKeys.js +4 -0
  47. package/src/utils/targets.js +19 -3
  48. package/src/utils/writeIfChanged.js +1 -1
  49. package/vitest.config.js +19 -0
@@ -11,6 +11,7 @@ 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
13
  import { stageWasiHttpInclude, wasiHttpHeader } from '../utils/wasiHttp.js';
14
+ import { appApiHeader, compilesAsApp, stageAppApiInclude } from '../utils/appApiHeader.js';
14
15
  import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
15
16
  import { withDependencyBridges } from '../utils/dependencyBridges.js';
16
17
  import { targetArchiveFlags } from '../utils/archiveFlags.js';
@@ -63,7 +64,12 @@ export default function createLib(target, fileType, options = {}) {
63
64
  const httpHeader = target.platform === 'wasi' && !state.config.build.withBuildConfig
64
65
  ? getFilesFingerprint([wasiHttpHeader(state.config.paths.cli)])
65
66
  : null;
66
- const flagsFingerprint = getContentHash(JSON.stringify(httpHeader ? [keyInputs, httpHeader] : keyInputs));
67
+ // An Android app links the project's archives from the build where React Native's shared library comes from.
68
+ const appArchives = target.platform === 'android' && target.runtimeEnv === 'app' ? 'archives' : null;
69
+ // An app's own code compiles against <crossbind/app.h>, with CROSSBIND_APP defined.
70
+ const appApi = compilesAsApp(target) ? getFilesFingerprint([appApiHeader()]) : null;
71
+ const extraKeys = [httpHeader, appArchives, appApi].filter(Boolean);
72
+ const flagsFingerprint = getContentHash(JSON.stringify(extraKeys.length ? [keyInputs, ...extraKeys] : keyInputs));
67
73
  const flagsChanged = !fs.existsSync(flagsFingerprintFile)
68
74
  || fs.readFileSync(flagsFingerprintFile, { encoding: 'utf8' }) !== flagsFingerprint;
69
75
 
@@ -149,10 +155,12 @@ export default function createLib(target, fileType, options = {}) {
149
155
  buildEnv.params.push('-e', e);
150
156
  });
151
157
  } else {
152
- // The source of an http build includes <crossbind/http.h>.
153
- const headerDirs = target.platform === 'wasi'
154
- ? [...(options.headerDirs ?? []), stageWasiHttpInclude(state.config.paths.cli, `${buildPath}/crossbind-wasi-http`)]
155
- : options.headerDirs;
158
+ // The source of an http build includes <crossbind/http.h>, and an app's own code <crossbind/app.h>.
159
+ const headerDirs = [
160
+ ...(options.headerDirs ?? []),
161
+ ...(target.platform === 'wasi' ? [stageWasiHttpInclude(state.config.paths.cli, `${buildPath}/crossbind-wasi-http`)] : []),
162
+ ...(compilesAsApp(target) ? [stageAppApiInclude(`${buildPath}/crossbind-app`)] : []),
163
+ ];
156
164
  buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
157
165
 
158
166
  triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import state from '../state/index.js';
3
3
  import getData from './getData.js';
4
+ import { compilesAsApp } from '../utils/appApiHeader.js';
4
5
 
5
6
  export default function getCmakeParameters(target, options = {}) {
6
7
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
@@ -32,11 +33,14 @@ export default function getCmakeParameters(target, options = {}) {
32
33
  ...(options.headerDirs || []),
33
34
  ];
34
35
 
36
+ // React Native loads a library on Android as a shared one; an app's host links the project's archives into itself.
35
37
  const sharedPlatforms = ['android'];
36
- const buildType = sharedPlatforms.includes(target.platform) ? 'SHARED' : 'STATIC';
38
+ const buildType = sharedPlatforms.includes(target.platform) && target.runtimeEnv !== 'app' ? 'SHARED' : 'STATIC';
37
39
  const otherBuildType = buildType === 'STATIC' ? 'SHARED' : 'STATIC';
38
40
 
39
- const cmakeCompileOptions = [...new Set(getData('cmake', target)?.compileOptions || [])];
41
+ // An app's own code can tell it is one, to reach <crossbind/app.h>.
42
+ const appOptions = compilesAsApp(target) ? ['-DCROSSBIND_APP'] : [];
43
+ const cmakeCompileOptions = [...new Set([...(getData('cmake', target)?.compileOptions || []), ...appOptions])];
40
44
 
41
45
  params.push(...[
42
46
  `-DPROJECT_NAME=${options.name || state.config.general.name}`,
@@ -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
- let libPaths = findFiles(`${d.paths.output}/prebuilt/${target.path}/lib/lib${fileName}.a`, { cwd: d.paths.project });
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 = findFiles(`${d.paths.output}/prebuilt/${target.releasePath}/lib/lib${fileName}.a`, { cwd: d.paths.project });
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 native executable) and which of them must
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
- const sourceLibCandidates = [
17
- `${build}/Source-${buildType}/${target.path}/lib${name}.a`,
18
- `${output}/prebuilt/${target.path}/lib/lib${name}.a`,
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 ? [`${build}/Bridge-${buildType}/${target.path}/lib${name}.a`] : []),
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,171 @@
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
+ // The launcher shapes an adaptive icon: the app's icon in the middle of a white square.
18
+ const ADAPTIVE_ICON = `<?xml version="1.0" encoding="utf-8"?>
19
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
20
+ <background android:drawable="@android:color/white" />
21
+ <foreground android:drawable="@mipmap/ic_launcher_foreground" />
22
+ </adaptive-icon>
23
+ `;
24
+
25
+ const DAY_NIGHT = '@android:style/Theme.DeviceDefault.DayNight';
26
+ // The activity's theme shows app.window.backgroundColor until the page paints, and the activity puts it behind the page,
27
+ // whose WebView would be white; transparent, which the activity takes for none, without it.
28
+ const backgroundValues = (color) => `<?xml version="1.0" encoding="utf-8"?>
29
+ <resources>
30
+ <color name="crossbind_background">${color ?? '#00000000'}</color>
31
+ <style name="CrossbindBackground" parent="${DAY_NIGHT}">
32
+ <item name="android:windowBackground">@color/crossbind_background</item>
33
+ </style>
34
+ </resources>
35
+ `;
36
+
37
+ // The Android permissions behind each of app.permissions, after the one every app asks for; the activity asks its user
38
+ // for them when the page first uses the device.
39
+ const INTERNET = ' <uses-permission android:name="android.permission.INTERNET" />';
40
+ const ANDROID_PERMISSIONS = {
41
+ camera: ['CAMERA'],
42
+ microphone: ['RECORD_AUDIO', 'MODIFY_AUDIO_SETTINGS'],
43
+ location: ['ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION'],
44
+ };
45
+ const usesPermissions = (permissions) => permissions
46
+ .flatMap((name) => ANDROID_PERMISSIONS[name])
47
+ .map((permission) => ` <uses-permission android:name="android.permission.${permission}" />`);
48
+
49
+ // The links of app.urlSchemes the activity opens, from a browser as well, before the end of the activity's element.
50
+ const ACTIVITY_END = ' </activity>';
51
+ const urlFilter = (schemes) => [
52
+ ' <intent-filter>',
53
+ ' <action android:name="android.intent.action.VIEW" />',
54
+ ' <category android:name="android.intent.category.DEFAULT" />',
55
+ ' <category android:name="android.intent.category.BROWSABLE" />',
56
+ ...schemes.map((scheme) => ` <data android:scheme="${scheme}" />`),
57
+ ' </intent-filter>',
58
+ ].join('\n');
59
+
60
+ const buildGradle = ({ id, label, version, abis, icon, launchMode, theme }) => `// crossbind writes this project anew on every build: the module comes prebuilt in src/main/jniLibs/, and the app's
61
+ // files are in src/main/assets/app/.
62
+ plugins {
63
+ id 'com.android.application'
64
+ }
65
+
66
+ android {
67
+ namespace 'dev.crossbind.app'
68
+ compileSdk ${TARGET_SDK}
69
+
70
+ defaultConfig {
71
+ applicationId ${groovyString(id)}
72
+ // The module's libraries are built against this API level.
73
+ minSdk ${ANDROID_API_LEVEL}
74
+ targetSdk ${TARGET_SDK}
75
+ versionCode ${version.code}
76
+ versionName ${groovyString(version.name)}
77
+ manifestPlaceholders = [appName: ${groovyString(label)}, appIcon: ${groovyString(icon)}, launchMode: ${groovyString(launchMode)}, appTheme: ${groovyString(theme)}]
78
+ ndk {
79
+ abiFilters ${abis.map(groovyString).join(', ')}
80
+ }
81
+ }
82
+
83
+ // A release is signed with the upload key crossbind's environment names, else with the debug key.
84
+ signingConfigs {
85
+ release {
86
+ def keystore = System.getenv('${SIGNING.keystore}')
87
+ if (keystore) {
88
+ storeFile file(keystore)
89
+ storePassword System.getenv('${SIGNING.password}')
90
+ keyAlias System.getenv('${SIGNING.alias}')
91
+ keyPassword System.getenv('${SIGNING.keyPassword}') ?: System.getenv('${SIGNING.password}')
92
+ }
93
+ }
94
+ }
95
+
96
+ buildTypes {
97
+ release {
98
+ signingConfig System.getenv('${SIGNING.keystore}') ? signingConfigs.release : signingConfigs.debug
99
+ }
100
+ }
101
+
102
+ // Release builds skip lint's vital checks, whose tools crossbind's android-app image leaves out: the module's code is
103
+ // crossbind's own.
104
+ lint {
105
+ checkReleaseBuilds false
106
+ }
107
+
108
+ packaging {
109
+ jniLibs {
110
+ // Hermes brings fbjni, and with it an older libc++_shared.so; the module's is the NDK's it was built with.
111
+ pickFirsts += ['**/libc++_shared.so']
112
+ // The libraries come stripped, and stripping them again would take an NDK of the plugin's own.
113
+ keepDebugSymbols += ['**/*.so']
114
+ }
115
+ }
116
+
117
+ // The page keeps all its files, an _app directory or a .well-known one among them, which Android leaves out by default.
118
+ androidResources {
119
+ ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
120
+ }
121
+
122
+ compileOptions {
123
+ sourceCompatibility JavaVersion.VERSION_17
124
+ targetCompatibility JavaVersion.VERSION_17
125
+ }
126
+ }
127
+
128
+ dependencies {
129
+ // Hermes' release build in every variant: it is the one the host links against.
130
+ implementation('com.facebook.hermes:hermes-android:${HERMES.version}') {
131
+ attributes {
132
+ attribute(com.android.build.api.attributes.BuildTypeAttr.ATTRIBUTE, objects.named(com.android.build.api.attributes.BuildTypeAttr, 'release'))
133
+ }
134
+ }
135
+ implementation 'androidx.webkit:webkit:${ANDROIDX_WEBKIT}'
136
+ }
137
+ `;
138
+
139
+ // An Android app as a Gradle project, written anew from the parts its ABI targets built: each brings its libraries,
140
+ // the first also the app's files. Gradle adds Hermes and androidx.webkit from Maven, and makes the APK or the bundle.
141
+ // The icons given are the files of the app's launcher icon under res/; without them it has the system's.
142
+ export default function writeGradleProject(to, { parts, id, label, version, icons = null, urlSchemes = [], background = null, permissions = [] }) {
143
+ const { name } = state.config.general;
144
+ 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.`);
145
+ fs.rmSync(to, { recursive: true, force: true });
146
+ fs.cpSync(path.join(resolveAppHostRoot(), 'android'), to, { recursive: true });
147
+ const main = path.join(to, 'app', 'src', 'main');
148
+ const manifest = path.join(main, 'AndroidManifest.xml');
149
+ const text = fs.readFileSync(manifest, 'utf8');
150
+ if (!text.includes(ACTIVITY_END) || !text.includes(INTERNET)) {
151
+ throw new Error(`crossbind: ${manifest}, from the app host, has no activity or permissions to give app.urlSchemes and app.permissions: update @crossbind/core-app-host to match crossbind.`);
152
+ }
153
+ fs.writeFileSync(manifest, text
154
+ .replace(INTERNET, [INTERNET, ...usesPermissions(permissions)].join('\n'))
155
+ .replace(ACTIVITY_END, urlSchemes.length > 0 ? `${urlFilter(urlSchemes)}\n${ACTIVITY_END}` : ACTIVITY_END));
156
+ parts.forEach(({ arch, dir }) => fs.cpSync(path.join(dir, 'lib'), path.join(main, 'jniLibs', arch), { recursive: true }));
157
+ fs.cpSync(path.join(parts[0].dir, 'app'), path.join(main, 'assets', 'app'), { recursive: true });
158
+ if (icons) {
159
+ Object.entries({ ...icons, 'mipmap-anydpi/ic_launcher.xml': ADAPTIVE_ICON }).forEach(([file, content]) => {
160
+ fs.mkdirSync(path.dirname(path.join(main, 'res', file)), { recursive: true });
161
+ fs.writeFileSync(path.join(main, 'res', file), content);
162
+ });
163
+ }
164
+ fs.mkdirSync(path.join(main, 'res', 'values'), { recursive: true });
165
+ fs.writeFileSync(path.join(main, 'res', 'values', 'background.xml'), backgroundValues(background));
166
+ fs.writeFileSync(path.join(to, 'app', 'build.gradle'), buildGradle({
167
+ id, label, version, abis: parts.map(({ arch }) => arch), icon: icons ? '@mipmap/ic_launcher' : '@android:drawable/sym_def_app_icon',
168
+ launchMode: urlSchemes.length > 0 ? 'singleTask' : 'standard', theme: background ? '@style/CrossbindBackground' : DAY_NIGHT,
169
+ }));
170
+ return to;
171
+ }
@@ -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
+ }
@@ -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 (buildNode does it for Windows addons).
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 addon; ${llvmText}` }];
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 addon; ${llvmText}` },
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 addon; 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)',
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
- // A Node-API addon compiles in crossbind's runtime: embind-jsi, which adapts Emscripten's embind,
151
- // and node-api-jsi. The loader bundles the JavaScript half of the same runtime.
152
- function nodeRuntimeRows({ isAddon }) {
153
- const versionOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
154
- const textOf = (file) => fs.readFileSync(file, 'utf8').trim();
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
- const jsiRoot = resolveEmbindJsiRoot();
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: 'the embind-jsi and embind-napi runtime and the bindings crossbind generates',
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
- ...shared,
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,166 @@ 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
+ const MONOCYPHER = { version: '4.0.3', sha256: '8cc9bc341a66249016db9bd70e9142d8d0aef9945973744b1ac05dbc55d8ee66' };
218
+ // The fbjni the Hermes release for Android depends on.
219
+ const FBJNI_VERSION = '0.7.0';
220
+
221
+ const HERMES_BUILDS = {
222
+ linux: {
223
+ sha256: HERMES_SOURCE.sha256,
224
+ sourceUrl: HERMES_SOURCE.url,
225
+ licenseNotes: "Hermes built from its release's source in the linux toolchain image, statically linked into the host",
226
+ },
227
+ win32: {
228
+ sha256: HERMES_SOURCE.sha256,
229
+ sourceUrl: HERMES_SOURCE.url,
230
+ licenseNotes: "Hermes built from its release's source in the windows toolchain image (llvm-mingw), statically linked into the host",
231
+ },
232
+ android: {
233
+ sha256: HERMES_ANDROID.sha256,
234
+ sourceUrl: HERMES_ANDROID.url,
235
+ licenseNotes: "React Native's Hermes for Android, whose libraries the app's Gradle build takes from Maven Central",
236
+ },
237
+ };
238
+
239
+ // What an Android app's Gradle build adds from Maven, and the C++ runtime the app carries beside its host.
240
+ function androidAppRows() {
241
+ const shared = {
242
+ npmName: null, sha256: null, licenseSelected: null, licenseText: null, isCopyleft: false,
243
+ };
244
+ const apache = { ...shared, license: 'Apache-2.0', licenseDeclared: 'Apache-2.0' };
245
+ const llvmLicense = 'Apache-2.0 WITH LLVM-exception';
246
+ return [
247
+ {
248
+ ...apache,
249
+ name: 'fbjni',
250
+ version: FBJNI_VERSION,
251
+ nativeVersion: FBJNI_VERSION,
252
+ sourceUrl: 'https://github.com/facebookincubator/fbjni',
253
+ licenseNotes: 'libfbjni.so, which Hermes loads and Gradle takes with it; license text: https://github.com/facebookincubator/fbjni/blob/main/LICENSE',
254
+ },
255
+ {
256
+ ...apache,
257
+ name: 'androidx-webkit',
258
+ version: ANDROIDX_WEBKIT,
259
+ nativeVersion: ANDROIDX_WEBKIT,
260
+ sourceUrl: 'https://developer.android.com/jetpack/androidx/releases/webkit',
261
+ 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",
262
+ },
263
+ {
264
+ ...shared,
265
+ name: 'llvm-runtimes',
266
+ version: null,
267
+ nativeVersion: null,
268
+ license: llvmLicense,
269
+ licenseDeclared: llvmLicense,
270
+ sourceUrl: 'https://github.com/llvm/llvm-project',
271
+ 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",
272
+ },
273
+ ];
274
+ }
275
+
276
+ // The WebView2 SDK headers a Windows host compiles against, and the NSIS its setup program carries. webview/webview
277
+ // loads the WebView2 Runtime itself, without the SDK's loader.
278
+ function windowsAppRows(hostRoot) {
279
+ const license = 'BSD-3-Clause';
280
+ return [{
281
+ npmName: '@crossbind/core-app-host',
282
+ name: 'webview2-sdk',
283
+ version: WEBVIEW2_SDK.version,
284
+ nativeVersion: WEBVIEW2_SDK.version,
285
+ sha256: WEBVIEW2_SDK.sha256,
286
+ license,
287
+ licenseDeclared: license,
288
+ licenseSelected: null,
289
+ isCopyleft: false,
290
+ sourceUrl: `https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/${WEBVIEW2_SDK.version}/microsoft.web.webview2.${WEBVIEW2_SDK.version}.nupkg`,
291
+ licenseNotes: "the WebView2 SDK's headers from the windows toolchain image, whose interface IDs the host compiles in",
292
+ licenseText: textOf(path.join(hostRoot, 'third_party', 'webview2', 'LICENSE')),
293
+ }, {
294
+ npmName: '@crossbind/core-app-host',
295
+ name: 'nsis',
296
+ version: '3.11',
297
+ nativeVersion: '3.11',
298
+ sha256: null,
299
+ license: 'Zlib AND MIT',
300
+ licenseDeclared: 'Zlib AND MIT',
301
+ licenseSelected: null,
302
+ isCopyleft: false,
303
+ sourceUrl: 'https://sources.debian.org/src/nsis/3.11-1/',
304
+ licenseNotes: "the installer stub and plugins the app's setup program carries, from Debian's nsis in the windows toolchain image: zlib/libpng, but for the System plugin's Call.S, MIT",
305
+ licenseText: textOf(path.join(hostRoot, 'third_party', 'nsis', 'LICENSE')),
306
+ }];
307
+ }
308
+
309
+ function appRuntimeRows(platform) {
310
+ const hostRoot = resolveAppHostRoot();
311
+ const hermesLicense = textOf(path.join(hostRoot, 'third_party', 'jsi', 'LICENSE'));
312
+ const mit = {
313
+ npmName: '@crossbind/core-app-host', license: 'MIT', licenseDeclared: 'MIT', licenseSelected: null, isCopyleft: false,
314
+ };
315
+ const hermes = HERMES_BUILDS[platform] ?? {
316
+ sha256: HERMES.sha256,
317
+ sourceUrl: hermesUrl(HERMES.version),
318
+ licenseNotes: "React Native's Hermes for Apple platforms, which the app carries as hermesvm.framework",
319
+ };
320
+ return [
321
+ ...runtimeRows(resolveAppEmbindJsiRoot(), 'the embind-jsi runtime, the app host and the bindings crossbind generates'),
322
+ {
323
+ ...mit,
324
+ ...hermes,
325
+ name: 'hermes',
326
+ version: HERMES.version,
327
+ nativeVersion: HERMES.version,
328
+ licenseText: hermesLicense,
329
+ },
330
+ {
331
+ ...mit,
332
+ name: 'jsi',
333
+ version: HERMES.version,
334
+ nativeVersion: HERMES.version,
335
+ sha256: JSI_SHA256,
336
+ sourceUrl: 'https://github.com/facebook/hermes',
337
+ licenseNotes: "JSI's jsi.cpp from the same Hermes release, compiled into the host",
338
+ licenseText: hermesLicense,
339
+ },
340
+ ...(platform === 'android' ? androidAppRows() : []),
341
+ ...(platform === 'win32' ? windowsAppRows(hostRoot) : []),
342
+ // A mobile app makes its window with UIKit or an Android activity, without webview/webview.
343
+ ...(['ios', 'android'].includes(platform) ? [] : [{
344
+ ...mit,
345
+ name: 'webview',
346
+ version: WEBVIEW.version,
347
+ nativeVersion: WEBVIEW.version,
348
+ sha256: WEBVIEW.sha256,
349
+ sourceUrl: 'https://github.com/webview/webview',
350
+ licenseNotes: 'webview/webview, with the local patch @crossbind/core-app-host lists',
351
+ licenseText: textOf(path.join(hostRoot, 'third_party', 'webview', 'LICENSE')),
352
+ }]),
353
+ // A desktop host checks its updates with Monocypher, which the linker leaves out of one without app.updater.
354
+ ...(['ios', 'android'].includes(platform) || state.config.app?.updater === undefined ? [] : [{
355
+ npmName: '@crossbind/core-app-host',
356
+ name: 'monocypher',
357
+ version: MONOCYPHER.version,
358
+ nativeVersion: MONOCYPHER.version,
359
+ sha256: MONOCYPHER.sha256,
360
+ license: 'BSD-2-Clause OR CC0-1.0',
361
+ licenseDeclared: 'BSD-2-Clause OR CC0-1.0',
362
+ licenseSelected: null,
363
+ isCopyleft: false,
364
+ sourceUrl: `https://monocypher.org/download/monocypher-${MONOCYPHER.version}.tar.gz`,
365
+ licenseNotes: "Monocypher's Ed25519 and SHA-512, compiled into the host, which check the app's updates",
366
+ licenseText: textOf(path.join(hostRoot, 'third_party', 'monocypher', 'LICENCE.md')),
367
+ }]),
368
+ ];
369
+ }
370
+
198
371
  async function buildRow(node) {
199
372
  const recipe = await loadJs(node.paths.project, 'crossbind.build');
200
373
  const manifest = node.package || null;
@@ -284,7 +457,8 @@ function assertConanStaged() {
284
457
  // platform (optional, e.g. 'wasi') additionally includes what that platform's
285
458
  // artifact statically links beyond the package graph: recipe-declared vendored
286
459
  // 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.
460
+ // addon of that platform compiles in, or without a platform the one its loader bundles;
461
+ // runtimeEnv 'app' adds what an app's host carries.
288
462
  export default async function collectLicenseRows(platform = null, { runtimeEnv = null } = {}) {
289
463
  assertConanStaged();
290
464
  // The root package is a component too when it is a port: leaf -wasi packages have no deps but ship
@@ -310,5 +484,6 @@ export default async function collectLicenseRows(platform = null, { runtimeEnv =
310
484
  if (platform === 'wasi') rows.push(...wasiToolchainRows());
311
485
  if (['linux', 'linuxmusl', 'win32'].includes(platform)) rows.push(...addonToolchainRows(platform));
312
486
  if (runtimeEnv === 'node') rows.push(...nodeRuntimeRows({ isAddon: platform !== null }));
487
+ if (runtimeEnv === 'app') rows.push(...appRuntimeRows(platform));
313
488
  return rows.sort((a, b) => a.name.localeCompare(b.name));
314
489
  }