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
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { spawnSync } from 'node:child_process';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  import { rollup } from 'rollup';
7
6
  import { nodeResolve } from '@rollup/plugin-node-resolve';
@@ -20,8 +19,8 @@ import writeXcodeProject, { HERMES_SLICES, IOS_SDKS } from './xcodeProject.js';
20
19
  import writeGradleProject, { ANDROID_ABIS } from './gradleProject.js';
21
20
  import { attachedDevice } from './androidDevice.js';
22
21
  import { packageAndroidRelease } from './androidGradle.js';
23
- import writeInstaller, { assertUnderBase } from './appInstaller.js';
24
- import writeUpdate from './appUpdate.js';
22
+ import { compileSwift, swiftLinkArgs } from './appSwift.js';
23
+ import { unpackPackages } from './linuxPackages.js';
25
24
  import { APPLE_SIGNING, appleSigning, notarizeApp } from './appleSigning.js';
26
25
  import { WINDOWS_SIGNING, signWindows, windowsSigning } from './windowsSigning.js';
27
26
  import state from '../state/index.js';
@@ -30,10 +29,15 @@ import findFiles from '../utils/findFiles.js';
30
29
  import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
31
30
  import { xmlPlist } from '../utils/plist.js';
32
31
  import {
33
- appBackgroundColor, appCategory, appCopyright, appDisplayName, appEncryption, appHeader, appId, appPermissions, appPrivacy, appUrlSchemes,
34
- usageDescriptions,
32
+ androidAppId, appBackgroundColor, appCategory, appCopyright, appDarkBackgroundColor, appDisplayName, appEncryption, appFiles, appHeader, appId,
33
+ appPermissions, appPrivacy, appUrlSchemes, iosAppGroup, iosMultipleWindows, urlSchemeOwners, usageDescriptions,
35
34
  } from '../utils/appOptions.js';
35
+ import { carriedFilesOf, copyCarriedFiles } from '../utils/appFiles.js';
36
+ import { activityTypeOwners, fileTypeOwners, linkOwners, macDocumentTypes, shortcutItemOwners } from '../utils/appLaunches.js';
36
37
  import { unreasonedApis, unreasonedWarning } from '../utils/requiredReasonApis.js';
38
+ import {
39
+ appExtensionsOf, extensionCmakeArgs, extensionPart, mergedEntitlements, mergedPlist, mergedPrivacy, registrationSource,
40
+ } from '../utils/appExtensions.js';
37
41
  import { androidIcons, iosIcon, linuxIcon, macIcon, readAppIcon, windowsIcon } from '../utils/appIcon.js';
38
42
  import writeIfChanged from '../utils/writeIfChanged.js';
