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.
- package/package.json +6 -1
- package/src/actions/androidDevice.js +77 -0
- package/src/actions/androidGradle.js +121 -0
- package/src/actions/appInstaller.js +174 -0
- package/src/actions/appUpdate.js +68 -0
- package/src/actions/appleSigning.js +96 -0
- package/src/actions/buildApp.js +509 -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 +13 -5
- package/src/actions/getCmakeParameters.js +6 -2
- package/src/actions/getDependLibs.js +8 -2
- package/src/actions/getLinkInputs.js +10 -7
- package/src/actions/gradleProject.js +171 -0
- package/src/actions/iosSimulator.js +63 -0
- package/src/actions/licenses.js +189 -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/windowsSigning.js +96 -0
- package/src/actions/xcodeProject.js +186 -0
- package/src/app/index.d.ts +129 -0
- package/src/app/index.js +184 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +8 -1
- package/src/assets/toolchain-digests.json +24 -18
- package/src/bin.js +84 -32
- package/src/index.js +7 -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/appApiHeader.js +29 -0
- package/src/utils/appIcon.js +174 -0
- package/src/utils/appOptions.js +284 -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/src/utils/writeIfChanged.js +1 -1
- package/vitest.config.js +19 -0
package/src/actions/createLib.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
154
|
-
|
|
155
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|
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,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
|
}
|