crossbind 2.0.0-beta.64 → 2.0.0-beta.65

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/actions/androidDevice.js +77 -0
  3. package/src/actions/androidGradle.js +121 -0
  4. package/src/actions/buildApp.js +434 -0
  5. package/src/actions/buildDependencies.js +4 -3
  6. package/src/actions/buildExternal.js +3 -3
  7. package/src/actions/buildLib.js +1 -1
  8. package/src/actions/buildNode.js +15 -10
  9. package/src/actions/createInterface.js +1 -2
  10. package/src/actions/createLib.js +4 -1
  11. package/src/actions/getCmakeParameters.js +2 -1
  12. package/src/actions/getDependLibs.js +8 -2
  13. package/src/actions/getLinkInputs.js +10 -7
  14. package/src/actions/gradleProject.js +110 -0
  15. package/src/actions/iosSimulator.js +63 -0
  16. package/src/actions/licenses.js +160 -14
  17. package/src/actions/nativeBridges.js +35 -0
  18. package/src/actions/run.js +3 -3
  19. package/src/actions/target.js +2 -1
  20. package/src/actions/xcodeProject.js +152 -0
  21. package/src/assets/js-runtime/adapters/worker-comlink.js +8 -1
  22. package/src/assets/toolchain-digests.json +24 -18
  23. package/src/bin.js +75 -32
  24. package/src/index.js +6 -0
  25. package/src/integration/getCrossbindScript.js +15 -1
  26. package/src/state/index.js +5 -1
  27. package/src/state/loadConfig.js +2 -0
  28. package/src/utils/appRustCrates.js +3 -1
  29. package/src/utils/downloadAndExtractFile.js +42 -14
  30. package/src/utils/embindRsFingerprint.js +10 -3
  31. package/src/utils/hermes.js +40 -0
  32. package/src/utils/plist.js +42 -0
  33. package/src/utils/pullDockerImage.js +9 -3
  34. package/src/utils/replaceBasePathForDocker.js +8 -0
  35. package/src/utils/resolveAppHost.js +21 -0
  36. package/src/utils/selectRunner.js +1 -1
  37. package/src/utils/systemKeys.js +4 -0
  38. package/src/utils/targets.js +19 -3
  39. package/vitest.config.js +5 -0
@@ -0,0 +1,35 @@
1
+ import createBridgeFile from './createInterface.js';
2
+ import { createImportedBridges, findAppNativeImports } from './createImportedBridges.js';
3
+ import getDependFilePath from '../integration/getDependFilePath.js';
4
+ import state from '../state/index.js';
5
+ import findFiles from '../utils/findFiles.js';
6
+ import {
7
+ boundHeaderSpecifiers, bridgeTargets, nodeBridgeTarget, resolveBoundHeaders,
8
+ } from '../utils/boundHeaders.js';
9
+
10
+ // The project's own headers, each with its bridge.
11
+ export function createBridges(target) {
12
+ let headers = [];
13
+ state.config.paths.header.forEach((headerPath) => {
14
+ headers.push(findFiles('**/*.h', { cwd: headerPath }));
15
+ });
16
+ headers = headers.filter((path) => !!path.toString()).flat();
17
+
18
+ return headers.map((header) => ({ file: header, bridge: createBridgeFile(header, target) }));
19
+ }
20
+
21
+ // The dependency headers the config binds whole, each read where one of the targets' packages ships it.
22
+ export function createBoundBridges(targets) {
23
+ const headers = resolveBoundHeaders(boundHeaderSpecifiers(state.config), bridgeTargets(targets, state.targets), getDependFilePath);
24
+ const wholeHeaders = headers.map((header) => header.file);
25
+ return headers.map((header) => ({ ...header, bridge: createBridgeFile(header.file, header.target, { wholeHeaders }) }));
26
+ }
27
+
28
+ // What a native binary binds, an addon or an app host: the project's headers, the dependency headers the config binds
29
+ // whole, and the ones the app's sources import.
30
+ export function createNativeBridges(targets) {
31
+ const boundHeaders = createBoundBridges(targets);
32
+ const ownHeaders = [...createBridges(nodeBridgeTarget(targets)), ...boundHeaders];
33
+ const imports = findAppNativeImports(bridgeTargets(targets, state.targets));
34
+ return { headers: [...ownHeaders, ...createImportedBridges(ownHeaders, imports)], imports };
35
+ }
@@ -22,7 +22,7 @@ import {
22
22
  } from '../utils/darwinToolchain.js';
23
23
 
24
24
  // Native builds can outrun Node's 1 MiB default pipe buffer; without a raised cap a successful build dies with ENOBUFS.
25
- const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
25
+ export const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
26
26
  // Containers share the memory of Docker's VM, and one compile at full -j can take most of a desktop VM
27
27
  // (GDAL's bindings peak at 7.5 of 7.7 GiB), so the exclusive steps of parallel builds take turns.
28
28
  const DOCKER_COMPILE_LOCK = path.join(os.homedir(), '.crossbind', 'docker-compile.lock');