39
43
  import { writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
@@ -41,8 +45,10 @@ import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
41
45
  import resolveAppHostRoot, { resolveAppEmbindJsiRoot } from '../utils/resolveAppHost.js';
42
46
  import resolveHermes, { HERMES } from '../utils/hermes.js';
43
47
  import { getDockerImage, imageRoleFor } from '../utils/pullDockerImage.js';
48
+ import { isUnderBase } from '../utils/replaceBasePathForDocker.js';
44
49
  import { chosenRunner } from '../utils/selectRunner.js';
45
50
  import { DARWIN_DEPLOYMENT_TARGET } from '../utils/darwinToolchain.js';
51
+ import extensionsInApp from './extensionsInApp.js';
46
52
 
47
53
  const cpuCount = Math.max(1, os.cpus().length - 1);
48
54
  const BRIDGE = 'AppBridge';
@@ -50,6 +56,17 @@ const LINUX_MULTIARCH = { x64: 'x86_64-linux-gnu', arm64: 'aarch64-linux-gnu' };
50
56
  // Where the android-app, linux and windows images keep the Hermes of the release crossbind pins.
51
57
  const IMAGE_HERMES = `/opt/crossbind/hermes/${HERMES.version}`;
52
58
 
59
+ // What the app's extensions add to an app of this platform.
60
+ const extensionsOf = (platform) => extensionPart(extensionsInApp(platform, (message) => console.warn(`crossbind: ${message}.`)), platform);
61
+
62
+ // The App Group an iOS app shares with the targets of its extensions' that ask for one, or none; app.ios.appGroup is
63
+ // checked all the same.
64
+ function appGroupOf(platform, extensions) {
65
+ if (platform !== 'ios') return null;
66
+ const group = iosAppGroup(state.config);
67
+ return extensions.targets.some(({ appGroup }) => appGroup) ? group : null;
68
+ }
69
+
53
70
  // Where an app keeps its executable and its files: a macOS bundle, a Linux or Windows directory, or the app folder of an
54
71
  // iOS app's Xcode project or of an Android app's Gradle project, which link the executable themselves.
55
72
  export function appLayout(target) {
@@ -123,24 +140,12 @@ export function appTeam() {
123
140
  return team;
124
141
  }
125
142
 
126
- // An Android application id: dot-separated parts of letters, digits and underscores that each start with a letter.
127
- const ANDROID_APP_ID = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
128
-
129
- // An Android app's id: the project's app.id, else one made from the project's name.
130
- export function androidAppId() {
131
- const id = state.config.app.id ?? `dev.crossbind.${state.config.general.name.replace(/[^A-Za-z0-9.]/g, '_')}`;
132
- if (!ANDROID_APP_ID.test(id)) {
133
- throw new Error(`crossbind: ${JSON.stringify(id)} (app.id, else made from the project's name) is no Android application id, which takes dot-separated parts of letters, digits and underscores that each start with a letter, as com.example.app does. Set app.id in crossbind.config.js.`);
134
- }
135
- return id;
136
- }
137
-
138
143
  // Apple takes dot-separated integers alone as an app's version.
139
144
  export const appVersion = () => (state.config.package?.version ?? '1.0.0').replace(/[-+].*$/, '');
140
145
 
141
146
  const BUILD_NUMBER = 'CROSSBIND_APP_BUILD_NUMBER';
142
147
  // Google Play takes no larger version code.
143
- const MAX_VERSION_CODE = 2100000000;
148
+ export const MAX_VERSION_CODE = 2100000000;
144
149
 
145
150
  // The number CI or a publish gives a build, which iOS takes as its build and Android as its version code:
146
151
  // CROSSBIND_APP_BUILD_NUMBER, a whole number both stores take, else none.
@@ -204,11 +209,16 @@ async function bundle(input, file, plugins) {
204
209
  await build.close();
205
210
  }
206
211
 
207
- // A development build claims no scheme: the built app, of the same id, owns them, and a link it opens shows its own page.
212
+ // A development build claims no scheme, no file type and no user activity: the built app, of the same id, owns them, and
213
+ // a link it opens shows its own page.
208
214
  const urlSchemesOf = (target) => (target.buildType === 'release' ? appUrlSchemes(state.config) : []);
215
+ const fileTypesOf = (target) => (target.buildType === 'release' ? fileTypeOwners(state.config) : []);
216
+ const activityTypesOf = (target) => (target.buildType === 'release' ? activityTypeOwners(state.config).map(({ type }) => type) : []);
209
217
 
210
- const infoPlist = (name, version, hasIcon, urlSchemes) => xmlPlist({
218
+ // The app's Info.plist, with the keys its extensions give.
219
+ const infoPlist = (name, version, hasIcon, urlSchemes, fileTypes, activityTypes) => xmlPlist(mergedPlist({
211
220
  CFBundleDisplayName: appDisplayName(state.config),
221
+ ...(fileTypes.length > 0 ? { CFBundleDocumentTypes: macDocumentTypes(fileTypes) } : {}),
212
222
  CFBundleExecutable: name,
213
223
  // Contents/Resources/icon.icns.
214
224
  ...(hasIcon ? { CFBundleIconFile: 'icon' } : {}),
@@ -224,8 +234,12 @@ const infoPlist = (name, version, hasIcon, urlSchemes) => xmlPlist({
224
234
  NSHighResolutionCapable: true,
225
235
  // The app menu's About item shows it.
226
236
  ...(appCopyright(state.config) ? { NSHumanReadableCopyright: appCopyright(state.config) } : {}),
237
+ ...(activityTypes.length > 0 ? { NSUserActivityTypes: activityTypes } : {}),
227
238
  ...usageDescriptions(state.config, 'darwin'),
228
- });
239
+ }, extensionsOf('darwin').plist));
240
+
241
+ // The app's own files, as app.files lists them, by the project's folder.
242
+ const ownFiles = () => ({ root: state.config.paths.project, paths: appFiles(state.config) });
229
243
 
230
244
  // The host's bytecode and the data the packages ship, beside the page's folder.
231
245
  function copyAppFiles(dir, hbc, target) {
@@ -234,6 +248,7 @@ function copyAppFiles(dir, hbc, target) {
234
248
  Object.entries(getData('data', target)).forEach(([source, dataName]) => {
235
249
  if (fs.existsSync(source)) fs.cpSync(source, `${dir}/data/${dataName}`, { recursive: true });
236
250
  });
251
+ copyCarriedFiles(dir, extensionsOf(target.platform).files, ownFiles());
237
252
  }
238
253
 
239
254
  // Everything but the page: the host and the app's files, its icon, and on macOS Hermes beside them. A Windows host
@@ -248,7 +263,7 @@ function assembleApp(target, { executable, hbc, hermes, icon }) {
248
263
  copyAppFiles(`${app}/${layout.resources}`, hbc, target);
249
264
  if (target.platform === 'linux' && icon) fs.writeFileSync(`${app}/${layout.resources}/icon.png`, linuxIcon(icon));
250
265
  if (target.platform !== 'darwin') return;
251
- fs.writeFileSync(`${app}/Contents/Info.plist`, infoPlist(name, appVersion(), Boolean(icon), urlSchemesOf(target)));
266
+ fs.writeFileSync(`${app}/Contents/Info.plist`, infoPlist(name, appVersion(), Boolean(icon), urlSchemesOf(target), fileTypesOf(target), activityTypesOf(target)));
252
267
  if (icon) fs.writeFileSync(`${app}/Contents/Resources/icon.icns`, macIcon(icon));
253
268
  const framework = `${app}/Contents/Frameworks/hermesvm.framework`;
254
269
  fs.cpSync(`${hermes}/Library/Frameworks/macosx/hermesvm.framework`, framework, { recursive: true, verbatimSymlinks: true });
@@ -301,20 +316,10 @@ const appParts = (target) => state.targets
301
316
  .filter((t) => t.platform === target.platform && t.runtimeEnv === 'app' && t.buildType === target.buildType && currentApps.has(t.appName))
302
317
  .map((t) => ({ arch: t.arch, dir: `${state.config.paths.build}/${t.appName}` }));
303
318
 
304
- // A Linux or Windows app also ships as one archive, next to its directory: a Windows one as a zip, made in the windows
305
- // image.
306
- function archiveApp(app, target) {
307
- if (target.platform === 'win32') {
308
- assertUnderBase(app, target);
309
- fs.rmSync(`${app}.zip`, { force: true });
310
- run(null, ['zip', '-q', '-r', '-X', `${path.basename(app)}.zip`, path.basename(app)], null, target, { workdir: path.dirname(app) });
311
- return;
312
- }
313
- const result = spawnSync('tar', ['-czf', `${app}.tar.gz`, '-C', path.dirname(app), path.basename(app)], {
314
- encoding: 'utf8',
315
- env: { ...process.env, COPYFILE_DISABLE: '1' },
316
- });
317
- if (result.status !== 0) throw new Error(`crossbind: cannot archive ${app}: ${result.stderr || result.error?.message}`);
319
+ // The container that signs a Windows app sees the project's base alone.
320
+ function assertUnderBase(app, target) {
321
+ if (isUnderBase(app, state.config.paths.base)) return;
322
+ throw new Error(`crossbind: the Windows app ${app} is outside ${state.config.paths.base}, the project's base and all the ${imageRoleFor(target)} container sees. Build the app into a directory under it.`);
318
323
  }
319
324
 
320
325
  const ENTITLEMENTS = {
@@ -329,10 +334,12 @@ const ENTITLEMENTS = {
329
334
  function signApp(app, target, signing) {
330
335
  const sign = signing ? ['--sign', signing.identity, '--options', 'runtime', '--timestamp'] : ['--sign', '-', '--timestamp=none'];
331
336
  run(null, ['codesign', '--force', ...sign, `${app}/Contents/Frameworks/hermesvm.framework`], null, target);
332
- // The hardened runtime keeps the app from the devices of app.permissions unless its signature claims them.
333
- const claims = Object.keys(appPermissions(state.config)).map((name) => ENTITLEMENTS[name]);
334
- const entitlements = claims.length > 0 ? `${state.config.paths.build}/${path.basename(app)}.entitlements` : null;
335
- if (entitlements) fs.writeFileSync(entitlements, xmlPlist(Object.fromEntries(claims.map((claim) => [claim, true]))));
337
+ // The hardened runtime keeps the app from the devices of app.permissions unless its signature claims them, beside what
338
+ // its extensions claim.
339
+ const devices = Object.fromEntries(Object.keys(appPermissions(state.config)).map((name) => [ENTITLEMENTS[name], true]));
340
+ const claims = mergedEntitlements([['app.permissions', devices], ...extensionsOf('darwin').entitlements]);
341
+ const entitlements = Object.keys(claims).length > 0 ? `${state.config.paths.build}/${path.basename(app)}.entitlements` : null;
342
+ if (entitlements) fs.writeFileSync(entitlements, xmlPlist(claims));
336
343
  run(null, ['codesign', '--force', ...sign, ...(entitlements ? ['--entitlements', entitlements] : []), app], null, target);
337
344
  }
338
345
 
@@ -363,22 +370,56 @@ function warnUnreasonedApis(project, privacy) {
363
370
  if (warning) console.warn(warning);
364
371
  }
365
372
 
366
- // Where publishApp last put this project's iOS and Android apps, for `crossbind app publish`, which runs Vite and so
367
- // cannot know the outDir it builds into.
373
+ // Where publishApp last put this project's apps, for `crossbind app publish`, which runs Vite and so cannot know the
374
+ // outDir it builds into: an iOS or Android app's project, and each arch's desktop app under its platform's name on the
375
+ // command line.
368
376
  const appOutputsFile = () => `${state.config.paths.build}/app-outputs.json`;
369
377
  const readAppOutputs = () => (fs.existsSync(appOutputsFile()) ? JSON.parse(fs.readFileSync(appOutputsFile(), 'utf8')) : {});
378
+ // What each desktop app's installers wrote beside it, as builds recorded them.
379
+ const shippedFiles = () => new Set(Object.values(readAppOutputs()).filter(Array.isArray).flat()
380
+ .flatMap(({ installers = [] }) => installers.flatMap(({ files }) => files)));
381
+ // The command line's names of the desktop platforms.
382
+ export const DESKTOP_PLATFORMS = { darwin: 'macos', linux: 'linux', win32: 'windows' };
383
+
384
+ // The platform an app is built for, which its page reads as import.meta.env.CROSSBIND_APP_PLATFORM: CROSSBIND_APP_PLATFORM,
385
+ // else this machine's desktop.
386
+ export const appPlatform = (env = process.env) => env.CROSSBIND_APP_PLATFORM || DESKTOP_PLATFORMS[process.platform];
387
+
388
+ // Forgets what the builds wrote, as a publish's own build writes it anew, but for the installers' files, which a folder the
389
+ // bundler kept holds still, for the page to leave out.
390
+ export function clearAppOutputs() {
391
+ const earlier = [...shippedFiles()];
392
+ if (earlier.length > 0) fs.writeFileSync(appOutputsFile(), JSON.stringify({ earlier: [{ installers: [{ files: earlier }] }] }));
393
+ else fs.rmSync(appOutputsFile(), { force: true });
394
+ }
370
395
 
371
- export const clearAppOutputs = () => fs.rmSync(appOutputsFile(), { force: true });
372
-
396
+ // What a publisher's upload gets of the app the build wrote for a platform: a desktop platform's apps, one for each arch.
373
397
  export function appOutputOf(platform) {
374
398
  const output = readAppOutputs()[platform];
375
399
  if (!output) throw new Error(`crossbind: the build wrote no ${platform} app, so there is nothing to publish.`);
376
- return output;
400
+ return Object.values(DESKTOP_PLATFORMS).includes(platform) ? { apps: output } : output;
377
401
  }
378
402
 
379
- function recordAppOutput(platform, to) {
403
+ const madeFile = (file) => (fs.existsSync(file) ? file : null);
404
+
405
+ // What a publisher's upload takes of an app: an iOS app's project, an Android app's with its APK, bundle and native
406
+ // debug symbols where the build made them, and each arch's desktop app with its installers.
407
+ export function recordAppOutput(target, to, { installers = [] } = {}) {
408
+ const outputs = readAppOutputs();
409
+ const app = path.resolve(to);
410
+ const desktop = DESKTOP_PLATFORMS[target.platform];
411
+ const mobile = target.platform === 'android'
412
+ ? { project: app, apk: madeFile(`${app}.apk`), bundle: madeFile(`${app}.aab`), symbols: madeFile(`${app}-symbols.zip`) }
413
+ : { project: app };
414
+ const recorded = desktop
415
+ ? { [desktop]: [...(outputs[desktop] ?? []).filter((entry) => entry.app !== app), {
416
+ app,
417
+ arch: target.arch,
418
+ installers: installers.map(({ name, files }) => ({ name, files: files.map((file) => path.resolve(file)) })),
419
+ }] }
420
+ : { [target.platform]: mobile };
380
421
  fs.mkdirSync(state.config.paths.build, { recursive: true });
381
- fs.writeFileSync(appOutputsFile(), `${JSON.stringify({ ...readAppOutputs(), [platform]: path.resolve(to) }, null, 4)}\n`);
422
+ fs.writeFileSync(appOutputsFile(), `${JSON.stringify({ ...outputs, ...recorded }, null, 4)}\n`);
382
423
  }
383
424
 
384
425
  // Where a build without a bundler puts an app: under its own name, or for iOS and Android as the project of its build
@@ -396,19 +437,35 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
396
437
  if (target.platform === 'ios') {
397
438
  const parts = appParts(target).map(({ arch, dir }) => ({ sdk: arch, dir }));
398
439
  const icon = readAppIcon(state.config);
399
- const privacy = appPrivacy(state.config);
440
+ const ownPrivacy = appPrivacy(state.config);
400
441
  const build = appBuildNumber();
401
- writeXcodeProject(to, {
442
+ // The app's own options are checked first, then what its extensions add.
443
+ const options = {
402
444
  parts, identity: { id: appId(state.config), team: appTeam() }, label: appDisplayName(state.config), version: appVersion(),
403
445
  build: build === null ? appVersion() : String(build), icon: icon && iosIcon(icon), urlSchemes: appUrlSchemes(state.config),
404
- background: appBackgroundColor(state.config), usage: usageDescriptions(state.config, 'ios'), encryption: appEncryption(state.config), privacy,
446
+ fileTypes: fileTypeOwners(state.config), links: linkOwners(state.config).map(({ link }) => link),
447
+ activityTypes: activityTypeOwners(state.config).map(({ type }) => type), shortcutItems: shortcutItemOwners(state.config),
448
+ background: appBackgroundColor(state.config), darkBackground: appDarkBackgroundColor(state.config), usage: usageDescriptions(state.config, 'ios'),
449
+ encryption: appEncryption(state.config), multipleWindows: iosMultipleWindows(state.config),
450
+ };
451
+ const extensions = extensionsOf('ios');
452
+ const privacy = mergedPrivacy(ownPrivacy, extensions.privacy);
453
+ writeXcodeProject(to, {
454
+ ...options, privacy, entitlements: mergedEntitlements(extensions.entitlements), plist: extensions.plist, targets: extensions.targets,
455
+ appGroup: appGroupOf('ios', extensions),
405
456
  });
406
457
  if (target.buildType === 'release') warnUnreasonedApis(to, privacy);
407
458
  } else if (target.platform === 'android') {
408
459
  const icon = readAppIcon(state.config);
460
+ const options = {
461
+ parts: appParts(target), id: androidAppId(state.config), label: appDisplayName(state.config), version: androidVersion(), icons: icon && androidIcons(icon),
462
+ urlSchemes: appUrlSchemes(state.config), background: appBackgroundColor(state.config), darkBackground: appDarkBackgroundColor(state.config),
463
+ permissions: Object.keys(appPermissions(state.config)),
464
+ fileTypes: fileTypeOwners(state.config), links: linkOwners(state.config).map(({ link }) => link), shortcutItems: shortcutItemOwners(state.config),
465
+ };
466
+ const extensions = extensionsOf('android');
409
467
  writeGradleProject(to, {
410
- parts: appParts(target), id: androidAppId(), label: appDisplayName(state.config), version: androidVersion(), icons: icon && androidIcons(icon),
411
- urlSchemes: appUrlSchemes(state.config), background: appBackgroundColor(state.config), permissions: Object.keys(appPermissions(state.config)),
468
+ ...options, java: extensions.java, extensionPermissions: extensions.permissions, manifests: extensions.manifests, resources: extensions.resources, maven: extensions.maven,
412
469
  });
413
470
  } else if (path.resolve(to) !== path.resolve(built)) {
414
471
  fs.rmSync(to, { recursive: true, force: true });
@@ -416,11 +473,14 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
416
473
  }
417
474
  if (page) {
418
475
  // The app may land inside the page's own directory, which it then leaves out, with every app built there and what
419
- // packages it.
420
- const apps = new Set([path.basename(to), ...state.targets.filter((t) => t.runtimeEnv === 'app')
421
- .flatMap((t) => [appOutputName(t), `${state.config.general.name}-${t.platform}-${t.arch}`])]);
422
- const isApp = (entry) => entry.endsWith('.app') || apps.has(entry.replace(/\.dmg$/, '.app'))
423
- || apps.has(entry.replace(/(\.tar\.gz|\.app\.zip|\.zip|\.apk|\.aab|\.deb|-setup\.exe|\.update\.json)$/, ''));
476
+ // packages it: what is named after a target of the app, and every installer's files as a build recorded them, which
477
+ // a folder the bundler left as it was holds still.
478
+ const apps = state.targets.filter((t) => t.runtimeEnv === 'app')
479
+ .flatMap((t) => [appOutputName(t), `${state.config.general.name}-${t.platform}-${t.arch}`]).filter((name) => !name.endsWith('.app'));
480
+ const shipped = shippedFiles();
481
+ const isApp = (entry) => entry === path.basename(to) || entry.endsWith('.app')
482
+ || apps.some((app) => entry === app || entry.startsWith(`${app}.`) || entry.startsWith(`${app}-`))
483
+ || shipped.has(path.resolve(page, entry));
424
484
  fs.readdirSync(page).filter((entry) => !isApp(entry)).forEach((entry) => {
425
485
  fs.cpSync(`${page}/${entry}`, `${to}/${appLayout(target).resources}/page/${entry}`, { recursive: true });
426
486
  });
@@ -432,17 +492,8 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
432
492
  } else if (target.platform === 'android' && target.buildType === 'release') {
433
493
  packageAndroidRelease(to);
434
494
  }
435
- if (['ios', 'android'].includes(target.platform)) {
436
- recordAppOutput(target.platform, to);
437
- } else if (target.platform !== 'darwin') {
438
- if (target.platform === 'win32') signWindowsApp(to, target, page);
439
- archiveApp(to, target);
440
- }
441
- // A desktop app with its page ships with what installs it, and with an updater's key with its update too.
442
- if (page && ['darwin', 'linux', 'win32'].includes(target.platform)) {
443
- writeInstaller(to, target);
444
- writeUpdate(to, target);
445
- }
495
+ if (['ios', 'android'].includes(target.platform)) recordAppOutput(target, to);
496
+ else if (target.platform === 'win32') signWindowsApp(to, target, page);
446
497
  return to;
447
498
  }
448
499
 
@@ -457,14 +508,32 @@ export default async function buildApp(target, { bridges = [], force = false } =
457
508
  logger.info(`[${target.path}] app skipped (cargo package - staticlib only)`);
458
509
  return false;
459
510
  }
460
- // Where the page opens files, a desktop host's title and window, a Windows one's version, and the app's icon: a bad
461
- // app section stops the build before anything compiles.
462
- const header = appHeader(state.config, target.platform);
511
+ // A desktop host's title and window, a Windows one's version, the app's icon and its extensions with their options, as
512
+ // where the page opens files: a bad app section stops the build before anything compiles.
463
513
  const icon = readAppIcon(state.config);
514
+ const extensions = extensionsOf(target.platform);
515
+ const header = appHeader(state.config, target.platform, appGroupOf(target.platform, extensions));
516
+ const carried = carriedFilesOf(extensions.files, ownFiles());
517
+ const carriedPaths = new Set(carried);
518
+ // Whether the extension that owns a launch has native code, or for the app's own one that holds the role.
519
+ const hasNative = (extension, role) => extensions.native.some(({ name, roles }) => (extension === null ? roles.includes(role) : name === extension));
520
+ // A desktop's system hands an app no https link: its owners there are those with native code, whose onOpenUrl a
521
+ // phone's code may share.
522
+ const isPhone = ['ios', 'android'].includes(target.platform);
523
+ const links = linkOwners(state.config).filter(({ extension }) => isPhone || hasNative(extension, 'links'));
524
+ // Another system continues no user activity: its owners there are those with native code, whose onUserActivity
525
+ // Apple's code may share.
526
+ const isApple = ['darwin', 'ios'].includes(target.platform);
527
+ const activities = activityTypeOwners(state.config).filter(({ extension }) => isApple || hasNative(extension, 'activities'));
528
+ // A desktop has no quick actions either.
529
+ const shortcuts = shortcutItemOwners(state.config).filter(({ extension }) => isPhone || hasNative(extension, 'shortcuts'));
530
+ const registration = registrationSource(extensions.native, urlSchemeOwners(state.config), fileTypeOwners(state.config), links, activities, shortcuts);
464
531
 
465
532
  const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
466
533
  const { build } = state.config.paths;
467
534
  const appHostRoot = resolveAppHostRoot();
535
+ appExtensionsOf(state.config.extensions).filter(({ replaces }) => replaces)
536
+ .forEach(({ name, root, replaces }) => logger.info(`[${target.path}] the extension ${name} of ${root} takes the host's ${replaces.join(', ')}`));
468
537
  const jsiRoot = resolveAppEmbindJsiRoot();
469
538
  const { hermes, cmakeArgs, inputs } = await hostInputs(target);
470
539
 
@@ -480,6 +549,9 @@ export default async function buildApp(target, { bridges = [], force = false } =
480
549
  });
481
550
  const { libs, linkArgs, extraSources } = linkInputs(target, { bridge: BRIDGE });
482
551
  const sharedLibs = libs.filter((lib) => lib.endsWith('.so'));
552
+ // The extensions' sources compile with the host, which links what they need: a framework on macOS, a library elsewhere.
553
+ // An iOS host is a static library, whose frameworks the app's Xcode project links.
554
+ const hostLinkArgs = [...linkArgs, ...(target.platform === 'darwin' ? extensions.frameworks.map((framework) => `-framework ${framework}`) : extensions.libraries)];
483
555
 
484
556
  const fingerprintFile = `${build}/${target.appName}.fingerprint`;
485
557
  const fingerprint = getContentHash(JSON.stringify({
@@ -488,7 +560,8 @@ export default async function buildApp(target, { bridges = [], force = false } =
488
560
  // A desktop app carries its icon; an iOS or Android one gets it as its project is written.
489
561
  icon: ['ios', 'android'].includes(target.platform) ? null : icon?.hash ?? null,
490
562
  // A macOS app's bundle takes its id, name and version from it.
491
- infoPlist: target.platform === 'darwin' ? infoPlist(state.config.general.name, appVersion(), Boolean(icon), urlSchemesOf(target)) : null,
563
+ infoPlist: target.platform === 'darwin'
564
+ ? infoPlist(state.config.general.name, appVersion(), Boolean(icon), urlSchemesOf(target), fileTypesOf(target), activityTypesOf(target)) : null,
492
565
  hermes: HERMES.version,
493
566
  inputs,
494
567
  linkArgs,
@@ -498,12 +571,25 @@ export default async function buildApp(target, { bridges = [], force = false } =
498
571
  }),
499
572
  env: getEnv(target),
500
573
  data: getData('data', target),
574
+ // What the app carries, by its size and time, as a large model would take long to read at each build.
575
+ carried: carried.map((file) => {
576
+ const stat = fs.statSync(file);
577
+ return { file, size: stat.size, mtimeMs: stat.mtimeMs };
578
+ }),
501
579
  version: state.config.package?.version,
502
580
  runtime: getFilesFingerprint(filesUnder([
503
581
  `${appHostRoot}/cpp`, `${appHostRoot}/js`, `${appHostRoot}/third_party`, `${jsiRoot}/cpp/src`, `${jsiRoot}/js`,
504
582
  `${state.config.paths.cli}/assets/js-runtime/adapters`,
505
583
  ])),
506
584
  extraSources: getFilesFingerprint(extraSources),
585
+ // An iOS app's frameworks reach its Xcode project through what the host's build writes.
586
+ extensions: {
587
+ registration,
588
+ files: getFilesFingerprint(filesUnder(extensions.roots).filter((file) => !carriedPaths.has(path.resolve(file)))),
589
+ compiled: extensions.compiled,
590
+ link: hostLinkArgs.slice(linkArgs.length),
591
+ frameworks: extensions.frameworks,
592
+ },
507
593
  }));
508
594
  const isChanged = !fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint;
509
595
  if (!force && !isChanged && fs.existsSync(`${build}/${target.appName}`) && fs.existsSync(`${build}/${target.appJsName}`)) {
@@ -521,11 +607,25 @@ export default async function buildApp(target, { bridges = [], force = false } =
521
607
  resetCMakeCacheForSource(workDir, `${appHostRoot}/cpp`, target);
522
608
  // The host includes it from its build directory; written only when it changed, it recompiles nothing otherwise.
523
609
  writeIfChanged(`${workDir}/crossbind_app.h`, header);
610
+ writeIfChanged(`${workDir}/extensions.cpp`, registration);
524
611
  // A Windows host's executable carries the app's icon, which its resource script takes from beside the header.
525
612
  if (target.platform === 'win32') {
526
613
  if (icon) writeIfChanged(`${workDir}/icon.ico`, windowsIcon(icon));
527
614
  else fs.rmSync(`${workDir}/icon.ico`, { force: true });
528
615
  }
616
+ // An Apple app's Swift, which CMake's Makefiles take none of: each extension's compiles first, its header joins the
617
+ // extension's includes, and its library the host's link, or an iOS app's Xcode project's.
618
+ const swift = ['darwin', 'ios'].includes(target.platform) ? compileSwift(target, extensions.compiled, workDir, buildType === 'Release') : [];
619
+ const swiftLink = swiftLinkArgs(target, swift.map(({ library }) => library));
620
+ // A Linux app's extensions' packages beside crossbind's sysroot, each extension's in a sysroot of its own, whose
621
+ // headers its sources include and whose libraries the host links against.
622
+ const sysroots = target.platform === 'linux' ? await unpackPackages(target, extensions.compiled, {
623
+ cache: `${build}/linux-packages`, workDir, unpack: (deb, root) => run(null, ['dpkg-deb', '-x', deb, root], platformPrefix, target),
624
+ }) : [];
625
+ const compiled = extensions.compiled.map((part) => ({
626
+ ...part, includes: [...part.includes, ...[...swift, ...sysroots].filter(({ name: owner }) => owner === part.name).flatMap(({ includes }) => includes)],
627
+ }));
628
+ const ownLink = target.platform === 'darwin' ? swiftLink : sysroots.flatMap(({ libDir }) => [`-L${libDir}`, `-Wl,-rpath-link,${libDir}`]);
529
629
  run(null, [
530
630
  // An iOS host builds with crossbind's iOS toolchain file rather than as an Xcode project of its own.
531
631
  target.platform === 'ios' ? 'ios-cmake' : 'cmake', `${appHostRoot}/cpp`,
@@ -534,7 +634,9 @@ export default async function buildApp(target, { bridges = [], force = false } =
534
634
  `-DCROSSBIND_JSI_ROOT=${jsiRoot}`,
535
635
  `-DCROSSBIND_HERMES=${hermes}`,
536
636
  `-DCROSSBIND_EXTRA_SOURCES=${extraSources.join(';')}`,
537
- `-DCROSSBIND_LINK_ARGS=${linkArgs.join(';')}`,
637
+ `-DCROSSBIND_EXTENSION_REGISTRATION=${workDir}/extensions.cpp`,
638
+ ...extensionCmakeArgs(compiled),
639
+ `-DCROSSBIND_LINK_ARGS=${[...hostLinkArgs, ...ownLink].join(';')}`,
538
640
  `-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
539
641
  // An Android release ships the packages' shared libraries stripped as well, and keeps them as they come.
540
642
  ...(target.platform === 'android' ? [`-DCROSSBIND_SHARED_LIBS=${sharedLibs.join(';')}`] : []),
@@ -552,7 +654,8 @@ export default async function buildApp(target, { bridges = [], force = false } =
552
654
  platformPrefix, target);
553
655
  await bundle(`${appHostRoot}/js/page.js`, `${build}/${target.appJsName}`, []);
554
656
  if (target.platform === 'ios') {
555
- assembleIosPart(target, { library: `${workDir}/lib${name}.a`, linkArgs, hbc: `${workDir}/host.hbc`, hermes });
657
+ const frameworks = extensions.frameworks.flatMap((framework) => ['-framework', framework]);
658
+ assembleIosPart(target, { library: `${workDir}/lib${name}.a`, linkArgs: [...linkArgs, ...frameworks, ...swiftLink], hbc: `${workDir}/host.hbc`, hermes });
556
659
  } else if (target.platform === 'android') {
557
660
  assembleAndroidPart(target, { libDir: workDir, sharedLibs, hbc: `${workDir}/host.hbc` });
558
661
  } else {
@@ -14,7 +14,7 @@ import {
14
14
  } from '../utils/dependencyRebuild.js';
15
15
  import withDirLock from '../utils/dirLock.js';
16
16
  import { prepareRustSysroot } from '../utils/rustSysroot.js';
17
- import prepareConanDependencies from './prepareConanDependencies.js';
17
+ import preparePackageSources from './preparePackageSources.js';
18
18
  import getAbsolutePath from '../utils/getAbsolutePath.js';
19
19
  import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
20
20
  import getParentPath from '../utils/getParentPath.js';
@@ -26,7 +26,7 @@ export default async function buildDependencies({ targetParams, rebuildOption })
26
26
  // sync cargo work downstream reads what it resolves. A project with nothing to rebuild still
27
27
  // needs its sysroot.
28
28
  await prepareRustSysroot(getBuildTargets(targetParams));
29
- await prepareConanDependencies(getBuildTargets(targetParams));
29
+ await preparePackageSources(getBuildTargets(targetParams));
30
30
  await buildMissingCargoDependencies(getBuildTargets(targetParams), targetParams);
31
31
 
32
32
  const rebuildDeps = getRebuildDeps(state.config.allDependencies, rebuildOption);
@@ -78,7 +78,7 @@ async function buildMissingCargoDependencies(buildTargets, targetParams) {
78
78
  const appConfig = state.config;
79
79
  const cargoDependencies = (appConfig.allDependencies ?? []).filter((dep) => dep !== appConfig && dep.export?.type === 'cargo');
80
80
  if (cargoDependencies.length === 0) return;
81
- // Bridges read every header for the first wasm target (see prepareConanDependencies), which an app configured for
81
+ // Bridges read every header for the first wasm target (see preparePackageSources), which an app configured for
82
82
  // another wasm runtime does not build: there a dependency built for the app's runtime alone counts as half built.
83
83
  const bridgeTarget = buildTargets.some(isWasm) ? state.targets.find(isWasm) : undefined;
84
84
  const extraTarget = bridgeTarget && !buildTargets.some((target) => target.path === bridgeTarget.path) ? bridgeTarget : undefined;
@@ -24,11 +24,16 @@ import toolchainNoticesDir from '../utils/toolchainNotices.js';
24
24
  const cpuCount = Math.max(1, os.cpus().length - 1);
25
25
  const WINDOWS_IMAGE_NOTICES = '/opt/licenses/llvm-mingw';
26
26
 
27
+ // What a build's output, a package's own packages and a repository's history hold, which no build reads.
28
+ const UNREAD_FOLDERS = ['.crossbind', 'node_modules', '.git'];
29
+
30
+ // The files under these folders a build reads, a link that leads nowhere left out.
27
31
  export function filesUnder(dirs) {
28
32
  return dirs.flatMap((dir) => (fs.existsSync(dir)
29
33
  ? fs.readdirSync(dir, { recursive: true })
34
+ .filter((file) => !file.split(/[\\/]/).some((part) => UNREAD_FOLDERS.includes(part)))
30
35
  .map((file) => `${dir}/${file}`)
31
- .filter((file) => fs.statSync(file).isFile())
36
+ .filter((file) => fs.statSync(file, { throwIfNoEntry: false })?.isFile())
32
37
  : []));
33
38
  }
34
39
 
@@ -6,7 +6,6 @@ import getLinkInputs from './getLinkInputs.js';
6
6
  import getData from './getData.js';
7
7
  import getEnv from './getEnv.js';
8
8
  import buildJs from './buildJs.js';
9
- import triggerExtensions from './extensions.js';
10
9
  import state from '../state/index.js';
11
10
  import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
12
11
  import logger from '../utils/logger.js';
@@ -128,8 +127,6 @@ export default async function buildWasm(target, options = {}) {
128
127
  if (!isProd && !/-s\s*STACK_OVERFLOW_CHECK=/.test(set)) emccFlags.push('-sSTACK_OVERFLOW_CHECK=1');
129
128
  }
130
129
 
131
- triggerExtensions('buildWasm', 'beforeBuild', [emccFlags]);
132
-
133
130
  if (target.runtime === 'mt' && !emccFlags.includes('-pthread')) {
134
131
  emccFlags.push('-pthread');
135
132
  emccFlags.push('-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency || 1, 2)');
@@ -203,8 +200,6 @@ export default async function buildWasm(target, options = {}) {
203
200
  logger.startStep(target, 'wasm');
204
201
  const t0 = performance.now();
205
202
 
206
- triggerExtensions('buildWasm', 'beforeBuildBrowser', [emccFlags]);
207
-
208
203
  const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
209
204
  run('em++', [
210
205
  '-lembind',
@@ -260,8 +255,6 @@ export default async function buildWasm(target, options = {}) {
260
255
  logger.startStep(target, 'wasm');
261
256
  const t0 = performance.now();
262
257
 
263
- triggerExtensions('buildWasm', 'beforeBuildEdge', [emccFlags]);
264
-
265
258
  const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
266
259
  run('em++', [
267
260
  '-lembind',
@@ -300,8 +293,6 @@ export default async function buildWasm(target, options = {}) {
300
293
  if (target.runtimeEnv === 'node') {
301
294
  logger.startStep(target, 'wasm');
302
295
 
303
- triggerExtensions('buildWasm', 'beforeBuildNodeJS', [emccFlags]);
304
-
305
296
  run('em++', [
306
297
  '-lembind',
307
298
  // The config's emccFlags follow the default level, so its -Oz or -Os wins (the last -O does).
@@ -5,9 +5,9 @@ import createBridgeFile from './createInterface.js';
5
5
  import getDependFilePath from '../integration/getDependFilePath.js';
6
6
  import resolveNativeImport from '../integration/resolveNativeImport.js';
7
7
  import { getRustSymbols } from '../integration/getCrossbindScript.js';
8
- import { findNativeImportsIn, nativeSpecifierTest } from '../utils/nativeImports.js';
8
+ import { findNativeImportsIn, nativeSpecifierTest, schemeTest } from '../utils/nativeImports.js';
9
+ import { importSchemesOf } from '../utils/packageSources.js';
9
10
 
10
- const SCHEME = /^(cargo|conan):/;
11
11
  const isBare = (specifier) => !specifier.startsWith('.') && !upath.isAbsolute(specifier);
12
12
 
13
13
  function realPath(file) {
@@ -18,13 +18,13 @@ function realPath(file) {
18
18
  }
19
19
  }
20
20
 
21
- // The first target whose packages ship the file. A cargo: or conan: import that none resolves fails the build with
22
- // what to declare or install, as it does in a bundler.
23
- function resolveImport(specifier, importer, targets) {
21
+ // The first target whose packages ship the file. An import by a scheme, as cargo: and conan: are, that none resolves
22
+ // fails the build with what to declare or install, as it does in a bundler.
23
+ function resolveImport(specifier, importer, targets, hasScheme) {
24
24
  let failure = null;
25
25
  for (const target of targets) {
26
26
  try {
27
- const file = SCHEME.test(specifier) ? getDependFilePath(specifier, target) : resolveNativeImport(specifier, importer, target);
27
+ const file = hasScheme(specifier) ? getDependFilePath(specifier, target) : resolveNativeImport(specifier, importer, target);
28
28
  if (file && fs.existsSync(file)) return { target, file };
29
29
  } catch (error) {
30
30
  failure ??= error;
@@ -47,8 +47,10 @@ export function findAppNativeImports(targets) {
47
47
  const projectDir = state.config.paths.project;
48
48
  const imports = new Map();
49
49
  if (!projectDir || !fs.existsSync(projectDir)) return imports;
50
- for (const { importer, specifier } of findNativeImportsIn(projectDir, nativeSpecifierTest(state.config.ext))) {
51
- const resolved = resolveImport(specifier, importer, targets);
50
+ const schemes = importSchemesOf(state.config);
51
+ const hasScheme = schemeTest(schemes);
52
+ for (const { importer, specifier } of findNativeImportsIn(projectDir, nativeSpecifierTest(state.config.ext, schemes))) {
53
+ const resolved = resolveImport(specifier, importer, targets, hasScheme);
52
54
  if (!resolved) {
53
55
  console.warn(`crossbind: ${upath.relative(projectDir, importer)} imports ${specifier}, which no dependency ships for ${targets.map((t) => t.path).join(', ')}.`);
54
56
  continue;