crossbind 2.0.0-beta.69 → 2.0.0-beta.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/actions/addExtension.js +6 -1
- package/src/actions/buildApp.js +10 -5
- package/src/actions/buildDependencies.js +3 -3
- package/src/actions/createImportedBridges.js +10 -8
- package/src/actions/createInterface.js +23 -21
- package/src/actions/createLib.js +3 -3
- package/src/actions/getAllBridges.js +20 -5
- package/src/actions/getDependLibs.js +5 -0
- package/src/actions/getLinkInputs.js +2 -1
- package/src/actions/gradleProject.js +13 -2
- package/src/actions/licenses.js +30 -24
- package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
- package/src/actions/writeRuntimeEntry.js +2 -2
- package/src/actions/xcodeProject.js +12 -6
- package/src/app/index.d.ts +19 -0
- package/src/app/index.js +25 -1
- package/src/assets/node-import-hooks.mjs +7 -6
- package/src/bin.js +17 -9
- package/src/index.js +2 -2
- package/src/integration/getCrossbindScript.js +3 -2
- package/src/integration/getDependFilePath.js +10 -10
- package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
- package/src/state/index.js +6 -5
- package/src/state/loadConfig.js +10 -4
- package/src/state/refreshPackageSources.js +19 -0
- package/src/utils/appExtensions.js +44 -28
- package/src/utils/appOptions.js +31 -10
- package/src/utils/appRustCrates.js +2 -2
- package/src/utils/bridgeTools.js +6 -1
- package/src/utils/cppDts.js +11 -11
- package/src/utils/extensionSteps.js +38 -1
- package/src/utils/nativeImports.js +9 -2
- package/src/utils/nodeDevBuild.js +2 -1
- package/src/utils/packageSourceApi.js +87 -0
- package/src/utils/packageSourceImport.js +69 -0
- package/src/utils/packageSourceInstall.js +162 -0
- package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
- package/src/utils/packageSources.js +108 -0
- package/src/utils/printableText.js +5 -0
- package/src/utils/runPackageSource.js +251 -0
- package/src/utils/rustBridgeGen.js +3 -3
- package/src/utils/targetToolchain.js +77 -0
- package/src/state/refreshConanDependencies.js +0 -17
- package/src/utils/conanDependencies.js +0 -66
- package/src/utils/conanImport.js +0 -58
- package/src/utils/conanInstall.js +0 -154
- package/src/utils/conanProfile.js +0 -203
- package/src/utils/runConan.js +0 -257
|
@@ -151,8 +151,9 @@ function objects(name, targetSettings, hasAssets, hasPrivacy, added) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// The asset catalog: the app's icon, from which Xcode makes every size, and the launch screen's color
|
|
155
|
-
|
|
154
|
+
// The asset catalog: the app's icon, from which Xcode makes every size, and the launch screen's color, with the one of
|
|
155
|
+
// the dark appearance where there is one.
|
|
156
|
+
function writeAssets(catalog, icon, background, darkBackground) {
|
|
156
157
|
const info = { author: 'xcode', version: 1 };
|
|
157
158
|
const assetSet = (set, contents) => {
|
|
158
159
|
fs.mkdirSync(`${catalog}/${set}`, { recursive: true });
|
|
@@ -163,9 +164,13 @@ function writeAssets(catalog, icon, background) {
|
|
|
163
164
|
assetSet('AppIcon.appiconset', { images: [{ filename: 'icon.png', idiom: 'universal', platform: 'ios', size: '1024x1024' }] });
|
|
164
165
|
fs.writeFileSync(`${catalog}/AppIcon.appiconset/icon.png`, icon);
|
|
165
166
|
}
|
|
167
|
+
const colorOf = (rgb) => {
|
|
168
|
+
const [red, green, blue] = [1, 3, 5].map((at) => `0x${rgb.slice(at, at + 2).toUpperCase()}`);
|
|
169
|
+
return { 'color-space': 'srgb', components: { alpha: '1.000', red, green, blue } };
|
|
170
|
+
};
|
|
166
171
|
if (background) {
|
|
167
|
-
const
|
|
168
|
-
assetSet(`${LAUNCH_COLOR}.colorset`, { colors: [{ color:
|
|
172
|
+
const dark = darkBackground ? [{ appearances: [{ appearance: 'luminosity', value: 'dark' }], color: colorOf(darkBackground), idiom: 'universal' }] : [];
|
|
173
|
+
assetSet(`${LAUNCH_COLOR}.colorset`, { colors: [{ color: colorOf(background), idiom: 'universal' }, ...dark] });
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
|
|
@@ -176,7 +181,8 @@ function writeAssets(catalog, icon, background) {
|
|
|
176
181
|
// where one of them asks for it.
|
|
177
182
|
export default function writeXcodeProject(to, {
|
|
178
183
|
parts, identity, label, version, build = version, icon = null, urlSchemes = [], fileTypes = [], links = [], activityTypes = [], shortcutItems = [],
|
|
179
|
-
background = null, usage = {}, encryption = null, multipleWindows = false, privacy = {}, entitlements = {}, plist = [], targets = [],
|
|
184
|
+
background = null, darkBackground = null, usage = {}, encryption = null, multipleWindows = false, privacy = {}, entitlements = {}, plist = [], targets = [],
|
|
185
|
+
appGroup = null,
|
|
180
186
|
}) {
|
|
181
187
|
const { name } = state.config.general;
|
|
182
188
|
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.`);
|
|
@@ -206,7 +212,7 @@ export default function writeXcodeProject(to, {
|
|
|
206
212
|
|
|
207
213
|
fs.writeFileSync(`${to}/main.m`, MAIN);
|
|
208
214
|
fs.writeFileSync(`${to}/Info.plist`, info);
|
|
209
|
-
writeAssets(`${to}/Assets.xcassets`, icon, background);
|
|
215
|
+
writeAssets(`${to}/Assets.xcassets`, icon, background, darkBackground);
|
|
210
216
|
const hasPrivacy = Object.keys(privacy).length > 0;
|
|
211
217
|
if (hasPrivacy) fs.writeFileSync(`${to}/PrivacyInfo.xcprivacy`, privacyManifest(privacy));
|
|
212
218
|
const hasEntitlements = Object.keys(signed).length > 0;
|
package/src/app/index.d.ts
CHANGED
|
@@ -24,6 +24,23 @@ export declare const app: {
|
|
|
24
24
|
launchArgs(): Promise<Launch>;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
/** Where the app runs, as its host tells the page before the page's own scripts run; the same through the app's run. */
|
|
28
|
+
export interface Device {
|
|
29
|
+
/** The platform the app is built for. */
|
|
30
|
+
readonly platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android';
|
|
31
|
+
/** The arch the app's process runs as. */
|
|
32
|
+
readonly arch: 'arm64' | 'x64';
|
|
33
|
+
/** What the device is to its user as the app starts: a phone; a tablet, an iPad or an Android device whose screen's shorter side has 600dp; or a desktop, a computer, a Chromebook or a Mac that runs an iOS app. */
|
|
34
|
+
readonly formFactor: 'phone' | 'tablet' | 'desktop';
|
|
35
|
+
/** The system's name and version, as its maker numbers it: iOS or iPadOS 18.4, Android 15 with its API level, macOS 26.1, Windows 10.0.26100, or a Linux distribution's, as Ubuntu 24.04, else its kernel's release. */
|
|
36
|
+
readonly os: { readonly name: string; readonly version: string; readonly apiLevel?: number };
|
|
37
|
+
/** Whether the iOS Simulator or an Android emulator runs the app. */
|
|
38
|
+
readonly isVirtual: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Where the app runs, read at once, as the page starts; reading it outside an app throws. */
|
|
42
|
+
export declare const device: Device;
|
|
43
|
+
|
|
27
44
|
/** What windows.set() changes of an open window: sizes and places in logical pixels, a page's size without the frame, and x and y the frame's place from the top left of the main screen, given together. A limit of 0 takes it away. */
|
|
28
45
|
export interface WindowChanges {
|
|
29
46
|
title?: string;
|
|
@@ -57,6 +74,8 @@ export interface WindowOptions extends Omit<WindowChanges, 'center' | 'minWidth'
|
|
|
57
74
|
backgroundColor?: string;
|
|
58
75
|
/** false opens a window without a title bar and its buttons, which the page moves with data-crossbind-drag or windows.startDrag(). */
|
|
59
76
|
frame?: boolean;
|
|
77
|
+
/** 'hidden' lays a macOS window's page under its title bar, whose buttons stay at its top left, as app.window.titleBar does; Windows and Linux keep their title bar and list it in ignored. */
|
|
78
|
+
titleBar?: 'visible' | 'hidden';
|
|
60
79
|
/** The id of the window it stays above, and closes with. */
|
|
61
80
|
parent?: number;
|
|
62
81
|
/** With a parent: a sheet of the parent's on macOS, and on Windows and Linux a window the parent takes no input under until it closes. */
|
package/src/app/index.js
CHANGED
|
@@ -39,8 +39,9 @@ const WINDOW_SHARED = {
|
|
|
39
39
|
};
|
|
40
40
|
const WINDOW_SET = { ...WINDOW_SHARED, center: 'boolean' };
|
|
41
41
|
const WINDOW_OPEN = {
|
|
42
|
-
...WINDOW_SHARED, path: 'string', name: 'string', backgroundColor: 'string', frame: 'boolean', parent: 'number', modal: 'boolean',
|
|
42
|
+
...WINDOW_SHARED, path: 'string', name: 'string', backgroundColor: 'string', frame: 'boolean', titleBar: 'string', parent: 'number', modal: 'boolean',
|
|
43
43
|
};
|
|
44
|
+
const TITLE_BARS = ['visible', 'hidden'];
|
|
44
45
|
const MAX_WINDOW_SIZE = 16384;
|
|
45
46
|
const MAX_WINDOW_PLACE = 1000000;
|
|
46
47
|
// The size a window opens at without one.
|
|
@@ -70,6 +71,10 @@ function checkedWindow(name, options, kinds) {
|
|
|
70
71
|
throw new TypeError(`crossbind: ${name}'s path ${JSON.stringify(path)} is no path of the app's own pages, as /settings.html is.`);
|
|
71
72
|
}
|
|
72
73
|
if (backgroundColor !== undefined && !COLOR.test(backgroundColor)) throw new TypeError(`crossbind: ${name}'s backgroundColor is no color of the form #rrggbb.`);
|
|
74
|
+
if (options.titleBar !== undefined && !TITLE_BARS.includes(options.titleBar)) throw new TypeError(`crossbind: ${name}'s titleBar is neither 'visible' nor 'hidden'.`);
|
|
75
|
+
if (options.frame === false && options.titleBar === 'hidden') {
|
|
76
|
+
throw new TypeError(`crossbind: ${name} hides the title bar of a window with a frame alone: drop titleBar, or frame: false.`);
|
|
77
|
+
}
|
|
73
78
|
if (parent !== undefined && !isWindowId(parent)) throw new TypeError(`crossbind: ${name}'s parent is no window's id.`);
|
|
74
79
|
if (options.modal && parent === undefined) throw new TypeError(`crossbind: ${name} opens a modal window above a parent: give it parent, the id of a window.`);
|
|
75
80
|
if (name === 'windows.open') checkedLimits(options);
|
|
@@ -96,6 +101,25 @@ function windowOf(name, id) {
|
|
|
96
101
|
return { id };
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
// What the app's host tells the page of where the app runs, from before the page's own scripts on.
|
|
105
|
+
function deviceOf(name) {
|
|
106
|
+
const device = globalThis.__crossbind_device;
|
|
107
|
+
if (device) return device[name];
|
|
108
|
+
if (globalThis.__crossbind_token) throw new Error(`crossbind: this app's host is older than crossbind/app; update @crossbind/core-app-host to read device.${name}`);
|
|
109
|
+
throw new Error(`crossbind: device.${name} works in an app alone, whose crossbind.config.js sets target.runtimeEnv: 'app'`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Where the app runs, the same through its run, which the page reads at once, as it starts: the platform and the arch
|
|
113
|
+
// the app is built for, what the device is to its user, its system's name and version, and whether a simulator or an
|
|
114
|
+
// emulator runs the app.
|
|
115
|
+
export const device = Object.freeze({
|
|
116
|
+
get platform() { return deviceOf('platform'); },
|
|
117
|
+
get arch() { return deviceOf('arch'); },
|
|
118
|
+
get formFactor() { return deviceOf('formFactor'); },
|
|
119
|
+
get os() { return deviceOf('os'); },
|
|
120
|
+
get isVirtual() { return deviceOf('isVirtual'); },
|
|
121
|
+
});
|
|
122
|
+
|
|
99
123
|
export const app = {
|
|
100
124
|
// The app's own folders, made when first asked for: { data, cache, logs }.
|
|
101
125
|
paths: async () => call('app.paths', {}),
|
|
@@ -5,7 +5,6 @@ import { registerHooks } from 'node:module';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
7
|
|
|
8
|
-
const SCHEME = /^(cargo|conan):/;
|
|
9
8
|
const isBare = (specifier) => !/^(\.{1,2}\/|\/|[A-Za-z]:[\\/]|file:)/.test(specifier);
|
|
10
9
|
|
|
11
10
|
function notBound(what, parentURL, cause) {
|
|
@@ -17,23 +16,25 @@ const exportOf = (name) => (typeof name === 'string' ? name : `${name.wire} as $
|
|
|
17
16
|
const proxyOf = (entry, names) => `export { ${['initNative', 'AllSymbols', ...names.map(exportOf)].join(', ')} } from ${JSON.stringify(entry)};\n`;
|
|
18
17
|
|
|
19
18
|
// Another hook may still serve it; Node's own resolver hands an unknown scheme on unresolved.
|
|
20
|
-
function resolveUnbound(specifier, context, nextResolve) {
|
|
19
|
+
function resolveUnbound(specifier, context, nextResolve, scheme) {
|
|
21
20
|
let resolved;
|
|
22
21
|
try {
|
|
23
22
|
resolved = nextResolve(specifier, context);
|
|
24
23
|
} catch (error) {
|
|
25
24
|
throw notBound(specifier, context.parentURL, error);
|
|
26
25
|
}
|
|
27
|
-
if (
|
|
26
|
+
if (scheme.test(resolved.url)) throw notBound(specifier, context.parentURL);
|
|
28
27
|
return resolved;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
// `natives` lists each bound file, relative to the entry, with the bare specifiers that name it and the names it
|
|
32
|
-
// exports, a name the entry exports under another as { local, wire }.
|
|
33
|
-
|
|
31
|
+
// exports, a name the entry exports under another as { local, wire }. `schemes` are those an import names a store
|
|
32
|
+
// by: the two every build has, for a register file an older build wrote, which names none.
|
|
33
|
+
export default function registerNativeImports(entry, { extensions, schemes = ['cargo', 'conan'], natives }) {
|
|
34
34
|
const entryUrl = new URL(entry).href;
|
|
35
35
|
const dir = path.dirname(fileURLToPath(entryUrl));
|
|
36
36
|
const isNative = new RegExp(`\\.(${extensions.join('|')})$`);
|
|
37
|
+
const scheme = new RegExp(`^(${schemes.join('|')}):`);
|
|
37
38
|
const byFile = new Map();
|
|
38
39
|
const bySpecifier = new Map();
|
|
39
40
|
natives.forEach(({ file, specifiers, names }) => {
|
|
@@ -46,7 +47,7 @@ export default function registerNativeImports(entry, { extensions, natives }) {
|
|
|
46
47
|
resolve(specifier, context, nextResolve) {
|
|
47
48
|
const url = bySpecifier.get(specifier);
|
|
48
49
|
if (url) return { url, format: 'module', shortCircuit: true };
|
|
49
|
-
if (isBare(specifier) && (
|
|
50
|
+
if (isBare(specifier) && (scheme.test(specifier) || isNative.test(specifier))) return resolveUnbound(specifier, context, nextResolve, scheme);
|
|
50
51
|
return nextResolve(specifier, context);
|
|
51
52
|
},
|
|
52
53
|
load(url, context, nextLoad) {
|
package/src/bin.js
CHANGED
|
@@ -44,7 +44,8 @@ import {
|
|
|
44
44
|
RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deleteRunner, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner,
|
|
45
45
|
} from './actions/runnerCommands.js';
|
|
46
46
|
import { cargoHome } from './utils/runCargo.js';
|
|
47
|
-
import {
|
|
47
|
+
import { containerRootOf, sourceRoot } from './utils/runPackageSource.js';
|
|
48
|
+
import { importSchemesOf, sourceNamesOf } from './utils/packageSources.js';
|
|
48
49
|
import { cleanDepsCache } from './utils/dependencyRebuild.js';
|
|
49
50
|
import collectLicenseRows from './actions/licenses.js';
|
|
50
51
|
import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './utils/licenseReport.js';
|
|
@@ -113,12 +114,18 @@ function declaredSbomPath(projectDir) {
|
|
|
113
114
|
const APP_PLATFORMS = ['android', 'ios', 'linux', 'macos', 'windows'];
|
|
114
115
|
|
|
115
116
|
function projectVite() {
|
|
117
|
+
let manifestPath;
|
|
116
118
|
try {
|
|
117
|
-
|
|
118
|
-
return path.join(path.dirname(manifest), JSON.parse(fs.readFileSync(manifest, 'utf8')).bin.vite);
|
|
119
|
+
manifestPath = createRequire(`${state.config.paths.project}/package.json`).resolve('vite/package.json');
|
|
119
120
|
} catch {
|
|
120
121
|
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.");
|
|
121
122
|
}
|
|
123
|
+
// A workspace can map vite to a package of Vite's API alone.
|
|
124
|
+
const { name, version, bin } = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
125
|
+
if (!bin?.vite) {
|
|
126
|
+
throw new Error(`crossbind: crossbind app runs the vite command, and the project's vite resolves to ${name} ${version}, which has none. Have vite resolve to Vite itself for the project, with an override where its workspace maps vite to another package.`);
|
|
127
|
+
}
|
|
128
|
+
return path.join(path.dirname(manifestPath), bin.vite);
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
// The project's Vite, as `crossbind app dev`, `build` and `publish` run it, and its exit code once it ends. A signal sent
|
|
@@ -310,17 +317,18 @@ imageArgument(commandDocker.command('create').description('create docker contain
|
|
|
310
317
|
.action((image) => {
|
|
311
318
|
// Google ships the linux NDK for x86_64 only, so an android or android-app container is always amd64.
|
|
312
319
|
const platform = ['android', 'android-app'].includes(image) ? 'linux/amd64' : undefined;
|
|
320
|
+
const sources = sourceNamesOf(state.config);
|
|
313
321
|
// Docker on Linux makes a missing mount source owned by root, which the user's builds cannot write.
|
|
314
|
-
[cargoHome(),
|
|
322
|
+
[cargoHome(), ...sources.map((name) => sourceRoot(name))].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
|
|
315
323
|
dockerExec([
|
|
316
324
|
'run', '-dit',
|
|
317
325
|
...(platform ? ['--platform', platform] : []),
|
|
318
326
|
'--name', dockerContainerName(image),
|
|
319
327
|
'-v', `${state.config.paths.base}:/tmp/crossbind/live`,
|
|
320
|
-
// Cargo's registry cache, shared with host builds and with every other project, and
|
|
321
|
-
//
|
|
328
|
+
// Cargo's registry cache, shared with host builds and with every other project, and each package
|
|
329
|
+
// source's store and work directories, which every other container build of it uses.
|
|
322
330
|
'-v', `${cargoHome()}:/var/cache/crossbind/cargo`,
|
|
323
|
-
'-v', `${
|
|
331
|
+
...sources.flatMap((name) => ['-v', `${sourceRoot(name)}:${containerRootOf(name)}`]),
|
|
324
332
|
getDockerImage(image, platform),
|
|
325
333
|
'bash',
|
|
326
334
|
]);
|
|
@@ -527,7 +535,7 @@ async function createNodeAddons(targetParams) {
|
|
|
527
535
|
if (headers.length && fs.existsSync(`${state.config.paths.output}/${loaderTarget.jsName}`)) {
|
|
528
536
|
const entry = `${state.config.paths.output}/node/napi.mjs`;
|
|
529
537
|
writeRuntimeEntry(entry, headers, ENTRY_RUNTIMES.node(loaderTarget.jsName));
|
|
530
|
-
if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext));
|
|
538
|
+
if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext), importSchemesOf(state.config));
|
|
531
539
|
else removeImportHooks(entry);
|
|
532
540
|
}
|
|
533
541
|
}
|
|
@@ -603,7 +611,7 @@ async function createWasmJs(targetParams, wasmOptions = {}) {
|
|
|
603
611
|
if (appHeaders.length && nodeTarget) {
|
|
604
612
|
const entry = `${state.config.paths.output}/node/wasm.mjs`;
|
|
605
613
|
writeRuntimeEntry(entry, appHeaders, ENTRY_RUNTIMES.node(nodeTarget.jsName));
|
|
606
|
-
if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext));
|
|
614
|
+
if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext), importSchemesOf(state.config));
|
|
607
615
|
else removeImportHooks(entry);
|
|
608
616
|
}
|
|
609
617
|
if (headers.length && edgeTarget) {
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ 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
6
|
export {
|
|
7
|
-
default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appTargetParams,
|
|
7
|
+
default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appPlatform, appTargetParams,
|
|
8
8
|
} from './actions/buildApp.js';
|
|
9
9
|
export { checkInstaller, shipApp } from './actions/appInstaller.js';
|
|
10
10
|
export { resolveAppPageApi } from './utils/resolveAppHost.js';
|
|
@@ -24,7 +24,7 @@ export { getTargetParams, getBuildTargets, getFilteredBuildTargets, getFilteredT
|
|
|
24
24
|
export { default as getCrossbindScript, getRustJsScript } from './integration/getCrossbindScript.js';
|
|
25
25
|
export { default as getDependFilePath } from './integration/getDependFilePath.js';
|
|
26
26
|
export { default as buildDependencies, getDependenciesStamp } from './actions/buildDependencies.js';
|
|
27
|
-
export { default as
|
|
27
|
+
export { default as preparePackageSources } from './actions/preparePackageSources.js';
|
|
28
28
|
export { default as findFiles } from './utils/findFiles.js';
|
|
29
29
|
export { getContentHash, getFileHash } from './utils/hash.js';
|
|
30
30
|
export {
|
|
@@ -5,6 +5,7 @@ import loadJson from '../utils/loadJson.js';
|
|
|
5
5
|
import state from '../state/index.js';
|
|
6
6
|
import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
|
|
7
7
|
import { parseCargoMarkerName } from '../utils/cargoImport.js';
|
|
8
|
+
import { cratesOf } from '../utils/packageSourceImport.js';
|
|
8
9
|
import { UNBOUND_MESSAGE } from '../assets/js-runtime/adapters/unboundMessage.js';
|
|
9
10
|
|
|
10
11
|
// `base` is where the bundler serves the app from, as Vite's `base` names it; webpack's public path wins at run time.
|
|
@@ -62,7 +63,7 @@ export function getRustSymbols(rsFile) {
|
|
|
62
63
|
// Marker under <cache>/rust-crates/: a direct crate import - the model comes from the
|
|
63
64
|
// upstream crate's own source, and the bridge is synthesized against the cargo dependency.
|
|
64
65
|
const { crateName, modulePath } = parseCargoMarkerName(path.basename(rsReal, '.rs'));
|
|
65
|
-
const cargoDeps = state.config
|
|
66
|
+
const cargoDeps = cratesOf(state.config);
|
|
66
67
|
if (Object.hasOwn(cargoDeps, crateName)
|
|
67
68
|
&& path.dirname(rsReal) === fs.realpathSync(path.join(state.config.paths.cache, 'rust-crates'))) {
|
|
68
69
|
const { exports } = createCrateImportBridge({
|
|
@@ -92,7 +93,7 @@ export function getRustSymbols(rsFile) {
|
|
|
92
93
|
projectPath: state.config.paths.project,
|
|
93
94
|
dtsMode: state.config.dts,
|
|
94
95
|
vectors,
|
|
95
|
-
cargoDependencies: state.config
|
|
96
|
+
cargoDependencies: cratesOf(state.config),
|
|
96
97
|
log: () => {},
|
|
97
98
|
});
|
|
98
99
|
}
|
|
@@ -2,8 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import state from '../state/index.js';
|
|
4
4
|
import { parseCargoImport, writeCargoMarker } from '../utils/cargoImport.js';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { cratesOf, sourceOfImport, resolveSourceHeader } from '../utils/packageSourceImport.js';
|
|
6
|
+
import refreshPackageSources from '../state/refreshPackageSources.js';
|
|
7
7
|
|
|
8
8
|
const LAYOUT = { header: 'include', module: 'swig', source: '' };
|
|
9
9
|
|
|
@@ -85,20 +85,20 @@ export default function getDependFilePath(source, target) {
|
|
|
85
85
|
// an undeclared crate is a hard, actionable error instead of a silent npm fallthrough.
|
|
86
86
|
if (source.startsWith('cargo:')) {
|
|
87
87
|
const cargoImport = parseCargoImport(source);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to cargoDependencies in crossbind.config.`);
|
|
88
|
+
if (!Object.hasOwn(cratesOf(state.config), cargoImport.crateName)) {
|
|
89
|
+
throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to packages.cargo in crossbind.config.`);
|
|
91
90
|
}
|
|
92
91
|
const marker = writeCargoMarker(state.config.paths.cache, cargoImport);
|
|
93
92
|
existsCache.set(marker, true);
|
|
94
93
|
return marker;
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
//
|
|
98
|
-
// port's prebuilt; from there the header takes the same path as any other.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
// `<source>:<package>/<header>`, as conan:zlib/zlib.h, names a header of a declared package of a package
|
|
97
|
+
// source, staged per target like a port's prebuilt; from there the header takes the same path as any other.
|
|
98
|
+
const packageSource = sourceOfImport(state.config, source);
|
|
99
|
+
if (packageSource) {
|
|
100
|
+
refreshPackageSources(state.config);
|
|
101
|
+
return resolveSourceHeader(state.config, packageSource.name, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
const matches = getMatchingPackages(source);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getFilledConfig, flattenDependencies } from './loadConfig.js';
|
|
2
2
|
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { sourceStageDir, sourcePackageDir } from '../utils/packageSourceImport.js';
|
|
4
|
+
import { declaredSources, dependenciesKey, dependenciesOf } from '../utils/packageSources.js';
|
|
5
|
+
import { readSourceManifest, sourceTargetName } from '../utils/packageSourceStage.js';
|
|
6
6
|
import { TARGETS, targetPathOf } from '../utils/targets.js';
|
|
7
7
|
|
|
8
8
|
// What a recipe declares for its consumers, per staged target since it declares other things on another
|
|
@@ -32,25 +32,29 @@ function targetSpecsOf(name, targets) {
|
|
|
32
32
|
// staged for it, so a name it lacks drops out there.
|
|
33
33
|
const libsOf = (name, targets) => [...new Set(Object.values(targets).flat().filter((entry) => entry.name === name).flatMap((entry) => entry.libs))];
|
|
34
34
|
|
|
35
|
-
export function
|
|
35
|
+
export function sourceDependencyConfigs(source, manifest, stageDir) {
|
|
36
36
|
return manifest.packages.map((pkg) => ({
|
|
37
|
-
general: { name:
|
|
37
|
+
general: { name: sourceTargetName(source, pkg.name), packageSource: { name: source, package: pkg } },
|
|
38
38
|
export: { type: 'cmake', libName: libsOf(pkg.name, manifest.targets) },
|
|
39
|
-
paths: { project:
|
|
39
|
+
paths: { project: sourcePackageDir(stageDir, pkg.name), output: `${sourcePackageDir(stageDir, pkg.name)}/dist` },
|
|
40
40
|
targetSpecs: targetSpecsOf(pkg.name, manifest.targets),
|
|
41
41
|
}));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
function stagedDependencies(config, source) {
|
|
45
|
+
const stageDir = sourceStageDir(config.paths.cache, source);
|
|
46
|
+
const manifest = readSourceManifest(source, stageDir, dependenciesKey(dependenciesOf(config, source)));
|
|
47
|
+
return manifest ? sourceDependencyConfigs(source, manifest, stageDir).map((d) => getFilledConfig(d, { isDepend: true })) : [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The staged packages of each source join the config as dependencies, so header resolution, the link, the
|
|
51
|
+
// cmake graph and the license rows see them like any port. Only packages staged for what the config declares
|
|
52
|
+
// now join; reading a manifest runs nothing, so state can do it while it loads.
|
|
53
|
+
export default function attachPackageSources(config) {
|
|
54
|
+
const declared = declaredSources(config);
|
|
55
|
+
if (declared.length === 0 && !config.dependencies?.some((d) => d.general.packageSource)) return;
|
|
56
|
+
const staged = declared.flatMap(({ name }) => stagedDependencies(config, name));
|
|
57
|
+
config.dependencies = [...config.dependencies.filter((d) => !d.general.packageSource), ...staged];
|
|
54
58
|
config.allDependencies = flattenDependencies(config.dependencies);
|
|
55
59
|
config.dependencyParameters = calculateDependencyParameters(config);
|
|
56
60
|
}
|
package/src/state/index.js
CHANGED
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf, wasiNamesOf, appNamesOf,
|
|
6
6
|
} from '../utils/targets.js';
|
|
7
7
|
import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
|
|
8
|
+
import { cratesOf } from '../utils/packageSourceImport.js';
|
|
8
9
|
import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
|
|
9
10
|
import loadConfig, { assertBinarySelection } from './loadConfig.js';
|
|
10
11
|
import { configChangesOf } from '../utils/configChanges.js';
|
|
11
|
-
import
|
|
12
|
+
import refreshPackageSources from './refreshPackageSources.js';
|
|
12
13
|
|
|
13
14
|
const cacheDir = `${process.cwd()}/.crossbind`;
|
|
14
15
|
|
|
@@ -28,7 +29,7 @@ async function initProcessState() {
|
|
|
28
29
|
state.cache = loadCacheState();
|
|
29
30
|
state.config = await loadConfig(undefined, undefined, { changes: configChangesOf() });
|
|
30
31
|
assertBinarySelection(state.config);
|
|
31
|
-
|
|
32
|
+
refreshPackageSources(state.config);
|
|
32
33
|
exportWasiSdkPath(state.config.system);
|
|
33
34
|
// Same idea for the docker image overrides: pullDockerImage.js resolves them from the
|
|
34
35
|
// environment alone, so it stays free of a state import (and of the cycle that would create).
|
|
@@ -74,7 +75,7 @@ async function initProcessState() {
|
|
|
74
75
|
// Cargo-crate import markers must exist BEFORE any bundler starts: metro resolves against
|
|
75
76
|
// its startup file map, so a marker first created mid-resolution is invisible to that very
|
|
76
77
|
// build. Bare crates come from the config, module imports from the app's own sources.
|
|
77
|
-
const cargoDeps = state.config
|
|
78
|
+
const cargoDeps = cratesOf(state.config);
|
|
78
79
|
const crateNames = Object.keys(cargoDeps);
|
|
79
80
|
const moduleImports = crateNames.length && state.config.paths.project
|
|
80
81
|
? findCargoModuleImportsIn(state.config.paths.project).filter((i) => Object.hasOwn(cargoDeps, i.crateName))
|
|
@@ -98,8 +99,8 @@ export function setAllDependecyPaths() {
|
|
|
98
99
|
state.config.allDependencyPaths[target.path] = { cmake: {} };
|
|
99
100
|
state.config.allDependencies.forEach((d) => {
|
|
100
101
|
state.config.allDependencyPaths[target.path].cmake[d.general.name] = `${d.paths.output}/prebuilt`;
|
|
101
|
-
// A
|
|
102
|
-
const libNames = d.general.
|
|
102
|
+
// A package source's recipe can name a library on another platform only (libpng is png16 on Windows).
|
|
103
|
+
const libNames = d.general.packageSource
|
|
103
104
|
? d.export.libName.filter((name) => [target.path, target.releasePath]
|
|
104
105
|
.some((targetPath) => fs.existsSync(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${name}.a`)))
|
|
105
106
|
: d.export.libName;
|
package/src/state/loadConfig.js
CHANGED
|
@@ -12,7 +12,7 @@ import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
|
|
|
12
12
|
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
13
13
|
import logger from '../utils/logger.js';
|
|
14
14
|
import { RUNTIME_ENVS } from '../utils/targets.js';
|
|
15
|
-
import {
|
|
15
|
+
import { checkedPackages, packageSourcesOf } from '../utils/packageSources.js';
|
|
16
16
|
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
17
17
|
import { listedExtension } from '../utils/appExtensions.js';
|
|
18
18
|
import { mergePatch } from '../utils/configChanges.js';
|
|
@@ -44,7 +44,9 @@ export default async function loadConfig(configDir = process.cwd(), configName =
|
|
|
44
44
|
const overrides = await loadJs(configDir, 'crossbind.overrides');
|
|
45
45
|
const exclude = resolveExcludedNames(overrides);
|
|
46
46
|
const seen = new Set();
|
|
47
|
-
const output = getFilledConfig(config, {
|
|
47
|
+
const output = getFilledConfig(config, {
|
|
48
|
+
isDepend: false, exclude, seen, replaces: overrides, file: `${configDir}/${configName}`,
|
|
49
|
+
});
|
|
48
50
|
output.excludedDependencies = exclude;
|
|
49
51
|
|
|
50
52
|
const build = await loadJs(configDir, 'crossbind.build');
|
|
@@ -126,8 +128,6 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
126
128
|
const newConfig = {
|
|
127
129
|
general: config.general || {},
|
|
128
130
|
dependencies,
|
|
129
|
-
cargoDependencies: config.cargoDependencies || {},
|
|
130
|
-
conanDependencies: normalizeConanDependencies(config.conanDependencies),
|
|
131
131
|
// 'sync' types the direct surface; 'promise' wraps every generated method return
|
|
132
132
|
// for useWorker-style runtimes (constructors stay sync-typed: await new X()).
|
|
133
133
|
dts: config.dts || 'sync',
|
|
@@ -214,6 +214,12 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
214
214
|
const twice = newConfig.extensions.find(({ name }, i) => newConfig.extensions.findIndex((other) => other.name === name) !== i);
|
|
215
215
|
if (twice) throw new Error(`crossbind: "extensions" lists two extensions named ${twice.name}.`);
|
|
216
216
|
|
|
217
|
+
// What the project declares under packages: cargo's crates, and each package source's packages, as packages.conan,
|
|
218
|
+
// as the source's own check takes them. A package installs nothing for the project that depends on it, so a
|
|
219
|
+
// dependency's own declarations are not read.
|
|
220
|
+
newConfig.packageSources = packageSourcesOf(newConfig.extensions);
|
|
221
|
+
newConfig.packages = checkedPackages(newConfig.packageSources, options.isDepend ? {} : config, options.file);
|
|
222
|
+
|
|
217
223
|
if (newConfig.target.runtime !== 'mt' && newConfig.allDependencies.some((d) => d?.target?.runtime === 'mt')) {
|
|
218
224
|
newConfig.target.runtime = 'mt';
|
|
219
225
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import attachPackageSources from './attachPackageSources.js';
|
|
2
|
+
import { sourceStageDir } from '../utils/packageSourceImport.js';
|
|
3
|
+
import { declaredSources } from '../utils/packageSources.js';
|
|
4
|
+
import { sourceManifestsStamp } from '../utils/packageSourceStage.js';
|
|
5
|
+
|
|
6
|
+
const attachedFrom = new WeakMap();
|
|
7
|
+
|
|
8
|
+
// Attaches what a build staged since this config last attached: a process that loaded state before
|
|
9
|
+
// the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
|
|
10
|
+
// it attached again.
|
|
11
|
+
export default function refreshPackageSources(config) {
|
|
12
|
+
const declared = declaredSources(config);
|
|
13
|
+
if (declared.length === 0) return false;
|
|
14
|
+
const stamp = declared.map(({ name }) => `${name}:${sourceManifestsStamp(sourceStageDir(config.paths.cache, name))}`).join('\n');
|
|
15
|
+
if (attachedFrom.get(config) === stamp) return false;
|
|
16
|
+
attachedFrom.set(config, stamp);
|
|
17
|
+
attachPackageSources(config);
|
|
18
|
+
return true;
|
|
19
|
+
}
|