@@ -124,7 +124,7 @@ const iosParams = [
124
124
 
125
125
  // The C++ compiler of the image that runs SWIG for the target, so its -dM macro table matches that parse.
126
126
  export function cxxPreprocessorFor(target) {
127
- if (imageRoleFor(target) !== 'android') return 'em++';
127
+ if (target?.platform !== 'android') return 'em++';
128
128
  return `${t}/${target.arch === 'x86_64' ? CROSSCOMPILER_x86_64 : CROSSCOMPILER_ARM64}-clang++`;
129
129
  }
130
130
 
@@ -196,7 +196,7 @@ export default function run(program, params = [], platformPrefix = null, target
196
196
  case 'android':
197
197
  [dProgram, ...dParams] = params;
198
198
  platformParams = target.arch === 'x86_64' ? androidParamsX86_64 : androidParamsArm64;
199
- if (dProgram === 'cmake') {
199
+ if (dProgram === 'cmake' && dParams[0] !== '--build' && dParams[0] !== '--install') {
200
200
  dParams = [
201
201
  ...dParams,
202
202
  `-DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK}/build/cmake/android.toolchain.cmake`,
@@ -28,7 +28,8 @@ export function getTargetParams(givenTargetParams = {}, noParamCheck = false) {
28
28
  }
29
29
 
30
30
  export function getBuildTargets(targetParams) {
31
- const { platform, arch, runtime, buildType, runtimeEnv } = targetParams;
31
+ // A lookup that names no runtime environment finds the targets that have none: the archives' alone.
32
+ const { platform, arch, runtime, buildType, runtimeEnv = [] } = targetParams;
32
33
  return state.targets.filter(t => (
33
34
  (!t.platform || platform.includes(t.platform))
34
35
  && (!t.arch || arch.includes(t.arch))
@@ -0,0 +1,152 @@
1
+ import fs from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import state from '../state/index.js';
4
+ import { IOS_DEPLOYMENT_TARGET } from '../utils/iosToolchain.js';
5
+ import { textPlist, xmlPlist } from '../utils/plist.js';
6
+
7
+ // The SDKs an iOS app builds for, one target each, and the slice of Hermes' release each takes.
8
+ export const IOS_SDKS = ['iphoneos', 'iphonesimulator'];
9
+ export const HERMES_SLICES = { iphoneos: 'ios-arm64', iphonesimulator: 'ios-arm64_x86_64-simulator' };
10
+ // What the host's own code needs from the system: Xcode adds it only for the sources it compiles itself.
11
+ const SYSTEM_LIBS = ['-framework', 'UIKit', '-framework', 'WebKit', '-framework', 'CoreGraphics', '-lc++'];
12
+
13
+ const MAIN = `// crossbind writes this project anew on every build: the app comes prebuilt in lib/, and its files are in app/.
14
+ int crossbind_app_main(int argc, char *argv[]);
15
+
16
+ int main(int argc, char *argv[]) { return crossbind_app_main(argc, argv); }
17
+ `;
18
+
19
+ const OBJECT_NAMES = ['project', 'mainGroup', 'products', 'target', 'product', 'main', 'info', 'app', 'hermes', 'mainBuild',
20
+ 'appBuild', 'hermesLink', 'hermesEmbed', 'sources', 'frameworks', 'resources', 'embed', 'projectDebug', 'projectRelease',
21
+ 'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs'];
22
+ // Xcode names its objects by 24 hex digits. Fixed ones leave the project unchanged while what it holds is.
23
+ const ID = Object.fromEntries(OBJECT_NAMES.map((name, index) => [name, `CB${(index + 1).toString(16).toUpperCase().padStart(22, '0')}`]));
24
+
25
+ const PROJECT_SETTINGS = {
26
+ SDKROOT: 'iphoneos',
27
+ SUPPORTED_PLATFORMS: 'iphoneos iphonesimulator',
28
+ IPHONEOS_DEPLOYMENT_TARGET: IOS_DEPLOYMENT_TARGET,
29
+ TARGETED_DEVICE_FAMILY: '1,2',
30
+ // crossbind's iOS archives are arm64 alone, for devices and the Simulator alike.
31
+ ARCHS: 'arm64',
32
+ ALWAYS_SEARCH_USER_PATHS: 'NO',
33
+ CLANG_ENABLE_OBJC_ARC: 'YES',
34
+ DEAD_CODE_STRIPPING: 'YES',
35
+ };
36
+
37
+ const ORIENTATIONS = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
38
+
39
+ const infoPlist = (name) => xmlPlist({
40
+ CFBundleDevelopmentRegion: 'en',
41
+ CFBundleDisplayName: name,
42
+ CFBundleExecutable: '$(EXECUTABLE_NAME)',
43
+ CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)',
44
+ CFBundleInfoDictionaryVersion: '6.0',
45
+ CFBundleName: '$(PRODUCT_NAME)',
46
+ CFBundlePackageType: 'APPL',
47
+ CFBundleShortVersionString: '$(MARKETING_VERSION)',
48
+ CFBundleVersion: '$(CURRENT_PROJECT_VERSION)',
49
+ LSRequiresIPhoneOS: true,
50
+ UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes: false },
51
+ UILaunchScreen: {},
52
+ UISupportedInterfaceOrientations: ORIENTATIONS,
53
+ 'UISupportedInterfaceOrientations~ipad': [...ORIENTATIONS, 'UIInterfaceOrientationPortraitUpsideDown'],
54
+ });
55
+
56
+ function objects(name, targetSettings) {
57
+ const file = (path, type) => ({ isa: 'PBXFileReference', lastKnownFileType: type, path, sourceTree: '<group>' });
58
+ const phase = (isa, files, extra = {}) => ({ isa, buildActionMask: 2147483647, files, runOnlyForDeploymentPostprocessing: 0, ...extra });
59
+ const configuration = (configurationName, buildSettings) => ({ isa: 'XCBuildConfiguration', buildSettings, name: configurationName });
60
+ const configurations = (debug, release) => ({
61
+ isa: 'XCConfigurationList', buildConfigurations: [debug, release], defaultConfigurationIsVisible: 0, defaultConfigurationName: 'Release',
62
+ });
63
+ return {
64
+ [ID.main]: file('main.m', 'sourcecode.c.objc'),
65
+ [ID.info]: file('Info.plist', 'text.plist.xml'),
66
+ [ID.app]: file('app', 'folder'),
67
+ [ID.hermes]: file('Frameworks/hermesvm.xcframework', 'wrapper.xcframework'),
68
+ [ID.product]: {
69
+ isa: 'PBXFileReference', explicitFileType: 'wrapper.application', includeInIndex: 0, path: `${name}.app`, sourceTree: 'BUILT_PRODUCTS_DIR',
70
+ },
71
+ [ID.mainBuild]: { isa: 'PBXBuildFile', fileRef: ID.main },
72
+ [ID.appBuild]: { isa: 'PBXBuildFile', fileRef: ID.app },
73
+ [ID.hermesLink]: { isa: 'PBXBuildFile', fileRef: ID.hermes },
74
+ [ID.hermesEmbed]: { isa: 'PBXBuildFile', fileRef: ID.hermes, settings: { ATTRIBUTES: ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] } },
75
+ [ID.sources]: phase('PBXSourcesBuildPhase', [ID.mainBuild]),
76
+ [ID.frameworks]: phase('PBXFrameworksBuildPhase', [ID.hermesLink]),
77
+ [ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild]),
78
+ [ID.embed]: phase('PBXCopyFilesBuildPhase', [ID.hermesEmbed], { dstPath: '', dstSubfolderSpec: 10, name: 'Embed Frameworks' }),
79
+ [ID.mainGroup]: { isa: 'PBXGroup', children: [ID.main, ID.info, ID.app, ID.hermes, ID.products], sourceTree: '<group>' },
80
+ [ID.products]: { isa: 'PBXGroup', children: [ID.product], name: 'Products', sourceTree: '<group>' },
81
+ [ID.target]: {
82
+ isa: 'PBXNativeTarget',
83
+ buildConfigurationList: ID.targetConfigs,
84
+ buildPhases: [ID.sources, ID.frameworks, ID.resources, ID.embed],
85
+ buildRules: [],
86
+ dependencies: [],
87
+ name,
88
+ productName: name,
89
+ productReference: ID.product,
90
+ productType: 'com.apple.product-type.application',
91
+ },
92
+ [ID.project]: {
93
+ isa: 'PBXProject',
94
+ buildConfigurationList: ID.projectConfigs,
95
+ compatibilityVersion: 'Xcode 14.0',
96
+ developmentRegion: 'en',
97
+ hasScannedForEncodings: 0,
98
+ knownRegions: ['en', 'Base'],
99
+ mainGroup: ID.mainGroup,
100
+ productRefGroup: ID.products,
101
+ projectDirPath: '',
102
+ projectRoot: '',
103
+ targets: [ID.target],
104
+ },
105
+ [ID.projectDebug]: configuration('Debug', PROJECT_SETTINGS),
106
+ [ID.projectRelease]: configuration('Release', PROJECT_SETTINGS),
107
+ [ID.targetDebug]: configuration('Debug', targetSettings),
108
+ [ID.targetRelease]: configuration('Release', targetSettings),
109
+ [ID.projectConfigs]: configurations(ID.projectDebug, ID.projectRelease),
110
+ [ID.targetConfigs]: configurations(ID.targetDebug, ID.targetRelease),
111
+ };
112
+ }
113
+
114
+ // An iOS app as an Xcode project, written anew from the parts its SDK targets built: each brings its archives and
115
+ // their link line, and its slice of Hermes; the first also the app's files. Xcode signs the app and runs it, in the
116
+ // Simulator or on a device.
117
+ export default function writeXcodeProject(to, { parts, identity, version }) {
118
+ const { name } = state.config.general;
119
+ if (parts.length === 0) throw new Error(`crossbind: no part of the iOS app ${name} is built, so there is no Xcode project to write.`);
120
+ fs.rmSync(to, { recursive: true, force: true });
121
+ fs.mkdirSync(`${to}/${name}.xcodeproj`, { recursive: true });
122
+ const linkFlags = {};
123
+ parts.forEach(({ sdk, dir }) => {
124
+ fs.cpSync(`${dir}/lib`, `${to}/lib/${sdk}`, { recursive: true });
125
+ // The quotes keep a project path with spaces in one argument.
126
+ const args = JSON.parse(fs.readFileSync(`${dir}/link.json`, 'utf8'))
127
+ .map((arg) => arg.replace(/\{lib\}\/(.+)$/, `"$(PROJECT_DIR)/lib/${sdk}/$1"`));
128
+ linkFlags[`OTHER_LDFLAGS[sdk=${sdk}*]`] = [...args, ...SYSTEM_LIBS];
129
+ });
130
+ fs.cpSync(`${parts[0].dir}/app`, `${to}/app`, { recursive: true });
131
+ const frameworks = parts.flatMap(({ dir }) => ['-framework', `${dir}/hermesvm.framework`]);
132
+ const result = spawnSync('xcodebuild', ['-create-xcframework', ...frameworks, '-output', `${to}/Frameworks/hermesvm.xcframework`], { encoding: 'utf8' });
133
+ if (result.status !== 0) throw new Error(`crossbind: cannot make Hermes' xcframework for ${to}: ${result.stderr || result.error?.message}`);
134
+
135
+ fs.writeFileSync(`${to}/main.m`, MAIN);
136
+ fs.writeFileSync(`${to}/Info.plist`, infoPlist(name));
137
+ const targetSettings = {
138
+ PRODUCT_NAME: name,
139
+ PRODUCT_BUNDLE_IDENTIFIER: identity.id,
140
+ DEVELOPMENT_TEAM: identity.team,
141
+ CODE_SIGN_STYLE: 'Automatic',
142
+ INFOPLIST_FILE: 'Info.plist',
143
+ MARKETING_VERSION: version,
144
+ CURRENT_PROJECT_VERSION: version,
145
+ LD_RUNPATH_SEARCH_PATHS: ['$(inherited)', '@executable_path/Frameworks'],
146
+ ...linkFlags,
147
+ };
148
+ fs.writeFileSync(`${to}/${name}.xcodeproj/project.pbxproj`, textPlist({
149
+ archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings), rootObject: ID.project,
150
+ }));
151
+ return to;
152
+ }
@@ -41,8 +41,12 @@ const ownsObject = (obj) => {
41
41
  return Boolean($$ && ($$.smartPtr || $$.count?.owned));
42
42
  };
43
43
 
44
+ // The module's own objects, its globals above all, live as long as the module, so letting go of a handle to one
45
+ // frees nothing.
46
+ const moduleObjects = new WeakSet();
47
+
44
48
  const freeIfOwned = (obj) => {
45
- if (ownsObject(obj) && !obj.isDeleted()) obj.delete();
49
+ if (ownsObject(obj) && !moduleObjects.has(obj) && !obj.isDeleted()) obj.delete();
46
50
  };
47
51
 
48
52
  // The worker side of a handle the main thread no longer reaches: its id goes, and the object with the last id.
@@ -297,6 +301,9 @@ export function moduleRoot(m) {
297
301
  configurable: true,
298
302
  });
299
303
  Object.defineProperty(m, RELEASE, { value: releaseEmbindObjects, configurable: true });
304
+ Object.values(m).forEach((value) => {
305
+ if (value !== null && typeof value === 'object') moduleObjects.add(value);
306
+ });
300
307
  return wrapWithVectorCoercion(m);
301
308
  }
302
309
 
@@ -1,49 +1,55 @@
1
1
  {
2
- "version": "1.0.13",
2
+ "version": "1.0.15",
3
3
  "registry": "ghcr.io/crossbind",
4
4
  "toolchains": {
5
5
  "rust": "1.99.1"
6
6
  },
7
7
  "images": {
8
8
  "rust-sysroot": {
9
- "index": "sha256:c545ea396bd8919f7a671617dbbdf184bd0d6eb904d3170dd120d6671db3e0a9",
9
+ "index": "sha256:720469aca3cff3f1abe69374882d2c48399864be3778225e7754e94036e3be7a",
10
10
  "platforms": {
11
- "linux/arm64": "sha256:c53ee0a31c52fdc6eba175e479218ddbff0c071654b5e93a08ae768cdbbfe35a",
12
- "linux/amd64": "sha256:1427255e619c3f5dcd3fa7fd284d15afbe00eef1a0798e08a9b50ebbb86c5234"
11
+ "linux/arm64": "sha256:3aa68ad3736491de26c6ca9b52a49c80e6291228cc17a05caba4c060ea68913c",
12
+ "linux/amd64": "sha256:e77357a8c763244a5a5b4086fb771e81bec385526432ec6e77fb316f39158c53"
13
13
  }
14
14
  },
15
15
  "base": {
16
- "index": "sha256:5f5d133504cb2c81ee67a93ca5795698423277522caac742478510d6158ed559",
16
+ "index": "sha256:a7c9342c50a493c990f8c26f1bec7e3919ad6010ce8a6cdfaa772487a1040989",
17
17
  "platforms": {
18
- "linux/arm64": "sha256:df1c82f4b7e00a4ab33e2423f6d5bbaa66dd67bc45b96e5498e3391ec5f55fae",
19
- "linux/amd64": "sha256:ffc0a178bc821e56975d9348b7e44d8081b9c6fb9351bc168b38d5063d9864ea"
18
+ "linux/arm64": "sha256:c9152c3ff4854ba898df1b9fe89e8a94fd29aa5832e8cc5a4bd5fa2e06cee10f",
19
+ "linux/amd64": "sha256:817fc177f488b975bbbd196d9ec9d1d3effbf2c59d1ed82e979fcf5124c4c1aa"
20
20
  }
21
21
  },
22
22
  "web": {
23
- "index": "sha256:b5ee271886c316404cae103c5412bc02d811ad48fd36a919fdf3a82af14fe688",
23
+ "index": "sha256:143c2e79a950f732913726f32559f7c70323accf2727b34a12f332df91b765ef",
24
24
  "platforms": {
25
- "linux/arm64": "sha256:91d35c9eac65f479f5139ba476b3daa3b517aab6f21f5206070af610cec5b62c",
26
- "linux/amd64": "sha256:f308cb1702f308d69d9f21eb99aa109cdbf62e655549b483580476d8f01fb07b"
25
+ "linux/arm64": "sha256:ebd0dbeee77a5fde4659b5f129713e5006cc205c629f837b56478e3691e85b9c",
26
+ "linux/amd64": "sha256:14429be7ee52afa0bd9c94248db3a84f90e74c29700a3f5f34da2989d7161431"
27
27
  }
28
28
  },
29
29
  "android": {
30
- "index": "sha256:a01ddeaed550091f45e720d09bd559978d4c46e396c4553b3fc4544b90033fd1",
30
+ "index": "sha256:249bdfda25dcd394c1e4955f3cd3e63140264697e9a543fbae1dce2ec3d80c38",
31
31
  "platforms": {
32
- "linux/amd64": "sha256:3bafd0a3f80bf7370a19522a3b75cb58a977d1b1b28940ed02b87db1058a8fd6"
32
+ "linux/amd64": "sha256:725375a79e64c93a7cc7e35f4708e36511f6a36212e0a3aaf44f6b5ec1057724"
33
+ }
34
+ },
35
+ "android-app": {
36
+ "index": "sha256:d739c6b72e1545b086f38fe5613b58f52877a36e14c2ffd763acdd0296802802",
37
+ "platforms": {
38
+ "linux/amd64": "sha256:a7124034f1ba3d888b3cd32c18051f87498c47ec515eac809b7440394c54ecb7"
33
39
  }
34
40
  },
35
41
  "linux": {
36
- "index": "sha256:c1d7f9672edb6eeea86172d8ba8cc8ba824f6c53134587ea247ea5dc126d6c43",
42
+ "index": "sha256:433af720cf95a99c4e007c0cfc98ab358c6ddc61fecca75a0b844f598d750b1b",
37
43
  "platforms": {
38
- "linux/amd64": "sha256:0ff9ee1d5c139eb97934919f7b9535af0eb2c3bebd9c1974fa95cfb143e3ccf7",
39
- "linux/arm64": "sha256:f476e4a95635c04aca741937c2174a1915731c52c9d182b247e17d4ca4163b33"
44
+ "linux/arm64": "sha256:0cf32948302c1a23294d28d20dd3b784790c7789b9e99bc8be56888bddb6aeb7",
45
+ "linux/amd64": "sha256:6da084ff433e792381aadd280b769aa1b09ba593e3e21ea4580f4736776e3cb9"
40
46
  }
41
47
  },
42
48
  "windows": {
43
- "index": "sha256:53b08c8918a84023ea90c6420fff783564aced0ec711fa0e7b71a09578643a32",
49
+ "index": "sha256:c1fb594cb245f6b0414f04cb7413881532438b204c7ce06f78f6deaea2d3cb24",
44
50
  "platforms": {
45
- "linux/arm64": "sha256:402f115c8eed9e6418cf9e47a6bcad2d456d15d032c9b2baaa54a5005f00c8ba",
46
- "linux/amd64": "sha256:50f359882015ca93c8b356a01bf03ce4cb098307f72c30122c7b6ad123eae339"
51
+ "linux/amd64": "sha256:3ee3a4f9c565b0d2adedc3c699871bf8de4c2d7034b2b7a24309f4a6c4d0e64c",
52
+ "linux/arm64": "sha256:44702d741848f96d71dfb0a5322c37c9c248f7a38fe9e7c09b7d307ce000cc3f"
47
53
  }
48
54
  }
49
55
  }
package/src/bin.js CHANGED
@@ -1,17 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import fs from 'node:fs';
4
- import { execFileSync } from 'node:child_process';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { createRequire } from 'node:module';
7
+ import { execFileSync, spawn } from 'node:child_process';
5
8
  import { Option } from 'commander';
6
9
 
7
10
  import { state } from './index.js';
8
11
  import { addSystemCommands, createProgram, handleFatal } from './program.js';
9
- import createBridgeFile from './actions/createInterface.js';
10
12
  import createLib from './actions/createLib.js';
11
13
  import buildWasm from './actions/buildWasm.js';
12
14
  import buildWasiCommand from './actions/buildWasiCommand.js';
13
15
  import buildNode, { addonsInPackages, bundleNodeLoader, publishNodeData } from './actions/buildNode.js';
14
16
  import buildNativeCommand from './actions/buildNativeCommand.js';
17
+ import buildApp, { appOutputPath, assertAppRunner, publishApp } from './actions/buildApp.js';
18
+ import { releaseSigning } from './actions/androidGradle.js';
15
19
  import buildExternal from './actions/buildExternal.js';
16
20
  import buildPackageTypes from './actions/buildTypes.js';
17
21
  import buildLib from './actions/buildLib.js';
@@ -19,8 +23,7 @@ import buildDependencies from './actions/buildDependencies.js';
19
23
  import writeRuntimeEntry, { removeImportHooks, writeImportHooks, writeImportManifest } from './actions/writeRuntimeEntry.js';
20
24
  import { bindsAppImports, createImportedBridges, findAppNativeImports } from './actions/createImportedBridges.js';
21
25
  import { nativeExtensions } from './utils/nativeImports.js';
22
- import getDependFilePath from './integration/getDependFilePath.js';
23
- import { boundHeaderSpecifiers, bridgeTargets, nodeBridgeTarget, resolveBoundHeaders } from './utils/boundHeaders.js';
26
+ import { createBridges, createNativeBridges } from './actions/nativeBridges.js';
24
27
  import { ENTRY_RUNTIMES } from './utils/runtimeEntries.js';
25
28
  import { publishNativeCommand, publishNodeAddon, publishWasiCommand } from './actions/publishBinary.js';
26
29
  import runCrossbindApp from './actions/run.js';
@@ -84,7 +87,8 @@ program.command('build')
84
87
  if (state.config.build.withBuildConfig) {
85
88
  buildExternal(targetParams);
86
89
  } else {
87
- build(targetParams, rebuildDeps, binaryRuntimeEnvs, { bindImports });
90
+ // An app builds only for the platforms -p names, so a plain build keeps making the archives alone.
91
+ build(targetParams, rebuildDeps, binaryRuntimeEnvs, { bindImports }, rest.platform ?? []);
88
92
  }
89
93
  });
