crossbind 0.0.1 → 2.0.0-beta.50
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 +92 -0
- package/CHANGELOG.md +145 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/package.json +52 -4
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildCargo.js +88 -0
- package/src/actions/buildDependencies.js +157 -0
- package/src/actions/buildExternal.js +57 -0
- package/src/actions/buildJs.js +93 -0
- package/src/actions/buildLib.js +115 -0
- package/src/actions/buildTypes.js +33 -0
- package/src/actions/buildWasiCommand.js +101 -0
- package/src/actions/buildWasm.js +322 -0
- package/src/actions/createInterface.js +172 -0
- package/src/actions/createLib.js +249 -0
- package/src/actions/createXCFramework.js +82 -0
- package/src/actions/extensions.js +7 -0
- package/src/actions/getAllBridges.js +20 -0
- package/src/actions/getCmakeParameters.js +66 -0
- package/src/actions/getData.js +46 -0
- package/src/actions/getDependLibs.js +25 -0
- package/src/actions/isSourceNewer.js +35 -0
- package/src/actions/licenses.js +156 -0
- package/src/actions/run.js +305 -0
- package/src/actions/target.js +54 -0
- package/src/assets/cmake/CMakeLists.txt +61 -0
- package/src/assets/cmake/dist.cmake +57 -0
- package/src/assets/cmake/ios.toolchain.cmake +1176 -0
- package/src/assets/cpp-runtime/browser.cpp +12 -0
- package/src/assets/cpp-runtime/commonBridges.cpp +17 -0
- package/src/assets/cpp-runtime/crossbindEmptySource.cpp +9 -0
- package/src/assets/cpp-runtime/node.cpp +19 -0
- package/src/assets/js-runtime/adapters/exception-decode.js +79 -0
- package/src/assets/js-runtime/adapters/fs-browser.js +173 -0
- package/src/assets/js-runtime/adapters/fs-node.js +28 -0
- package/src/assets/js-runtime/adapters/path-fs.js +6 -0
- package/src/assets/js-runtime/adapters/path-url.js +8 -0
- package/src/assets/js-runtime/adapters/vector-coercion.js +209 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +309 -0
- package/src/assets/js-runtime/browser.js +16 -0
- package/src/assets/js-runtime/core.js +193 -0
- package/src/assets/js-runtime/edge.js +18 -0
- package/src/assets/js-runtime/node.js +20 -0
- package/src/assets/packaging/crossbind-package.podspec +17 -0
- package/src/assets/wasi-runtime/stubs.c +84 -0
- package/src/bin.js +416 -0
- package/src/index.js +19 -0
- package/src/integration/getCrossbindScript.js +180 -0
- package/src/integration/getDependFilePath.js +132 -0
- package/src/runtime/wasiRun.mjs +118 -0
- package/src/state/calculateDependencyParameters.js +97 -0
- package/src/state/index.js +128 -0
- package/src/state/loadConfig.js +215 -0
- package/src/utils/appRustCrates.js +106 -0
- package/src/utils/binTools.js +76 -0
- package/src/utils/bridgeAsyncGuard.js +37 -0
- package/src/utils/cargoTarget.js +15 -0
- package/src/utils/cppDts.js +201 -0
- package/src/utils/cppFieldBindings.js +23 -0
- package/src/utils/dependencyRebuild.js +99 -0
- package/src/utils/dirLock.js +71 -0
- package/src/utils/downloadAndExtractFile.js +102 -0
- package/src/utils/familyManifest.js +17 -0
- package/src/utils/findFiles.js +5 -0
- package/src/utils/fixPackageName.js +7 -0
- package/src/utils/flattenConfigForTable.js +75 -0
- package/src/utils/getAbsolutePath.js +14 -0
- package/src/utils/getCMakeListsFilePath.js +17 -0
- package/src/utils/getOsUserAndGroupId.js +11 -0
- package/src/utils/getParentPath.js +12 -0
- package/src/utils/hash.js +33 -0
- package/src/utils/inputStamp.js +28 -0
- package/src/utils/licenseReport.js +94 -0
- package/src/utils/linkLayout.js +34 -0
- package/src/utils/loadJs.js +31 -0
- package/src/utils/loadJson.js +16 -0
- package/src/utils/logger.js +110 -0
- package/src/utils/ociArtifact.js +124 -0
- package/src/utils/overrideDependency.js +99 -0
- package/src/utils/provenance.js +54 -0
- package/src/utils/pullDockerImage.js +105 -0
- package/src/utils/replaceBasePathForDocker.js +18 -0
- package/src/utils/resolveEmbindRust.js +66 -0
- package/src/utils/runCargo.js +228 -0
- package/src/utils/rustBridgeGen.js +1013 -0
- package/src/utils/rustMt.js +76 -0
- package/src/utils/rustSysroot.js +171 -0
- package/src/utils/rustSysrootPin.js +11 -0
- package/src/utils/systemKeys.js +29 -0
- package/src/utils/targets.js +109 -0
- package/src/utils/wasiToolchain.js +47 -0
- package/src/utils/writeIfChanged.js +8 -0
- package/src/utils/writeJson.js +9 -0
- package/vitest.config.js +40 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import state, { setAllDependecyPaths } from '../state/index.js';
|
|
3
|
+
import loadConfig from '../state/loadConfig.js';
|
|
4
|
+
import loadJson from '../utils/loadJson.js';
|
|
5
|
+
import writeJson from '../utils/writeJson.js';
|
|
6
|
+
import logger from '../utils/logger.js';
|
|
7
|
+
import { getBuildTargets } from './target.js';
|
|
8
|
+
import buildExternal from './buildExternal.js';
|
|
9
|
+
import buildLib from './buildLib.js';
|
|
10
|
+
import createXCFramework from './createXCFramework.js';
|
|
11
|
+
import { mergeBuildOverride, getOverrideKey } from '../utils/overrideDependency.js';
|
|
12
|
+
import {
|
|
13
|
+
isCached, getRebuildDeps, orderByDependencies, computeDependenciesStamp,
|
|
14
|
+
} from '../utils/dependencyRebuild.js';
|
|
15
|
+
import withDirLock from '../utils/dirLock.js';
|
|
16
|
+
import { prepareRustSysroot } from '../utils/rustSysroot.js';
|
|
17
|
+
|
|
18
|
+
export default async function buildDependencies({ targetParams, rebuildOption }) {
|
|
19
|
+
// Before the early returns below: this is the one async step every build path awaits, and the
|
|
20
|
+
// sync cargo work downstream reads what it resolves. A project with nothing to rebuild still
|
|
21
|
+
// needs its sysroot.
|
|
22
|
+
await prepareRustSysroot(getBuildTargets(targetParams));
|
|
23
|
+
await buildMissingCargoDependencies(getBuildTargets(targetParams), targetParams);
|
|
24
|
+
|
|
25
|
+
const rebuildDeps = getRebuildDeps(state.config.allDependencies, rebuildOption);
|
|
26
|
+
if (rebuildDeps.length === 0) return;
|
|
27
|
+
|
|
28
|
+
const targets = getBuildTargets(targetParams);
|
|
29
|
+
if (targets.length === 0) return;
|
|
30
|
+
const hasIos = targets.some((t) => t.platform === 'ios');
|
|
31
|
+
|
|
32
|
+
const appConfig = state.config;
|
|
33
|
+
const ordered = orderByDependencies(rebuildDeps);
|
|
34
|
+
|
|
35
|
+
for (const dep of ordered) {
|
|
36
|
+
const name = dep.general.name;
|
|
37
|
+
const depsDir = `${appConfig.paths.cache}/deps/${name}`;
|
|
38
|
+
const key = getOverrideKey(dep);
|
|
39
|
+
|
|
40
|
+
const isUsable = await withDirLock(`${depsDir}.lock`, async () => {
|
|
41
|
+
if (isCached(depsDir, targets, key, dep, hasIos)) {
|
|
42
|
+
logger.info(`crossbind: dependency "${name}" rebuild up to date (cached).`);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return rebuildDependency({
|
|
46
|
+
dep, name, depsDir, key, targetParams, hasIos, appConfig,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
if (!isUsable) continue;
|
|
50
|
+
|
|
51
|
+
dep.paths.output = `${depsDir}/dist`;
|
|
52
|
+
if (hasIos) dep.paths.project = depsDir;
|
|
53
|
+
setAllDependecyPaths();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A cargo dependency carries every target in ONE package, so unlike a platform-split port there
|
|
58
|
+
// is no "this sibling isn't for this target" case: a missing prebuilt means it was never built.
|
|
59
|
+
// Nothing else builds these packages - buildDependencies only rebuilds deps that carry a source
|
|
60
|
+
// recipe, and a cargo package carries none - so an app with a plugin used to need a manual
|
|
61
|
+
// pre-build step nobody had written down. Worse, only the cmake link asserts the dependency is
|
|
62
|
+
// there: on wasm a missing one silently drops out and the module builds clean, then dies at init.
|
|
63
|
+
// Build it here instead, in the dependency's own package, where its prebuilt belongs.
|
|
64
|
+
async function buildMissingCargoDependencies(targets, targetParams) {
|
|
65
|
+
if (targets.length === 0) return;
|
|
66
|
+
const appConfig = state.config;
|
|
67
|
+
const missing = (appConfig.allDependencies ?? []).filter((dep) => dep !== appConfig
|
|
68
|
+
&& dep.export?.type === 'cargo'
|
|
69
|
+
&& targets.some((target) => !dep.functions.isEnabled(target)));
|
|
70
|
+
|
|
71
|
+
for (const dep of missing) {
|
|
72
|
+
const name = dep.general.name;
|
|
73
|
+
// isEnabled reads the filesystem on every call, so a build that lands while this one waits
|
|
74
|
+
// for the lock is picked up without any state to refresh.
|
|
75
|
+
await withDirLock(`${dep.paths.output}.autobuild.lock`, async () => {
|
|
76
|
+
if (targets.every((target) => dep.functions.isEnabled(target))) return;
|
|
77
|
+
logger.info(`crossbind: building cargo dependency "${name}" - it has no prebuilt for this target yet…`);
|
|
78
|
+
const scoped = await loadConfig(dep.paths.project);
|
|
79
|
+
const prev = state.config;
|
|
80
|
+
state.config = scoped;
|
|
81
|
+
try {
|
|
82
|
+
buildLib(targetParams);
|
|
83
|
+
} finally {
|
|
84
|
+
state.config = prev;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The build above is the fix; this is the guard behind it. assertDependsBuilt only runs while
|
|
90
|
+
// the cmake link is being configured, so a cached native build skips the check and a missing
|
|
91
|
+
// cargo dependency reaches the app anyway - measured on wasm as a clean BUILD=0 whose module
|
|
92
|
+
// then failed 22 of 43 conformance features at init. Here the answer is known either way.
|
|
93
|
+
const unresolved = missing.filter((dep) => targets.some((target) => !dep.functions.isEnabled(target)));
|
|
94
|
+
if (unresolved.length > 0) {
|
|
95
|
+
const names = unresolved.map((dep) => `"${dep.general.name}"`).join(', ');
|
|
96
|
+
throw new Error(`crossbind: cargo ${unresolved.length > 1 ? 'dependencies' : 'dependency'} ${names} still `
|
|
97
|
+
+ `${unresolved.length > 1 ? 'have' : 'has'} no prebuilt for ${targets.map((t) => t.path).join(', ')} `
|
|
98
|
+
+ 'after building. Linking without it produces a module that builds clean and dies at init.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function rebuildDependency({
|
|
103
|
+
dep, name, depsDir, key, targetParams, hasIos, appConfig,
|
|
104
|
+
}) {
|
|
105
|
+
const scoped = await loadConfig(dep.paths.project);
|
|
106
|
+
if (!scoped.build?.withBuildConfig) {
|
|
107
|
+
logger.info(`crossbind: dependency "${name}" has no source build recipe (crossbind.build); using prebuilt.`);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const override = dep.overrideBuild;
|
|
112
|
+
if (override) {
|
|
113
|
+
scoped.build = mergeBuildOverride(scoped.build, override);
|
|
114
|
+
if (override.nativeVersion) {
|
|
115
|
+
scoped.package = { ...scoped.package, nativeVersion: override.nativeVersion };
|
|
116
|
+
}
|
|
117
|
+
if (override.targetSpecs) scoped.targetSpecs = [...(scoped.targetSpecs || []), ...override.targetSpecs];
|
|
118
|
+
if (override.export) scoped.export = { ...scoped.export, ...override.export };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// All build I/O must stay under the app base (Docker mount) — never the dep's own node_modules.
|
|
122
|
+
// output = depsDir/dist (libs) and project = depsDir (xcframework) mirror a normal package layout
|
|
123
|
+
// so createXCFramework's project->output relative path resolves; paths.project stays the dep pkg
|
|
124
|
+
// dir for the recipe's copyToSource asset reads.
|
|
125
|
+
scoped.paths.base = appConfig.paths.base;
|
|
126
|
+
scoped.paths.output = `${depsDir}/dist`;
|
|
127
|
+
scoped.paths.build = `${depsDir}/build`;
|
|
128
|
+
scoped.paths.cache = depsDir;
|
|
129
|
+
scoped.allDependencyPaths = appConfig.allDependencyPaths;
|
|
130
|
+
|
|
131
|
+
const stale = loadJson(`${depsDir}/.crossbind-rebuild.json`)?.key !== key;
|
|
132
|
+
if (stale) fs.rmSync(depsDir, { recursive: true, force: true });
|
|
133
|
+
fs.mkdirSync(depsDir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
logger.info(`crossbind: rebuilding dependency "${name}" from source (v${scoped.package.nativeVersion})…`);
|
|
136
|
+
const prev = state.config;
|
|
137
|
+
state.config = scoped;
|
|
138
|
+
try {
|
|
139
|
+
await buildExternal(targetParams, { skipXcframework: hasIos });
|
|
140
|
+
if (hasIos) {
|
|
141
|
+
createXCFramework({
|
|
142
|
+
paths: { project: depsDir, output: `${depsDir}/dist` },
|
|
143
|
+
export: { libName: dep.export.libName },
|
|
144
|
+
targetParams,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
state.config = prev;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
writeJson(`${depsDir}/.crossbind-rebuild.json`, { key });
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getDependenciesStamp() {
|
|
156
|
+
return computeDependenciesStamp(state.config.allDependencies, state.config.paths.cache);
|
|
157
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import replace from 'replace';
|
|
3
|
+
import downloadAndExtractFile from '../utils/downloadAndExtractFile.js';
|
|
4
|
+
import { getBuildTargets } from './target.js';
|
|
5
|
+
import state from '../state/index.js';
|
|
6
|
+
import buildLib from './buildLib.js';
|
|
7
|
+
import buildBinTools from './buildBinTools.js';
|
|
8
|
+
|
|
9
|
+
export default async function buildExternal(targetParams, options = {}) {
|
|
10
|
+
const version = state.config.package.nativeVersion;
|
|
11
|
+
if (!version) {
|
|
12
|
+
throw new Error(`crossbind: "${state.config.general?.name}" has a crossbind.build recipe but no "nativeVersion" in package.json — cannot build from source.`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { getURL, sha256, replaceList, copyToSource, copyToDist } = state.config.build;
|
|
16
|
+
const isNewlyCreated = await downloadAndExtractFile(getURL(version), state.config.paths.build, sha256);
|
|
17
|
+
const sourcePath = `${state.config.paths.build}/source`;
|
|
18
|
+
if (isNewlyCreated && replaceList) {
|
|
19
|
+
replaceList.forEach(({ regex, replacement, paths }) => {
|
|
20
|
+
replace({
|
|
21
|
+
regex, replacement, paths: paths.map((p) => `${sourcePath}/${p}`), recursive: false, silent: true,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isNewlyCreated && copyToSource) {
|
|
27
|
+
Object.entries(copyToSource).forEach(([key, value]) => {
|
|
28
|
+
fs.copyFileSync(`${state.config.paths.project}/${key}`, `${sourcePath}/${value}`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
buildLib(targetParams, options);
|
|
33
|
+
|
|
34
|
+
if (state.config.build.bin) {
|
|
35
|
+
await buildBinTools(targetParams);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (copyToDist) {
|
|
39
|
+
const targets = getBuildTargets(targetParams);
|
|
40
|
+
Object.entries(copyToDist).forEach(([key, value]) => {
|
|
41
|
+
const values = [];
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
values.push(...value);
|
|
44
|
+
} else {
|
|
45
|
+
values.push(value);
|
|
46
|
+
}
|
|
47
|
+
values.forEach(v => {
|
|
48
|
+
targets.forEach(target => {
|
|
49
|
+
const assetPath = `${state.config.paths.output}/prebuilt/${target.path}/${v}`;
|
|
50
|
+
if (!fs.existsSync(assetPath)) {
|
|
51
|
+
fs.copyFileSync(`${state.config.paths.project}/${key}`, assetPath);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { rollup } from 'rollup';
|
|
4
|
+
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
5
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
6
|
+
import virtual from '@rollup/plugin-virtual';
|
|
7
|
+
import state from '../state/index.js';
|
|
8
|
+
import getData from './getData.js';
|
|
9
|
+
|
|
10
|
+
const SUPPRESSED_WARN_CODES = new Set([
|
|
11
|
+
'MISSING_GLOBAL_NAME',
|
|
12
|
+
'MISSING_NODE_BUILTINS',
|
|
13
|
+
'MIXED_EXPORTS',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function suppressNodeBuiltinWarnings(warning, defaultHandler) {
|
|
17
|
+
if (SUPPRESSED_WARN_CODES.has(warning.code)) return;
|
|
18
|
+
if (warning.code === 'UNRESOLVED_IMPORT' && /^node:/.test(warning.exporter || warning.source || warning.id || '')) return;
|
|
19
|
+
defaultHandler(warning);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const nodeLibs = {
|
|
23
|
+
fs: 'export default {};', 'node:fs': 'export default {};',
|
|
24
|
+
path: 'export default {};', 'node:path': 'export default {};',
|
|
25
|
+
crypto: 'export default {};', 'node:crypto': 'export default {};',
|
|
26
|
+
buffer: 'export default {};', 'node:buffer': 'export default {};',
|
|
27
|
+
stream: 'export default {};', 'node:stream': 'export default {};',
|
|
28
|
+
string_decoder: 'export default {};', 'node:string_decoder': 'export default {};',
|
|
29
|
+
util: 'export default {};', 'node:util': 'export default {};',
|
|
30
|
+
os: 'export default {};', 'node:os': 'export default {};',
|
|
31
|
+
'node:module': 'export default {};',
|
|
32
|
+
'node:worker_threads': 'export default {};',
|
|
33
|
+
ws: 'export default {};',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const options = {
|
|
37
|
+
browser: {
|
|
38
|
+
plugins: [virtual(nodeLibs), nodeResolve(), commonjs({ transformMixedEsModules: true, ignoreTryCatch: 'remove' })],
|
|
39
|
+
output: {
|
|
40
|
+
format: 'umd',
|
|
41
|
+
name: 'initNative',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
node: {
|
|
45
|
+
plugins: [nodeResolve(), commonjs()],
|
|
46
|
+
output: {
|
|
47
|
+
format: 'umd',
|
|
48
|
+
name: 'initNative',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
options.edge = options.browser;
|
|
53
|
+
|
|
54
|
+
export default async function buildJS(target) {
|
|
55
|
+
const entryJS = `${state.config.paths.cli}/assets/js-runtime/${target.runtimeEnv}.js`;
|
|
56
|
+
const rawEnv = getData('env', target);
|
|
57
|
+
const resolvedEnv = Object.fromEntries(
|
|
58
|
+
Object.entries(rawEnv).map(([key, value]) => [
|
|
59
|
+
key,
|
|
60
|
+
typeof value === 'function' ? value(state, target) : value,
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
const env = JSON.stringify(resolvedEnv);
|
|
64
|
+
const systemConfig = `export default {
|
|
65
|
+
env: ${env},
|
|
66
|
+
useWorker: !!globalThis.Worker,
|
|
67
|
+
general: {
|
|
68
|
+
name: '${state.config.general.name}',
|
|
69
|
+
},
|
|
70
|
+
fs: {
|
|
71
|
+
opfs: ${target.runtime === 'mt'},
|
|
72
|
+
},
|
|
73
|
+
paths: {
|
|
74
|
+
wasm: '${target.wasmName}',
|
|
75
|
+
data: '${target.dataTxtName}',
|
|
76
|
+
worker: '${target.rawJsName}',
|
|
77
|
+
js: '${target.jsName}',
|
|
78
|
+
}
|
|
79
|
+
}`;
|
|
80
|
+
|
|
81
|
+
// fs.renameSync(input, `${input}.raw.js`);
|
|
82
|
+
const option = options[target.runtimeEnv];
|
|
83
|
+
option.plugins = [virtual({
|
|
84
|
+
'crossbind/systemConfig': systemConfig,
|
|
85
|
+
'crossbind/module': `export { default } from '${state.config.paths.build}/${target.rawJsName}';`,
|
|
86
|
+
}), ...option.plugins];
|
|
87
|
+
option.input = entryJS;
|
|
88
|
+
option.output.file = `${state.config.paths.build}/${target.jsName}`;
|
|
89
|
+
option.onwarn = suppressNodeBuiltinWarnings;
|
|
90
|
+
const bundle = await rollup(option);
|
|
91
|
+
await bundle.write(option.output);
|
|
92
|
+
// fs.rmSync(`${input}.raw.js`, { force: true });
|
|
93
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import createLib from './createLib.js';
|
|
3
|
+
import { isNativeSourceNewerThan } from './isSourceNewer.js';
|
|
4
|
+
import createXCFramework from './createXCFramework.js';
|
|
5
|
+
import { getBuildTargets, getFilteredTargetSpec } from './target.js';
|
|
6
|
+
import state from '../state/index.js';
|
|
7
|
+
import logger from '../utils/logger.js';
|
|
8
|
+
import findFiles from '../utils/findFiles.js';
|
|
9
|
+
|
|
10
|
+
export default function buildLib(targetParams, options = {}) {
|
|
11
|
+
let isChanged = false;
|
|
12
|
+
const targets = getBuildTargets(targetParams);
|
|
13
|
+
if (targets.length === 0) {
|
|
14
|
+
console.error('No targets found for the given parameters.', targetParams);
|
|
15
|
+
throw new Error('No targets found for the given parameters.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
targets.forEach((target) => {
|
|
19
|
+
// Cargo re-enters the build unconditionally: cargo is the incremental cache, and the
|
|
20
|
+
// existence-only skip below would keep serving a stale staged staticlib after source edits.
|
|
21
|
+
const isCargo = state.config.export?.type === 'cargo';
|
|
22
|
+
if (isCargo || !fs.existsSync(`${state.config.paths.output}/prebuilt/${target.path}/lib`)) {
|
|
23
|
+
createLib(target, 'Source', { buildSource: true });
|
|
24
|
+
|
|
25
|
+
const modules = [];
|
|
26
|
+
state.config.paths.module.forEach((modulePath) => {
|
|
27
|
+
modules.push(...findFiles('**/*.i', { cwd: modulePath }));
|
|
28
|
+
modules.push(...findFiles('*.i', { cwd: modulePath }));
|
|
29
|
+
});
|
|
30
|
+
if (modules.length > 0) {
|
|
31
|
+
fs.mkdirSync(`${state.config.paths.output}/prebuilt/${target.path}/swig`, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
modules.forEach((modulePath) => {
|
|
34
|
+
const fileName = modulePath.split('/').at(-1);
|
|
35
|
+
fs.copyFileSync(modulePath, `${state.config.paths.output}/prebuilt/${target.path}/swig/${fileName}`);
|
|
36
|
+
});
|
|
37
|
+
isChanged = true;
|
|
38
|
+
} else {
|
|
39
|
+
// The skip is existence-only; without this warning a source edit is served stale silently.
|
|
40
|
+
if (isNativeSourceNewerThan(`${state.config.paths.output}/prebuilt/${target.path}/lib`)) {
|
|
41
|
+
logger.info(`[${target.path}] lib cached but native sources are newer - delete the prebuilt output (rm -rf dist .crossbind) and rebuild to pick them up`);
|
|
42
|
+
}
|
|
43
|
+
logger.cachedStep(target, 'lib');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (isChanged && fs.existsSync(`${state.config.paths.build}/Source-Release/prebuilt`)) {
|
|
48
|
+
fs.cpSync(`${state.config.paths.build}/Source-Release/prebuilt`, `${state.config.paths.output}/prebuilt`, { recursive: true, dereference: true });
|
|
49
|
+
}
|
|
50
|
+
if (isChanged && fs.existsSync(`${state.config.paths.build}/Source-Debug/prebuilt`)) {
|
|
51
|
+
fs.cpSync(`${state.config.paths.build}/Source-Debug/prebuilt`, `${state.config.paths.output}/prebuilt`, { recursive: true, dereference: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!options.skipXcframework) {
|
|
55
|
+
createXCFramework();
|
|
56
|
+
|
|
57
|
+
const iosTargets = getBuildTargets({ platform: ['ios'], arch: ['iphoneos'], runtime: ['mt'], buildType: ['release'] });
|
|
58
|
+
const podSpecs = findFiles('*.podspec', { cwd: state.config.paths.project });
|
|
59
|
+
if (podSpecs.length === 0 && targets.length > 0) {
|
|
60
|
+
const iosTarget = iosTargets[0];
|
|
61
|
+
const resources = getFilteredTargetSpec(state.config.targetSpecs, iosTarget).map(s => s.data).filter(s => s).map(d => Object.keys(d)).flat();
|
|
62
|
+
const uniqueResources = [...new Set(resources)].map(r => `dist/prebuilt/${iosTarget.path}/${r}`);
|
|
63
|
+
const xcFrameworks = [];
|
|
64
|
+
xcFrameworks.push(...state.config.export.libName.map((l) => `${l}.xcframework`));
|
|
65
|
+
if (!xcFrameworks.some(f => !fs.existsSync(`${state.config.paths.project}/${f}`))) {
|
|
66
|
+
xcFrameworks.push(...state.config.dependencies.map((d) => d.export.libName.map((l) => `${l}.xcframework`)).flat());
|
|
67
|
+
// Cargo producers register through an init-array constructor that nothing references,
|
|
68
|
+
// so the linker would dead-strip it. Generated bridges pin their keep symbol with -u
|
|
69
|
+
// (NEVER force_load: every Rust staticlib bundles libstd, and fully loading two of
|
|
70
|
+
// them duplicates thousands of std symbols); manual-bindings crates have no keep
|
|
71
|
+
// symbol and fall back to force_load - safe only while they are the single loaded
|
|
72
|
+
// Rust archive in the app.
|
|
73
|
+
let ldFlags = '';
|
|
74
|
+
if (state.config.export?.type === 'cargo') {
|
|
75
|
+
const crateLibRs = `${state.config.paths.project}/${state.config.export.crate ?? 'crate'}/src/lib.rs`.replace('/./', '/');
|
|
76
|
+
const isManual = fs.existsSync(crateLibRs) && fs.readFileSync(crateLibRs, 'utf8').includes('bindings!');
|
|
77
|
+
const flags = state.config.export.libName.map((l) => (isManual
|
|
78
|
+
? `-force_load $(PODS_XCFRAMEWORKS_BUILD_DIR)/${state.config.general.name}/lib${l}.a`
|
|
79
|
+
: `-Wl,-u,_crossbind_keep_${l}`)).join(' ');
|
|
80
|
+
ldFlags = `, 'OTHER_LDFLAGS' => '${flags}'`;
|
|
81
|
+
}
|
|
82
|
+
const distPodSpecContent = fs.readFileSync(`${state.config.paths.cli}/assets/packaging/crossbind-package.podspec`, { encoding: 'utf8', flag: 'r' })
|
|
83
|
+
// module_name must be a valid C99 identifier (pod names may carry dashes).
|
|
84
|
+
.replace('___PROJECT_MODULE_NAME___', state.config.general.name.replace(/[^a-zA-Z0-9_]/g, '_'))
|
|
85
|
+
.replaceAll('___PROJECT_NAME___', state.config.general.name)
|
|
86
|
+
.replace('___PROJECT_FRAMEWORKS___', xcFrameworks.map(f => `'${f}'`).join(', '))
|
|
87
|
+
.replace('___PROJECT_RESOURCES___', JSON.stringify(uniqueResources))
|
|
88
|
+
.replace('___PROJECT_LDFLAGS___', ldFlags);
|
|
89
|
+
fs.writeFileSync(`${state.config.paths.project}/${state.config.general.name}.podspec`, distPodSpecContent);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (fs.existsSync(`${state.config.paths.output}/prebuilt`)) {
|
|
95
|
+
// A partial build (e.g. `crossbind build -p wasm`) must not clobber the other platforms'
|
|
96
|
+
// entries: consumers resolve this CMakeLists for ios/android too, and a target missing
|
|
97
|
+
// from the list silently drops the dependency's include dirs and libs. Advertise every
|
|
98
|
+
// target already present in dist alongside the ones just built.
|
|
99
|
+
const distTargets = fs.readdirSync(`${state.config.paths.output}/prebuilt`, { withFileTypes: true })
|
|
100
|
+
.filter((e) => e.isDirectory() && fs.existsSync(`${state.config.paths.output}/prebuilt/${e.name}/lib`))
|
|
101
|
+
.map((e) => e.name);
|
|
102
|
+
const hostTargets = [...new Set([...distTargets, ...targets.map((t) => t.path)])];
|
|
103
|
+
const distCmakeContent = fs.readFileSync(`${state.config.paths.cli}/assets/cmake/dist.cmake`, { encoding: 'utf8', flag: 'r' })
|
|
104
|
+
.replace('___PROJECT_NAME___', state.config.general.name)
|
|
105
|
+
.replace('___PROJECT_HOST___', hostTargets.join(';'))
|
|
106
|
+
.replace('___PROJECT_LIBS___', state.config.export.libName.join(';'))
|
|
107
|
+
.replace('___PROJECT_WHOLE_ARCHIVE___', (() => {
|
|
108
|
+
if (state.config.export?.type !== 'cargo') return '';
|
|
109
|
+
const crateLibRs = `${state.config.paths.project}/${state.config.export.crate ?? 'crate'}/src/lib.rs`.replace('/./', '/');
|
|
110
|
+
const isManual = fs.existsSync(crateLibRs) && fs.readFileSync(crateLibRs, 'utf8').includes('bindings!');
|
|
111
|
+
return isManual ? 'FORCE' : 'KEEP';
|
|
112
|
+
})());
|
|
113
|
+
fs.writeFileSync(`${state.config.paths.output}/prebuilt/CMakeLists.txt`, distCmakeContent);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import state from '../state/index.js';
|
|
4
|
+
import findFiles from '../utils/findFiles.js';
|
|
5
|
+
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
6
|
+
import { parseCppSurface, emitCppDts } from '../utils/cppDts.js';
|
|
7
|
+
import logger from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
// Package type publishing (opt-in: config `types: true`): one combined .d.ts over every
|
|
10
|
+
// public header, written to <output>/types/index.d.ts, and package.json wired the way
|
|
11
|
+
// hand-maintained typings already are - `types` plus a greedy `typesVersions` "*.h" map -
|
|
12
|
+
// so a consumer's `import ... from '<pkg>/<any>.h'` resolves to the generated file.
|
|
13
|
+
export default function buildTypes() {
|
|
14
|
+
if (state.config.types !== true) return;
|
|
15
|
+
const exts = state.config.ext.header.join(',');
|
|
16
|
+
const headers = [...new Set(state.config.paths.header.flatMap((dir) => findFiles(`${dir}/**/*.{${exts}}`)))].sort();
|
|
17
|
+
if (headers.length === 0) return;
|
|
18
|
+
|
|
19
|
+
const combined = headers.map((h) => fs.readFileSync(h, 'utf8')).join('\n');
|
|
20
|
+
const model = parseCppSurface(combined, (m) => logger.info(m));
|
|
21
|
+
const names = model.classes.map((c) => c.name);
|
|
22
|
+
const dtsFile = `${state.config.paths.output}/types/index.d.ts`;
|
|
23
|
+
writeIfChanged(dtsFile, emitCppDts(model, names, state.config.dts));
|
|
24
|
+
|
|
25
|
+
const manifestFile = `${state.config.paths.project}/package.json`;
|
|
26
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
27
|
+
const relative = `./${path.relative(state.config.paths.project, dtsFile)}`;
|
|
28
|
+
const next = { ...manifest, types: relative, typesVersions: { '*': { '*.h': [relative] } } };
|
|
29
|
+
if (JSON.stringify({ t: next.types, v: next.typesVersions }) !== JSON.stringify({ t: manifest.types, v: manifest.typesVersions })) {
|
|
30
|
+
fs.writeFileSync(manifestFile, `${JSON.stringify(next, null, 4)}\n`);
|
|
31
|
+
}
|
|
32
|
+
logger.info(`crossbind: package types -> ${relative} (${names.length} classes from ${headers.length} headers)`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import run from './run.js';
|
|
3
|
+
import getDependLibs from './getDependLibs.js';
|
|
4
|
+
import getData from './getData.js';
|
|
5
|
+
import state from '../state/index.js';
|
|
6
|
+
import logger from '../utils/logger.js';
|
|
7
|
+
import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
|
|
8
|
+
import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
9
|
+
import { wasiCxxFlags, WASI_LINK_LIBS } from '../utils/wasiToolchain.js';
|
|
10
|
+
|
|
11
|
+
// Links a platform:'wasi' target into one command module: main() is the entry, deps are DCE'd, data lands as a real dir for --dir preopens.
|
|
12
|
+
export default async function buildWasiCommand(target, options = {}) {
|
|
13
|
+
const isProd = target.buildType === 'release';
|
|
14
|
+
const buildType = isProd ? 'Release' : 'Debug';
|
|
15
|
+
|
|
16
|
+
if (state.config.export.bundle === false) {
|
|
17
|
+
logger.info(`[${target.path}] wasi command skipped (export.bundle = false)`);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (state.config.export?.type === 'cargo') {
|
|
21
|
+
logger.info(`[${target.path}] wasi command skipped (cargo packages have no cargo wasi triple)`);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sourceLibCandidates = [
|
|
26
|
+
`${state.config.paths.build}/Source-${buildType}/${target.path}/lib${state.config.general.name}.a`,
|
|
27
|
+
`${state.config.paths.output}/prebuilt/${target.path}/lib/lib${state.config.general.name}.a`,
|
|
28
|
+
];
|
|
29
|
+
const libs = [
|
|
30
|
+
...getDependLibs(target),
|
|
31
|
+
sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const binary = getData('binary', target);
|
|
35
|
+
const wasiFlags = binary?.wasiFlags || [];
|
|
36
|
+
|
|
37
|
+
const wholeArchiveAll = state.config.export.wholeArchive === true;
|
|
38
|
+
const wholeArchiveNames = new Set();
|
|
39
|
+
state.config.dependencyParameters.getCmakeDepends(target).forEach((dep) => {
|
|
40
|
+
if (dep.export.wholeArchive === true) {
|
|
41
|
+
(dep.export.libName || []).forEach((name) => wholeArchiveNames.add(name));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// crt1 references main only weakly: the source archive must be force-included.
|
|
45
|
+
const linkLibs = buildLinkLibArgs(libs, { wholeArchiveAll, wholeArchiveNames });
|
|
46
|
+
|
|
47
|
+
const stubs = `${state.config.paths.cli}/assets/wasi-runtime/stubs.c`;
|
|
48
|
+
const linkFingerprintFile = `${state.config.paths.build}/${target.wasmName}.fingerprint`;
|
|
49
|
+
const linkFingerprint = getContentHash(JSON.stringify({
|
|
50
|
+
linkLayout: 'wasi-v2',
|
|
51
|
+
wasiFlags,
|
|
52
|
+
wholeArchiveAll,
|
|
53
|
+
wholeArchiveNames: [...wholeArchiveNames].sort(),
|
|
54
|
+
libs: libs.map((lib) => {
|
|
55
|
+
const stat = fs.existsSync(lib) ? fs.statSync(lib) : null;
|
|
56
|
+
return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
|
|
57
|
+
}),
|
|
58
|
+
data: getData('data', target),
|
|
59
|
+
runtime: getFilesFingerprint([stubs]),
|
|
60
|
+
}));
|
|
61
|
+
const linkChanged = !fs.existsSync(linkFingerprintFile)
|
|
62
|
+
|| fs.readFileSync(linkFingerprintFile, { encoding: 'utf8' }) !== linkFingerprint;
|
|
63
|
+
|
|
64
|
+
if (!options.force && !linkChanged && fs.existsSync(`${state.config.paths.build}/${target.wasmName}`)) {
|
|
65
|
+
logger.cachedStep(target, 'wasi command');
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
logger.startStep(target, 'wasi command');
|
|
70
|
+
const t0 = performance.now();
|
|
71
|
+
// stubs.c lives outside the docker mount (paths.cli); stage it next to the artifact.
|
|
72
|
+
const stagedStubs = `${state.config.paths.build}/crossbind-wasi-stubs.c`;
|
|
73
|
+
fs.mkdirSync(state.config.paths.build, { recursive: true });
|
|
74
|
+
fs.copyFileSync(stubs, stagedStubs);
|
|
75
|
+
run(null, [
|
|
76
|
+
'wasi-clang++',
|
|
77
|
+
stagedStubs,
|
|
78
|
+
...wasiCxxFlags(),
|
|
79
|
+
...(isProd ? ['-O3'] : ['-O0']),
|
|
80
|
+
...wasiFlags,
|
|
81
|
+
...linkLibs,
|
|
82
|
+
...WASI_LINK_LIBS,
|
|
83
|
+
'-o', `${state.config.paths.build}/${target.wasmName}`,
|
|
84
|
+
], null, target);
|
|
85
|
+
const ms = Math.round(performance.now() - t0);
|
|
86
|
+
logger.doneStep(target, 'wasi command', ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`);
|
|
87
|
+
|
|
88
|
+
// Dep data lands as a real folder next to the artifact for --dir preopens.
|
|
89
|
+
Object.entries(getData('data', target)).forEach(([key, value]) => {
|
|
90
|
+
if (fs.existsSync(key)) {
|
|
91
|
+
const dataPath = `${state.config.paths.build}/data/${value}`;
|
|
92
|
+
if (!fs.existsSync(dataPath)) {
|
|
93
|
+
fs.mkdirSync(dataPath, { recursive: true });
|
|
94
|
+
fs.cpSync(key, dataPath, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(linkFingerprintFile, linkFingerprint);
|
|
100
|
+
return true;
|
|
101
|
+
}
|