crossbind 2.0.0-beta.68 → 2.0.0-beta.69

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 (57) hide show
  1. package/README.md +1 -1
  2. package/package.json +69 -59
  3. package/src/actions/addExtension.js +191 -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 +172 -74
  8. package/src/actions/buildNode.js +6 -1
  9. package/src/actions/buildWasm.js +0 -9
  10. package/src/actions/createInterface.js +17 -8
  11. package/src/actions/createLib.js +0 -4
  12. package/src/actions/extensionApi.js +298 -0
  13. package/src/actions/extensionCommands.js +41 -0
  14. package/src/actions/extensionsInApp.js +19 -0
  15. package/src/actions/gradleProject.js +196 -16
  16. package/src/actions/iosDevice.js +2 -2
  17. package/src/actions/iosSimulator.js +3 -3
  18. package/src/actions/licenses.js +83 -34
  19. package/src/actions/linuxPackages.js +48 -0
  20. package/src/actions/mavenVerification.js +37 -0
  21. package/src/actions/publish.js +220 -0
  22. package/src/actions/windowsSigning.js +1 -1
  23. package/src/actions/xcodeProject.js +49 -24
  24. package/src/actions/xcodeTargets.js +122 -0
  25. package/src/actions/xcodebuild.js +65 -0
  26. package/src/app/index.d.ts +126 -133
  27. package/src/app/index.js +126 -188
  28. package/src/assets/configure/install.sh +0 -0
  29. package/src/assets/toolchain-digests.json +20 -20
  30. package/src/bin.js +20 -28
  31. package/src/index.js +2 -1
  32. package/src/program.js +32 -3
  33. package/src/state/index.js +2 -1
  34. package/src/state/loadConfig.js +16 -5
  35. package/src/utils/appExtensions.js +761 -0
  36. package/src/utils/appFiles.js +58 -0
  37. package/src/utils/appLaunches.js +249 -0
  38. package/src/utils/appOptions.js +109 -100
  39. package/src/utils/appTargets.js +168 -0
  40. package/src/utils/bridgeTools.js +66 -0
  41. package/src/utils/configChanges.js +30 -0
  42. package/src/utils/configWriter.js +143 -26
  43. package/src/utils/extensionSteps.js +135 -0
  44. package/src/utils/openUrl.js +2 -1
  45. package/src/utils/plist.js +5 -0
  46. package/src/utils/resolveAppHost.js +7 -0
  47. package/src/utils/systemConfig.js +16 -3
  48. package/vitest.config.js +6 -1
  49. package/src/actions/appSetup.js +0 -116
  50. package/src/actions/appStoreConnect.js +0 -188
  51. package/src/actions/appUpdate.js +0 -68
  52. package/src/actions/extensions.js +0 -7
  53. package/src/actions/googlePlay.js +0 -213
  54. package/src/actions/googleSignIn.js +0 -78
  55. package/src/actions/playSetup.js +0 -19
  56. package/src/actions/publishAndroid.js +0 -102
  57. package/src/actions/publishIos.js +0 -146
@@ -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, appDisplayName, appEncryption, appFiles, appHeader, appId, appPermissions, appPrivacy,
33
+ 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,52 @@ 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
+ // Forgets what the builds wrote, as a publish's own build writes it anew, but for the installers' files, which a folder the
385
+ // bundler kept holds still, for the page to leave out.
386
+ export function clearAppOutputs() {
387
+ const earlier = [...shippedFiles()];
388
+ if (earlier.length > 0) fs.writeFileSync(appOutputsFile(), JSON.stringify({ earlier: [{ installers: [{ files: earlier }] }] }));
389
+ else fs.rmSync(appOutputsFile(), { force: true });
390
+ }
370
391
 
371
- export const clearAppOutputs = () => fs.rmSync(appOutputsFile(), { force: true });
372
-
392
+ // 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
393
  export function appOutputOf(platform) {
374
394
  const output = readAppOutputs()[platform];
375
395
  if (!output) throw new Error(`crossbind: the build wrote no ${platform} app, so there is nothing to publish.`);
376
- return output;
396
+ return Object.values(DESKTOP_PLATFORMS).includes(platform) ? { apps: output } : output;
377
397
  }
378
398
 