90
94
 
@@ -99,13 +103,50 @@ function declaredSbomPath(projectDir) {
99
103
  }
100
104
  }
101
105
 
106
+ // An app's page is the project's Vite build, which @crossbind/plugin-vite makes into the app of the platform named.
107
+ const APP_PLATFORMS = ['android', 'ios', 'linux', 'macos', 'windows'];
108
+
109
+ function projectVite() {
110
+ try {
111
+ const manifest = createRequire(`${state.config.paths.project}/package.json`).resolve('vite/package.json');
112
+ return path.join(path.dirname(manifest), JSON.parse(fs.readFileSync(manifest, 'utf8')).bin.vite);
113
+ } catch {
114
+ throw new Error("crossbind: crossbind app builds the app's page with the project's Vite, which the project does not have. Add vite and @crossbind/plugin-vite to it, as the desktop-app template has them.");
115
+ }
116
+ }
117
+
118
+ const commandApp = program.command('app').description('build the project as an app, or open it on the dev server');
119
+ [
120
+ ['dev', 'open the app on the dev server, and open it anew as its native code changes'],
121
+ ['build', 'build the app with its page: for Android also its APK, and with an upload key its bundle'],
122
+ ].forEach(([name, description]) => commandApp.command(name).description(description)
123
+ .addOption(new Option('-p, --platform <platform>', "the app's platform (default: CROSSBIND_APP_PLATFORM, else this machine's)").choices(APP_PLATFORMS))
124
+ // Vite's own options, as --port or --mode, go on to Vite.
125
+ .allowUnknownOption()
126
+ .allowExcessArguments()
127
+ .action(({ platform }, command) => {
128
+ if (state.config.target.runtimeEnv !== 'app') {
129
+ throw new Error("crossbind: crossbind app builds an app, which this project is not: set target: { runtimeEnv: 'app' } in crossbind.config.js, as the desktop-app template does.");
130
+ }
131
+ // An Android build checks its upload key before the native build, not after it.
132
+ if (name === 'build' && (platform ?? process.env.CROSSBIND_APP_PLATFORM) === 'android') releaseSigning();
133
+ const vite = spawn(process.execPath, [projectVite(), ...(name === 'build' ? ['build'] : []), ...command.args], {
134
+ stdio: 'inherit',
135
+ cwd: state.config.paths.project,
136
+ env: platform ? { ...process.env, CROSSBIND_APP_PLATFORM: platform } : process.env,
137
+ });
138
+ // A signal sent to crossbind alone reaches Vite too, which ends through its exit hooks; Ctrl-C reaches both.
139
+ ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) => process.prependListener(signal, () => vite.kill(signal)));
140
+ vite.on('exit', (code, signal) => process.exit(code ?? 128 + os.constants.signals[signal]));
141
+ }));
142
+
102
143
  program.command('licenses')
