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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/package.json +70 -59
  3. package/src/actions/addExtension.js +196 -0
  4. package/src/actions/androidGradle.js +26 -17
  5. package/src/actions/appInstaller.js +69 -153
  6. package/src/actions/appSwift.js +66 -0
  7. package/src/actions/buildApp.js +177 -74
  8. package/src/actions/buildDependencies.js +3 -3
  9. package/src/actions/buildNode.js +6 -1
  10. package/src/actions/buildWasm.js +0 -9
  11. package/src/actions/createImportedBridges.js +10 -8
  12. package/src/actions/createInterface.js +40 -29
  13. package/src/actions/createLib.js +3 -7
  14. package/src/actions/extensionApi.js +298 -0
  15. package/src/actions/extensionCommands.js +41 -0
  16. package/src/actions/extensionsInApp.js +19 -0
  17. package/src/actions/getAllBridges.js +20 -5
  18. package/src/actions/getDependLibs.js +5 -0
  19. package/src/actions/getLinkInputs.js +2 -1
  20. package/src/actions/gradleProject.js +207 -16
  21. package/src/actions/iosDevice.js +2 -2
  22. package/src/actions/iosSimulator.js +3 -3
  23. package/src/actions/licenses.js +113 -58
  24. package/src/actions/linuxPackages.js +48 -0
  25. package/src/actions/mavenVerification.js +37 -0
  26. package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
  27. package/src/actions/publish.js +220 -0
  28. package/src/actions/windowsSigning.js +1 -1
  29. package/src/actions/writeRuntimeEntry.js +2 -2
  30. package/src/actions/xcodeProject.js +60 -29
  31. package/src/actions/xcodeTargets.js +122 -0
  32. package/src/actions/xcodebuild.js +65 -0
  33. package/src/app/index.d.ts +144 -132
  34. package/src/app/index.js +147 -185
  35. package/src/assets/configure/install.sh +0 -0
  36. package/src/assets/node-import-hooks.mjs +7 -6
  37. package/src/assets/toolchain-digests.json +20 -20
  38. package/src/bin.js +37 -37
  39. package/src/index.js +4 -3
  40. package/src/integration/getCrossbindScript.js +3 -2
  41. package/src/integration/getDependFilePath.js +10 -10
  42. package/src/program.js +32 -3
  43. package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
  44. package/src/state/index.js +8 -6
  45. package/src/state/loadConfig.js +26 -9
  46. package/src/state/refreshPackageSources.js +19 -0
  47. package/src/utils/appExtensions.js +777 -0
  48. package/src/utils/appFiles.js +58 -0
  49. package/src/utils/appLaunches.js +249 -0
  50. package/src/utils/appOptions.js +142 -112
  51. package/src/utils/appRustCrates.js +2 -2
  52. package/src/utils/appTargets.js +168 -0
  53. package/src/utils/bridgeTools.js +71 -0
  54. package/src/utils/configChanges.js +30 -0
  55. package/src/utils/configWriter.js +143 -26
  56. package/src/utils/cppDts.js +11 -11
  57. package/src/utils/extensionSteps.js +172 -0
  58. package/src/utils/nativeImports.js +9 -2
  59. package/src/utils/nodeDevBuild.js +2 -1
  60. package/src/utils/openUrl.js +2 -1
  61. package/src/utils/packageSourceApi.js +87 -0
  62. package/src/utils/packageSourceImport.js +69 -0
  63. package/src/utils/packageSourceInstall.js +162 -0
  64. package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
  65. package/src/utils/packageSources.js +108 -0
  66. package/src/utils/plist.js +5 -0
  67. package/src/utils/printableText.js +5 -0
  68. package/src/utils/resolveAppHost.js +7 -0
  69. package/src/utils/runPackageSource.js +251 -0
  70. package/src/utils/rustBridgeGen.js +3 -3
  71. package/src/utils/systemConfig.js +16 -3
  72. package/src/utils/targetToolchain.js +77 -0
  73. package/vitest.config.js +6 -1
  74. package/src/actions/appSetup.js +0 -116
  75. package/src/actions/appStoreConnect.js +0 -188
  76. package/src/actions/appUpdate.js +0 -68
  77. package/src/actions/extensions.js +0 -7
  78. package/src/actions/googlePlay.js +0 -213
  79. package/src/actions/googleSignIn.js +0 -78
  80. package/src/actions/playSetup.js +0 -19
  81. package/src/actions/publishAndroid.js +0 -102
  82. package/src/actions/publishIos.js +0 -146
  83. package/src/state/refreshConanDependencies.js +0 -17
  84. package/src/utils/conanDependencies.js +0 -66
  85. package/src/utils/conanImport.js +0 -58
  86. package/src/utils/conanInstall.js +0 -154
  87. package/src/utils/conanProfile.js +0 -203
  88. 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.cargoDependencies ?? {});
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
- const usesPermissions = (permissions) => permissions
46
- .flatMap((name) => ANDROID_PERMISSIONS[name])
47
- .map((permission) => ` <uses-permission android:name="android.permission.${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
- // The links of app.urlSchemes the activity opens, from a browser as well, before the end of the activity's element.
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 urlFilter = (schemes) => [
52
- ' <intent-filter>',
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
- ...schemes.map((scheme) => ` <data android:scheme="${scheme}" />`),
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, '&amp;').replace(/</g, '&lt;')
157
+ .replace(/>/g, '&gt;');
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
- 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
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 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 = [] }) {
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, urlSchemes.length > 0 ? `${urlFilter(urlSchemes)}\n${ACTIVITY_END}` : 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', theme: background ? '@style/CrossbindBackground' : DAY_NIGHT,
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
  }
@@ -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 './appStoreConnect.js';
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. The process returned carries the app's output
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
  }
@@ -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 third_party READMEs of @crossbind/core-app-host
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, and the NSIS its setup program carries. webview/webview
277
- // loads the WebView2 Runtime itself, without the SDK's loader.
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 patch @crossbind/core-app-host lists',
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
- // A Conan package's facts come from its recipe, and its texts from the licenses/ its package ships.
423
- function conanRow(node) {
424
- const { conan } = node.general;
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: conan.name,
472
+ name: pkg.name,
433
473
  npmName: null,
434
474
  version: null,
435
- nativeVersion: conan.version,
436
- license: conan.license,
475
+ nativeVersion: pkg.version,
476
+ license: pkg.license,
437
477
  licenseDeclared: null,
438
478
  licenseSelected: null,
439
- licenseNotes: `built from the ConanCenter recipe ${conan.ref}`,
440
- sha256: conan.source?.sha256 ?? null,
441
- sourceUrl: conan.source?.url ?? conan.homepage ?? null,
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(conan.license),
444
- purl: `pkg:conan/${conan.name}@${conan.version}`,
483
+ isCopyleft: isCopyleft(pkg.license),
484
+ purl: `pkg:${source}/${pkg.name}@${pkg.version}`,
445
485
  };
446
486
  }
447
487
 
448
- // Conan packages join the graph from the stage a build makes; without it they would be left out unseen.
449
- function assertConanStaged() {
450
- const attached = new Set(state.config.allDependencies.filter((d) => d.general.conan).map((d) => d.general.conan.name));
451
- const missing = Object.keys(state.config.conanDependencies ?? {}).filter((name) => !attached.has(name));
452
- if (missing.length > 0) {
453
- throw new Error(`crossbind: the Conan packages ${missing.join(', ')} are not installed for the current conanDependencies, so their rows would be missing. Build the project first.`);
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
- assertConanStaged();
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 = (node) => (node.general.conan ? `conan:${node.general.conan.name}` : node.general.alias?.package ?? node.package?.name);
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?.conan || (index > 0 && node.package?.name && !node.package.private)));
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.conan ? conanRow(node) : await buildRow(node));
478
- // A Conan package's folder is written from what its recipe produced: nothing there is run.
479
- if (platform && !node.general.conan) {
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') rows.push(...appRuntimeRows(platform));
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
  }