crossbind 2.0.0-beta.64 → 2.0.0-beta.66
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 +6 -1
- package/src/actions/androidDevice.js +77 -0
- package/src/actions/androidGradle.js +121 -0
- package/src/actions/appInstaller.js +174 -0
- package/src/actions/appUpdate.js +68 -0
- package/src/actions/appleSigning.js +96 -0
- package/src/actions/buildApp.js +509 -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 +13 -5
- package/src/actions/getCmakeParameters.js +6 -2
- package/src/actions/getDependLibs.js +8 -2
- package/src/actions/getLinkInputs.js +10 -7
- package/src/actions/gradleProject.js +171 -0
- package/src/actions/iosSimulator.js +63 -0
- package/src/actions/licenses.js +189 -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/windowsSigning.js +96 -0
- package/src/actions/xcodeProject.js +186 -0
- package/src/app/index.d.ts +129 -0
- package/src/app/index.js +184 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +8 -1
- package/src/assets/toolchain-digests.json +24 -18
- package/src/bin.js +84 -32
- package/src/index.js +7 -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/appApiHeader.js +29 -0
- package/src/utils/appIcon.js +174 -0
- package/src/utils/appOptions.js +284 -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/src/utils/writeIfChanged.js +1 -1
- package/vitest.config.js +19 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { rollup } from 'rollup';
|
|
7
|
+
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
8
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
9
|
+
import virtual from '@rollup/plugin-virtual';
|
|
10
|
+
import run from './run.js';
|
|
11
|
+
import createLib from './createLib.js';
|
|
12
|
+
import getData from './getData.js';
|
|
13
|
+
import getEnv from './getEnv.js';
|
|
14
|
+
import { copyWindowsNotices, filesUnder, linkInputs } from './buildNode.js';
|
|
15
|
+
import { suppressNodeBuiltinWarnings } from './buildJs.js';
|
|
16
|
+
import { isNativeSourceNewerThan } from './isSourceNewer.js';
|
|
17
|
+
import { createNativeBridges } from './nativeBridges.js';
|
|
18
|
+
import { getTargetParams } from './target.js';
|
|
19
|
+
import writeXcodeProject, { HERMES_SLICES, IOS_SDKS } from './xcodeProject.js';
|
|
20
|
+
import writeGradleProject, { ANDROID_ABIS } from './gradleProject.js';
|
|
21
|
+
import { attachedDevice } from './androidDevice.js';
|
|
22
|
+
import { packageAndroidRelease } from './androidGradle.js';
|
|
23
|
+
import writeInstaller, { assertUnderBase } from './appInstaller.js';
|
|
24
|
+
import writeUpdate from './appUpdate.js';
|
|
25
|
+
import { APPLE_SIGNING, appleSigning, notarizeApp } from './appleSigning.js';
|
|
26
|
+
import { WINDOWS_SIGNING, signWindows, windowsSigning } from './windowsSigning.js';
|
|
27
|
+
import state from '../state/index.js';
|
|
28
|
+
import logger from '../utils/logger.js';
|
|
29
|
+
import findFiles from '../utils/findFiles.js';
|
|
30
|
+
import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
|
|
31
|
+
import { xmlPlist } from '../utils/plist.js';
|
|
32
|
+
import {
|
|
33
|
+
appBackgroundColor, appDisplayName, appHeader, appId, appPermissions, appUrlSchemes, usageDescriptions,
|
|
34
|
+
} from '../utils/appOptions.js';
|
|
35
|
+
import { androidIcons, iosIcon, linuxIcon, macIcon, readAppIcon, windowsIcon } from '../utils/appIcon.js';
|
|
36
|
+
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
37
|
+
import { writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
|
|
38
|
+
import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
|
|
39
|
+
import resolveAppHostRoot, { resolveAppEmbindJsiRoot } from '../utils/resolveAppHost.js';
|
|
40
|
+
import resolveHermes, { HERMES } from '../utils/hermes.js';
|
|
41
|
+
import { getDockerImage, imageRoleFor } from '../utils/pullDockerImage.js';
|
|
42
|
+
import { chosenRunner } from '../utils/selectRunner.js';
|
|
43
|
+
import { DARWIN_DEPLOYMENT_TARGET } from '../utils/darwinToolchain.js';
|
|
44
|
+
|
|
45
|
+
const cpuCount = Math.max(1, os.cpus().length - 1);
|
|
46
|
+
const BRIDGE = 'AppBridge';
|
|
47
|
+
const LINUX_MULTIARCH = { x64: 'x86_64-linux-gnu', arm64: 'aarch64-linux-gnu' };
|
|
48
|
+
// Where the android-app, linux and windows images keep the Hermes of the release crossbind pins.
|
|
49
|
+
const IMAGE_HERMES = `/opt/crossbind/hermes/${HERMES.version}`;
|
|
50
|
+
|
|
51
|
+
// Where an app keeps its executable and its files: a macOS bundle, a Linux or Windows directory, or the app folder of an
|
|
52
|
+
// iOS app's Xcode project or of an Android app's Gradle project, which link the executable themselves.
|
|
53
|
+
export function appLayout(target) {
|
|
54
|
+
const { name } = state.config.general;
|
|
55
|
+
if (target.platform === 'darwin') return { executable: `Contents/MacOS/${name}`, resources: 'Contents/Resources' };
|
|
56
|
+
if (target.platform === 'ios') return { executable: null, resources: 'app' };
|
|
57
|
+
if (target.platform === 'android') return { executable: null, resources: 'app/src/main/assets/app' };
|
|
58
|
+
return { executable: target.platform === 'win32' ? `${name}.exe` : name, resources: 'resources' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The name a bundler gives the app beside the page it built. An iOS app is one Xcode project for both SDKs, and an
|
|
62
|
+
// Android app one Gradle project for both ABIs.
|
|
63
|
+
export function appOutputName(target) {
|
|
64
|
+
const { name } = state.config.general;
|
|
65
|
+
if (target.platform === 'darwin') return `${name}.app`;
|
|
66
|
+
if (['ios', 'android'].includes(target.platform)) return `${name}-${target.platform}`;
|
|
67
|
+
return `${name}-${target.platform}-${target.arch}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// An Android, Linux or Windows app builds in one of crossbind's images, which a local or remote runner does not have.
|
|
71
|
+
export function assertAppRunner(target) {
|
|
72
|
+
if (['darwin', 'ios'].includes(target.platform)) return;
|
|
73
|
+
const runner = chosenRunner(state.config.system);
|
|
74
|
+
if (['DOCKER_RUN', 'DOCKER_EXEC'].includes(runner)) return;
|
|
75
|
+
const app = { android: 'an Android', win32: 'a Windows' }[target.platform] ?? 'a Linux';
|
|
76
|
+
throw new Error(`crossbind: ${app} app builds in crossbind's ${imageRoleFor(target)} image, which RUNNER=${runner} does not use. Build it with RUNNER=DOCKER_RUN.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The app a bundler builds: this machine's desktop app, or with CROSSBIND_APP_PLATFORM an iOS app for both SDKs, an
|
|
80
|
+
// Android app for both ABIs, or a Linux or Windows app for both arches, which builds in Docker on any machine. During
|
|
81
|
+
// development only what runs it: the Simulator, the attached device, or this machine.
|
|
82
|
+
export function appTargetParams({ dev = false } = {}, env = process.env) {
|
|
83
|
+
const platform = env.CROSSBIND_APP_PLATFORM;
|
|
84
|
+
const app = (platforms, archs) => {
|
|
85
|
+
platforms.forEach((name) => assertAppRunner({ platform: name, runtimeEnv: 'app' }));
|
|
86
|
+
return getTargetParams({ platform: platforms, arch: archs, runtime: ['mt'], runtimeEnv: ['app'] }, true);
|
|
87
|
+
};
|
|
88
|
+
if (!platform) return app([process.platform], [process.arch]);
|
|
89
|
+
if (platform === 'android') return app(['android'], dev ? [attachedDevice().abi] : ANDROID_ABIS);
|
|
90
|
+
if (platform === 'linux') {
|
|
91
|
+
if (dev && process.platform !== 'linux') {
|
|
92
|
+
throw new Error('crossbind: a Linux app runs on Linux, and so does its development session; build it here with crossbind app build -p linux.');
|
|
93
|
+
}
|
|
94
|
+
return app(['linux'], dev ? [process.arch] : Object.keys(LINUX_MULTIARCH));
|
|
95
|
+
}
|
|
96
|
+
if (platform === 'macos') {
|
|
97
|
+
if (process.platform !== 'darwin') throw new Error('crossbind: macOS apps build on macOS.');
|
|
98
|
+
return app(['darwin'], [process.arch]);
|
|
99
|
+
}
|
|
100
|
+
if (platform === 'windows') {
|
|
101
|
+
if (dev && process.platform !== 'win32') {
|
|
102
|
+
throw new Error('crossbind: a Windows app runs on Windows, and so does its development session; build it here with crossbind app build -p windows.');
|
|
103
|
+
}
|
|
104
|
+
return app(['win32'], dev ? [process.arch] : ['x64', 'arm64']);
|
|
105
|
+
}
|
|
106
|
+
if (platform !== 'ios') {
|
|
107
|
+
throw new Error(`crossbind: CROSSBIND_APP_PLATFORM=${platform} names no platform an app builds for. Set it to android, ios, linux, macos or windows, or leave it unset for this machine's desktop app.`);
|
|
108
|
+
}
|
|
109
|
+
if (process.platform !== 'darwin') throw new Error('crossbind: iOS apps build on macOS, with Xcode.');
|
|
110
|
+
return getTargetParams({ platform: ['ios'], arch: dev ? ['iphonesimulator'] : IOS_SDKS, runtime: ['mt'], runtimeEnv: ['app'] }, true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The Apple team that signs an iOS app: the project's app.team, else the machine's XCODE_DEVELOPMENT_TEAM, else none.
|
|
114
|
+
function appTeam() {
|
|
115
|
+
const team = state.config.app.team ?? state.config.system.XCODE_DEVELOPMENT_TEAM ?? '';
|
|
116
|
+
if (team && !/^[A-Z0-9]{10}$/.test(team)) {
|
|
117
|
+
throw new Error(`crossbind: ${JSON.stringify(team)} (app.team or XCODE_DEVELOPMENT_TEAM) is no Apple team id, which is ten capital letters and digits.`);
|
|
118
|
+
}
|
|
119
|
+
return team;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// An Android app's id: the project's app.id, else one made from the project's name.
|
|
123
|
+
function androidAppId() {
|
|
124
|
+
const id = state.config.app.id ?? `dev.crossbind.${state.config.general.name.replace(/[^A-Za-z0-9.]/g, '_')}`;
|
|
125
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/.test(id)) {
|
|
126
|
+
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.`);
|
|
127
|
+
}
|
|
128
|
+
return id;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Apple takes dot-separated integers alone as an app's version.
|
|
132
|
+
const appVersion = () => (state.config.package?.version ?? '1.0.0').replace(/[-+].*$/, '');
|
|
133
|
+
|
|
134
|
+
// Android takes any name for an app's version, and a positive integer that grows from release to release as its code.
|
|
135
|
+
function androidVersion() {
|
|
136
|
+
const [major = 0, minor = 0, patch = 0] = appVersion().split('.').map(Number);
|
|
137
|
+
return { name: state.config.package?.version ?? '1.0.0', code: Math.max(1, major * 1000000 + minor * 1000 + patch) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The apps this run built or found current. A part of an iOS app that an earlier run built may be stale.
|
|
141
|
+
const currentApps = new Set();
|
|
142
|
+
|
|
143
|
+
// What a host compiles and links against beside the project's archives: Hermes, on Linux WebKitGTK and on Windows the
|
|
144
|
+
// WebView2 SDK's headers. Apple's Hermes is React Native's release on this machine; the others are in the image the host
|
|
145
|
+
// builds in, android-app, linux or windows, whose reference stands for them in the fingerprint. An Android host shares
|
|
146
|
+
// the C++ runtime of Hermes' libraries.
|
|
147
|
+
async function hostInputs(target) {
|
|
148
|
+
if (['darwin', 'ios'].includes(target.platform)) {
|
|
149
|
+
const hermes = await resolveHermes();
|
|
150
|
+
return { hermes, cmakeArgs: [], inputs: [hermes] };
|
|
151
|
+
}
|
|
152
|
+
assertAppRunner(target);
|
|
153
|
+
const inputs = [getDockerImage(imageRoleFor(target), target.platform === 'android' ? 'linux/amd64' : undefined)];
|
|
154
|
+
if (target.platform === 'android') {
|
|
155
|
+
return { hermes: `${IMAGE_HERMES}/android`, cmakeArgs: ['-DANDROID_STL=c++_shared'], inputs };
|
|
156
|
+
}
|
|
157
|
+
if (target.platform === 'win32') {
|
|
158
|
+
return { hermes: `${IMAGE_HERMES}/win32-${target.arch}`, cmakeArgs: ['-DCROSSBIND_WEBVIEW2_INCLUDE=/opt/crossbind/webview2/include'], inputs };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
hermes: `${IMAGE_HERMES}/linux-${target.arch}`,
|
|
162
|
+
cmakeArgs: [
|
|
163
|
+
`-DCROSSBIND_GTK_SYSROOT=/opt/crossbind/webkitgtk/${LINUX_MULTIARCH[target.arch]}`,
|
|
164
|
+
`-DCROSSBIND_GTK_MULTIARCH=${LINUX_MULTIARCH[target.arch]}`,
|
|
165
|
+
],
|
|
166
|
+
inputs,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The app host package's scripts import crossbind's own runtime modules under crossbind/runtime/.
|
|
171
|
+
const runtimeModules = () => ({
|
|
172
|
+
name: 'crossbind-runtime-modules',
|
|
173
|
+
resolveId: (id) => (id.startsWith('crossbind/runtime/')
|
|
174
|
+
? `${state.config.paths.cli}/assets/js-runtime/${id.slice('crossbind/runtime/'.length)}`
|
|
175
|
+
: null),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
async function bundle(input, file, plugins) {
|
|
179
|
+
const build = await rollup({ input, plugins, onwarn: suppressNodeBuiltinWarnings });
|
|
180
|
+
await build.write({ file, format: 'iife' });
|
|
181
|
+
await build.close();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// A development build claims no scheme: the built app, of the same id, owns them, and a link it opens shows its own page.
|
|
185
|
+
const urlSchemesOf = (target) => (target.buildType === 'release' ? appUrlSchemes(state.config) : []);
|
|
186
|
+
|
|
187
|
+
const infoPlist = (name, version, hasIcon, urlSchemes) => xmlPlist({
|
|
188
|
+
CFBundleDisplayName: appDisplayName(state.config),
|
|
189
|
+
CFBundleExecutable: name,
|
|
190
|
+
// Contents/Resources/icon.icns.
|
|
191
|
+
...(hasIcon ? { CFBundleIconFile: 'icon' } : {}),
|
|
192
|
+
CFBundleIdentifier: appId(state.config),
|
|
193
|
+
CFBundleName: appDisplayName(state.config),
|
|
194
|
+
CFBundlePackageType: 'APPL',
|
|
195
|
+
CFBundleShortVersionString: version,
|
|
196
|
+
...(urlSchemes.length > 0 ? { CFBundleURLTypes: [{ CFBundleURLName: appId(state.config), CFBundleURLSchemes: urlSchemes }] } : {}),
|
|
197
|
+
CFBundleVersion: version,
|
|
198
|
+
LSMinimumSystemVersion: DARWIN_DEPLOYMENT_TARGET,
|
|
199
|
+
NSPrincipalClass: 'NSApplication',
|
|
200
|
+
NSHighResolutionCapable: true,
|
|
201
|
+
...usageDescriptions(state.config, 'darwin'),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// The host's bytecode and the data the packages ship, beside the page's folder.
|
|
205
|
+
function copyAppFiles(dir, hbc, target) {
|
|
206
|
+
fs.mkdirSync(`${dir}/page`, { recursive: true });
|
|
207
|
+
fs.copyFileSync(hbc, `${dir}/host.hbc`);
|
|
208
|
+
Object.entries(getData('data', target)).forEach(([source, dataName]) => {
|
|
209
|
+
if (fs.existsSync(source)) fs.cpSync(source, `${dir}/data/${dataName}`, { recursive: true });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Everything but the page: the host and the app's files, its icon, and on macOS Hermes beside them. A Windows host
|
|
214
|
+
// carries its icon itself.
|
|
215
|
+
function assembleApp(target, { executable, hbc, hermes, icon }) {
|
|
216
|
+
const app = `${state.config.paths.build}/${target.appName}`;
|
|
217
|
+
const { name } = state.config.general;
|
|
218
|
+
const layout = appLayout(target);
|
|
219
|
+
fs.rmSync(app, { recursive: true, force: true });
|
|
220
|
+
fs.mkdirSync(path.dirname(`${app}/${layout.executable}`), { recursive: true });
|
|
221
|
+
fs.copyFileSync(executable, `${app}/${layout.executable}`);
|
|
222
|
+
copyAppFiles(`${app}/${layout.resources}`, hbc, target);
|
|
223
|
+
if (target.platform === 'linux' && icon) fs.writeFileSync(`${app}/${layout.resources}/icon.png`, linuxIcon(icon));
|
|
224
|
+
if (target.platform !== 'darwin') return;
|
|
225
|
+
fs.writeFileSync(`${app}/Contents/Info.plist`, infoPlist(name, appVersion(), Boolean(icon), urlSchemesOf(target)));
|
|
226
|
+
if (icon) fs.writeFileSync(`${app}/Contents/Resources/icon.icns`, macIcon(icon));
|
|
227
|
+
const framework = `${app}/Contents/Frameworks/hermesvm.framework`;
|
|
228
|
+
fs.cpSync(`${hermes}/Library/Frameworks/macosx/hermesvm.framework`, framework, { recursive: true, verbatimSymlinks: true });
|
|
229
|
+
// The release carries Intel and Apple silicon; an app of one arch needs its own half.
|
|
230
|
+
const library = `${framework}/Versions/Current/hermesvm`;
|
|
231
|
+
run(null, ['lipo', library, '-thin', target.arch === 'x64' ? 'x86_64' : 'arm64', '-output', `${library}.thin`], null, target);
|
|
232
|
+
fs.renameSync(`${library}.thin`, library);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// What an iOS SDK target brings to the app's Xcode project: the host and the archives it links, copied beside a link
|
|
236
|
+
// line that names them under {lib}, its slice of Hermes, and the app's files.
|
|
237
|
+
function assembleIosPart(target, { library, linkArgs, hbc, hermes }) {
|
|
238
|
+
const part = `${state.config.paths.build}/${target.appName}`;
|
|
239
|
+
fs.rmSync(part, { recursive: true, force: true });
|
|
240
|
+
fs.mkdirSync(`${part}/lib`, { recursive: true });
|
|
241
|
+
let count = 0;
|
|
242
|
+
const place = (file) => {
|
|
243
|
+
const name = `${count++}-${path.basename(file)}`;
|
|
244
|
+
fs.copyFileSync(file, `${part}/lib/${name}`);
|
|
245
|
+
return `{lib}/${name}`;
|
|
246
|
+
};
|
|
247
|
+
const args = [`-Wl,-force_load,${place(library)}`, ...linkArgs.map((arg) => {
|
|
248
|
+
const archive = arg.match(/^(-Wl,-force_load,)?(\/.+\.a)$/);
|
|
249
|
+
if (archive) return `${archive[1] ?? ''}${place(archive[2])}`;
|
|
250
|
+
if (arg.endsWith('.a')) throw new Error(`crossbind: the iOS app cannot take ${arg} into its Xcode project, as it names no absolute path.`);
|
|
251
|
+
return arg;
|
|
252
|
+
})];
|
|
253
|
+
fs.writeFileSync(`${part}/link.json`, JSON.stringify(args, null, 2));
|
|
254
|
+
fs.cpSync(`${hermes}/Library/Frameworks/universal/hermesvm.xcframework/${HERMES_SLICES[target.arch]}/hermesvm.framework`,
|
|
255
|
+
`${part}/hermesvm.framework`, { recursive: true, verbatimSymlinks: true });
|
|
256
|
+
copyAppFiles(`${part}/app`, hbc, target);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// What an Android ABI target brings to the app's Gradle project: the host, the libraries beside it and the packages'
|
|
260
|
+
// shared libraries it links, and the app's files.
|
|
261
|
+
function assembleAndroidPart(target, { libDir, sharedLibs, hbc }) {
|
|
262
|
+
const part = `${state.config.paths.build}/${target.appName}`;
|
|
263
|
+
fs.rmSync(part, { recursive: true, force: true });
|
|
264
|
+
fs.mkdirSync(`${part}/lib`, { recursive: true });
|
|
265
|
+
[...['libcrossbind_app.so', 'libjsi.so', 'libc++_shared.so'].map((file) => `${libDir}/${file}`), ...sharedLibs]
|
|
266
|
+
.forEach((file) => fs.copyFileSync(file, `${part}/lib/${path.basename(file)}`));
|
|
267
|
+
copyAppFiles(`${part}/app`, hbc, target);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// The parts of an iOS or Android app this run has for its build type, one per SDK or ABI.
|
|
271
|
+
const appParts = (target) => state.targets
|
|
272
|
+
.filter((t) => t.platform === target.platform && t.runtimeEnv === 'app' && t.buildType === target.buildType && currentApps.has(t.appName))
|
|
273
|
+
.map((t) => ({ arch: t.arch, dir: `${state.config.paths.build}/${t.appName}` }));
|
|
274
|
+
|
|
275
|
+
// A Linux or Windows app also ships as one archive, next to its directory: a Windows one as a zip, made in the windows
|
|
276
|
+
// image.
|
|
277
|
+
function archiveApp(app, target) {
|
|
278
|
+
if (target.platform === 'win32') {
|
|
279
|
+
assertUnderBase(app, target);
|
|
280
|
+
fs.rmSync(`${app}.zip`, { force: true });
|
|
281
|
+
run(null, ['zip', '-q', '-r', '-X', `${path.basename(app)}.zip`, path.basename(app)], null, target, { workdir: path.dirname(app) });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const result = spawnSync('tar', ['-czf', `${app}.tar.gz`, '-C', path.dirname(app), path.basename(app)], {
|
|
285
|
+
encoding: 'utf8',
|
|
286
|
+
env: { ...process.env, COPYFILE_DISABLE: '1' },
|
|
287
|
+
});
|
|
288
|
+
if (result.status !== 0) throw new Error(`crossbind: cannot archive ${app}: ${result.stderr || result.error?.message}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const ENTITLEMENTS = {
|
|
292
|
+
camera: 'com.apple.security.device.camera',
|
|
293
|
+
microphone: 'com.apple.security.device.audio-input',
|
|
294
|
+
location: 'com.apple.security.personal-information.location',
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Apple silicon runs signed code only, and a bundle's signature covers every file in it, so an app is signed once the
|
|
298
|
+
// page is in: with the Developer ID the environment names, in the hardened runtime notarizing asks for, or else ad hoc,
|
|
299
|
+
// which runs on the machine that built it.
|
|
300
|
+
function signApp(app, target, signing) {
|
|
301
|
+
const sign = signing ? ['--sign', signing.identity, '--options', 'runtime', '--timestamp'] : ['--sign', '-', '--timestamp=none'];
|
|
302
|
+
run(null, ['codesign', '--force', ...sign, `${app}/Contents/Frameworks/hermesvm.framework`], null, target);
|
|
303
|
+
// The hardened runtime keeps the app from the devices of app.permissions unless its signature claims them.
|
|
304
|
+
const claims = Object.keys(appPermissions(state.config)).map((name) => ENTITLEMENTS[name]);
|
|
305
|
+
const entitlements = claims.length > 0 ? `${state.config.paths.build}/${path.basename(app)}.entitlements` : null;
|
|
306
|
+
if (entitlements) fs.writeFileSync(entitlements, xmlPlist(Object.fromEntries(claims.map((claim) => [claim, true]))));
|
|
307
|
+
run(null, ['codesign', '--force', ...sign, ...(entitlements ? ['--entitlements', entitlements] : []), app], null, target);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// A Windows app is signed before its zip and its setup program are made of it; one with its page that is not, the build
|
|
311
|
+
// says SmartScreen warns of.
|
|
312
|
+
function signWindowsApp(app, target, page) {
|
|
313
|
+
assertUnderBase(app, target);
|
|
314
|
+
const signing = windowsSigning();
|
|
315
|
+
if (signing) signWindows([`${app}/${appLayout(target).executable}`], signing);
|
|
316
|
+
else if (page) console.warn(`crossbind: ${path.basename(app)} is not signed, so SmartScreen warns of it and of its setup program once they are downloaded. Set ${WINDOWS_SIGNING.storetype} and the key it names to sign them with Authenticode.`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// A macOS app with its page is the one that ships: Apple notarizes it, or the build says why another Mac refuses it.
|
|
320
|
+
function shipMacApp(app, signing) {
|
|
321
|
+
const notarize = `${APPLE_SIGNING.key}, ${APPLE_SIGNING.keyId} and ${APPLE_SIGNING.issuer} to have Apple notarize it`;
|
|
322
|
+
if (signing?.notary) notarizeApp(app, signing.notary);
|
|
323
|
+
else if (signing) console.warn(`crossbind: ${path.basename(app)} is signed with your Developer ID but not notarized, so another Mac opens it only once its user allows it in System Settings. Set ${notarize}.`);
|
|
324
|
+
else console.warn(`crossbind: ${path.basename(app)} is signed ad hoc, which opens it on this Mac alone. Set ${APPLE_SIGNING.identity} to sign it with your Developer ID, and ${notarize}.`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Where a build without a bundler puts an app: under its own name, or for iOS and Android as the project of its build
|
|
328
|
+
// type.
|
|
329
|
+
export function appOutputPath(target) {
|
|
330
|
+
const { output } = state.config.paths;
|
|
331
|
+
if (!['ios', 'android'].includes(target.platform)) return `${output}/${target.appName}`;
|
|
332
|
+
return `${output}/${appOutputName(target)}${target.buildType === 'debug' ? '.debug' : ''}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The built app with a page's files at `to`: the bundler's output for its page, or the project's output for a build
|
|
336
|
+
// with none. An iOS app is its Xcode project and an Android app its Gradle project, with every SDK or ABI built so far.
|
|
337
|
+
export function publishApp(target, { to = appOutputPath(target), page = null } = {}) {
|
|
338
|
+
const built = `${state.config.paths.build}/${target.appName}`;
|
|
339
|
+
if (target.platform === 'ios') {
|
|
340
|
+
const parts = appParts(target).map(({ arch, dir }) => ({ sdk: arch, dir }));
|
|
341
|
+
const icon = readAppIcon(state.config);
|
|
342
|
+
writeXcodeProject(to, {
|
|
343
|
+
parts, identity: { id: appId(state.config), team: appTeam() }, label: appDisplayName(state.config), version: appVersion(), icon: icon && iosIcon(icon),
|
|
344
|
+
urlSchemes: appUrlSchemes(state.config), background: appBackgroundColor(state.config), usage: usageDescriptions(state.config, 'ios'),
|
|
345
|
+
});
|
|
346
|
+
} else if (target.platform === 'android') {
|
|
347
|
+
const icon = readAppIcon(state.config);
|
|
348
|
+
writeGradleProject(to, {
|
|
349
|
+
parts: appParts(target), id: androidAppId(), label: appDisplayName(state.config), version: androidVersion(), icons: icon && androidIcons(icon),
|
|
350
|
+
urlSchemes: appUrlSchemes(state.config), background: appBackgroundColor(state.config), permissions: Object.keys(appPermissions(state.config)),
|
|
351
|
+
});
|
|
352
|
+
} else if (path.resolve(to) !== path.resolve(built)) {
|
|
353
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
354
|
+
fs.cpSync(built, to, { recursive: true, verbatimSymlinks: true });
|
|
355
|
+
}
|
|
356
|
+
if (page) {
|
|
357
|
+
// The app may land inside the page's own directory, which it then leaves out, with every app built there and what
|
|
358
|
+
// packages it.
|
|
359
|
+
const apps = new Set([path.basename(to), ...state.targets.filter((t) => t.runtimeEnv === 'app')
|
|
360
|
+
.flatMap((t) => [appOutputName(t), `${state.config.general.name}-${t.platform}-${t.arch}`])]);
|
|
361
|
+
const isApp = (entry) => entry.endsWith('.app') || apps.has(entry.replace(/\.dmg$/, '.app'))
|
|
362
|
+
|| apps.has(entry.replace(/(\.tar\.gz|\.app\.zip|\.zip|\.apk|\.aab|\.deb|-setup\.exe|\.update\.json)$/, ''));
|
|
363
|
+
fs.readdirSync(page).filter((entry) => !isApp(entry)).forEach((entry) => {
|
|
364
|
+
fs.cpSync(`${page}/${entry}`, `${to}/${appLayout(target).resources}/page/${entry}`, { recursive: true });
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (target.platform === 'darwin') {
|
|
368
|
+
const signing = appleSigning();
|
|
369
|
+
signApp(to, target, signing);
|
|
370
|
+
if (page) shipMacApp(to, signing);
|
|
371
|
+
} else if (target.platform === 'android' && target.buildType === 'release') {
|
|
372
|
+
packageAndroidRelease(to);
|
|
373
|
+
} else if (!['ios', 'android'].includes(target.platform)) {
|
|
374
|
+
if (target.platform === 'win32') signWindowsApp(to, target, page);
|
|
375
|
+
archiveApp(to, target);
|
|
376
|
+
}
|
|
377
|
+
// A desktop app with its page ships with what installs it, and with an updater's key with its update too.
|
|
378
|
+
if (page && ['darwin', 'linux', 'win32'].includes(target.platform)) {
|
|
379
|
+
writeInstaller(to, target);
|
|
380
|
+
writeUpdate(to, target);
|
|
381
|
+
}
|
|
382
|
+
return to;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Builds an app's host from the project's archives and the given bridges, its Hermes bytecode and the page runtime the
|
|
386
|
+
// page loads as crossbind.js, and puts them into an app without a page.
|
|
387
|
+
export default async function buildApp(target, { bridges = [], force = false } = {}) {
|
|
388
|
+
if (['darwin', 'ios'].includes(target.platform) && process.platform !== 'darwin') {
|
|
389
|
+
logger.info(`[${target.path}] app skipped (macOS and iOS apps build on a macOS host)`);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
if (state.config.export.type === 'cargo') {
|
|
393
|
+
logger.info(`[${target.path}] app skipped (cargo package - staticlib only)`);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
// Where the page opens files, a desktop host's title and window, a Windows one's version, and the app's icon: a bad
|
|
397
|
+
// app section stops the build before anything compiles.
|
|
398
|
+
const header = appHeader(state.config, target.platform);
|
|
399
|
+
const icon = readAppIcon(state.config);
|
|
400
|
+
|
|
401
|
+
const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
|
|
402
|
+
const { build } = state.config.paths;
|
|
403
|
+
const appHostRoot = resolveAppHostRoot();
|
|
404
|
+
const jsiRoot = resolveAppEmbindJsiRoot();
|
|
405
|
+
const { hermes, cmakeArgs, inputs } = await hostInputs(target);
|
|
406
|
+
|
|
407
|
+
// The bridges compile against the embind runtime, which speaks the JSI of the Hermes the host links. Android's
|
|
408
|
+
// Hermes comes without JSI's headers, which the host package has from the same release.
|
|
409
|
+
const headerDirs = [`${jsiRoot}/cpp/src`, `${hermes}/include`, ...(target.platform === 'android' ? [`${appHostRoot}/third_party/jsi/include`] : [])];
|
|
410
|
+
createLib(target, BRIDGE, {
|
|
411
|
+
buildSource: false,
|
|
412
|
+
nativeGlob: [`${state.config.paths.cli}/assets/cpp-runtime/commonBridges.cpp`, ...bridges],
|
|
413
|
+
headerDirs,
|
|
414
|
+
// Headers in the image count by their path, which names the Hermes release.
|
|
415
|
+
inputs: headerDirs.flatMap((dir) => (fs.existsSync(dir) ? findFiles('**/*.h', { cwd: dir }) : [dir])),
|
|
416
|
+
});
|
|
417
|
+
const { libs, linkArgs, extraSources } = linkInputs(target, { bridge: BRIDGE });
|
|
418
|
+
|
|
419
|
+
const fingerprintFile = `${build}/${target.appName}.fingerprint`;
|
|
420
|
+
const fingerprint = getContentHash(JSON.stringify({
|
|
421
|
+
builder: getFilesFingerprint([fileURLToPath(import.meta.url), fileURLToPath(new URL('../utils/appIcon.js', import.meta.url))]),
|
|
422
|
+
header,
|
|
423
|
+
// A desktop app carries its icon; an iOS or Android one gets it as its project is written.
|
|
424
|
+
icon: ['ios', 'android'].includes(target.platform) ? null : icon?.hash ?? null,
|
|
425
|
+
// A macOS app's bundle takes its id, name and version from it.
|
|
426
|
+
infoPlist: target.platform === 'darwin' ? infoPlist(state.config.general.name, appVersion(), Boolean(icon), urlSchemesOf(target)) : null,
|
|
427
|
+
hermes: HERMES.version,
|
|
428
|
+
inputs,
|
|
429
|
+
linkArgs,
|
|
430
|
+
libs: libs.map((lib) => {
|
|
431
|
+
const stat = fs.existsSync(lib) ? fs.statSync(lib) : null;
|
|
432
|
+
return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
|
|
433
|
+
}),
|
|
434
|
+
env: getEnv(target),
|
|
435
|
+
data: getData('data', target),
|
|
436
|
+
version: state.config.package?.version,
|
|
437
|
+
runtime: getFilesFingerprint(filesUnder([
|
|
438
|
+
`${appHostRoot}/cpp`, `${appHostRoot}/js`, `${appHostRoot}/third_party`, `${jsiRoot}/cpp/src`, `${jsiRoot}/js`,
|
|
439
|
+
`${state.config.paths.cli}/assets/js-runtime/adapters`,
|
|
440
|
+
])),
|
|
441
|
+
extraSources: getFilesFingerprint(extraSources),
|
|
442
|
+
}));
|
|
443
|
+
const isChanged = !fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint;
|
|
444
|
+
if (!force && !isChanged && fs.existsSync(`${build}/${target.appName}`) && fs.existsSync(`${build}/${target.appJsName}`)) {
|
|
445
|
+
logger.cachedStep(target, 'app');
|
|
446
|
+
currentApps.add(target.appName);
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const platformPrefix = `App-${buildType}`;
|
|
451
|
+
const workDir = `${build}/${platformPrefix}/${target.path}`;
|
|
452
|
+
const { name } = state.config.general;
|
|
453
|
+
// An Android app's activity loads its host by this one name.
|
|
454
|
+
const hostName = target.platform === 'android' ? 'crossbind_app' : name;
|
|
455
|
+
logger.startStep(target, 'app');
|
|
456
|
+
resetCMakeCacheForSource(workDir, `${appHostRoot}/cpp`, target);
|
|
457
|
+
// The host includes it from its build directory; written only when it changed, it recompiles nothing otherwise.
|
|
458
|
+
writeIfChanged(`${workDir}/crossbind_app.h`, header);
|
|
459
|
+
// A Windows host's executable carries the app's icon, which its resource script takes from beside the header.
|
|
460
|
+
if (target.platform === 'win32') {
|
|
461
|
+
if (icon) writeIfChanged(`${workDir}/icon.ico`, windowsIcon(icon));
|
|
462
|
+
else fs.rmSync(`${workDir}/icon.ico`, { force: true });
|
|
463
|
+
}
|
|
464
|
+
run(null, [
|
|
465
|
+
// An iOS host builds with crossbind's iOS toolchain file rather than as an Xcode project of its own.
|
|
466
|
+
target.platform === 'ios' ? 'ios-cmake' : 'cmake', `${appHostRoot}/cpp`,
|
|
467
|
+
`-DCMAKE_BUILD_TYPE=${buildType}`,
|
|
468
|
+
`-DCROSSBIND_APP_FILE=${hostName}`,
|
|
469
|
+
`-DCROSSBIND_JSI_ROOT=${jsiRoot}`,
|
|
470
|
+
`-DCROSSBIND_HERMES=${hermes}`,
|
|
471
|
+
`-DCROSSBIND_EXTRA_SOURCES=${extraSources.join(';')}`,
|
|
472
|
+
`-DCROSSBIND_LINK_ARGS=${linkArgs.join(';')}`,
|
|
473
|
+
`-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
|
|
474
|
+
...cmakeArgs,
|
|
475
|
+
], platformPrefix, target);
|
|
476
|
+
run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target, { exclusive: true });
|
|
477
|
+
if (target.platform === 'win32') copyWindowsNotices(target);
|
|
478
|
+
|
|
479
|
+
const systemConfig = `export default ${JSON.stringify({ env: getEnv(target) })};`;
|
|
480
|
+
await bundle(`${appHostRoot}/js/host.js`, `${workDir}/host.js`, [
|
|
481
|
+
virtual({ 'crossbind/systemConfig': systemConfig }), runtimeModules(), nodeResolve(), commonjs(),
|
|
482
|
+
]);
|
|
483
|
+
// Without -Xes6-block-scoping, hermesc compiles let and const as var, and closures made in a loop share one binding.
|
|
484
|
+
run(null, [`${hermes}/bin/hermesc`, '-O', '-Xes6-block-scoping', '-emit-binary', '-out', `${workDir}/host.hbc`, `${workDir}/host.js`],
|
|
485
|
+
platformPrefix, target);
|
|
486
|
+
await bundle(`${appHostRoot}/js/page.js`, `${build}/${target.appJsName}`, []);
|
|
487
|
+
if (target.platform === 'ios') {
|
|
488
|
+
assembleIosPart(target, { library: `${workDir}/lib${name}.a`, linkArgs, hbc: `${workDir}/host.hbc`, hermes });
|
|
489
|
+
} else if (target.platform === 'android') {
|
|
490
|
+
assembleAndroidPart(target, { libDir: workDir, sharedLibs: libs.filter((lib) => lib.endsWith('.so')), hbc: `${workDir}/host.hbc` });
|
|
491
|
+
} else {
|
|
492
|
+
assembleApp(target, { executable: `${workDir}/${path.basename(appLayout(target).executable)}`, hbc: `${workDir}/host.hbc`, hermes, icon });
|
|
493
|
+
}
|
|
494
|
+
logger.doneStep(target, 'app');
|
|
495
|
+
|
|
496
|
+
fs.writeFileSync(fingerprintFile, fingerprint);
|
|
497
|
+
writeNativeSourceStamp(`${build}/${target.appName}`, state.config.paths.native);
|
|
498
|
+
currentApps.add(target.appName);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// The app of the project as it stands, for a bundler plugin, which needs the host before the page asks for anything:
|
|
503
|
+
// the bridges a plain build binds, found in the project's headers and sources, then its own archive and the host.
|
|
504
|
+
export async function buildProjectApp(target) {
|
|
505
|
+
const bridges = createNativeBridges([target]).headers.flatMap((header) => header.bridge ?? []);
|
|
506
|
+
const force = isNativeSourceNewerThan(`${state.config.paths.build}/${target.appName}`);
|
|
507
|
+
createLib(target, 'Source', { force, buildSource: true });
|
|
508
|
+
return buildApp(target, { bridges, force });
|
|
509
|
+
}
|
|
@@ -83,12 +83,13 @@ async function buildMissingCargoDependencies(buildTargets, targetParams) {
|
|
|
83
83
|
const bridgeTarget = buildTargets.some(isWasm) ? state.targets.find(isWasm) : undefined;
|
|
84
84
|
const extraTarget = bridgeTarget && !buildTargets.some((target) => target.path === bridgeTarget.path) ? bridgeTarget : undefined;
|
|
85
85
|
const targets = extraTarget ? [...buildTargets, extraTarget] : buildTargets;
|
|
86
|
-
// A prebuilt from another embind-rs
|
|
87
|
-
|
|
86
|
+
// A prebuilt from another embind-rs, or for an image's target from another rustc, counts as missing: linked, it
|
|
87
|
+
// carries stale glue or clashes with the others.
|
|
88
88
|
const prebuiltOf = (dep, target) => [target.path, target.releasePath]
|
|
89
89
|
.map((targetPath) => `${dep.paths.output}/prebuilt/${targetPath}`)
|
|
90
90
|
.find((dir) => fs.existsSync(dir)) ?? `${dep.paths.output}/prebuilt/${target.path}`;
|
|
91
|
-
const isBuilt = (dep, target) => dep.functions.isEnabled(target)
|
|
91
|
+
const isBuilt = (dep, target) => dep.functions.isEnabled(target)
|
|
92
|
+
&& !isEmbindRsFingerprintStale(prebuiltOf(dep, target), getEmbindRsFingerprint(target));
|
|
92
93
|
const missing = cargoDependencies.filter((dep) => targets.some((target) => !isBuilt(dep, target)));
|
|
93
94
|
|
|
94
95
|
for (const dep of missing) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import replace from 'replace';
|
|
3
|
-
import downloadAndExtractFile from '../utils/downloadAndExtractFile.js';
|
|
3
|
+
import downloadAndExtractFile, { sourcesOf } from '../utils/downloadAndExtractFile.js';
|
|
4
4
|
import copyToDistSource from '../utils/copyToDistSource.js';
|
|
5
5
|
import { getBuildTargets } from './target.js';
|
|
6
6
|
import state from '../state/index.js';
|
|
@@ -14,8 +14,8 @@ export default async function buildExternal(targetParams, options = {}) {
|
|
|
14
14
|
throw new Error(`crossbind: "${state.config.general?.name}" has a crossbind.build recipe but no "nativeVersion" in package.json — cannot build from source.`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const {
|
|
18
|
-
const isNewlyCreated = await downloadAndExtractFile(
|
|
17
|
+
const { sha256, replaceList, copyToSource, copyToDist } = state.config.build;
|
|
18
|
+
const isNewlyCreated = await downloadAndExtractFile(sourcesOf(state.config.build, version), state.config.paths.build, sha256);
|
|
19
19
|
const sourcePath = `${state.config.paths.build}/source`;
|
|
20
20
|
if (isNewlyCreated && replaceList) {
|
|
21
21
|
replaceList.forEach(({ regex, replacement, paths }) => {
|
package/src/actions/buildLib.js
CHANGED
|
@@ -54,7 +54,7 @@ export default function buildLib(targetParams, options = {}) {
|
|
|
54
54
|
fs.copyFileSync(modulePath, `${state.config.paths.output}/prebuilt/${target.path}/swig/${fileName}`);
|
|
55
55
|
});
|
|
56
56
|
writeSourceFingerprint(libdir, sourceFingerprint);
|
|
57
|
-
if (isCargo) writeEmbindRsFingerprint(libdir, getEmbindRsFingerprint());
|
|
57
|
+
if (isCargo) writeEmbindRsFingerprint(libdir, getEmbindRsFingerprint(target));
|
|
58
58
|
isChanged = true;
|
|
59
59
|
} else {
|
|
60
60
|
logger.cachedStep(target, 'lib');
|
package/src/actions/buildNode.js
CHANGED
|
@@ -24,7 +24,7 @@ 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
|
-
function filesUnder(dirs) {
|
|
27
|
+
export function filesUnder(dirs) {
|
|
28
28
|
return dirs.flatMap((dir) => (fs.existsSync(dir)
|
|
29
29
|
? fs.readdirSync(dir, { recursive: true })
|
|
30
30
|
.map((file) => `${dir}/${file}`)
|
|
@@ -32,18 +32,21 @@ function filesUnder(dirs) {
|
|
|
32
32
|
: []));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// The addon's link, and an app host's, whose bridges come from an archive of their own.
|
|
36
|
+
export function linkInputs(target, { bridge } = {}) {
|
|
37
|
+
const isMachO = ['darwin', 'ios'].includes(target.platform);
|
|
36
38
|
const {
|
|
37
39
|
libs, appRustLibs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
|
|
38
40
|
} = getLinkInputs(target, {
|
|
39
41
|
// The linker loads the archive member that defines a -u symbol; Mach-O prefixes C names with _.
|
|
40
|
-
keepFlag: (name) => `-Wl,-u,${
|
|
42
|
+
keepFlag: (name) => `-Wl,-u,${isMachO ? '_' : ''}crossbind_keep_${name}`,
|
|
43
|
+
bridge,
|
|
41
44
|
});
|
|
42
45
|
// The app's Rust goes first: what a package's kept member needs from std and rustc's allocator shims is then
|
|
43
46
|
// already the app's, and the package's own copies stay in its archive instead of defining them twice.
|
|
44
47
|
const ordered = [...appRustLibs, ...libs.filter((lib) => !appRustLibs.includes(lib))];
|
|
45
48
|
const linkArgs = [
|
|
46
|
-
...buildLinkLibArgs(ordered, { wholeArchiveAll, wholeArchiveNames, forceLoad:
|
|
49
|
+
...buildLinkLibArgs(ordered, { wholeArchiveAll, wholeArchiveNames, forceLoad: isMachO }),
|
|
47
50
|
...rustKeepFlags,
|
|
48
51
|
// The system libraries the archives need, declared by the packages that bring them in.
|
|
49
52
|
...(getData('binary', target)?.addonFlags ?? []),
|
|
@@ -116,6 +119,13 @@ export function bundleNodeLoader(target) {
|
|
|
116
119
|
return bundleLoader(target, resolveEmbindNapiRoot(), nodeLoaderConfig(target));
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
// The mingw-w64 runtime and winpthreads in a Windows addon or app ask for their notices in binary
|
|
123
|
+
// distributions. The windows image carries them; a host toolchain may not.
|
|
124
|
+
export function copyWindowsNotices(target) {
|
|
125
|
+
const notices = toolchainNoticesDir('win32');
|
|
126
|
+
run(null, ['sh', '-c', `if [ -d ${WINDOWS_IMAGE_NOTICES} ]; then mkdir -p ${notices} && cp ${WINDOWS_IMAGE_NOTICES}/* ${notices}/; fi`], null, target);
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
// Links one Node-API addon from the project's archives and bundles the loader that picks the
|
|
120
130
|
// addon matching the running process.
|
|
121
131
|
export default async function buildNode(target, options = {}) {
|
|
@@ -170,12 +180,7 @@ export default async function buildNode(target, options = {}) {
|
|
|
170
180
|
`-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
|
|
171
181
|
], platformPrefix, target);
|
|
172
182
|
run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target, { exclusive: true });
|
|
173
|
-
if (target.platform === 'win32')
|
|
174
|
-
// The mingw-w64 runtime and winpthreads in the addon ask for their notices in binary
|
|
175
|
-
// distributions. The windows image carries them; a host toolchain may not.
|
|
176
|
-
const notices = toolchainNoticesDir('win32');
|
|
177
|
-
run(null, ['sh', '-c', `if [ -d ${WINDOWS_IMAGE_NOTICES} ]; then mkdir -p ${notices} && cp ${WINDOWS_IMAGE_NOTICES}/* ${notices}/; fi`], null, target);
|
|
178
|
-
}
|
|
183
|
+
if (target.platform === 'win32') copyWindowsNotices(target);
|
|
179
184
|
fs.copyFileSync(`${build}/${platformPrefix}/${target.path}/${target.addonName}`, `${build}/${target.addonName}`);
|
|
180
185
|
logger.doneStep(target, 'addon');
|
|
181
186
|
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
} from '../utils/swigInterface.js';
|
|
22
22
|
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
23
23
|
import { withDirLockSync } from '../utils/dirLock.js';
|
|
24
|
-
import { imageRoleFor } from '../utils/pullDockerImage.js';
|
|
25
24
|
import { conanImportOfHeader } from '../utils/conanImport.js';
|
|
26
25
|
import run, { cxxPreprocessorFor } from './run.js';
|
|
27
26
|
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
@@ -355,7 +354,7 @@ const RELEASE_DEFINE = '-DNDEBUG';
|
|
|
355
354
|
// One bridge serves every desktop target, so SWIG and the macro table it reads come from the web image's em++,
|
|
356
355
|
// which defines no operating system; an android bridge is read with the NDK it compiles with. The headers
|
|
357
356
|
// still come from the target's own packages.
|
|
358
|
-
const toolTargetFor = (target) => (
|
|
357
|
+
const toolTargetFor = (target) => (target?.platform === 'android' ? target : { ...target, platform: 'wasm' });
|
|
359
358
|
|
|
360
359
|
function dumpMacros(outputFile, includes, includePath, target, defines = []) {
|
|
361
360
|
const tool = toolTargetFor(target);
|