103
144
  .description('list bundled native dependencies with SPDX license, versions and source URL')
104
145
  .option('--notices [file]', 'write a THIRD-PARTY-NOTICES markdown file (default: THIRD-PARTY-NOTICES.md)')
105
146
  .option('--sbom [file]', 'write a CycloneDX SBOM json file (default: sbom.cdx.json)')
106
147
  .option('--check', 'exit non-zero when a license field is missing or not valid SPDX')
107
148
  .option('--platform <platform>', "also list what this platform's artifact statically links beyond the package graph (vendored copies, toolchain runtime)")
108
- .addOption(new Option('-e, --runtime-env <runtimeEnv>', 'also list the crossbind runtime an addon of --platform compiles in, or without --platform the one its loader bundles').choices(['node']))
149
+ .addOption(new Option('-e, --runtime-env <runtimeEnv>', "also list the crossbind runtime an addon of --platform compiles in (without --platform the one its loader bundles), or what an app's host carries").choices(['node', 'app']))
109
150
  .option('--json [file]', 'write the rows, license texts included, as JSON (default: licenses.json)')
110
151
  .option('--package', "write this Node-API package's LICENSE, sbom.cdx.json and package.json license field (with -e node)")
111
152
  .action(async (options) => {
@@ -175,7 +216,7 @@ program.command('clean-deps')
175
216
  });