379
- function recordAppOutput(platform, to) {
399
+ const madeFile = (file) => (fs.existsSync(file) ? file : null);
400
+
401
+ // What a publisher's upload takes of an app: an iOS app's project, an Android app's with its APK, bundle and native
402
+ // debug symbols where the build made them, and each arch's desktop app with its installers.
403
+ export function recordAppOutput(target, to, { installers = [] } = {}) {
404
+ const outputs = readAppOutputs();
405
+ const app = path.resolve(to);
406
+ const desktop = DESKTOP_PLATFORMS[target.platform];
407
+ const mobile = target.platform === 'android'
408
+ ? { project: app, apk: madeFile(`${app}.apk`), bundle: madeFile(`${app}.aab`), symbols: madeFile(`${app}-symbols.zip`) }
409
+ : { project: app };
410
+ const recorded = desktop
411
+ ? { [desktop]: [...(outputs[desktop] ?? []).filter((entry) => entry.app !== app), {
412
+ app,
413
+ arch: target.arch,
414
+ installers: installers.map(({ name, files }) => ({ name, files: files.map((file) => path.resolve(file)) })),
415
+ }] }
416
+ : { [target.platform]: mobile };
380
417
  fs.mkdirSync(state.config.paths.build, { recursive: true });
381
- fs.writeFileSync(appOutputsFile(), `${JSON.stringify({ ...readAppOutputs(), [platform]: path.resolve(to) }, null, 4)}\n`);
418
+ fs.writeFileSync(appOutputsFile(), `${JSON.stringify({ ...outputs, ...recorded }, null, 4)}\n`);
382
419
  }
383
420
 
384
421
  // 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 +433,34 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
396
433
  if (target.platform === 'ios') {
397
434
  const parts = appParts(target).map(({ arch, dir }) => ({ sdk: arch, dir }));
398
435
  const icon = readAppIcon(state.config);
399
- const privacy = appPrivacy(state.config);
436
+ const ownPrivacy = appPrivacy(state.config);
400
437
  const build = appBuildNumber();
401
- writeXcodeProject(to, {
438
+ // The app's own options are checked first, then what its extensions add.
439
+ const options = {
402
440
  parts, identity: { id: appId(state.config), team: appTeam() }, label: appDisplayName(state.config), version: appVersion(),
403
441
  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,
442
+ fileTypes: fileTypeOwners(state.config), links: linkOwners(state.config).map(({ link }) => link),
443
+ activityTypes: activityTypeOwners(state.config).map(({ type }) => type), shortcutItems: shortcutItemOwners(state.config),
444
+ background: appBackgroundColor(state.config), usage: usageDescriptions(state.config, 'ios'), encryption: appEncryption(state.config),
445
+ multipleWindows: iosMultipleWindows(state.config),
446
+ };
447
+ const extensions = extensionsOf('ios');
448
+ const privacy = mergedPrivacy(ownPrivacy, extensions.privacy);
449
+ writeXcodeProject(to, {
450
+ ...options, privacy, entitlements: mergedEntitlements(extensions.entitlements), plist: extensions.plist, targets: extensions.targets,
451
+ appGroup: appGroupOf('ios', extensions),
405
452
  });
406
453
  if (target.buildType === 'release') warnUnreasonedApis(to, privacy);
407
454
  } else if (target.platform === 'android') {
408
455
  const icon = readAppIcon(state.config);
409
- writeGradleProject(to, {
410
- parts: appParts(target), id: androidAppId(), label: appDisplayName(state.config), version: androidVersion(), icons: icon && androidIcons(icon),
456
+ const options = {
457
+ parts: appParts(target), id: androidAppId(state.config), label: appDisplayName(state.config), version: androidVersion(), icons: icon && androidIcons(icon),
411
458
  urlSchemes: appUrlSchemes(state.config), background: appBackgroundColor(state.config), permissions: Object.keys(appPermissions(state.config)),
459
+ fileTypes: fileTypeOwners(state.config), links: linkOwners(state.config).map(({ link }) => link), shortcutItems: shortcutItemOwners(state.config),
460
+ };
461
+ const extensions = extensionsOf('android');
462
+ writeGradleProject(to, {
463
+ ...options, java: extensions.java, extensionPermissions: extensions.permissions, manifests: extensions.manifests, resources: extensions.resources, maven: extensions.maven,
412
464
  });
413
465
  } else if (path.resolve(to) !== path.resolve(built)) {
414
466
  fs.rmSync(to, { recursive: true, force: true });
@@ -416,11 +468,14 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
416
468
  }
417
469
  if (page) {
418
470
  // 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)$/, ''));
471
+ // packages it: what is named after a target of the app, and every installer's files as a build recorded them, which
472
+ // a folder the bundler left as it was holds still.
473
+ const apps = state.targets.filter((t) => t.runtimeEnv === 'app')
474
+ .flatMap((t) => [appOutputName(t), `${state.config.general.name}-${t.platform}-${t.arch}`]).filter((name) => !name.endsWith('.app'));
475
+ const shipped = shippedFiles();
476
+ const isApp = (entry) => entry === path.basename(to) || entry.endsWith('.app')
477
+ || apps.some((app) => entry === app || entry.startsWith(`${app}.`) || entry.startsWith(`${app}-`))
478
+ || shipped.has(path.resolve(page, entry));
424
479
  fs.readdirSync(page).filter((entry) => !isApp(entry)).forEach((entry) => {
425
480
  fs.cpSync(`${page}/${entry}`, `${to}/${appLayout(target).resources}/page/${entry}`, { recursive: true });
426
481
  });
@@ -432,17 +487,8 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
432
487
  } else if (target.platform === 'android' && target.buildType === 'release') {
433
488
  packageAndroidRelease(to);
434
489
  }
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
- }
490
+ if (['ios', 'android'].includes(target.platform)) recordAppOutput(target, to);
491
+ else if (target.platform === 'win32') signWindowsApp(to, target, page);
446
492
  return to;
447
493
  }
448
494
 
@@ -457,14 +503,32 @@ export default async function buildApp(target, { bridges = [], force = false } =
457
503
  logger.info(`[${target.path}] app skipped (cargo package - staticlib only)`);
458
504
  return false;
459
505
  }
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);
506
+ // A desktop host's title and window, a Windows one's version, the app's icon and its extensions with their options, as
507
+ // where the page opens files: a bad app section stops the build before anything compiles.
463
508
  const icon = readAppIcon(state.config);
509
+ const extensions = extensionsOf(target.platform);
510
+ const header = appHeader(state.config, target.platform, appGroupOf(target.platform, extensions));
511
+ const carried = carriedFilesOf(extensions.files, ownFiles());
512
+ const carriedPaths = new Set(carried);
513
+ // Whether the extension that owns a launch has native code, or for the app's own one that holds the role.
514
+ const hasNative = (extension, role) => extensions.native.some(({ name, roles }) => (extension === null ? roles.includes(role) : name === extension));
515
+ // A desktop's system hands an app no https link: its owners there are those with native code, whose onOpenUrl a
516
+ // phone's code may share.
517
+ const isPhone = ['ios', 'android'].includes(target.platform);
518
+ const links = linkOwners(state.config).filter(({ extension }) => isPhone || hasNative(extension, 'links'));
519
+ // Another system continues no user activity: its owners there are those with native code, whose onUserActivity
520
+ // Apple's code may share.
521
+ const isApple = ['darwin', 'ios'].includes(target.platform);
522
+ const activities = activityTypeOwners(state.config).filter(({ extension }) => isApple || hasNative(extension, 'activities'));
523
+ // A desktop has no quick actions either.
524
+ const shortcuts = shortcutItemOwners(state.config).filter(({ extension }) => isPhone || hasNative(extension, 'shortcuts'));
525
+ const registration = registrationSource(extensions.native, urlSchemeOwners(state.config), fileTypeOwners(state.config), links, activities, shortcuts);
464
526
 
465
527
  const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
466
528
  const { build } = state.config.paths;
467
529
  const appHostRoot = resolveAppHostRoot();
530
+ appExtensionsOf(state.config.extensions).filter(({ replaces }) => replaces)
531
+ .forEach(({ name, root, replaces }) => logger.info(`[${target.path}] the extension ${name} of ${root} takes the host's ${replaces.join(', ')}`));
468
532
  const jsiRoot = resolveAppEmbindJsiRoot();
469
533
  const { hermes, cmakeArgs, inputs } = await hostInputs(target);
470
534
 
@@ -480,6 +544,9 @@ export default async function buildApp(target, { bridges = [], force = false } =
480
544
  });
481
545
  const { libs, linkArgs, extraSources } = linkInputs(target, { bridge: BRIDGE });
482
546
  const sharedLibs = libs.filter((lib) => lib.endsWith('.so'));
547
+ // The extensions' sources compile with the host, which links what they need: a framework on macOS, a library elsewhere.
548
+ // An iOS host is a static library, whose frameworks the app's Xcode project links.
549
+ const hostLinkArgs = [...linkArgs, ...(target.platform === 'darwin' ? extensions.frameworks.map((framework) => `-framework ${framework}`) : extensions.libraries)];
483
550
 
484
551
  const fingerprintFile = `${build}/${target.appName}.fingerprint`;
485
552
  const fingerprint = getContentHash(JSON.stringify({
@@ -488,7 +555,8 @@ export default async function buildApp(target, { bridges = [], force = false } =
488
555
  // A desktop app carries its icon; an iOS or Android one gets it as its project is written.
489
556
  icon: ['ios', 'android'].includes(target.platform) ? null : icon?.hash ?? null,
490
557
  // 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,
558
+ infoPlist: target.platform === 'darwin'
559
+ ? infoPlist(state.config.general.name, appVersion(), Boolean(icon), urlSchemesOf(target), fileTypesOf(target), activityTypesOf(target)) : null,
492
560
  hermes: HERMES.version,
493
561
  inputs,
494
562
  linkArgs,
@@ -498,12 +566,25 @@ export default async function buildApp(target, { bridges = [], force = false } =
498
566
  }),
499
567
  env: getEnv(target),
500
568
  data: getData('data', target),
569
+ // What the app carries, by its size and time, as a large model would take long to read at each build.
570
+ carried: carried.map((file) => {
571
+ const stat = fs.statSync(file);
572
+ return { file, size: stat.size, mtimeMs: stat.mtimeMs };
573
+ }),
501
574
  version: state.config.package?.version,
502
575
  runtime: getFilesFingerprint(filesUnder([
503
576
  `${appHostRoot}/cpp`, `${appHostRoot}/js`, `${appHostRoot}/third_party`, `${jsiRoot}/cpp/src`, `${jsiRoot}/js`,
504
577
  `${state.config.paths.cli}/assets/js-runtime/adapters`,
505
578
  ])),
506
579
  extraSources: getFilesFingerprint(extraSources),
580
+ // An iOS app's frameworks reach its Xcode project through what the host's build writes.
581
+ extensions: {
582
+ registration,
583
+ files: getFilesFingerprint(filesUnder(extensions.roots).filter((file) => !carriedPaths.has(path.resolve(file)))),
584
+ compiled: extensions.compiled,
585
+ link: hostLinkArgs.slice(linkArgs.length),
586
+ frameworks: extensions.frameworks,
587
+ },
507
588
  }));
508
589
  const isChanged = !fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint;
509
590
  if (!force && !isChanged && fs.existsSync(`${build}/${target.appName}`) && fs.existsSync(`${build}/${target.appJsName}`)) {
@@ -521,11 +602,25 @@ export default async function buildApp(target, { bridges = [], force = false } =
521
602
  resetCMakeCacheForSource(workDir, `${appHostRoot}/cpp`, target);
522
603
  // The host includes it from its build directory; written only when it changed, it recompiles nothing otherwise.
523
604
  writeIfChanged(`${workDir}/crossbind_app.h`, header);
605
+ writeIfChanged(`${workDir}/extensions.cpp`, registration);
524
606
  // A Windows host's executable carries the app's icon, which its resource script takes from beside the header.
525
607
  if (target.platform === 'win32') {
526
608
  if (icon) writeIfChanged(`${workDir}/icon.ico`, windowsIcon(icon));
527
609
  else fs.rmSync(`${workDir}/icon.ico`, { force: true });
528
610
  }
611
+ // An Apple app's Swift, which CMake's Makefiles take none of: each extension's compiles first, its header joins the
612
+ // extension's includes, and its library the host's link, or an iOS app's Xcode project's.
613
+ const swift = ['darwin', 'ios'].includes(target.platform) ? compileSwift(target, extensions.compiled, workDir, buildType === 'Release') : [];
614
+ const swiftLink = swiftLinkArgs(target, swift.map(({ library }) => library));
615
+ // A Linux app's extensions' packages beside crossbind's sysroot, each extension's in a sysroot of its own, whose
616
+ // headers its sources include and whose libraries the host links against.
617
+ const sysroots = target.platform === 'linux' ? await unpackPackages(target, extensions.compiled, {
618
+ cache: `${build}/linux-packages`, workDir, unpack: (deb, root) => run(null, ['dpkg-deb', '-x', deb, root], platformPrefix, target),
619
+ }) : [];
620
+ const compiled = extensions.compiled.map((part) => ({
621
+ ...part, includes: [...part.includes, ...[...swift, ...sysroots].filter(({ name: owner }) => owner === part.name).flatMap(({ includes }) => includes)],
622
+ }));
623
+ const ownLink = target.platform === 'darwin' ? swiftLink : sysroots.flatMap(({ libDir }) => [`-L${libDir}`, `-Wl,-rpath-link,${libDir}`]);
529
624
  run(null, [
530
625
  // An iOS host builds with crossbind's iOS toolchain file rather than as an Xcode project of its own.
531
626
  target.platform === 'ios' ? 'ios-cmake' : 'cmake', `${appHostRoot}/cpp`,
@@ -534,7 +629,9 @@ export default async function buildApp(target, { bridges = [], force = false } =
534
629
  `-DCROSSBIND_JSI_ROOT=${jsiRoot}`,
535
630
  `-DCROSSBIND_HERMES=${hermes}`,
536
631
  `-DCROSSBIND_EXTRA_SOURCES=${extraSources.join(';')}`,
537
- `-DCROSSBIND_LINK_ARGS=${linkArgs.join(';')}`,
632
+ `-DCROSSBIND_EXTENSION_REGISTRATION=${workDir}/extensions.cpp`,
633
+ ...extensionCmakeArgs(compiled),
634
+ `-DCROSSBIND_LINK_ARGS=${[...hostLinkArgs, ...ownLink].join(';')}`,
538
635
  `-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
539
636
  // An Android release ships the packages' shared libraries stripped as well, and keeps them as they come.
540
637
  ...(target.platform === 'android' ? [`-DCROSSBIND_SHARED_LIBS=${sharedLibs.join(';')}`] : []),
@@ -552,7 +649,8 @@ export default async function buildApp(target, { bridges = [], force = false } =
552
649
  platformPrefix, target);
553
650
  await bundle(`${appHostRoot}/js/page.js`, `${build}/${target.appJsName}`, []);
554
651
  if (target.platform === 'ios') {
555
- assembleIosPart(target, { library: `${workDir}/lib${name}.a`, linkArgs, hbc: `${workDir}/host.hbc`, hermes });
652
+ const frameworks = extensions.frameworks.flatMap((framework) => ['-framework', framework]);
653
+ assembleIosPart(target, { library: `${workDir}/lib${name}.a`, linkArgs: [...linkArgs, ...frameworks, ...swiftLink], hbc: `${workDir}/host.hbc`, hermes });
556
654
  } else if (target.platform === 'android') {
557
655
  assembleAndroidPart(target, { libDir: workDir, sharedLibs, hbc: `${workDir}/host.hbc` });
558
656
  } else {
@@ -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).
@@ -24,6 +24,9 @@ import { withDirLockSync } from '../utils/dirLock.js';
24
24
  import { conanImportOfHeader } from '../utils/conanImport.js';
25
25
  import run, { cxxPreprocessorFor } from './run.js';
26
26
  import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
27
+ import {
28
+ loadBridgeTools, runPreprocessor, runSwig, usesBridgeTools,
29
+ } from '../utils/bridgeTools.js';
27
30
 
28
31
  // Part of every interface hash, so interfaces cached by an older generator are rebuilt.
29
32
  const INTERFACE_FORMAT = 'swig-macros-4';
@@ -54,6 +57,8 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
54
57
  // dependency built since.
55
58
  refreshConanDependencies(state.config);
56
59
  refreshBuiltDependencies(state.config);
60
+ // Without bridge-tools on a Mac the macro step would fail quietly, and its interface would be kept without macros.
61
+ if (usesBridgeTools(target)) loadBridgeTools();
57
62
  const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
58
63
  if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
59
64
  fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
@@ -353,21 +358,25 @@ const RELEASE_DEFINE = '-DNDEBUG';
353
358
 
354
359
  // One bridge serves every desktop target, so SWIG and the macro table it reads come from the web image's em++,
355
360
  // which defines no operating system; an android bridge is read with the NDK it compiles with. The headers
356
- // still come from the target's own packages.
361
+ // still come from the target's own packages. On a Mac, bridge-tools makes the same bridge, in the build directory the
362
+ // image's tools run in.
357
363
  const toolTargetFor = (target) => (target?.platform === 'android' ? target : { ...target, platform: 'wasm' });
358
364
 
359
365
  function dumpMacros(outputFile, includes, includePath, target, defines = []) {
360
- const tool = toolTargetFor(target);
361
- run(cxxPreprocessorFor(tool), [
366
+ const args = [
362
367
  '-x', 'c++', '-std=c++17', '-dM', '-E', ...defines,
363
368
  ...includePath,
364
369
  ...includes.flatMap((header) => ['-include', header]),
365
370
  '-o', outputFile,
366
371
  '/dev/null',
367
- ], null, tool);
372
+ ];
373
+ if (usesBridgeTools(target)) runPreprocessor(args, state.config.paths.build);
374
+ else run(cxxPreprocessorFor(toolTargetFor(target)), args, null, toolTargetFor(target));
368
375
  return parseMacroDump(fs.readFileSync(outputFile, 'utf8'));
369
376
  }
370
377
 
378
+ const runSwigFor = (args, target) => (usesBridgeTools(target) ? runSwig(args, state.config.paths.build) : run('swig', args, null, toolTargetFor(target)));
379
+
371
380
  function swigIncludePath(target, sourceDir) {
372
381
  const allHeaders = state.config.dependencyParameters.headerPathWithDepends.split(';');
373
382
  const includePath = [
@@ -468,7 +477,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
468
477
  const bridgeFilePath = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
469
478
  fs.writeFileSync(keyOf(bridgeFilePath), '');
470
479
  // #error lines guard branches SWIG evaluates without the compiler's predefined macros, so they only warn.
471
- const swig = () => run('swig', [
480
+ const swig = () => runSwigFor([
472
481
  '-c++',
473
482
  '-embind',
474
483
  '-cpperraswarn',
@@ -476,7 +485,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
476
485
  ...(swigView ? [`-I${swigView.dir}`] : []),
477
486
  ...swigIncludePath(target, sourceDir),
478
487
  interfaceFilePath,
479
- ], null, toolTargetFor(target));
488
+ ], target);
480
489
  try {
481
490
  swig();
482
491
  } catch (e) {
@@ -517,8 +526,8 @@ function createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, s
517
526
  const hash = getContentHash([BRIDGE_FORMAT, getFileHash(file), sourceHash, swigView?.hash ?? ''].join('\n'));
518
527
  if (!fs.existsSync(`${bridge}.exports.json`) || !fs.existsSync(keyOf(bridge)) || fs.readFileSync(keyOf(bridge), 'utf8') !== hash) {
519
528
  fs.writeFileSync(keyOf(bridge), '');
520
- run('swig', ['-c++', '-embind', '-cpperraswarn', '-o', bridge,
521
- ...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], null, toolTargetFor(target));
529
+ runSwigFor(['-c++', '-embind', '-cpperraswarn', '-o', bridge,
530
+ ...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], target);
522
531
  fs.writeFileSync(keyOf(bridge), hash);
523
532
  }
524
533
  return `${bridge}.exports.json`;
@@ -6,7 +6,6 @@ import run from './run.js';
6
6
  import buildCargo from './buildCargo.js';
7
7
  import { cargoTripleFor } from '../utils/cargoTarget.js';
8
8
  import getCmakeParameters from './getCmakeParameters.js';
9
- import triggerExtensions from './extensions.js';
10
9
  import state from '../state/index.js';
11
10
  import logger from '../utils/logger.js';
12
11
  import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
@@ -132,7 +131,6 @@ export default function createLib(target, fileType, options = {}) {
132
131
 
133
132
  const extraLibs = getExtraLibs ? getExtraLibs(target) : [];
134
133
 
135
- triggerExtensions('createLib', 'setFlagWithBuildConfig', [buildEnv, cFlags, ldFlags]);
136
134
  const archiveFlags = targetArchiveFlags(target);
137
135
  cFlags.push(...archiveFlags);
138
136
  ldFlags.push(...archiveFlags);
@@ -165,8 +163,6 @@ export default function createLib(target, fileType, options = {}) {
165
163
  ];
166
164
  buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
167
165
 
168
- triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
169
-
170
166
  const archiveFlags = targetArchiveFlags(target).join(' ');
171
167
  if (archiveFlags) {
172
168
  buildEnv.params.push('-e', `CFLAGS=${archiveFlags}`);