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.
- package/package.json +1 -1
- package/src/actions/androidDevice.js +77 -0
- package/src/actions/androidGradle.js +121 -0
- package/src/actions/buildApp.js +434 -0
- package/src/actions/buildDependencies.js +4 -3
- package/src/actions/buildExternal.js +3 -3
- package/src/actions/buildLib.js +1 -1
- package/src/actions/buildNode.js +15 -10
- package/src/actions/createInterface.js +1 -2
- package/src/actions/createLib.js +4 -1
- package/src/actions/getCmakeParameters.js +2 -1
- package/src/actions/getDependLibs.js +8 -2
- package/src/actions/getLinkInputs.js +10 -7
- package/src/actions/gradleProject.js +110 -0
- package/src/actions/iosSimulator.js +63 -0
- package/src/actions/licenses.js +160 -14
- package/src/actions/nativeBridges.js +35 -0
- package/src/actions/run.js +3 -3
- package/src/actions/target.js +2 -1
- package/src/actions/xcodeProject.js +152 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +8 -1
- package/src/assets/toolchain-digests.json +24 -18
- package/src/bin.js +75 -32
- package/src/index.js +6 -0
- package/src/integration/getCrossbindScript.js +15 -1
- package/src/state/index.js +5 -1
- package/src/state/loadConfig.js +2 -0
- package/src/utils/appRustCrates.js +3 -1
- package/src/utils/downloadAndExtractFile.js +42 -14
- package/src/utils/embindRsFingerprint.js +10 -3
- package/src/utils/hermes.js +40 -0
- package/src/utils/plist.js +42 -0
- package/src/utils/pullDockerImage.js +9 -3
- package/src/utils/replaceBasePathForDocker.js +8 -0
- package/src/utils/resolveAppHost.js +21 -0
- package/src/utils/selectRunner.js +1 -1
- package/src/utils/systemKeys.js +4 -0
- package/src/utils/targets.js +19 -3
- 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
|
+
}
|
package/src/actions/run.js
CHANGED
|
@@ -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 (
|
|
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`,
|
package/src/actions/target.js
CHANGED
|
@@ -28,7 +28,8 @@ export function getTargetParams(givenTargetParams = {}, noParamCheck = false) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function getBuildTargets(targetParams) {
|
|
31
|
-
|
|
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.
|
|
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:
|
|
9
|
+
"index": "sha256:720469aca3cff3f1abe69374882d2c48399864be3778225e7754e94036e3be7a",
|
|
10
10
|
"platforms": {
|
|
11
|
-
"linux/arm64": "sha256:
|
|
12
|
-
"linux/amd64": "sha256:
|
|
11
|
+
"linux/arm64": "sha256:3aa68ad3736491de26c6ca9b52a49c80e6291228cc17a05caba4c060ea68913c",
|
|
12
|
+
"linux/amd64": "sha256:e77357a8c763244a5a5b4086fb771e81bec385526432ec6e77fb316f39158c53"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"base": {
|
|
16
|
-
"index": "sha256:
|
|
16
|
+
"index": "sha256:a7c9342c50a493c990f8c26f1bec7e3919ad6010ce8a6cdfaa772487a1040989",
|
|
17
17
|
"platforms": {
|
|
18
|
-
"linux/arm64": "sha256:
|
|
19
|
-
"linux/amd64": "sha256:
|
|
18
|
+
"linux/arm64": "sha256:c9152c3ff4854ba898df1b9fe89e8a94fd29aa5832e8cc5a4bd5fa2e06cee10f",
|
|
19
|
+
"linux/amd64": "sha256:817fc177f488b975bbbd196d9ec9d1d3effbf2c59d1ed82e979fcf5124c4c1aa"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"web": {
|
|
23
|
-
"index": "sha256:
|
|
23
|
+
"index": "sha256:143c2e79a950f732913726f32559f7c70323accf2727b34a12f332df91b765ef",
|
|
24
24
|
"platforms": {
|
|
25
|
-
"linux/arm64": "sha256:
|
|
26
|
-
"linux/amd64": "sha256:
|
|
25
|
+
"linux/arm64": "sha256:ebd0dbeee77a5fde4659b5f129713e5006cc205c629f837b56478e3691e85b9c",
|
|
26
|
+
"linux/amd64": "sha256:14429be7ee52afa0bd9c94248db3a84f90e74c29700a3f5f34da2989d7161431"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"android": {
|
|
30
|
-
"index": "sha256:
|
|
30
|
+
"index": "sha256:249bdfda25dcd394c1e4955f3cd3e63140264697e9a543fbae1dce2ec3d80c38",
|
|
31
31
|
"platforms": {
|
|
32
|
-
"linux/amd64": "sha256:
|
|
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:
|
|
42
|
+
"index": "sha256:433af720cf95a99c4e007c0cfc98ab358c6ddc61fecca75a0b844f598d750b1b",
|
|
37
43
|
"platforms": {
|
|
38
|
-
"linux/
|
|
39
|
-
"linux/
|
|
44
|
+
"linux/arm64": "sha256:0cf32948302c1a23294d28d20dd3b784790c7789b9e99bc8be56888bddb6aeb7",
|
|
45
|
+
"linux/amd64": "sha256:6da084ff433e792381aadd280b769aa1b09ba593e3e21ea4580f4736776e3cb9"
|
|
40
46
|
}
|
|
41
47
|
},
|
|
42
48
|
"windows": {
|
|
43
|
-
"index": "sha256:
|
|
49
|
+
"index": "sha256:c1fb594cb245f6b0414f04cb7413881532438b204c7ce06f78f6deaea2d3cb24",
|
|
44
50
|
"platforms": {
|
|
45
|
-
"linux/
|
|
46
|
-
"linux/
|
|
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
|
|
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
|
|
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
|
-
|
|
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>',
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/src/state/index.js
CHANGED
|
@@ -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();
|
package/src/state/loadConfig.js
CHANGED
|
@@ -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
|
|
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]',
|