176
217
 
177
218
  // A container belongs to one image: the web container carries no NDK, and android runs a forced platform.
178
- const IMAGE_ROLES = ['web', 'android', 'linux', 'windows'];
219
+ const IMAGE_ROLES = ['web', 'android', 'android-app', 'linux', 'windows'];
179
220
  const dockerContainerName = (role) => {
180
221
  if (!IMAGE_ROLES.includes(role)) {
181
222
  console.error(`crossbind: unknown image "${role}" - expected one of ${IMAGE_ROLES.join(', ')}.`);
@@ -205,8 +246,8 @@ const imageArgument = (command) => command.argument('[image]', `image to act on
205
246
 
206
247
  imageArgument(commandDocker.command('create').description('create docker container'))
207
248
  .action((image) => {
208
- // Google ships the linux NDK for x86_64 only, so an android container is always amd64.
209
- const platform = image === 'android' ? 'linux/amd64' : undefined;
249
+ // Google ships the linux NDK for x86_64 only, so an android or android-app container is always amd64.
250
+ const platform = ['android', 'android-app'].includes(image) ? 'linux/amd64' : undefined;
210
251
  // Docker on Linux makes a missing mount source owned by root, which the user's builds cannot write.
211
252
  [cargoHome(), conanRoot()].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
212
253
  dockerExec([
@@ -306,11 +347,12 @@ function onlyLoadsPackagedAddons(binaryParams) {
306
347
  return targets.every((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm') && addonsInPackages();
307
348
  }
308
349
 
309
- async function build(targetParams, rebuildOption, binaryRuntimeEnvs, wasmOptions = {}) {
350
+ async function build(targetParams, rebuildOption, binaryRuntimeEnvs, wasmOptions = {}, appPlatforms = []) {
310
351
  await buildDependencies({ targetParams, rebuildOption });
311
352
  const binaryParams = { ...targetParams, runtimeEnv: binaryRuntimeEnvs };
312
353
  if (!onlyLoadsPackagedAddons(binaryParams)) {
313
- buildLib(targetParams);
354
+ // An app links its archives into itself, so it publishes no xcframework or podspec of them.
355
+ buildLib(targetParams, { skipXcframework: binaryRuntimeEnvs.includes('app') });
314
356
  }
315
357
  if (binaryRuntimeEnvs.length === 0) {
316
358
  logger.info(`crossbind: no runtime environment was selected, so only the archives were built - pass -e (${runtimeEnvs.join(', ')}) for a binary.`);
@@ -318,6 +360,7 @@ async function build(targetParams, rebuildOption, binaryRuntimeEnvs, wasmOptions
318
360
  await createWasmJs(binaryParams, wasmOptions);
319
361
  await createNodeAddons(binaryParams);
320
362
  await createNativeCommands(binaryParams);
363
+ await createApps({ ...binaryParams, platform: appPlatforms });
321
364
  await createWasiCommands(binaryParams);
322
365
  }
323
366
  buildPackageTypes();
@@ -336,6 +379,26 @@ async function createNativeCommands(targetParams) {
336
379
  }
337
380
  }
338
381
 
382
+ // An app built without a bundler has no page; the Vite plugin puts one in.
383
+ async function createApps(targetParams) {
384
+ const targets = getBuildTargets(targetParams).filter((target) => target.runtimeEnv === 'app');
385
+ if (targets.length === 0) {
386
+ return;
387
+ }
388
+ targets.forEach(assertAppRunner);
389
+ const bridges = createNativeBridges(targets).headers.flatMap((header) => header.bridge ?? []);
390
+ // An iOS or Android app is one project for all its SDKs or ABIs, written once they are built, and every time, as it
391
+ // also takes the app's identity and the host package's template.
392
+ const projects = new Map();
393
+ for (const target of targets) {
394
+ const isProject = ['ios', 'android'].includes(target.platform);
395
+ const built = await buildApp(target, { bridges, force: !isProject && !fs.existsSync(appOutputPath(target)) });
396
+ if (isProject) projects.set(appOutputPath(target), target);
397
+ else if (built) publishApp(target);
398
+ }
399
+ projects.forEach((target) => publishApp(target));
400
+ }
401
+
339
402
  async function createWasiCommands(targetParams) {
340
403
  const targets = getFilteredBuildTargets(targetParams, { platform: 'wasi' });
341
404
  if (targets.length === 0) {
@@ -351,23 +414,6 @@ async function createWasiCommands(targetParams) {
351
414
  }
352
415
  }
353
416
 
354
- function createBridges(target) {
355
- let headers = [];
356
- state.config.paths.header.forEach((headerPath) => {
357
- headers.push(findFiles('**/*.h', { cwd: headerPath }));
358
- });
359
- headers = headers.filter((path) => !!path.toString()).flat();
360
-
361
- return headers.map((header) => ({ file: header, bridge: createBridgeFile(header, target) }));
362
- }
363
-
364
- // The dependency headers the config binds whole, each read where one of the targets' packages ships it.
365
- function createBoundBridges(targets) {
366
- const headers = resolveBoundHeaders(boundHeaderSpecifiers(state.config), bridgeTargets(targets, state.targets), getDependFilePath);
367
- const wholeHeaders = headers.map((header) => header.file);
368
- return headers.map((header) => ({ ...header, bridge: createBridgeFile(header.file, header.target, { wholeHeaders }) }));
369
- }
370
-
371
417
  // A package whose platform packages link its addons ships what loads them: the loader and the data.
372
418
  async function publishLoaders(targets) {
373
419
  fs.mkdirSync(state.config.paths.output, { recursive: true });
@@ -389,10 +435,7 @@ async function createNodeAddons(targetParams) {
389
435
  `${resolveEmbindJsiRoot()}/cpp/src`,
390
436
  `${resolveEmbindNapiRoot()}/third_party/node-api-jsi/jsi`,
391
437
  ];
392
- const boundHeaders = createBoundBridges(targets);
393
- const ownHeaders = [...createBridges(nodeBridgeTarget(targets)), ...boundHeaders];
394
- const imports = findAppNativeImports(bridgeTargets(targets, state.targets));
395
- const headers = [...ownHeaders, ...createImportedBridges(ownHeaders, imports)];
438
+ const { headers, imports } = createNativeBridges(targets);
396
439
  const opt = {
397
440
  buildSource: false,
398
441
  nativeGlob: [
package/src/index.js CHANGED
@@ -3,6 +3,12 @@ export { default as getParentPath } from './utils/getParentPath.js';
3
3
  export { default as createLib } from './actions/createLib.js';
4
4
  export { default as createBridgeFile } from './actions/createInterface.js';
5
5
  export { default as buildWasm } from './actions/buildWasm.js';
6
+ export {
7
+ default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appTargetParams,
8
+ } from './actions/buildApp.js';
9
+ export { default as openInSimulator, buildForSimulator } from './actions/iosSimulator.js';
10
+ export { default as openOnDevice } from './actions/androidDevice.js';
11
+ export { buildApk } from './actions/androidGradle.js';
6
12
  export { default as isSourceNewer } from './actions/isSourceNewer.js';
7
13
  export { default as getData } from './actions/getData.js';
8
14
  export { default as getCmakeParameters } from './actions/getCmakeParameters.js';
@@ -112,7 +112,9 @@ function buildScript(target, symbols, base) {
112
112
  throw new Error('The target is not available!');
113
113
  }
114
114
  const env = JSON.stringify(getEnv(target));
115
- const getPlatformScript = target.platform === 'wasm' ? getWebScript : getReactNativeScript;
115
+ let getPlatformScript = getReactNativeScript;
116
+ if (target.platform === 'wasm') getPlatformScript = getWebScript;
117
+ else if (target.runtimeEnv === 'app') getPlatformScript = getAppScript;
116
118
 
117
119
  let symbolExportDefineString = '';
118
120
  let symbolExportAssignString = '';
@@ -200,6 +202,18 @@ function getReactNativeScript(env) {
200
202
  `;
201
203
  }
202
204
 
205
+ // An app's page loads the runtime the app serves as crossbind.js, which asks the app's host for the module. The build's
206
+ // env is the host's own, so the page passes on only what initNative() is given.
207
+ function getAppScript(env, base) {
208
+ return `
209
+ function __crossbindBoot(config) {
210
+ const base = new URL((typeof __webpack_public_path__ === 'string' ? __webpack_public_path__ : ${JSON.stringify(base)}) || './', document.baseURI).href;
211
+ return import(/* webpackIgnore: true */ /* @vite-ignore */ base + 'crossbind.js')
212
+ .then(() => window.Crossbind.initNative(config));
213
+ }
214
+ `;
215
+ }
216
+
203
217
  function getWebScript(env, base) {
204
218
  const params = `{
205
219
  path: base,
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import loadJson from '../utils/loadJson.js';
3
3
  import writeJson from '../utils/writeJson.js';
4
4
  import {
5
- TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf, wasiNamesOf,
5
+ TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf, wasiNamesOf, appNamesOf,
6
6
  } from '../utils/targets.js';
7
7
  import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
8
8
  import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
@@ -35,6 +35,7 @@ async function initProcessState() {
35
35
  DOCKER_REGISTRY_MIRROR: 'CROSSBIND_REGISTRY_MIRROR',
36
36
  DOCKER_IMAGE_WEB: 'CROSSBIND_IMAGE_WEB',
37
37
  DOCKER_IMAGE_ANDROID: 'CROSSBIND_IMAGE_ANDROID',
38
+ DOCKER_IMAGE_ANDROID_APP: 'CROSSBIND_IMAGE_ANDROID_APP',
38
39
  DOCKER_IMAGE_LINUX: 'CROSSBIND_IMAGE_LINUX',
39
40
  DOCKER_IMAGE_WINDOWS: 'CROSSBIND_IMAGE_WINDOWS',
40
41
  }).forEach(([key, envKey]) => {
@@ -62,6 +63,9 @@ async function initProcessState() {
62
63
  if (target.runtimeEnv === 'native') {
63
64
  Object.assign(target, nativeCommandNamesOf(target, state.config.general.name));
64
65
  }
66
+ if (target.runtimeEnv === 'app') {
67
+ Object.assign(target, appNamesOf(target, state.config.general.name));
68
+ }
65
69
  });
66
70
 
67
71
  setAllDependecyPaths();
@@ -133,6 +133,8 @@ export function getFilledConfig(config, options = { isDepend: false }) {
133
133
  targetSpecs: config.targetSpecs || [],
134
134
  build: config.build || {},
135
135
  target: config.target || {},
136
+ // An app's identity: its bundle id, and on iOS the Apple team that signs it.
137
+ app: config.app || {},
136
138
  extensions: config.extensions || [],
137
139
  package: null,
138
140
  functions: config.functions || {},
@@ -80,7 +80,9 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
80
80
  ...crates.map((c) => `${c.replaceAll('_', '-')}-crossbind-app = { path = "${relPath(superDir, `${root}/${c}`)}" }`),
81
81
  '',
82
82
  '[profile.release]',
83
- 'panic = "abort"',
83
+ // Unwinding, as in a package's bridge, whose shims raise a panic as a JS exception: an abort runtime here could
84
+ // end the process on a panic in any crate. wasm still aborts, as its std does.
85
+ 'panic = "unwind"',
84
86
  '',
85
87
  ...crates.flatMap((c) => [`[profile.release.package.${c.replaceAll('_', '-')}-crossbind-app]`, 'codegen-units = 1', '']),
86
88
  '[workspace]',