crossbind 2.0.0-beta.63 → 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/AGENTS.md +2 -1
- package/package.json +2 -2
- package/src/actions/androidDevice.js +77 -0
- package/src/actions/androidGradle.js +121 -0
- package/src/actions/buildApp.js +434 -0
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildDependencies.js +4 -3
- package/src/actions/buildExternal.js +3 -3
- package/src/actions/buildJs.js +2 -9
- package/src/actions/buildLib.js +1 -1
- package/src/actions/buildNode.js +19 -16
- package/src/actions/buildWasiCommand.js +10 -5
- package/src/actions/buildWasm.js +24 -3
- package/src/actions/createImportedBridges.js +7 -0
- package/src/actions/createInterface.js +4 -5
- package/src/actions/createLib.js +17 -2
- package/src/actions/getCmakeParameters.js +2 -1
- package/src/actions/getData.js +2 -1
- package/src/actions/getDependLibs.js +8 -2
- package/src/actions/getEnv.js +7 -0
- 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/runnerCommands.js +16 -4
- package/src/actions/target.js +2 -1
- package/src/actions/writeRuntimeEntry.js +18 -7
- package/src/actions/xcodeProject.js +152 -0
- package/src/assets/cpp-runtime/node.cpp +18 -1
- package/src/assets/js-runtime/adapters/worker-comlink.js +58 -0
- package/src/assets/js-runtime/core.js +5 -4
- package/src/assets/toolchain-digests.json +25 -19
- package/src/assets/wasi-runtime/http/README.md +17 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
- package/src/assets/wasi-runtime/http/handler.cpp +298 -0
- package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
- package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
- package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
- package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
- package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
- package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
- package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
- package/src/bin.js +102 -209
- package/src/cli.js +14 -0
- package/src/index.js +6 -0
- package/src/integration/getCrossbindScript.js +19 -4
- package/src/program.js +169 -0
- package/src/state/index.js +8 -3
- package/src/state/loadConfig.js +6 -11
- package/src/utils/appRustCrates.js +7 -1
- package/src/utils/bridgeExtras.js +74 -6
- package/src/utils/cargoLock.js +19 -0
- package/src/utils/cppDts.js +2 -0
- package/src/utils/downloadAndExtractFile.js +42 -14
- package/src/utils/embindObjectLifetime.js +35 -0
- package/src/utils/embindRsFingerprint.js +10 -3
- package/src/utils/hermes.js +40 -0
- package/src/utils/packageLicense.js +4 -2
- package/src/utils/plist.js +42 -0
- package/src/utils/pullDockerImage.js +9 -3
- package/src/utils/replaceBasePathForDocker.js +8 -0
- package/src/utils/resetCMakeCacheForSource.js +26 -0
- package/src/utils/resolveAppHost.js +21 -0
- package/src/utils/rustBridgeGen.js +13 -4
- package/src/utils/rustMt.js +5 -0
- package/src/utils/rustSysroot.js +7 -5
- package/src/utils/selectRunner.js +1 -1
- package/src/utils/swigInterface.js +5 -4
- package/src/utils/systemConfig.js +13 -0
- package/src/utils/systemKeys.js +4 -0
- package/src/utils/targets.js +30 -3
- package/src/utils/wasiHttp.js +50 -0
- package/vitest.config.js +5 -0
|
@@ -74,9 +74,11 @@ export function nodePackageLicense({ name, version, repository, projectPath, add
|
|
|
74
74
|
|
|
75
75
|
export function setManifestLicense(dir, expression) {
|
|
76
76
|
const file = path.join(dir, 'package.json');
|
|
77
|
-
const
|
|
77
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
78
|
+
const manifest = JSON.parse(text);
|
|
78
79
|
if (manifest.license === expression) return;
|
|
79
|
-
|
|
80
|
+
const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? 4;
|
|
81
|
+
fs.writeFileSync(file, `${JSON.stringify({ ...manifest, license: expression }, null, indent)}\n`);
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export function writeNodePackageLicense(dir, meta) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Property lists as Apple's tools read them: XML for an Info.plist, the older text form for an Xcode project.
|
|
2
|
+
|
|
3
|
+
const XML_INDENT = ' ';
|
|
4
|
+
const escapeXml = (text) => String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
5
|
+
|
|
6
|
+
function xmlLines(value, indent) {
|
|
7
|
+
if (typeof value === 'boolean') return [`${indent}<${value}/>`];
|
|
8
|
+
const inner = indent + XML_INDENT;
|
|
9
|
+
if (Array.isArray(value)) return [`${indent}<array>`, ...value.flatMap((item) => xmlLines(item, inner)), `${indent}</array>`];
|
|
10
|
+
if (typeof value === 'object') {
|
|
11
|
+
const entries = Object.entries(value);
|
|
12
|
+
if (entries.length === 0) return [`${indent}<dict/>`];
|
|
13
|
+
return [
|
|
14
|
+
`${indent}<dict>`,
|
|
15
|
+
...entries.flatMap(([key, item]) => [`${inner}<key>${escapeXml(key)}</key>`, ...xmlLines(item, inner)]),
|
|
16
|
+
`${indent}</dict>`,
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
return [`${indent}<string>${escapeXml(value)}</string>`];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function xmlPlist(dict) {
|
|
23
|
+
return '<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
24
|
+
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
|
|
25
|
+
+ `<plist version="1.0">\n${xmlLines(dict, '').join('\n')}\n</plist>\n`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Every value of the text form is a string, quoted here whatever it holds.
|
|
29
|
+
const quote = (text) => `"${String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
30
|
+
|
|
31
|
+
function textValue(value, indent) {
|
|
32
|
+
const inner = `${indent}\t`;
|
|
33
|
+
if (Array.isArray(value)) return `(\n${value.map((item) => `${inner}${textValue(item, inner)},\n`).join('')}${indent})`;
|
|
34
|
+
if (typeof value === 'object') {
|
|
35
|
+
return `{\n${Object.entries(value).map(([key, item]) => `${inner}${quote(key)} = ${textValue(item, inner)};\n`).join('')}${indent}}`;
|
|
36
|
+
}
|
|
37
|
+
return quote(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function textPlist(root) {
|
|
41
|
+
return `// !$*UTF8*$!\n${textValue(root, '')}\n`;
|
|
42
|
+
}
|
|
@@ -8,7 +8,11 @@ const table = createRequire(import.meta.url)('../assets/toolchain-digests.json')
|
|
|
8
8
|
const REGISTRY = table.registry;
|
|
9
9
|
const IMAGE_VERSION = table.version;
|
|
10
10
|
const OVERRIDE_KEYS = {
|
|
11
|
-
web: 'CROSSBIND_IMAGE_WEB',
|
|
11
|
+
web: 'CROSSBIND_IMAGE_WEB',
|
|
12
|
+
android: 'CROSSBIND_IMAGE_ANDROID',
|
|
13
|
+
'android-app': 'CROSSBIND_IMAGE_ANDROID_APP',
|
|
14
|
+
linux: 'CROSSBIND_IMAGE_LINUX',
|
|
15
|
+
windows: 'CROSSBIND_IMAGE_WINDOWS',
|
|
12
16
|
};
|
|
13
17
|
const IMAGES = Object.fromEntries(
|
|
14
18
|
Object.keys(OVERRIDE_KEYS).map((role) => [
|
|
@@ -21,9 +25,11 @@ const IMAGES = Object.fromEntries(
|
|
|
21
25
|
);
|
|
22
26
|
|
|
23
27
|
// Which image a target compiles in: wasm and wasi share the web image, linux and linuxmusl the linux
|
|
24
|
-
// image, ios and darwin never reach docker.
|
|
28
|
+
// image, ios and darwin never reach docker. An Android app builds in android-app, the android image with
|
|
29
|
+
// what packages the app.
|
|
25
30
|
export function imageRoleFor(target) {
|
|
26
|
-
if (target?.platform === 'android'
|
|
31
|
+
if (target?.platform === 'android') return target.runtimeEnv === 'app' ? 'android-app' : 'android';
|
|
32
|
+
if (target?.platform === 'linux') return 'linux';
|
|
27
33
|
if (target?.platform === 'linuxmusl') return 'linux';
|
|
28
34
|
if (target?.platform === 'win32') return 'windows';
|
|
29
35
|
return 'web';
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
1
3
|
export const DOCKER_BASE = '/tmp/crossbind/live';
|
|
2
4
|
|
|
5
|
+
// Whether a container, which sees the project's base alone, sees dir.
|
|
6
|
+
export function isUnderBase(dir, basePath) {
|
|
7
|
+
const relative = path.relative(basePath, path.resolve(dir));
|
|
8
|
+
return !/^\.\.(?:[\\/]|$)/.test(relative) && !path.isAbsolute(relative);
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export default function replaceBasePathForDocker(data, basePath) {
|
|
4
12
|
if (typeof data === 'string' || data instanceof String) {
|
|
5
13
|
return data.replaceAll(basePath, DOCKER_BASE);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import state from '../state/index.js';
|
|
3
|
+
import replaceBasePathForDocker from './replaceBasePathForDocker.js';
|
|
4
|
+
import { isLocalRunner } from './selectRunner.js';
|
|
5
|
+
import { HOST_BUILT_PLATFORMS } from './targets.js';
|
|
6
|
+
|
|
7
|
+
const normalize = (dir) => dir.replaceAll('\\', '/').replace(/\/+$/, '');
|
|
8
|
+
|
|
9
|
+
// The directory as the cmake run() starts sees it: a container, Docker or a remote runner, has the project at
|
|
10
|
+
// DOCKER_BASE; macOS and iOS builds and the LOCAL runner see this machine's paths.
|
|
11
|
+
function seenByCMake(dir, target) {
|
|
12
|
+
const isLocal = HOST_BUILT_PLATFORMS.includes(target.platform) || isLocalRunner(state.config.system);
|
|
13
|
+
return isLocal ? dir : replaceBasePathForDocker(dir, state.config.paths.base);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A CMake build tree remembers the source directory it was configured from and refuses another one. The CMake
|
|
17
|
+
// projects crossbind ships move with its version (node_modules/.pnpm/crossbind@<version>/...), so a tree configured
|
|
18
|
+
// from another directory starts afresh, as `cmake --fresh` would.
|
|
19
|
+
export default function resetCMakeCacheForSource(buildPath, sourceDir, target) {
|
|
20
|
+
const cache = `${buildPath}/CMakeCache.txt`;
|
|
21
|
+
if (!fs.existsSync(cache)) return;
|
|
22
|
+
const configuredFrom = fs.readFileSync(cache, 'utf8').match(/^CMAKE_HOME_DIRECTORY:INTERNAL=(.*)$/m)?.[1];
|
|
23
|
+
if (configuredFrom !== undefined && normalize(configuredFrom) === normalize(seenByCMake(sourceDir, target))) return;
|
|
24
|
+
fs.rmSync(cache, { force: true });
|
|
25
|
+
fs.rmSync(`${buildPath}/CMakeFiles`, { recursive: true, force: true });
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import createCompanionResolver from './resolveCompanionPackage.js';
|
|
5
|
+
|
|
6
|
+
const PKG = '@crossbind/core-app-host';
|
|
7
|
+
|
|
8
|
+
const resolveAppHostRoot = createCompanionResolver({
|
|
9
|
+
pkg: PKG,
|
|
10
|
+
siblingDir: 'app-host',
|
|
11
|
+
siblingMarker: 'cpp/src/main.cpp',
|
|
12
|
+
missingMessage: `crossbind: apps need ${PKG} - add it to your devDependencies.`,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export default resolveAppHostRoot;
|
|
16
|
+
|
|
17
|
+
// The embind runtime ships in @crossbind/core-embind-jsi, a dependency of the app host package.
|
|
18
|
+
export function resolveAppEmbindJsiRoot() {
|
|
19
|
+
const req = createRequire(path.join(resolveAppHostRoot(), 'package.json'));
|
|
20
|
+
return path.dirname(fs.realpathSync(req.resolve('@crossbind/core-embind-jsi/package.json')));
|
|
21
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { followCargoLock } from './cargoLock.js';
|
|
4
5
|
import resolveEmbindRustRoot, { embindRustVersion } from './resolveEmbindRust.js';
|
|
5
6
|
import runCargo, { toHostPath } from './runCargo.js';
|
|
6
7
|
import writeIfChanged from './writeIfChanged.js';
|
|
@@ -303,7 +304,9 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
|
|
|
303
304
|
// through the root and through a module registers once and is the same JS class for both.
|
|
304
305
|
const crateSourceCache = new Map();
|
|
305
306
|
const crateModelCache = new Map();
|
|
306
|
-
export function createCrateImportBridge({
|
|
307
|
+
export function createCrateImportBridge({
|
|
308
|
+
crateName, modulePath = [], spec, cacheDir, projectDir, dtsMode = 'sync', log = console.log,
|
|
309
|
+
}) {
|
|
307
310
|
const importName = [crateName, ...modulePath].join('/');
|
|
308
311
|
const crateIdent = crateName.replaceAll('-', '_');
|
|
309
312
|
const stem = `crate_${crateName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}`;
|
|
@@ -343,7 +346,9 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
|
|
|
343
346
|
// cargo metadata needs a resolvable lib target before the real bridge exists.
|
|
344
347
|
if (!fs.existsSync(`${dir}/src/lib.rs`)) writeIfChanged(`${dir}/src/lib.rs`, '// crossbind placeholder\n');
|
|
345
348
|
|
|
346
|
-
const { srcDir, features } = crateSource({
|
|
349
|
+
const { srcDir, features } = crateSource({
|
|
350
|
+
dir, depLine, crateName, importName, projectDir,
|
|
351
|
+
});
|
|
347
352
|
const ownKey = modulePath.join('::');
|
|
348
353
|
const imports = [];
|
|
349
354
|
for (const importPath of crateImports(cacheDir, crateName, modulePath)) {
|
|
@@ -385,10 +390,14 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
|
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
// cargo metadata locates the crate's source and the feature set it resolves to (fetching the crate
|
|
388
|
-
// on first use); both hold for every import of the crate.
|
|
389
|
-
|
|
393
|
+
// on first use); both hold for every import of the crate. It resolves with the project's pins, so
|
|
394
|
+
// the bridge is made from the version the app links.
|
|
395
|
+
function crateSource({
|
|
396
|
+
dir, depLine, crateName, importName, projectDir,
|
|
397
|
+
}) {
|
|
390
398
|
const cacheKey = `${dir}|${depLine}`;
|
|
391
399
|
if (crateSourceCache.has(cacheKey)) return crateSourceCache.get(cacheKey);
|
|
400
|
+
followCargoLock(projectDir, dir);
|
|
392
401
|
let meta;
|
|
393
402
|
try {
|
|
394
403
|
const probe = runCargo(['metadata', '--format-version', '1', '--manifest-path', `${dir}/Cargo.toml`], {
|
package/src/utils/rustMt.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
// One argv per flag: runCargo joins these with cargo's 0x1F separator, so a flag must not
|
|
@@ -39,6 +40,10 @@ export function cargoTargetDirFor(baseDir, target) {
|
|
|
39
40
|
// points at whatever version that image pinned, so the CLI carries no version of its own.
|
|
40
41
|
const CONTAINER_SYSROOT = '/opt/crossbind/rust/current';
|
|
41
42
|
|
|
43
|
+
// True only inside the toolchain image. A CLI running there with RUNNER=LOCAL (the playground
|
|
44
|
+
// compiler) has these trees, but not rustup's stock wasm target.
|
|
45
|
+
export const hasContainerSysroot = () => fs.existsSync(CONTAINER_SYSROOT);
|
|
46
|
+
|
|
42
47
|
export function cargoBuildInvocation({
|
|
43
48
|
target, triple, targetDir, manifestPath, sysroot = false,
|
|
44
49
|
}) {
|
package/src/utils/rustSysroot.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import withDirLock from './dirLock.js';
|
|
6
6
|
import fetchOciLayer from './ociArtifact.js';
|
|
7
7
|
import { cargoRunner } from './runCargo.js';
|
|
8
|
-
import { isMtWasm } from './rustMt.js';
|
|
8
|
+
import { hasContainerSysroot, isMtWasm } from './rustMt.js';
|
|
9
9
|
import PIN from './rustSysrootPin.js';
|
|
10
10
|
|
|
11
11
|
// The second consumption channel for the Rust sysroots. A containerized build gets them as an image
|
|
@@ -149,7 +149,7 @@ let resolved = null;
|
|
|
149
149
|
// Called from buildDependencies, the one async step every build path awaits before the sync work
|
|
150
150
|
// starts. Only downloads when a target will actually consume it.
|
|
151
151
|
export async function prepareRustSysroot(targets, log = console.log) {
|
|
152
|
-
if (resolved || !PIN) return resolved;
|
|
152
|
+
if (resolved || !PIN || hasContainerSysroot()) return resolved;
|
|
153
153
|
if (!targets.some((target) => isMtWasm(target) && cargoRunner(target) === 'LOCAL')) return null;
|
|
154
154
|
try {
|
|
155
155
|
resolved = await ensureRustSysroot({ image: PIN.image, index: PIN.index });
|
|
@@ -161,11 +161,13 @@ export async function prepareRustSysroot(targets, log = console.log) {
|
|
|
161
161
|
return resolved;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees
|
|
165
|
-
// absolute path where a downloaded artifact answers,
|
|
166
|
-
// st needs nothing - rustup's stock std is already
|
|
164
|
+
// What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees (also a
|
|
165
|
+
// LOCAL build running inside the image), an absolute path where a downloaded artifact answers,
|
|
166
|
+
// false where mt must rebuild std on nightly. st needs nothing - rustup's stock std is already
|
|
167
|
+
// correct for it.
|
|
167
168
|
export function sysrootFor(target) {
|
|
168
169
|
if (cargoRunner(target) !== 'LOCAL') return true;
|
|
170
|
+
if (target.platform === 'wasm' && hasContainerSysroot()) return true;
|
|
169
171
|
if (!isMtWasm(target)) return false;
|
|
170
172
|
return resolved ?? false;
|
|
171
173
|
}
|
|
@@ -7,7 +7,7 @@ const SYSTEM_CONFIG = '~/.crossbind.json';
|
|
|
7
7
|
const SHARED = { key: 'REMOTE_URL', url: 'CROSSBIND_REMOTE_URL', token: 'CROSSBIND_TOKEN' };
|
|
8
8
|
|
|
9
9
|
export const remoteVariables = (role) => {
|
|
10
|
-
const suffix = `_${role.toUpperCase()}`;
|
|
10
|
+
const suffix = `_${role.toUpperCase().replaceAll('-', '_')}`;
|
|
11
11
|
return { key: `${SHARED.key}${suffix}`, url: `${SHARED.url}${suffix}`, token: `${SHARED.token}${suffix}` };
|
|
12
12
|
};
|
|
13
13
|
|
|
@@ -75,8 +75,9 @@ export function buildInterfaceContent({
|
|
|
75
75
|
// The marker keeps a preamble that starts with #define apart from the macro block the retry drops.
|
|
76
76
|
const preambleBlock = preamble.length ? `// package preamble\n${preamble.join('\n')}\n\n` : '';
|
|
77
77
|
const extraBlock = extras.length ? `${extras.join('\n')}\n\n` : '';
|
|
78
|
-
// Named constants are asked for ahead of the macros, which may define
|
|
79
|
-
// SWIG binds an interface's own #define lines, and the prelude only carries other headers'
|
|
78
|
+
// Named constants and functions are asked for ahead of the macros, which may define an imported name. Every constant
|
|
79
|
+
// is asked for after them: SWIG binds an interface's own #define lines, and the prelude only carries other headers'
|
|
80
|
+
// values for the parse.
|
|
80
81
|
const everyConstant = constants === ALL_NAMES;
|
|
81
82
|
const module = moduleName.replace(/\W/g, '_');
|
|
82
83
|
return `#ifndef _${module}_I
|
|
@@ -88,10 +89,10 @@ ${extraBlock}%{
|
|
|
88
89
|
${includes}
|
|
89
90
|
%}
|
|
90
91
|
|
|
91
|
-
${everyConstant ? '' : constantRequests(constants)}%feature("shared_ptr");
|
|
92
|
+
${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}%feature("shared_ptr");
|
|
92
93
|
%feature("polymorphic_shared_ptr");
|
|
93
94
|
|
|
94
|
-
${macros}${
|
|
95
|
+
${macros}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
|
|
95
96
|
|
|
96
97
|
#endif
|
|
97
98
|
`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import loadJson from './loadJson.js';
|
|
3
|
+
import systemKeys from './systemKeys.js';
|
|
4
|
+
|
|
5
|
+
export const systemConfigPath = () => `${os.homedir()}/.crossbind.json`;
|
|
6
|
+
|
|
7
|
+
export default function loadSystemConfig() {
|
|
8
|
+
const system = loadJson(systemConfigPath()) || {};
|
|
9
|
+
const defaults = Object.entries(systemKeys)
|
|
10
|
+
.filter(([key]) => !(key in system))
|
|
11
|
+
.map(([key, value]) => [key, value.default]);
|
|
12
|
+
return { ...system, ...Object.fromEntries(defaults) };
|
|
13
|
+
}
|
package/src/utils/systemKeys.js
CHANGED
|
@@ -48,6 +48,10 @@ const systemKeys = {
|
|
|
48
48
|
description: 'Image reference used for android builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_ANDROID environment variable overrides it.',
|
|
49
49
|
default: '',
|
|
50
50
|
},
|
|
51
|
+
DOCKER_IMAGE_ANDROID_APP: {
|
|
52
|
+
description: 'Image reference used for Android apps instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_ANDROID_APP environment variable overrides it.',
|
|
53
|
+
default: '',
|
|
54
|
+
},
|
|
51
55
|
DOCKER_IMAGE_LINUX: {
|
|
52
56
|
description: 'Image reference used for linux (Node.js addon) builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_LINUX environment variable overrides it.',
|
|
53
57
|
default: '',
|
package/src/utils/targets.js
CHANGED
|
@@ -68,6 +68,12 @@ const BASE_TARGETS = [
|
|
|
68
68
|
{
|
|
69
69
|
platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'wasi',
|
|
70
70
|
},
|
|
71
|
+
{
|
|
72
|
+
platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'http',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'http',
|
|
76
|
+
},
|
|
71
77
|
{
|
|
72
78
|
platform: 'android', arch: 'arm64-v8a', runtime: 'mt', buildType: 'release',
|
|
73
79
|
},
|
|
@@ -142,12 +148,17 @@ const BASE_TARGETS = [
|
|
|
142
148
|
},
|
|
143
149
|
];
|
|
144
150
|
|
|
145
|
-
// A native executable links the same archives as the Node-API addon of its platform and arch
|
|
151
|
+
// A native executable links the same archives as the Node-API addon of its platform and arch, and so does an app's
|
|
152
|
+
// host on macOS, glibc Linux and Windows. An iOS or Android app links the archives React Native does, one target per
|
|
153
|
+
// SDK or ABI.
|
|
146
154
|
export const TARGETS = [
|
|
147
155
|
...BASE_TARGETS,
|
|
148
156
|
...BASE_TARGETS
|
|
149
157
|
.filter((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm')
|
|
150
158
|
.map((target) => ({ ...target, runtimeEnv: 'native' })),
|
|
159
|
+
...BASE_TARGETS
|
|
160
|
+
.filter((target) => (target.runtimeEnv === 'node' && ['darwin', 'linux', 'win32'].includes(target.platform)) || ['ios', 'android'].includes(target.platform))
|
|
161
|
+
.map((target) => ({ ...target, runtimeEnv: 'app' })),
|
|
151
162
|
];
|
|
152
163
|
|
|
153
164
|
// Built only when named with -p: a plain `crossbind build` keeps producing what it did before
|
|
@@ -157,8 +168,8 @@ export const OPT_IN_PLATFORMS = ['darwin', 'linux', 'linuxmusl', 'win32'];
|
|
|
157
168
|
// Built on the host itself: Apple's linker and SDKs exist in no toolchain image.
|
|
158
169
|
export const HOST_BUILT_PLATFORMS = ['ios', 'darwin'];
|
|
159
170
|
|
|
160
|
-
// The runtime environment names the binary a build makes; android and ios
|
|
161
|
-
//
|
|
171
|
+
// The runtime environment names the binary a build makes; on android and ios React Native links
|
|
172
|
+
// the archives, and the app is the one binary.
|
|
162
173
|
export const runtimeEnvsOf = (platform) => [
|
|
163
174
|
...new Set(TARGETS.filter((target) => target.platform === platform && target.runtimeEnv).map((target) => target.runtimeEnv)),
|
|
164
175
|
];
|
|
@@ -192,6 +203,22 @@ export function nodeAddonNamesOf(target, projectName) {
|
|
|
192
203
|
};
|
|
193
204
|
}
|
|
194
205
|
|
|
206
|
+
// A wasi build is one component, with no JS glue: the command keeps the plain name, an http component adds its own.
|
|
207
|
+
export const wasiNamesOf = (target, projectName) => ({
|
|
208
|
+
wasmName: `${projectName}-${targetPathOf(target)}${target.runtimeEnv === 'http' ? '.http' : ''}.wasm`,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// One app per platform, arch and build type, and one page runtime per build type for all of them. A macOS app is a
|
|
212
|
+
// bundle; a Linux app a directory; an iOS or Android one what its SDK or ABI brings to the app's Xcode or Gradle
|
|
213
|
+
// project.
|
|
214
|
+
export function appNamesOf(target, projectName) {
|
|
215
|
+
const suffix = target.buildType === 'debug' ? '.debug' : '';
|
|
216
|
+
return {
|
|
217
|
+
appName: `${projectName}.${target.platform}-${target.arch}${suffix}${target.platform === 'darwin' ? '.app' : ''}`,
|
|
218
|
+
appJsName: `${projectName}.app${suffix}.js`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
195
222
|
// One executable per platform, arch and build type, named like the addons it replaces.
|
|
196
223
|
export function nativeCommandNamesOf(target, projectName) {
|
|
197
224
|
const suffix = target.buildType === 'debug' ? '.debug' : '';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The runtime of an http build: crossbind's wasi:http/handler export, which calls the app's
|
|
2
|
+
// crossbind::http::handle, and the bindings wit-bindgen generated for it (assets/wasi-runtime/http/README.md).
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const RUNTIME = 'assets/wasi-runtime/http';
|
|
8
|
+
// The stack of each request: wasi-libc mallocs one per concurrent request, with no guard below it.
|
|
9
|
+
const STACK_SIZE = 1024 * 1024;
|
|
10
|
+
const HEADER = 'include/crossbind/http.h';
|
|
11
|
+
const LINKED = [
|
|
12
|
+
'handler.cpp',
|
|
13
|
+
'bindings/crossbind_http.c',
|
|
14
|
+
'bindings/crossbind_http.h',
|
|
15
|
+
'bindings/crossbind_http_component_type.o',
|
|
16
|
+
HEADER,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// paths.cli can lie outside the docker mount, so the files are copied into the build tree first.
|
|
20
|
+
function stage(cliPath, dir, files) {
|
|
21
|
+
files.forEach((file) => {
|
|
22
|
+
fs.mkdirSync(path.dirname(`${dir}/${file}`), { recursive: true });
|
|
23
|
+
fs.copyFileSync(`${cliPath}/${RUNTIME}/${file}`, `${dir}/${file}`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const wasiHttpFiles = (cliPath) => LINKED.map((file) => `${cliPath}/${RUNTIME}/${file}`);
|
|
28
|
+
|
|
29
|
+
export const wasiHttpHeader = (cliPath) => `${cliPath}/${RUNTIME}/${HEADER}`;
|
|
30
|
+
|
|
31
|
+
// The include dir of a wasi source build, where the app's handler finds <crossbind/http.h>.
|
|
32
|
+
export function stageWasiHttpInclude(cliPath, dir) {
|
|
33
|
+
stage(cliPath, dir, [HEADER]);
|
|
34
|
+
return `${dir}/include`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// What an http link adds to a command's: the handler export, its bindings (C, hence -x c) with the world
|
|
38
|
+
// they implement, the reactor model, which has no main, and a stack as large as the wasm builds'.
|
|
39
|
+
export function stageWasiHttpLink(cliPath, dir) {
|
|
40
|
+
stage(cliPath, dir, LINKED);
|
|
41
|
+
return [
|
|
42
|
+
`-I${dir}/include`,
|
|
43
|
+
`-I${dir}/bindings`,
|
|
44
|
+
`${dir}/handler.cpp`,
|
|
45
|
+
'-x', 'c', `${dir}/bindings/crossbind_http.c`, '-x', 'none',
|
|
46
|
+
`${dir}/bindings/crossbind_http_component_type.o`,
|
|
47
|
+
'-mexec-model=reactor',
|
|
48
|
+
`-Wl,-z,stack-size=${STACK_SIZE}`,
|
|
49
|
+
];
|
|
50
|
+
}
|
package/vitest.config.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
1
2
|
import { defineConfig } from 'vitest/config';
|
|
2
3
|
|
|
3
4
|
export default defineConfig({
|
|
5
|
+
// The app host package's scripts import crossbind's runtime modules the way buildApp bundles them.
|
|
6
|
+
resolve: {
|
|
7
|
+
alias: [{ find: /^crossbind\/runtime\//, replacement: fileURLToPath(new URL('./src/assets/js-runtime/', import.meta.url)) }],
|
|
8
|
+
},
|
|
4
9
|
// loadJs/loadConfig dynamically import config files written to os.tmpdir()
|
|
5
10
|
// during tests. Vite 6 tightened server.fs.allow and rejects those out-of-root
|
|
6
11
|
// paths ("Does the file exist?"); relax the fs check for the test runner.
|