crossbind 2.0.0-beta.68 → 2.0.0-beta.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +70 -59
- package/src/actions/addExtension.js +196 -0
- package/src/actions/androidGradle.js +26 -17
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/buildApp.js +177 -74
- package/src/actions/buildDependencies.js +3 -3
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createImportedBridges.js +10 -8
- package/src/actions/createInterface.js +40 -29
- package/src/actions/createLib.js +3 -7
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- package/src/actions/getAllBridges.js +20 -5
- package/src/actions/getDependLibs.js +5 -0
- package/src/actions/getLinkInputs.js +2 -1
- package/src/actions/gradleProject.js +207 -16
- package/src/actions/iosDevice.js +2 -2
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +113 -58
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
- package/src/actions/publish.js +220 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/writeRuntimeEntry.js +2 -2
- package/src/actions/xcodeProject.js +60 -29
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +144 -132
- package/src/app/index.js +147 -185
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/node-import-hooks.mjs +7 -6
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +37 -37
- package/src/index.js +4 -3
- package/src/integration/getCrossbindScript.js +3 -2
- package/src/integration/getDependFilePath.js +10 -10
- package/src/program.js +32 -3
- package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
- package/src/state/index.js +8 -6
- package/src/state/loadConfig.js +26 -9
- package/src/state/refreshPackageSources.js +19 -0
- package/src/utils/appExtensions.js +777 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +142 -112
- package/src/utils/appRustCrates.js +2 -2
- package/src/utils/appTargets.js +168 -0
- package/src/utils/bridgeTools.js +71 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +143 -26
- package/src/utils/cppDts.js +11 -11
- package/src/utils/extensionSteps.js +172 -0
- package/src/utils/nativeImports.js +9 -2
- package/src/utils/nodeDevBuild.js +2 -1
- package/src/utils/openUrl.js +2 -1
- package/src/utils/packageSourceApi.js +87 -0
- package/src/utils/packageSourceImport.js +69 -0
- package/src/utils/packageSourceInstall.js +162 -0
- package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
- package/src/utils/packageSources.js +108 -0
- package/src/utils/plist.js +5 -0
- package/src/utils/printableText.js +5 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/runPackageSource.js +251 -0
- package/src/utils/rustBridgeGen.js +3 -3
- package/src/utils/systemConfig.js +16 -3
- package/src/utils/targetToolchain.js +77 -0
- package/vitest.config.js +6 -1
- package/src/actions/appSetup.js +0 -116
- package/src/actions/appStoreConnect.js +0 -188
- package/src/actions/appUpdate.js +0 -68
- package/src/actions/extensions.js +0 -7
- package/src/actions/googlePlay.js +0 -213
- package/src/actions/googleSignIn.js +0 -78
- package/src/actions/playSetup.js +0 -19
- package/src/actions/publishAndroid.js +0 -102
- package/src/actions/publishIos.js +0 -146
- package/src/state/refreshConanDependencies.js +0 -17
- package/src/utils/conanDependencies.js +0 -66
- package/src/utils/conanImport.js +0 -58
- package/src/utils/conanInstall.js +0 -154
- package/src/utils/conanProfile.js +0 -203
- package/src/utils/runConan.js +0 -257
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import getDependLibs from './getDependLibs.js';
|
|
3
3
|
import state from '../state/index.js';
|
|
4
4
|
import buildAppRustCrates from '../utils/appRustCrates.js';
|
|
5
|
+
import { cratesOf } from '../utils/packageSourceImport.js';
|
|
5
6
|
|
|
6
7
|
// The archives of a final link (wasm, Node-API addon, native executable or app host) and which of them must
|
|
7
8
|
// survive whole. keepFlag spells "keep this symbol" for the target's linker; a native executable
|
|
@@ -22,7 +23,7 @@ export default function getLinkInputs(target, { keepFlag, withBridge = true, bri
|
|
|
22
23
|
const bridgeLibCandidates = builtLib(bridge);
|
|
23
24
|
// App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
|
|
24
25
|
// single fully-loaded Rust archive of the link (see the libstd rule below).
|
|
25
|
-
const appRustLibs = buildAppRustCrates(target, cache, state.config
|
|
26
|
+
const appRustLibs = buildAppRustCrates(target, cache, cratesOf(state.config));
|
|
26
27
|
const libs = [
|
|
27
28
|
...getDependLibs(target),
|
|
28
29
|
...appRustLibs,
|
|
@@ -5,6 +5,8 @@ import { ANDROID_API_LEVEL } from '../utils/androidToolchain.js';
|
|
|
5
5
|
import { HERMES } from '../utils/hermes.js';
|
|
6
6
|
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
7
7
|
import { SIGNING } from './androidGradle.js';
|
|
8
|
+
import { appLinkParts } from '../utils/appLaunches.js';
|
|
9
|
+
import { mergedVerification } from './mavenVerification.js';
|
|
8
10
|
|
|
9
11
|
// The ABIs an Android app builds for: phones and Apple silicon's emulators, and the emulators of x86_64 machines.
|
|
10
12
|
export const ANDROID_ABIS = ['arm64-v8a', 'x86_64'];
|
|
@@ -34,6 +36,21 @@ const backgroundValues = (color) => `<?xml version="1.0" encoding="utf-8"?>
|
|
|
34
36
|
</resources>
|
|
35
37
|
`;
|
|
36
38
|
|
|
39
|
+
// The color while the system is dark, where app.window.backgroundColor gives one, which Android takes in place of the other.
|
|
40
|
+
const darkBackgroundValues = (color) => `<?xml version="1.0" encoding="utf-8"?>
|
|
41
|
+
<resources>
|
|
42
|
+
<color name="crossbind_background">${color}</color>
|
|
43
|
+
</resources>
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
// The devices of app.permissions, which alone the page may ask for, whatever an extension's Java asks Android for.
|
|
47
|
+
const permissionValues = (permissions) => `<?xml version="1.0" encoding="utf-8"?>
|
|
48
|
+
<resources>
|
|
49
|
+
<string-array name="crossbind_permissions">${permissions.map((name) => `\n <item>${name}</item>`).join('')}
|
|
50
|
+
</string-array>
|
|
51
|
+
</resources>
|
|
52
|
+
`;
|
|
53
|
+
|
|
37
54
|
// The Android permissions behind each of app.permissions, after the one every app asks for; the activity asks its user
|
|
38
55
|
// for them when the page first uses the device.
|
|
39
56
|
const INTERNET = ' <uses-permission android:name="android.permission.INTERNET" />';
|
|
@@ -42,22 +59,138 @@ const ANDROID_PERMISSIONS = {
|
|
|
42
59
|
microphone: ['RECORD_AUDIO', 'MODIFY_AUDIO_SETTINGS'],
|
|
43
60
|
location: ['ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION'],
|
|
44
61
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.map((permission) => `
|
|
62
|
+
// The permissions the app asks for beside INTERNET, each once: app.permissions' and its extensions'.
|
|
63
|
+
const usesPermissions = (permissions, extensionPermissions) => [...new Set([
|
|
64
|
+
...permissions.flatMap((name) => ANDROID_PERMISSIONS[name]).map((permission) => `android.permission.${permission}`),
|
|
65
|
+
...extensionPermissions,
|
|
66
|
+
])].filter((permission) => permission !== 'android.permission.INTERNET')
|
|
67
|
+
.map((permission) => ` <uses-permission android:name="${permission}" />`);
|
|
48
68
|
|
|
49
|
-
//
|
|
69
|
+
// What an extension's own manifest adds to the app's: the elements of its manifest, and those of its application, which
|
|
70
|
+
// go inside the app's, whose attributes are the app's alone. The app's manifest declares the android and tools prefixes.
|
|
71
|
+
const APPLICATION_END = ' </application>';
|
|
72
|
+
const PREFIXES = ['android', 'tools'];
|
|
73
|
+
|
|
74
|
+
// A manifest's text without its comments, removed until none is left, as removing one may join others into one.
|
|
75
|
+
function withoutComments(text) {
|
|
76
|
+
let current = text;
|
|
77
|
+
let previous;
|
|
78
|
+
do {
|
|
79
|
+
previous = current;
|
|
80
|
+
current = current.replace(/<!--[\s\S]*?-->/g, '');
|
|
81
|
+
} while (current !== previous);
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function manifestParts(name, file) {
|
|
86
|
+
const text = withoutComments(fs.readFileSync(file, 'utf8').replace(/<\?xml[\s\S]*?\?>/, ''));
|
|
87
|
+
const root = /^\s*<manifest\b([^>]*)>([\s\S]*)<\/manifest>\s*$/.exec(text);
|
|
88
|
+
if (!root) throw new Error(`crossbind: the extension ${name}'s manifest ${file} is no <manifest> of elements, as an AndroidManifest.xml is.`);
|
|
89
|
+
const other = [...root[1].matchAll(/xmlns:([\w-]+)/g)].map(([, prefix]) => prefix).find((prefix) => !PREFIXES.includes(prefix));
|
|
90
|
+
if (other) throw new Error(`crossbind: the extension ${name}'s manifest ${file} declares the prefix ${other}, which the app's has not: it takes android and tools.`);
|
|
91
|
+
const attribute = [...root[1].matchAll(/([\w:.-]+)\s*=/g)].map(([, key]) => key).find((key) => !key.startsWith('xmlns'));
|
|
92
|
+
if (attribute) throw new Error(`crossbind: the extension ${name}'s manifest ${file} gives its <manifest> ${attribute}, which is the app's own.`);
|
|
93
|
+
// The permissions it asks for are those its permissions list, which the app asks for once each.
|
|
94
|
+
if (/<uses-permission[\s/>]/.test(root[2])) {
|
|
95
|
+
throw new Error(`crossbind: the extension ${name}'s manifest ${file} asks for a permission with <uses-permission>, which its app.android.permissions lists, as the app asks for each once.`);
|
|
96
|
+
}
|
|
97
|
+
const applications = [...root[2].matchAll(/<application\b([^>]*?)(?:\/>|>([\s\S]*?)<\/application>)/g)];
|
|
98
|
+
if (applications.length > 1) throw new Error(`crossbind: the extension ${name}'s manifest ${file} holds ${applications.length} <application> elements, of which it takes one.`);
|
|
99
|
+
const [application] = applications;
|
|
100
|
+
if (application && application[1].trim() !== '') {
|
|
101
|
+
throw new Error(`crossbind: the extension ${name}'s manifest ${file} gives its <application> attributes, which are the app's own: it holds what the extension adds to the app's alone.`);
|
|
102
|
+
}
|
|
103
|
+
return { manifest: application ? root[2].replace(application[0], '') : root[2], application: application?.[2] ?? '' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// An extension's elements, as the app's manifest holds them, at the indentation of their place there.
|
|
107
|
+
function indented(text, indent) {
|
|
108
|
+
const lines = text.split('\n').filter((line, index, all) => line.trim() !== '' || (index > all.findIndex((one) => one.trim() !== '')
|
|
109
|
+
&& index < all.findLastIndex((one) => one.trim() !== '')));
|
|
110
|
+
const margin = Math.min(...lines.filter((line) => line.trim() !== '').map((line) => /^[ \t]*/.exec(line)[0].length));
|
|
111
|
+
return lines.map((line) => (line.trim() === '' ? '' : `${indent}${line.slice(margin).trimEnd()}`)).join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// What the activity opens, from a browser or a file manager as well, before the end of the activity's element.
|
|
50
115
|
const ACTIVITY_END = ' </activity>';
|
|
51
|
-
const
|
|
52
|
-
|
|
116
|
+
const viewFilter = (data, isVerified = false) => [
|
|
117
|
+
` <intent-filter${isVerified ? ' android:autoVerify="true"' : ''}>`,
|
|
53
118
|
' <action android:name="android.intent.action.VIEW" />',
|
|
54
119
|
' <category android:name="android.intent.category.DEFAULT" />',
|
|
55
120
|
' <category android:name="android.intent.category.BROWSABLE" />',
|
|
56
|
-
...
|
|
121
|
+
...data.map((attributes) => ` <data ${attributes} />`),
|
|
57
122
|
' </intent-filter>',
|
|
58
123
|
].join('\n');
|
|
124
|
+
// The links of the app's schemes.
|
|
125
|
+
const urlFilter = (schemes) => viewFilter(schemes.map((scheme) => `android:scheme="${scheme}"`));
|
|
126
|
+
// The files of the app's types, which other apps hand it as content: addresses alone, as a file: one would have the app
|
|
127
|
+
// read its own: by their MIME types, as Android matches what a content provider gives, else by their names' extensions,
|
|
128
|
+
// which a provider's path shows or not; .*\\. takes one dot, and Android's pattern a single match.
|
|
129
|
+
const fileFilters = (fileTypes) => fileTypes.map(({ extensions, mimeTypes }) => viewFilter(['android:scheme="content"', ...(mimeTypes.length > 0
|
|
130
|
+
? mimeTypes.map((mime) => `android:mimeType="${mime}"`)
|
|
131
|
+
: ['android:host="*"', 'android:mimeType="*/*"', ...extensions.flatMap((ending) => [`android:pathPattern=".*\\\\.${ending}"`, `android:pathPattern=".*\\\\..*\\\\.${ending}"`])])]));
|
|
132
|
+
// The https links of app.links, which Android verifies against each site's assetlinks.json.
|
|
133
|
+
const linkFilters = (links) => links.map((link) => {
|
|
134
|
+
const { host, path: prefix } = appLinkParts(link);
|
|
135
|
+
return viewFilter(['android:scheme="https"', `android:host="${host}"`, ...(prefix ? [`android:pathPrefix="${prefix}"`] : [])], true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// The app's quick actions, Android's static shortcuts of its icon, which shows the app's icon beside each, or the
|
|
139
|
+
// launcher's own without one: Android empties a static shortcut's task first, so a tap starts the host's trampoline,
|
|
140
|
+
// which hands it to the activity that runs, with its page. Each label is a resource, as Android takes none other.
|
|
141
|
+
const SHORTCUTS_META = ' <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />';
|
|
142
|
+
const shortcutsXml = (items, id, icon) => `<?xml version="1.0" encoding="utf-8"?>
|
|
143
|
+
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
|
144
|
+
${items.map(({ type }, index) => [
|
|
145
|
+
` <shortcut android:shortcutId="${type}"${icon ? ` android:icon="${icon}"` : ''} android:shortcutShortLabel="@string/crossbind_shortcut_${index}">`,
|
|
146
|
+
` <intent android:action="dev.crossbind.app.SHORTCUT" android:targetPackage="${id}" android:targetClass="dev.crossbind.app.CrossbindShortcutActivity">`,
|
|
147
|
+
// Android reads an extra's value of digits, or true or false, as a number or a boolean: after a prefix, each is text.
|
|
148
|
+
` <extra android:name="dev.crossbind.app.shortcut" android:value="type:${type}" />`,
|
|
149
|
+
' </intent>',
|
|
150
|
+
' </shortcut>',
|
|
151
|
+
].join('\n')).join('\n')}
|
|
152
|
+
</shortcuts>
|
|
153
|
+
`;
|
|
154
|
+
// Text as a string resource holds it as it is: XML's escapes, and a backslash before what aapt reads otherwise, a quote,
|
|
155
|
+
// an apostrophe, a backslash and an @ or a ? that starts it after any space; formatted="false" keeps a % as it is.
|
|
156
|
+
const androidText = (text) => text.replace(/[\\'"]/g, '\\$&').replace(/^(\s*)([@?])/, '$1\\$2').replace(/&/g, '&').replace(/</g, '<')
|
|
157
|
+
.replace(/>/g, '>');
|
|
158
|
+
const shortcutLabels = (items) => `<?xml version="1.0" encoding="utf-8"?>
|
|
159
|
+
<resources>
|
|
160
|
+
${items.map(({ title }, index) => ` <string name="crossbind_shortcut_${index}" formatted="false">${androidText(title)}</string>`).join('\n')}
|
|
161
|
+
</resources>
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
// The class of each extension's Java, which the activity calls by the extension's name, in the extensions' order.
|
|
165
|
+
const extensionsJava = (java) => `// crossbind writes this file from the app's extensions: the class of each one's Java, under its name, and the holder of
|
|
166
|
+
// each of the host's roles it holds.
|
|
167
|
+
package dev.crossbind.app;
|
|
168
|
+
|
|
169
|
+
import java.util.Collections;
|
|
170
|
+
import java.util.HashMap;
|
|
171
|
+
import java.util.LinkedHashMap;
|
|
172
|
+
import java.util.Map;
|
|
59
173
|
|
|
60
|
-
|
|
174
|
+
final class Extensions {
|
|
175
|
+
// In the order of the app's extensions, in which each hears what the activity does.
|
|
176
|
+
static final Map<String, Extension> ALL;
|
|
177
|
+
// By the role, as page-dialogs, whose requests the activity hands its holder's Java.
|
|
178
|
+
static final Map<String, Extension> ROLES;
|
|
179
|
+
|
|
180
|
+
static {
|
|
181
|
+
Map<String, Extension> all = new LinkedHashMap<>();${java.map(({ name, className }) => `
|
|
182
|
+
all.put(${JSON.stringify(name)}, new ${className}());`).join('')}
|
|
183
|
+
ALL = Collections.unmodifiableMap(all);
|
|
184
|
+
Map<String, Extension> roles = new HashMap<>();${java.flatMap(({ name, roles = [] }) => roles.map((role) => `
|
|
185
|
+
roles.put(${JSON.stringify(role)}, all.get(${JSON.stringify(name)}));`)).join('')}
|
|
186
|
+
ROLES = Collections.unmodifiableMap(roles);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private Extensions() {}
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
const buildGradle = ({ id, label, version, abis, icon, launchMode, theme, libraries = [] }) => `// crossbind writes this project anew on every build: the module comes prebuilt in src/main/jniLibs/, and the app's
|
|
61
194
|
// files are in src/main/assets/app/.
|
|
62
195
|
plugins {
|
|
63
196
|
id 'com.android.application'
|
|
@@ -133,26 +266,62 @@ dependencies {
|
|
|
133
266
|
}
|
|
134
267
|
}
|
|
135
268
|
implementation 'androidx.webkit:webkit:${ANDROIDX_WEBKIT}'
|
|
136
|
-
}
|
|
269
|
+
${libraries.map((library) => ` implementation '${library}'\n`).join('')}}
|
|
137
270
|
`;
|
|
138
271
|
|
|
139
272
|
// 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
|
|
141
|
-
//
|
|
142
|
-
|
|
273
|
+
// the first also the app's files. Gradle adds Hermes, androidx.webkit and its extensions' libraries from Maven, and makes
|
|
274
|
+
// the APK or the bundle.
|
|
275
|
+
// The icons given are the files of the app's launcher icon under res/; without them it has the system's. Each of java
|
|
276
|
+
// is an extension's folder of Java sources, by package, which the app's sources take beside the activity, and the
|
|
277
|
+
// class in it the activity calls.
|
|
278
|
+
export default function writeGradleProject(to, {
|
|
279
|
+
parts, id, label, version, icons = null, urlSchemes = [], fileTypes = [], links = [], shortcutItems = [], background = null, darkBackground = null,
|
|
280
|
+
permissions = [], java = [], extensionPermissions = [], manifests = [], resources = [], maven = [],
|
|
281
|
+
}) {
|
|
143
282
|
const { name } = state.config.general;
|
|
144
283
|
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.`);
|
|
284
|
+
// What the extensions add is read before the project goes, which a manifest crossbind refuses then leaves as it was.
|
|
285
|
+
const added = manifests.map(({ name: extension, file }) => manifestParts(extension, file));
|
|
286
|
+
// An app whose extensions ask Maven for libraries checks every file Gradle resolves, which it then resolves online.
|
|
287
|
+
const verification = maven.length > 0 ? mergedVerification(
|
|
288
|
+
fs.readFileSync(path.join(resolveAppHostRoot(), 'android-verification-metadata.xml'), 'utf8'),
|
|
289
|
+
maven.map(({ name: extension, verification: file }) => ({ name: extension, xml: fs.readFileSync(file, 'utf8') })),
|
|
290
|
+
) : null;
|
|
145
291
|
fs.rmSync(to, { recursive: true, force: true });
|
|
146
292
|
fs.cpSync(path.join(resolveAppHostRoot(), 'android'), to, { recursive: true });
|
|
293
|
+
if (verification) fs.writeFileSync(path.join(to, 'gradle', 'verification-metadata.xml'), verification);
|
|
147
294
|
const main = path.join(to, 'app', 'src', 'main');
|
|
148
295
|
const manifest = path.join(main, 'AndroidManifest.xml');
|
|
149
296
|
const text = fs.readFileSync(manifest, 'utf8');
|
|
150
|
-
if (!text.includes(ACTIVITY_END) || !text.includes(INTERNET)) {
|
|
297
|
+
if (!text.includes(ACTIVITY_END) || !text.includes(INTERNET) || !text.includes(APPLICATION_END)) {
|
|
151
298
|
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
299
|
}
|
|
300
|
+
const own = (key, indent) => added.map((part) => part[key]).filter((part) => part.trim() !== '').map((part) => indented(part, indent));
|
|
301
|
+
// A replacer, as the extensions' text may hold what a replacement string reads as a pattern, as $&.
|
|
153
302
|
fs.writeFileSync(manifest, text
|
|
154
|
-
.replace(INTERNET, [INTERNET, ...usesPermissions(permissions)].join('\n'))
|
|
155
|
-
.replace(ACTIVITY_END,
|
|
303
|
+
.replace(INTERNET, () => [INTERNET, ...usesPermissions(permissions, extensionPermissions), ...own('manifest', ' ')].join('\n'))
|
|
304
|
+
.replace(ACTIVITY_END, () => [
|
|
305
|
+
...(urlSchemes.length > 0 ? [urlFilter(urlSchemes)] : []), ...fileFilters(fileTypes), ...linkFilters(links),
|
|
306
|
+
...(shortcutItems.length > 0 ? [SHORTCUTS_META] : []), ACTIVITY_END,
|
|
307
|
+
].join('\n'))
|
|
308
|
+
.replace(APPLICATION_END, () => [...own('application', ' '), APPLICATION_END].join('\n')));
|
|
309
|
+
fs.writeFileSync(path.join(main, 'java', 'dev', 'crossbind', 'app', 'Extensions.java'), extensionsJava(java));
|
|
310
|
+
// The Java and Kotlin sources of an extension's folder alone, which may hold its manifest and resources as well: the
|
|
311
|
+
// Android Gradle plugin compiles Kotlin beside the Java.
|
|
312
|
+
java.forEach(({ name: extension, dir }) => {
|
|
313
|
+
const sources = path.join(main, 'java');
|
|
314
|
+
const files = fs.readdirSync(dir, { recursive: true }).filter((file) => /\.(java|kt)$/.test(file) && fs.statSync(path.join(dir, file), { throwIfNoEntry: false })?.isFile());
|
|
315
|
+
files.forEach((file) => {
|
|
316
|
+
if (fs.existsSync(path.join(sources, file))) {
|
|
317
|
+
throw new Error(`crossbind: the extension ${extension} has ${file} in its Java, which the app's Gradle project has already.`);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
files.forEach((file) => {
|
|
321
|
+
fs.mkdirSync(path.dirname(path.join(sources, file)), { recursive: true });
|
|
322
|
+
fs.copyFileSync(path.join(dir, file), path.join(sources, file));
|
|
323
|
+
});
|
|
324
|
+
});
|
|
156
325
|
parts.forEach(({ arch, dir }) => fs.cpSync(path.join(dir, 'lib'), path.join(main, 'jniLibs', arch), { recursive: true }));
|
|
157
326
|
// A release keeps its libraries as linked beside the stripped ones it ships, for Google Play's crash reports.
|
|
158
327
|
parts.filter(({ dir }) => fs.existsSync(path.join(dir, 'symbols')))
|
|
@@ -166,9 +335,31 @@ export default function writeGradleProject(to, { parts, id, label, version, icon
|
|
|
166
335
|
}
|
|
167
336
|
fs.mkdirSync(path.join(main, 'res', 'values'), { recursive: true });
|
|
168
337
|
fs.writeFileSync(path.join(main, 'res', 'values', 'background.xml'), backgroundValues(background));
|
|
338
|
+
if (background && darkBackground) {
|
|
339
|
+
fs.mkdirSync(path.join(main, 'res', 'values-night'), { recursive: true });
|
|
340
|
+
fs.writeFileSync(path.join(main, 'res', 'values-night', 'background.xml'), darkBackgroundValues(darkBackground));
|
|
341
|
+
}
|
|
342
|
+
fs.writeFileSync(path.join(main, 'res', 'values', 'permissions.xml'), permissionValues(permissions));
|
|
343
|
+
if (shortcutItems.length > 0) {
|
|
344
|
+
fs.mkdirSync(path.join(main, 'res', 'xml'), { recursive: true });
|
|
345
|
+
fs.writeFileSync(path.join(main, 'res', 'xml', 'shortcuts.xml'), shortcutsXml(shortcutItems, id, icons ? '@mipmap/ic_launcher' : null));
|
|
346
|
+
fs.writeFileSync(path.join(main, 'res', 'values', 'shortcuts.xml'), shortcutLabels(shortcutItems));
|
|
347
|
+
}
|
|
348
|
+
// An extension's resources join the app's, of which they replace none.
|
|
349
|
+
resources.forEach(({ name: extension, dir }) => {
|
|
350
|
+
fs.readdirSync(dir, { recursive: true }).forEach((file) => {
|
|
351
|
+
const into = path.join(main, 'res', file);
|
|
352
|
+
if (fs.existsSync(into) && !fs.statSync(into).isDirectory()) {
|
|
353
|
+
throw new Error(`crossbind: the extension ${extension} has ${file} in its resources, which the app's Gradle project has already.`);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
fs.cpSync(dir, path.join(main, 'res'), { recursive: true });
|
|
357
|
+
});
|
|
169
358
|
fs.writeFileSync(path.join(to, 'app', 'build.gradle'), buildGradle({
|
|
170
359
|
id, label, version, abis: parts.map(({ arch }) => arch), icon: icons ? '@mipmap/ic_launcher' : '@android:drawable/sym_def_app_icon',
|
|
171
|
-
launchMode: urlSchemes.length > 0 ? 'singleTask' : 'standard',
|
|
360
|
+
launchMode: urlSchemes.length + fileTypes.length + links.length + shortcutItems.length > 0 ? 'singleTask' : 'standard',
|
|
361
|
+
theme: background ? '@style/CrossbindBackground' : DAY_NIGHT,
|
|
362
|
+
libraries: [...new Set(maven.flatMap(({ coordinates }) => coordinates))],
|
|
172
363
|
}));
|
|
173
364
|
return to;
|
|
174
365
|
}
|
package/src/actions/iosDevice.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import { EXEC_MAX_BUFFER } from './run.js';
|
|
5
5
|
import { appTeam } from './buildApp.js';
|
|
6
|
-
import { xcodebuild, xcodeTeams } from './
|
|
6
|
+
import { xcodebuild, xcodeTeams } from './xcodebuild.js';
|
|
7
7
|
import state from '../state/index.js';
|
|
8
8
|
import { appId } from '../utils/appOptions.js';
|
|
9
9
|
|
|
@@ -171,7 +171,7 @@ export function buildForIosDevice(project, device) {
|
|
|
171
171
|
xcodebuild(`the build for ${device.name}`, [
|
|
172
172
|
'-project', `${project}/${name}.xcodeproj`, '-scheme', name, '-configuration', 'Debug', '-sdk', 'iphoneos', '-destination', `id=${device.udid}`,
|
|
173
173
|
'-allowProvisioningUpdates', '-allowProvisioningDeviceRegistration', `SYMROOT=${products}`, 'build',
|
|
174
|
-
], `${products}/device.log`, { team, id: appId(state.config) });
|
|
174
|
+
], { log: `${products}/device.log`, facts: { team, id: appId(state.config) } });
|
|
175
175
|
return `${products}/Debug-iphoneos/${name}.app`;
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -43,8 +43,8 @@ export function buildForSimulator(project, configuration = 'Release') {
|
|
|
43
43
|
return `${products}/${configuration}-iphonesimulator/${name}.app`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Opens a Simulator build of an iOS app, with `env` in its environment
|
|
47
|
-
// and ends with the app; close() ends the app.
|
|
46
|
+
// Opens a Simulator build of an iOS app, with `env` in its environment, in the Simulator of the device returned. The
|
|
47
|
+
// process returned carries the app's output and ends with the app; close() ends the app.
|
|
48
48
|
export default function openInSimulator(app, { env = {}, show = false, stdio = 'pipe' } = {}) {
|
|
49
49
|
const device = simulator();
|
|
50
50
|
// Simulator.app only shows the device, which runs the app without it as well.
|
|
@@ -59,5 +59,5 @@ export default function openInSimulator(app, { env = {}, show = false, stdio = '
|
|
|
59
59
|
childEnv[`SIMCTL_CHILD_${key}`] = value;
|
|
60
60
|
});
|
|
61
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]) };
|
|
62
|
+
return { child, device, close: () => spawnSync('xcrun', ['simctl', 'terminate', device, bundleId]) };
|
|
63
63
|
}
|
package/src/actions/licenses.js
CHANGED
|
@@ -12,7 +12,12 @@ import resolveAppHostRoot, { resolveAppEmbindJsiRoot } from '../utils/resolveApp
|
|
|
12
12
|
import {
|
|
13
13
|
HERMES, HERMES_ANDROID, HERMES_SOURCE, hermesUrl,
|
|
14
14
|
} from '../utils/hermes.js';
|
|
15
|
+
import { installersFor } from './appInstaller.js';
|
|
16
|
+
import { DESKTOP_PLATFORMS } from './buildApp.js';
|
|
17
|
+
import extensionsInApp from './extensionsInApp.js';
|
|
15
18
|
import { ANDROIDX_WEBKIT } from './gradleProject.js';
|
|
19
|
+
import { extensionPackage, extensionPart } from '../utils/appExtensions.js';
|
|
20
|
+
import { declaredSources, declarationOf, dependenciesOf } from '../utils/packageSources.js';
|
|
16
21
|
import { shippedBundledLicense } from '../utils/bundledLicenses.js';
|
|
17
22
|
import toolchainNoticesDir from '../utils/toolchainNotices.js';
|
|
18
23
|
|
|
@@ -208,13 +213,12 @@ function nodeRuntimeRows({ isAddon }) {
|
|
|
208
213
|
}];
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
// What an app's host carries beside crossbind's runtime, pinned as the
|
|
216
|
+
// What an app's host carries beside crossbind's runtime, pinned as the READMEs beside them in @crossbind/core-app-host
|
|
212
217
|
// record: React Native's Hermes, JSI's jsi.cpp from the same release, webview/webview, and on Windows the WebView2 SDK's
|
|
213
218
|
// headers.
|
|
214
219
|
const WEBVIEW = { version: 'cbbdee44afff22867de9fd88a9fc8350d9bdd399', sha256: '10e972a2327b5681474f4aa4499e505eaa4ff659aa995380386f08fd6fc1b763' };
|
|
215
220
|
const WEBVIEW2_SDK = { version: '1.0.4258.31', sha256: '56f7f4b8bf9aee4b8efefbbdd4f67d5f74ebd1b100ed0806da71bf76af481aa9' };
|
|
216
221
|
const JSI_SHA256 = 'cb6274a22abc93ecc970066539815e716d3e47716e8f93a9971cf4eb5918ffc1';
|
|
217
|
-
const MONOCYPHER = { version: '4.0.3', sha256: '8cc9bc341a66249016db9bd70e9142d8d0aef9945973744b1ac05dbc55d8ee66' };
|
|
218
222
|
// The fbjni the Hermes release for Android depends on.
|
|
219
223
|
const FBJNI_VERSION = '0.7.0';
|
|
220
224
|
|
|
@@ -273,8 +277,8 @@ function androidAppRows() {
|
|
|
273
277
|
];
|
|
274
278
|
}
|
|
275
279
|
|
|
276
|
-
// The WebView2 SDK headers a Windows host compiles against
|
|
277
|
-
//
|
|
280
|
+
// The WebView2 SDK headers a Windows host compiles against. webview/webview loads the WebView2 Runtime itself, without the
|
|
281
|
+
// SDK's loader.
|
|
278
282
|
function windowsAppRows(hostRoot) {
|
|
279
283
|
const license = 'BSD-3-Clause';
|
|
280
284
|
return [{
|
|
@@ -290,22 +294,73 @@ function windowsAppRows(hostRoot) {
|
|
|
290
294
|
sourceUrl: `https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/${WEBVIEW2_SDK.version}/microsoft.web.webview2.${WEBVIEW2_SDK.version}.nupkg`,
|
|
291
295
|
licenseNotes: "the WebView2 SDK's headers from the windows toolchain image, whose interface IDs the host compiles in",
|
|
292
296
|
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
297
|
}];
|
|
307
298
|
}
|
|
308
299
|
|
|
300
|
+
const APP_PLATFORMS = ['darwin', 'ios', 'android', 'linux', 'win32'];
|
|
301
|
+
|
|
302
|
+
// The extensions an app of this platform has, of any platform's without one: crossbind's own that bring it code, and those
|
|
303
|
+
// crossbind.config.js lists.
|
|
304
|
+
function extensionsIn(platform) {
|
|
305
|
+
if (!platform) return [...new Map(APP_PLATFORMS.flatMap((one) => extensionsIn(one)).map((extension) => [extension.name, extension])).values()];
|
|
306
|
+
const listed = (state.config.extensions ?? []).map(({ name }) => name);
|
|
307
|
+
const hasCode = (extension) => {
|
|
308
|
+
const { native, java } = extensionPart([extension], platform);
|
|
309
|
+
return native.length > 0 || java.length > 0;
|
|
310
|
+
};
|
|
311
|
+
return extensionsInApp(platform).filter((extension) => listed.includes(extension.name) || hasCode(extension));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// A row of the code of others an extension carries, by what its licenses list says of it.
|
|
315
|
+
const carriedRow = (entry, root, notes) => ({
|
|
316
|
+
npmName: null,
|
|
317
|
+
name: entry.name,
|
|
318
|
+
version: null,
|
|
319
|
+
nativeVersion: entry.version,
|
|
320
|
+
sha256: entry.sha256 ?? null,
|
|
321
|
+
license: entry.license,
|
|
322
|
+
licenseDeclared: entry.license,
|
|
323
|
+
licenseSelected: null,
|
|
324
|
+
isCopyleft: isCopyleft(entry.license),
|
|
325
|
+
sourceUrl: entry.source ?? null,
|
|
326
|
+
licenseNotes: entry.notes ?? notes,
|
|
327
|
+
licenseText: textOf(path.join(root, entry.file)),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// What the installers of a desktop app carry into what they make.
|
|
331
|
+
const installerRows = (platform) => (DESKTOP_PLATFORMS[platform] ? installersFor({ platform }) : [])
|
|
332
|
+
.flatMap(({ name, root, installer }) => (installer.licenses ?? []).map((entry) => carriedRow(entry, root, `carried by what the installer ${name} makes`)));
|
|
333
|
+
|
|
334
|
+
// What an app's extensions bring into it: each extension package's own code, and the code of others each carries, as its
|
|
335
|
+
// app.licenses lists it. An extension in the project's own folders is the project's code, as the app is.
|
|
336
|
+
function extensionRows(platform) {
|
|
337
|
+
return extensionsIn(platform).flatMap((extension) => {
|
|
338
|
+
const own = extensionPackage(extension.root, state.config.paths.base);
|
|
339
|
+
const owner = own?.manifest.name ?? null;
|
|
340
|
+
const license = own?.manifest.license ?? null;
|
|
341
|
+
const shipped = own && packageLicenseFile(own.dir);
|
|
342
|
+
const packageRows = own && !own.manifest.private ? [{
|
|
343
|
+
npmName: owner,
|
|
344
|
+
name: owner,
|
|
345
|
+
version: own.manifest.version ?? null,
|
|
346
|
+
nativeVersion: null,
|
|
347
|
+
license,
|
|
348
|
+
licenseDeclared: license,
|
|
349
|
+
licenseSelected: null,
|
|
350
|
+
isCopyleft: Boolean(license) && isCopyleft(license),
|
|
351
|
+
sourceUrl: own.manifest.homepage ?? null,
|
|
352
|
+
licenseNotes: `the app extension ${extension.name}, which the app's host compiles in`,
|
|
353
|
+
licenseText: shipped ? textOf(shipped) : null,
|
|
354
|
+
}] : [];
|
|
355
|
+
const carried = (extension.app.licenses ?? []).map((entry) => carriedRow(
|
|
356
|
+
entry,
|
|
357
|
+
extension.root,
|
|
358
|
+
`carried by the app extension ${extension.name}${owner ? ` of ${owner}` : ''}, which the app's host compiles in`,
|
|
359
|
+
));
|
|
360
|
+
return [...packageRows, ...carried];
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
309
364
|
function appRuntimeRows(platform) {
|
|
310
365
|
const hostRoot = resolveAppHostRoot();
|
|
311
366
|
const hermesLicense = textOf(path.join(hostRoot, 'third_party', 'jsi', 'LICENSE'));
|
|
@@ -347,24 +402,9 @@ function appRuntimeRows(platform) {
|
|
|
347
402
|
nativeVersion: WEBVIEW.version,
|
|
348
403
|
sha256: WEBVIEW.sha256,
|
|
349
404
|
sourceUrl: 'https://github.com/webview/webview',
|
|
350
|
-
licenseNotes: 'webview/webview, with the local
|
|
405
|
+
licenseNotes: 'webview/webview, with the local patches @crossbind/core-app-host lists',
|
|
351
406
|
licenseText: textOf(path.join(hostRoot, 'third_party', 'webview', 'LICENSE')),
|
|
352
407
|
}]),
|
|
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
408
|
];
|
|
369
409
|
}
|
|
370
410
|
|
|
@@ -419,9 +459,9 @@ async function buildRow(node) {
|
|
|
419
459
|
};
|
|
420
460
|
}
|
|
421
461
|
|
|
422
|
-
//
|
|
423
|
-
function
|
|
424
|
-
const {
|
|
462
|
+
// The facts of a package source's package come from its recipe, and its texts from the licenses/ its package ships.
|
|
463
|
+
function sourcePackageRow(node) {
|
|
464
|
+
const { name: source, package: pkg } = node.general.packageSource;
|
|
425
465
|
const licenseDir = `${node.paths.project}/licenses`;
|
|
426
466
|
const files = fs.existsSync(licenseDir)
|
|
427
467
|
? fs.readdirSync(licenseDir, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile())
|
|
@@ -429,29 +469,32 @@ function conanRow(node) {
|
|
|
429
469
|
const texts = files.map((entry) => path.join(entry.parentPath, entry.name)).sort()
|
|
430
470
|
.map((file) => `=== ${path.relative(licenseDir, file)} ===\n\n${fs.readFileSync(file, 'utf8').trim()}`);
|
|
431
471
|
return {
|
|
432
|
-
name:
|
|
472
|
+
name: pkg.name,
|
|
433
473
|
npmName: null,
|
|
434
474
|
version: null,
|
|
435
|
-
nativeVersion:
|
|
436
|
-
license:
|
|
475
|
+
nativeVersion: pkg.version,
|
|
476
|
+
license: pkg.license,
|
|
437
477
|
licenseDeclared: null,
|
|
438
478
|
licenseSelected: null,
|
|
439
|
-
licenseNotes: `built from the
|
|
440
|
-
sha256:
|
|
441
|
-
sourceUrl:
|
|
479
|
+
licenseNotes: `built from the ${source} package ${pkg.ref}`,
|
|
480
|
+
sha256: pkg.source?.sha256 ?? null,
|
|
481
|
+
sourceUrl: pkg.source?.url ?? pkg.homepage ?? null,
|
|
442
482
|
licenseText: texts.length > 0 ? texts.join('\n\n') : null,
|
|
443
|
-
isCopyleft: isCopyleft(
|
|
444
|
-
purl: `pkg
|
|
483
|
+
isCopyleft: isCopyleft(pkg.license),
|
|
484
|
+
purl: `pkg:${source}/${pkg.name}@${pkg.version}`,
|
|
445
485
|
};
|
|
446
486
|
}
|
|
447
487
|
|
|
448
|
-
//
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
488
|
+
// The packages of package sources join the graph from the stage a build makes; without it they would be left out unseen.
|
|
489
|
+
function assertSourcesStaged() {
|
|
490
|
+
declaredSources(state.config).forEach(({ name }) => {
|
|
491
|
+
const attached = new Set(state.config.allDependencies.filter((d) => d.general.packageSource?.name === name)
|
|
492
|
+
.map((d) => d.general.packageSource.package.name));
|
|
493
|
+
const missing = Object.keys(dependenciesOf(state.config, name)).filter((pkg) => !attached.has(pkg));
|
|
494
|
+
if (missing.length > 0) {
|
|
495
|
+
throw new Error(`crossbind: the ${name} packages ${missing.join(', ')} are not installed for the current ${declarationOf(name)}, so their rows would be missing. Build the project first.`);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
455
498
|
}
|
|
456
499
|
|
|
457
500
|
// platform (optional, e.g. 'wasi') additionally includes what that platform's
|
|
@@ -460,23 +503,25 @@ function assertConanStaged() {
|
|
|
460
503
|
// addon of that platform compiles in, or without a platform the one its loader bundles;
|
|
461
504
|
// runtimeEnv 'app' adds what an app's host carries.
|
|
462
505
|
export default async function collectLicenseRows(platform = null, { runtimeEnv = null } = {}) {
|
|
463
|
-
|
|
506
|
+
assertSourcesStaged();
|
|
464
507
|
// The root package is a component too when it is a port: leaf -wasi packages have no deps but ship
|
|
465
508
|
// their own upstream. A dependency that is no port, such as a library a project publishes, is one by
|
|
466
509
|
// its package name; a private package is the project's own code, like the app.
|
|
467
|
-
const keyOf = (
|
|
510
|
+
const keyOf = ({ general, package: manifest }) => (general.packageSource
|
|
511
|
+
? `${general.packageSource.name}:${general.packageSource.package.name}`
|
|
512
|
+
: general.alias?.package ?? manifest?.name);
|
|
468
513
|
const nodes = [state.config, ...state.config.allDependencies]
|
|
469
514
|
.filter((node, index) => node?.paths?.project
|
|
470
|
-
&& (node.general?.alias?.package || node.general?.
|
|
515
|
+
&& (node.general?.alias?.package || node.general?.packageSource || (index > 0 && node.package?.name && !node.package.private)));
|
|
471
516
|
const rows = [];
|
|
472
517
|
const seen = new Set();
|
|
473
518
|
for (const node of nodes) {
|
|
474
519
|
const key = keyOf(node);
|
|
475
520
|
if (seen.has(key)) continue;
|
|
476
521
|
seen.add(key);
|
|
477
|
-
rows.push(node.general.
|
|
478
|
-
// A
|
|
479
|
-
if (platform && !node.general.
|
|
522
|
+
rows.push(node.general.packageSource ? sourcePackageRow(node) : await buildRow(node));
|
|
523
|
+
// A package source's package folder is written from what its recipe produced: nothing there is run.
|
|
524
|
+
if (platform && !node.general.packageSource) {
|
|
480
525
|
const familyProjects = nodes.filter((other) => keyOf(other) === key).map((other) => other.paths.project);
|
|
481
526
|
rows.push(...await bundledRowsOf(node, platform, familyProjects));
|
|
482
527
|
}
|
|
@@ -484,6 +529,16 @@ export default async function collectLicenseRows(platform = null, { runtimeEnv =
|
|
|
484
529
|
if (platform === 'wasi') rows.push(...wasiToolchainRows());
|
|
485
530
|
if (['linux', 'linuxmusl', 'win32'].includes(platform)) rows.push(...addonToolchainRows(platform));
|
|
486
531
|
if (runtimeEnv === 'node') rows.push(...nodeRuntimeRows({ isAddon: platform !== null }));
|
|
487
|
-
if (runtimeEnv === 'app')
|
|
532
|
+
if (runtimeEnv === 'app') {
|
|
533
|
+
rows.push(...appRuntimeRows(platform));
|
|
534
|
+
// A library two extensions carry, or one the app has already, is listed once for each version the app ships.
|
|
535
|
+
const versionOf = (row) => `${row.name}@${row.nativeVersion ?? row.version}`;
|
|
536
|
+
const listed = new Set(rows.map(versionOf));
|
|
537
|
+
for (const row of [...extensionRows(platform), ...installerRows(platform)]) {
|
|
538
|
+
if (listed.has(versionOf(row))) continue;
|
|
539
|
+
listed.add(versionOf(row));
|
|
540
|
+
rows.push(row);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
488
543
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
489
544
|
}
|