@xspect-build/cross-build 1.0.0

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/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @xspect-build/cross-build
2
+
3
+ Cross-compile Rust projects without Docker.
4
+
5
+ This package extracts the cross-compilation logic from `@napi-rs/cli` into a standalone package that can be used by any Rust project, not just those using NAPI-RS.
6
+
7
+ ## Features
8
+
9
+ - **Cross-compile for Linux targets** using `@napi-rs/cross-toolchain`
10
+ - **Cross-compile for Windows** using `cargo-xwin`
11
+ - **Cross-compile for other platforms** using `cargo-zigbuild`
12
+ - **Android NDK support** for Android targets
13
+ - **WASI support** for WebAssembly targets
14
+ - **OpenHarmony support** for HarmonyOS targets
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @xspect-build/cross-build
20
+ # or
21
+ yarn add @xspect-build/cross-build
22
+ # or
23
+ pnpm add @xspect-build/cross-build
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Basic Build
29
+
30
+ ```typescript
31
+ import { build } from '@xspect-build/cross-build'
32
+
33
+ // Cross-compile for Linux ARM64
34
+ const result = await build({
35
+ target: 'aarch64-unknown-linux-gnu',
36
+ release: true,
37
+ useNapiCross: true,
38
+ })
39
+
40
+ console.log('Build succeeded:', result.success)
41
+ console.log('Environment variables used:', result.envs)
42
+ ```
43
+
44
+ ### Get Cross-Compile Environment Variables
45
+
46
+ If you want to get the environment variables needed for cross-compilation without actually building, you can use `getCrossCompileEnv`:
47
+
48
+ ```typescript
49
+ import { getCrossCompileEnv } from '@xspect-build/cross-build'
50
+
51
+ const env = getCrossCompileEnv({
52
+ target: 'aarch64-unknown-linux-gnu',
53
+ useNapiCross: true,
54
+ })
55
+
56
+ console.log(env)
57
+ // {
58
+ // CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: '...',
59
+ // TARGET_CC: '...',
60
+ // TARGET_CXX: '...',
61
+ // ...
62
+ // }
63
+ ```
64
+
65
+ ### Parse Target Triple
66
+
67
+ ```typescript
68
+ import { parseTriple } from '@xspect-build/cross-build'
69
+
70
+ const target = parseTriple('aarch64-unknown-linux-gnu')
71
+ console.log(target)
72
+ // {
73
+ // triple: 'aarch64-unknown-linux-gnu',
74
+ // platform: 'linux',
75
+ // arch: 'arm64',
76
+ // abi: 'gnu',
77
+ // platformArchABI: 'linux-arm64-gnu'
78
+ // }
79
+ ```
80
+
81
+ ## Options
82
+
83
+ ### `CrossBuildOptions`
84
+
85
+ | Option | Type | Description |
86
+ |--------|------|-------------|
87
+ | `target` | `string` | Target triple (e.g., 'aarch64-unknown-linux-gnu') |
88
+ | `cwd` | `string` | Working directory for the build |
89
+ | `manifestPath` | `string` | Path to Cargo.toml manifest file |
90
+ | `targetDir` | `string` | Directory for build artifacts |
91
+ | `release` | `boolean` | Build in release mode |
92
+ | `verbose` | `boolean` | Show verbose output |
93
+ | `profile` | `string` | Build profile (e.g., 'release', 'dev') |
94
+ | `useNapiCross` | `boolean` | Use @napi-rs/cross-toolchain for Linux cross-compilation |
95
+ | `crossCompile` | `boolean` | Use cargo-zigbuild/cargo-xwin for cross-compilation |
96
+ | `useCross` | `boolean` | Use cross-rs instead of cargo |
97
+ | `strip` | `boolean` | Strip debug symbols |
98
+ | `package` | `string` | Package name in workspace |
99
+ | `bin` | `string` | Binary name to build |
100
+ | `features` | `string[]` | Features to enable |
101
+ | `allFeatures` | `boolean` | Enable all features |
102
+ | `noDefaultFeatures` | `boolean` | Disable default features |
103
+ | `cargoArgs` | `string[]` | Additional cargo arguments |
104
+ | `env` | `Record<string, string>` | Additional environment variables |
105
+
106
+ ## Supported Targets
107
+
108
+ ### Linux Targets (via `@napi-rs/cross-toolchain`)
109
+
110
+ - `aarch64-unknown-linux-gnu`
111
+ - `aarch64-unknown-linux-musl`
112
+ - `x86_64-unknown-linux-gnu`
113
+ - `x86_64-unknown-linux-musl`
114
+ - `armv7-unknown-linux-gnueabihf`
115
+ - `riscv64gc-unknown-linux-gnu`
116
+ - `powerpc64le-unknown-linux-gnu`
117
+ - `s390x-unknown-linux-gnu`
118
+ - And more...
119
+
120
+ ### Windows Targets (via `cargo-xwin`)
121
+
122
+ - `x86_64-pc-windows-msvc`
123
+ - `i686-pc-windows-msvc`
124
+ - `aarch64-pc-windows-msvc`
125
+
126
+ ### Other Platforms (via `cargo-zigbuild`)
127
+
128
+ - macOS (cross-arch)
129
+ - FreeBSD
130
+ - And more...
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,125 @@
1
+ import { type Target } from './target.js';
2
+ /**
3
+ * Options for cross-compiling Rust projects
4
+ */
5
+ export interface CrossBuildOptions {
6
+ /**
7
+ * The target triple to build for (e.g., 'aarch64-unknown-linux-gnu')
8
+ */
9
+ target?: string;
10
+ /**
11
+ * Working directory for the build
12
+ */
13
+ cwd?: string;
14
+ /**
15
+ * Path to Cargo.toml manifest file
16
+ */
17
+ manifestPath?: string;
18
+ /**
19
+ * Directory for build artifacts
20
+ */
21
+ targetDir?: string;
22
+ /**
23
+ * Build in release mode
24
+ */
25
+ release?: boolean;
26
+ /**
27
+ * Show verbose output
28
+ */
29
+ verbose?: boolean;
30
+ /**
31
+ * Build profile (e.g., 'release', 'dev')
32
+ */
33
+ profile?: string;
34
+ /**
35
+ * Use @napi-rs/cross-toolchain for Linux cross-compilation
36
+ */
37
+ useNapiCrossToolChain?: boolean;
38
+ /**
39
+ * Use cargo-zigbuild/cargo-xwin for cross-compilation
40
+ */
41
+ crossCompile?: boolean;
42
+ /**
43
+ * Use cross-rs instead of cargo
44
+ */
45
+ useCross?: boolean;
46
+ /**
47
+ * Strip debug symbols
48
+ */
49
+ strip?: boolean;
50
+ /**
51
+ * Package name in workspace
52
+ */
53
+ package?: string;
54
+ /**
55
+ * Binary name to build
56
+ */
57
+ bin?: string;
58
+ /**
59
+ * Features to enable
60
+ */
61
+ features?: string[];
62
+ /**
63
+ * Enable all features
64
+ */
65
+ allFeatures?: boolean;
66
+ /**
67
+ * Disable default features
68
+ */
69
+ noDefaultFeatures?: boolean;
70
+ /**
71
+ * Additional cargo arguments
72
+ */
73
+ cargoArgs?: string[];
74
+ /**
75
+ * Additional environment variables
76
+ */
77
+ env?: Record<string, string>;
78
+ }
79
+ /**
80
+ * Result of the cross-build operation
81
+ */
82
+ export interface CrossBuildResult {
83
+ /**
84
+ * Whether the build succeeded
85
+ */
86
+ success: boolean;
87
+ /**
88
+ * The target that was built
89
+ */
90
+ target: Target;
91
+ /**
92
+ * Environment variables that were set
93
+ */
94
+ envs: Record<string, string>;
95
+ }
96
+ /**
97
+ * Cross-compile a Rust project
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { build } from '@xspect-build/cross-build'
102
+ *
103
+ * const result = await build({
104
+ * target: 'aarch64-unknown-linux-gnu',
105
+ * release: true,
106
+ * useNapiCross: true,
107
+ * })
108
+ * ```
109
+ */
110
+ export declare function build(options?: CrossBuildOptions): Promise<CrossBuildResult>;
111
+ /**
112
+ * Get environment variables needed for cross-compilation without actually building
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { getCrossCompileEnv } from '@xspect-build/cross-build'
117
+ *
118
+ * const env = getCrossCompileEnv({
119
+ * target: 'aarch64-unknown-linux-gnu',
120
+ * useNapiCross: true,
121
+ * })
122
+ * console.log(env)
123
+ * ```
124
+ */
125
+ export declare function getCrossCompileEnv(options?: CrossBuildOptions): Record<string, string>;
@@ -0,0 +1,2 @@
1
+ export declare function tryInstallCargoBinary(name: string, bin: string): void;
2
+ export declare function ensureRustTarget(target: string): void;
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { cac } from "cac";
4
+ import { execSync, spawn } from "node:child_process";
5
+ import { existsSync, mkdirSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { bgGreen, bgRed, bgYellow, black, green, red, white } from "colorette";
9
+ import { createDebug } from "obug";
10
+ const debugFactory = (namespace)=>{
11
+ const debug = createDebug(`cross-build:${namespace}`, {
12
+ formatters: {
13
+ i (v) {
14
+ return green(v);
15
+ }
16
+ }
17
+ });
18
+ debug.info = (...args)=>console.error(black(bgGreen(' INFO ')), ...args);
19
+ debug.warn = (...args)=>console.error(black(bgYellow(' WARNING ')), ...args);
20
+ debug.error = (...args)=>console.error(white(bgRed(' ERROR ')), ...args.map((arg)=>arg instanceof Error ? arg.stack ?? arg.message : arg));
21
+ return debug;
22
+ };
23
+ const log_debug = debugFactory('core');
24
+ function tryInstallCargoBinary(name, bin) {
25
+ if (detectCargoBinary(bin)) return void log_debug('Cargo binary already installed: %s', name);
26
+ try {
27
+ log_debug('Installing cargo binary: %s', name);
28
+ execSync(`cargo install ${name}`, {
29
+ stdio: 'inherit'
30
+ });
31
+ } catch (e) {
32
+ throw new Error(`Failed to install cargo binary: ${name}`, {
33
+ cause: e
34
+ });
35
+ }
36
+ }
37
+ function detectCargoBinary(bin) {
38
+ log_debug('Detecting cargo binary: %s', bin);
39
+ try {
40
+ execSync(`cargo help ${bin}`, {
41
+ stdio: 'ignore'
42
+ });
43
+ log_debug('Cargo binary detected: %s', bin);
44
+ return true;
45
+ } catch {
46
+ log_debug('Cargo binary not detected: %s', bin);
47
+ return false;
48
+ }
49
+ }
50
+ function ensureRustTarget(target) {
51
+ try {
52
+ const installedTargets = execSync('rustup target list --installed', {
53
+ encoding: 'utf8'
54
+ });
55
+ if (installedTargets.includes(target)) return;
56
+ log_debug('Installing rust target: %s', target);
57
+ execSync(`rustup target add ${target}`, {
58
+ stdio: 'inherit'
59
+ });
60
+ } catch (e) {
61
+ log_debug.warn(`Failed to check or install rust target ${target}. Make sure rustup is installed and available in PATH.`, e);
62
+ }
63
+ }
64
+ const SUB_SYSTEMS = new Set([
65
+ 'android',
66
+ 'ohos'
67
+ ]);
68
+ const CpuToNodeArch = {
69
+ x86_64: 'x64',
70
+ aarch64: 'arm64',
71
+ i686: 'ia32',
72
+ armv7: 'arm',
73
+ loongarch64: 'loong64',
74
+ riscv64gc: 'riscv64',
75
+ powerpc64le: 'ppc64'
76
+ };
77
+ const SysToNodePlatform = {
78
+ linux: 'linux',
79
+ freebsd: 'freebsd',
80
+ darwin: 'darwin',
81
+ windows: 'win32',
82
+ ohos: 'openharmony'
83
+ };
84
+ const TARGET_LINKER = {
85
+ 'aarch64-unknown-linux-musl': 'aarch64-linux-musl-gcc',
86
+ 'loongarch64-unknown-linux-gnu': 'loongarch64-linux-gnu-gcc-13',
87
+ 'riscv64gc-unknown-linux-gnu': 'riscv64-linux-gnu-gcc',
88
+ 'powerpc64le-unknown-linux-gnu': 'powerpc64le-linux-gnu-gcc',
89
+ 's390x-unknown-linux-gnu': 's390x-linux-gnu-gcc'
90
+ };
91
+ function parseTriple(rawTriple) {
92
+ if ('wasm32-wasi' === rawTriple || 'wasm32-wasi-preview1-threads' === rawTriple || rawTriple.startsWith('wasm32-wasip')) return {
93
+ triple: rawTriple,
94
+ platformArchABI: 'wasm32-wasi',
95
+ platform: 'wasi',
96
+ arch: 'wasm32',
97
+ abi: 'wasi'
98
+ };
99
+ const triple = rawTriple.endsWith('eabi') ? `${rawTriple.slice(0, -4)}-eabi` : rawTriple;
100
+ const triples = triple.split('-');
101
+ let cpu;
102
+ let sys;
103
+ let abi = null;
104
+ if (2 === triples.length) [cpu, sys] = triples;
105
+ else [cpu, , sys, abi = null] = triples;
106
+ if (abi && SUB_SYSTEMS.has(abi)) {
107
+ sys = abi;
108
+ abi = null;
109
+ }
110
+ const platform = SysToNodePlatform[sys] ?? sys;
111
+ const arch = CpuToNodeArch[cpu] ?? cpu;
112
+ return {
113
+ triple: rawTriple,
114
+ platformArchABI: abi ? `${platform}-${arch}-${abi}` : `${platform}-${arch}`,
115
+ platform,
116
+ arch,
117
+ abi
118
+ };
119
+ }
120
+ function getSystemDefaultTarget() {
121
+ const host = execSync("rustc -vV", {
122
+ env: process.env
123
+ }).toString('utf8').split('\n').find((line)=>line.startsWith('host: '));
124
+ const triple = host?.slice('host: '.length);
125
+ if (!triple) throw new TypeError("Can not parse target triple from host");
126
+ return parseTriple(triple);
127
+ }
128
+ function getTargetLinker(target) {
129
+ return TARGET_LINKER[target];
130
+ }
131
+ function targetToEnvVar(target) {
132
+ return target.replace(/-/g, '_').toUpperCase();
133
+ }
134
+ const build_debug = debugFactory('build');
135
+ const build_require = createRequire(import.meta.url);
136
+ async function build(options = {}) {
137
+ const builder = new CrossBuilder(options);
138
+ return builder.build();
139
+ }
140
+ class CrossBuilder {
141
+ options;
142
+ args = [];
143
+ envs = {};
144
+ target;
145
+ cwd;
146
+ constructor(options){
147
+ this.options = options;
148
+ this.target = options.target ? parseTriple(options.target) : process.env.CARGO_BUILD_TARGET ? parseTriple(process.env.CARGO_BUILD_TARGET) : getSystemDefaultTarget();
149
+ this.cwd = options.cwd ?? process.cwd();
150
+ }
151
+ getEnvs() {
152
+ this.pickCrossToolchain();
153
+ this.setEnvs();
154
+ return {
155
+ ...this.envs
156
+ };
157
+ }
158
+ async build() {
159
+ this.pickBinary().setPackage().setFeatures().setTarget().ensureTarget().pickCrossToolchain().setEnvs().setBypassArgs();
160
+ return this.exec();
161
+ }
162
+ ensureTarget() {
163
+ if (this.target.triple) ensureRustTarget(this.target.triple);
164
+ return this;
165
+ }
166
+ pickCrossToolchain() {
167
+ if (false === this.options.useNapiCrossToolChain) return this;
168
+ if (!this.target.triple.includes('linux')) {
169
+ if (true === this.options.useNapiCrossToolChain) build_debug.warn(`Skipping @napi-rs/cross-toolchain because target ${this.target.triple} is not Linux.`);
170
+ return this;
171
+ }
172
+ if (this.options.useCross) build_debug.warn('You are trying to use both `useCross` and `useNapiCross` options, `useCross` will be ignored.');
173
+ if (this.options.crossCompile) build_debug.warn('You are trying to use both `crossCompile` and `useNapiCross` options, `crossCompile` will be ignored.');
174
+ try {
175
+ const { version, download } = build_require('@napi-rs/cross-toolchain');
176
+ const alias = {
177
+ 's390x-unknown-linux-gnu': 's390x-ibm-linux-gnu'
178
+ };
179
+ const toolchainPath = join(homedir(), '.napi-rs', 'cross-toolchain', version, this.target.triple);
180
+ mkdirSync(toolchainPath, {
181
+ recursive: true
182
+ });
183
+ if (existsSync(join(toolchainPath, 'package.json'))) build_debug(`Toolchain ${toolchainPath} exists, skip extracting`);
184
+ else {
185
+ const tarArchive = download(process.arch, this.target.triple);
186
+ tarArchive.unpack(toolchainPath);
187
+ }
188
+ const upperCaseTarget = targetToEnvVar(this.target.triple);
189
+ const crossTargetName = alias[this.target.triple] ?? this.target.triple;
190
+ const linkerEnv = `CARGO_TARGET_${upperCaseTarget}_LINKER`;
191
+ this.setEnvIfNotExists(linkerEnv, join(toolchainPath, 'bin', `${crossTargetName}-gcc`));
192
+ this.setEnvIfNotExists('TARGET_SYSROOT', join(toolchainPath, crossTargetName, 'sysroot'));
193
+ this.setEnvIfNotExists('TARGET_AR', join(toolchainPath, 'bin', `${crossTargetName}-ar`));
194
+ this.setEnvIfNotExists('TARGET_RANLIB', join(toolchainPath, 'bin', `${crossTargetName}-ranlib`));
195
+ this.setEnvIfNotExists('TARGET_READELF', join(toolchainPath, 'bin', `${crossTargetName}-readelf`));
196
+ this.setEnvIfNotExists('TARGET_C_INCLUDE_PATH', join(toolchainPath, crossTargetName, 'sysroot', 'usr', 'include/'));
197
+ this.setEnvIfNotExists('TARGET_CC', join(toolchainPath, 'bin', `${crossTargetName}-gcc`));
198
+ this.setEnvIfNotExists('TARGET_CXX', join(toolchainPath, 'bin', `${crossTargetName}-g++`));
199
+ this.envs[`CXX_${upperCaseTarget}`] = join(toolchainPath, 'bin', `${crossTargetName}-g++`);
200
+ const snakeCaseTarget = this.target.triple.replace(/-/g, '_');
201
+ this.envs[`CC_${snakeCaseTarget}`] = join(toolchainPath, 'bin', `${crossTargetName}-gcc`);
202
+ this.envs[`CXX_${snakeCaseTarget}`] = join(toolchainPath, 'bin', `${crossTargetName}-g++`);
203
+ this.envs[`AR_${snakeCaseTarget}`] = join(toolchainPath, 'bin', `${crossTargetName}-ar`);
204
+ this.setEnvIfNotExists('BINDGEN_EXTRA_CLANG_ARGS', `--sysroot=${this.envs.TARGET_SYSROOT}`);
205
+ if (process.env.TARGET_CC?.startsWith('clang') || process.env.CC?.startsWith('clang') && !process.env.TARGET_CC) {
206
+ const TARGET_CFLAGS = process.env.TARGET_CFLAGS ?? '';
207
+ this.envs.TARGET_CFLAGS = `--sysroot=${this.envs.TARGET_SYSROOT} --gcc-toolchain=${toolchainPath} ${TARGET_CFLAGS}`;
208
+ }
209
+ if (process.env.CXX?.startsWith('clang++') && !process.env.TARGET_CXX || process.env.TARGET_CXX?.startsWith('clang++')) {
210
+ const TARGET_CXXFLAGS = process.env.TARGET_CXXFLAGS ?? '';
211
+ this.envs.TARGET_CXXFLAGS = `--sysroot=${this.envs.TARGET_SYSROOT} --gcc-toolchain=${toolchainPath} ${TARGET_CXXFLAGS}`;
212
+ }
213
+ this.envs.PATH = this.envs.PATH ? `${toolchainPath}/bin:${this.envs.PATH}:${process.env.PATH}` : `${toolchainPath}/bin:${process.env.PATH}`;
214
+ } catch (e) {
215
+ build_debug.warn('Pick cross toolchain failed', e);
216
+ }
217
+ return this;
218
+ }
219
+ pickBinary() {
220
+ if (this.options.crossCompile) if ('win32' === this.target.platform) if ('win32' === process.platform) build_debug.warn('You are trying to cross compile to win32 platform on win32 platform which is unnecessary.');
221
+ else {
222
+ build_debug('Use %i', 'cargo-xwin');
223
+ tryInstallCargoBinary('cargo-xwin', 'xwin');
224
+ this.args.push('xwin', 'build');
225
+ if ('ia32' === this.target.arch) this.envs.XWIN_ARCH = 'x86';
226
+ return this;
227
+ }
228
+ else if ('linux' === this.target.platform && 'linux' === process.platform && this.target.arch === process.arch && function(abi) {
229
+ const glibcVersionRuntime = process.report?.getReport()?.header?.glibcVersionRuntime;
230
+ const libc = glibcVersionRuntime ? 'gnu' : 'musl';
231
+ return abi === libc;
232
+ }(this.target.abi)) build_debug.warn('You are trying to cross compile to linux target on linux platform which is unnecessary.');
233
+ else if ('darwin' === this.target.platform && 'darwin' === process.platform) build_debug.warn('You are trying to cross compile to darwin target on darwin platform which is unnecessary.');
234
+ else {
235
+ build_debug('Use %i', 'cargo-zigbuild');
236
+ tryInstallCargoBinary('cargo-zigbuild', 'zigbuild');
237
+ this.args.push('zigbuild');
238
+ return this;
239
+ }
240
+ this.args.push('build');
241
+ return this;
242
+ }
243
+ setPackage() {
244
+ const args = [];
245
+ if (this.options.package) args.push('--package', this.options.package);
246
+ if (this.options.bin) args.push('--bin', this.options.bin);
247
+ if (args.length) {
248
+ build_debug('Set package flags: ');
249
+ build_debug(' %O', args);
250
+ this.args.push(...args);
251
+ }
252
+ return this;
253
+ }
254
+ setTarget() {
255
+ build_debug('Set compiling target to: ');
256
+ build_debug(' %i', this.target.triple);
257
+ this.args.push('--target', this.target.triple);
258
+ return this;
259
+ }
260
+ setEnvs() {
261
+ let rustflags = process.env.RUSTFLAGS ?? process.env.CARGO_BUILD_RUSTFLAGS ?? '';
262
+ if (this.target.abi?.includes('musl') && !rustflags.includes('target-feature=-crt-static')) rustflags += ' -C target-feature=-crt-static';
263
+ if (this.options.strip && !rustflags.includes('link-arg=-s')) rustflags += ' -C link-arg=-s';
264
+ if (rustflags.length) this.envs.RUSTFLAGS = rustflags;
265
+ const linker = this.options.crossCompile ? void 0 : getTargetLinker(this.target.triple);
266
+ const linkerEnv = `CARGO_TARGET_${targetToEnvVar(this.target.triple)}_LINKER`;
267
+ if (linker && !process.env[linkerEnv] && !this.envs[linkerEnv]) this.envs[linkerEnv] = linker;
268
+ if ('android' === this.target.platform) this.setAndroidEnv();
269
+ if ('wasi' === this.target.platform) this.setWasiEnv();
270
+ if ('openharmony' === this.target.platform) this.setOpenHarmonyEnv();
271
+ if (this.options.env) Object.assign(this.envs, this.options.env);
272
+ build_debug('Set envs: ');
273
+ Object.entries(this.envs).forEach(([k, v])=>{
274
+ build_debug(' %i', `${k}=${v}`);
275
+ });
276
+ return this;
277
+ }
278
+ setAndroidEnv() {
279
+ const { ANDROID_NDK_LATEST_HOME } = process.env;
280
+ if (!ANDROID_NDK_LATEST_HOME) build_debug.warn(`${red('ANDROID_NDK_LATEST_HOME')} environment variable is missing`);
281
+ if ('android' === process.platform) return;
282
+ const targetArch = 'arm' === this.target.arch ? 'armv7a' : 'aarch64';
283
+ const targetPlatform = 'arm' === this.target.arch ? 'androideabi24' : 'android24';
284
+ const hostPlatform = 'darwin' === process.platform ? 'darwin' : 'win32' === process.platform ? 'windows' : 'linux';
285
+ Object.assign(this.envs, {
286
+ CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-android24-clang`,
287
+ CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-androideabi24-clang`,
288
+ TARGET_CC: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang`,
289
+ TARGET_CXX: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang++`,
290
+ TARGET_AR: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/llvm-ar`,
291
+ TARGET_RANLIB: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/llvm-ranlib`,
292
+ ANDROID_NDK: ANDROID_NDK_LATEST_HOME,
293
+ PATH: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin${'win32' === process.platform ? ';' : ':'}${process.env.PATH}`
294
+ });
295
+ }
296
+ setWasiEnv() {
297
+ const { WASI_SDK_PATH } = process.env;
298
+ if (WASI_SDK_PATH && existsSync(WASI_SDK_PATH)) {
299
+ this.envs.CARGO_TARGET_WASM32_WASI_PREVIEW1_THREADS_LINKER = join(WASI_SDK_PATH, 'bin', 'wasm-ld');
300
+ this.envs.CARGO_TARGET_WASM32_WASIP1_LINKER = join(WASI_SDK_PATH, 'bin', 'wasm-ld');
301
+ this.envs.CARGO_TARGET_WASM32_WASIP1_THREADS_LINKER = join(WASI_SDK_PATH, 'bin', 'wasm-ld');
302
+ this.envs.CARGO_TARGET_WASM32_WASIP2_LINKER = join(WASI_SDK_PATH, 'bin', 'wasm-ld');
303
+ this.setEnvIfNotExists('TARGET_CC', join(WASI_SDK_PATH, 'bin', 'clang'));
304
+ this.setEnvIfNotExists('TARGET_CXX', join(WASI_SDK_PATH, 'bin', 'clang++'));
305
+ this.setEnvIfNotExists('TARGET_AR', join(WASI_SDK_PATH, 'bin', 'ar'));
306
+ this.setEnvIfNotExists('TARGET_RANLIB', join(WASI_SDK_PATH, 'bin', 'ranlib'));
307
+ this.setEnvIfNotExists('TARGET_CFLAGS', `--target=wasm32-wasi-threads --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj`);
308
+ this.setEnvIfNotExists('TARGET_CXXFLAGS', `--target=wasm32-wasi-threads --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj`);
309
+ this.setEnvIfNotExists("TARGET_LDFLAGS", `-fuse-ld=${WASI_SDK_PATH}/bin/wasm-ld --target=wasm32-wasi-threads`);
310
+ }
311
+ }
312
+ setOpenHarmonyEnv() {
313
+ const { OHOS_SDK_PATH, OHOS_SDK_NATIVE } = process.env;
314
+ const ndkPath = OHOS_SDK_PATH ? `${OHOS_SDK_PATH}/native` : OHOS_SDK_NATIVE;
315
+ if (!ndkPath && 'openharmony' !== process.platform) return void build_debug.warn(`${red('OHOS_SDK_PATH')} or ${red('OHOS_SDK_NATIVE')} environment variable is missing`);
316
+ const linkerName = `CARGO_TARGET_${this.target.triple.toUpperCase().replace(/-/g, '_')}_LINKER`;
317
+ const ranPath = `${ndkPath}/llvm/bin/llvm-ranlib`;
318
+ const arPath = `${ndkPath}/llvm/bin/llvm-ar`;
319
+ const ccPath = `${ndkPath}/llvm/bin/${this.target.triple}-clang`;
320
+ const cxxPath = `${ndkPath}/llvm/bin/${this.target.triple}-clang++`;
321
+ const asPath = `${ndkPath}/llvm/bin/llvm-as`;
322
+ const ldPath = `${ndkPath}/llvm/bin/ld.lld`;
323
+ const stripPath = `${ndkPath}/llvm/bin/llvm-strip`;
324
+ const objDumpPath = `${ndkPath}/llvm/bin/llvm-objdump`;
325
+ const objCopyPath = `${ndkPath}/llvm/bin/llvm-objcopy`;
326
+ const nmPath = `${ndkPath}/llvm/bin/llvm-nm`;
327
+ const binPath = `${ndkPath}/llvm/bin`;
328
+ const libPath = `${ndkPath}/llvm/lib`;
329
+ this.setEnvIfNotExists('LIBCLANG_PATH', libPath);
330
+ this.setEnvIfNotExists('DEP_ATOMIC', 'clang_rt.builtins');
331
+ this.setEnvIfNotExists(linkerName, ccPath);
332
+ this.setEnvIfNotExists('TARGET_CC', ccPath);
333
+ this.setEnvIfNotExists('TARGET_CXX', cxxPath);
334
+ this.setEnvIfNotExists('TARGET_AR', arPath);
335
+ this.setEnvIfNotExists('TARGET_RANLIB', ranPath);
336
+ this.setEnvIfNotExists('TARGET_AS', asPath);
337
+ this.setEnvIfNotExists('TARGET_LD', ldPath);
338
+ this.setEnvIfNotExists('TARGET_STRIP', stripPath);
339
+ this.setEnvIfNotExists('TARGET_OBJDUMP', objDumpPath);
340
+ this.setEnvIfNotExists('TARGET_OBJCOPY', objCopyPath);
341
+ this.setEnvIfNotExists('TARGET_NM', nmPath);
342
+ this.envs.PATH = `${binPath}${'win32' === process.platform ? ';' : ':'}${process.env.PATH}`;
343
+ }
344
+ setFeatures() {
345
+ const args = [];
346
+ if (this.options.allFeatures && this.options.noDefaultFeatures) throw new Error('Cannot specify --all-features and --no-default-features together');
347
+ if (this.options.allFeatures) args.push('--all-features');
348
+ else if (this.options.noDefaultFeatures) args.push('--no-default-features');
349
+ if (this.options.features) args.push('--features', ...this.options.features);
350
+ build_debug('Set features flags: ');
351
+ build_debug(' %O', args);
352
+ this.args.push(...args);
353
+ return this;
354
+ }
355
+ setBypassArgs() {
356
+ if (this.options.release) this.args.push('--release');
357
+ if (this.options.verbose) this.args.push('--verbose');
358
+ if (this.options.targetDir) this.args.push('--target-dir', this.options.targetDir);
359
+ if (this.options.profile) this.args.push('--profile', this.options.profile);
360
+ if (this.options.manifestPath) this.args.push('--manifest-path', this.options.manifestPath);
361
+ if (this.options.cargoArgs?.length) this.args.push(...this.options.cargoArgs);
362
+ return this;
363
+ }
364
+ async exec() {
365
+ build_debug('Start cross-building');
366
+ build_debug(' %i', `cargo ${this.args.join(' ')}`);
367
+ if (this.options.useCross && this.options.crossCompile) throw new Error('`useCross` and `crossCompile` cannot be used together');
368
+ const command = process.env.CARGO ?? (this.options.useCross ? 'cross' : 'cargo');
369
+ return new Promise((resolve, reject)=>{
370
+ const buildProcess = spawn(command, this.args, {
371
+ env: {
372
+ ...process.env,
373
+ ...this.envs
374
+ },
375
+ stdio: 'inherit',
376
+ cwd: this.cwd
377
+ });
378
+ buildProcess.once('exit', (code)=>{
379
+ if (0 === code) {
380
+ build_debug('%i', 'Build completed successfully!');
381
+ resolve({
382
+ success: true,
383
+ target: this.target,
384
+ envs: {
385
+ ...this.envs
386
+ }
387
+ });
388
+ } else reject(new Error(`Build failed with exit code ${code}`));
389
+ });
390
+ buildProcess.once('error', (e)=>{
391
+ reject(new Error(`Build failed with error: ${e.message}`, {
392
+ cause: e
393
+ }));
394
+ });
395
+ });
396
+ }
397
+ setEnvIfNotExists(env, value) {
398
+ if (!process.env[env]) this.envs[env] = value;
399
+ }
400
+ }
401
+ const cli_require = createRequire(import.meta.url);
402
+ const pkg = cli_require('../package.json');
403
+ const cli = cac('cross-build');
404
+ cli.command('[...args]', 'Build the project').option('--target <target>', 'Target triple (e.g., aarch64-unknown-linux-gnu)').option('--cwd <cwd>', 'Working directory').option('--manifest-path <path>', 'Path to Cargo.toml manifest file').option('--target-dir <dir>', 'Directory for build artifacts').option('--release', 'Build in release mode').option('--verbose', 'Show verbose output').option('--profile <profile>', 'Build profile (e.g., release, dev)').option('--use-napi-cross-toolchain', 'Use @napi-rs/cross-toolchain for Linux cross-compilation', {
405
+ default: true
406
+ }).option('--cross-compile', 'Use cargo-zigbuild/cargo-xwin for cross-compilation').option('--use-cross', 'Use cross-rs instead of cargo').option('--strip', 'Strip debug symbols').option('--package <package>', 'Package name in workspace').option('--bin <bin>', 'Binary name to build').option('--features <features>', 'Features to enable (comma separated)').option('--all-features', 'Enable all features').option('--no-default-features', 'Disable default features').option('--cargo-args <args>', 'Additional cargo arguments (comma separated)').option('--env <env>', 'Additional environment variables (key=value, comma separated)').action(async (_args, options)=>{
407
+ try {
408
+ const features = options.features ? options.features.split(',') : void 0;
409
+ const cargoArgs = options.cargoArgs ? options.cargoArgs.split(',') : void 0;
410
+ const env = options.env ? options.env.split(',').reduce((acc, curr)=>{
411
+ const [key, value] = curr.split('=');
412
+ if (key && value) acc[key] = value;
413
+ return acc;
414
+ }, {}) : void 0;
415
+ const result = await build({
416
+ ...options,
417
+ features,
418
+ cargoArgs,
419
+ env,
420
+ noDefaultFeatures: false === options.defaultFeatures ? true : void 0
421
+ });
422
+ if (!result.success) process.exit(1);
423
+ } catch (e) {
424
+ console.error(e);
425
+ process.exit(1);
426
+ }
427
+ });
428
+ cli.help();
429
+ cli.version(pkg.version);
430
+ cli.parse();
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @xspect-build/cross-build
3
+ *
4
+ * Cross-compile Rust projects without Docker
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { build, getCrossCompileEnv } from '@xspect-build/cross-build'
9
+ *
10
+ * // Cross-compile for Linux ARM64
11
+ * const result = await build({
12
+ * target: 'aarch64-unknown-linux-gnu',
13
+ * release: true,
14
+ * useNapiCross: true,
15
+ * })
16
+ *
17
+ * // Or just get the environment variables needed
18
+ * const env = getCrossCompileEnv({
19
+ * target: 'aarch64-unknown-linux-gnu',
20
+ * useNapiCross: true,
21
+ * })
22
+ * ```
23
+ */
24
+ export { build, getCrossCompileEnv, type CrossBuildOptions, type CrossBuildResult, } from './build';
25
+ export { parseTriple, getSystemDefaultTarget, getTargetLinker, targetToEnvVar, TARGET_LINKER, type Target, type Platform, } from './target.js';
26
+ export { tryInstallCargoBinary } from './cargo';
27
+ export { debugFactory, debug } from './log.js';
package/dist/index.js ADDED
@@ -0,0 +1,403 @@
1
+ import { execSync, spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { bgGreen, bgRed, bgYellow, black, green, red, white } from "colorette";
7
+ import { createDebug } from "obug";
8
+ const debugFactory = (namespace)=>{
9
+ const debug = createDebug(`cross-build:${namespace}`, {
10
+ formatters: {
11
+ i (v) {
12
+ return green(v);
13
+ }
14
+ }
15
+ });
16
+ debug.info = (...args)=>console.error(black(bgGreen(' INFO ')), ...args);
17
+ debug.warn = (...args)=>console.error(black(bgYellow(' WARNING ')), ...args);
18
+ debug.error = (...args)=>console.error(white(bgRed(' ERROR ')), ...args.map((arg)=>arg instanceof Error ? arg.stack ?? arg.message : arg));
19
+ return debug;
20
+ };
21
+ const log_debug = debugFactory('core');
22
+ function tryInstallCargoBinary(name, bin) {
23
+ if (detectCargoBinary(bin)) return void log_debug('Cargo binary already installed: %s', name);
24
+ try {
25
+ log_debug('Installing cargo binary: %s', name);
26
+ execSync(`cargo install ${name}`, {
27
+ stdio: 'inherit'
28
+ });
29
+ } catch (e) {
30
+ throw new Error(`Failed to install cargo binary: ${name}`, {
31
+ cause: e
32
+ });
33
+ }
34
+ }
35
+ function detectCargoBinary(bin) {
36
+ log_debug('Detecting cargo binary: %s', bin);
37
+ try {
38
+ execSync(`cargo help ${bin}`, {
39
+ stdio: 'ignore'
40
+ });
41
+ log_debug('Cargo binary detected: %s', bin);
42
+ return true;
43
+ } catch {
44
+ log_debug('Cargo binary not detected: %s', bin);
45
+ return false;
46
+ }
47
+ }
48
+ function ensureRustTarget(target) {
49
+ try {
50
+ const installedTargets = execSync('rustup target list --installed', {
51
+ encoding: 'utf8'
52
+ });
53
+ if (installedTargets.includes(target)) return;
54
+ log_debug('Installing rust target: %s', target);
55
+ execSync(`rustup target add ${target}`, {
56
+ stdio: 'inherit'
57
+ });
58
+ } catch (e) {
59
+ log_debug.warn(`Failed to check or install rust target ${target}. Make sure rustup is installed and available in PATH.`, e);
60
+ }
61
+ }
62
+ const SUB_SYSTEMS = new Set([
63
+ 'android',
64
+ 'ohos'
65
+ ]);
66
+ const CpuToNodeArch = {
67
+ x86_64: 'x64',
68
+ aarch64: 'arm64',
69
+ i686: 'ia32',
70
+ armv7: 'arm',
71
+ loongarch64: 'loong64',
72
+ riscv64gc: 'riscv64',
73
+ powerpc64le: 'ppc64'
74
+ };
75
+ const SysToNodePlatform = {
76
+ linux: 'linux',
77
+ freebsd: 'freebsd',
78
+ darwin: 'darwin',
79
+ windows: 'win32',
80
+ ohos: 'openharmony'
81
+ };
82
+ const TARGET_LINKER = {
83
+ 'aarch64-unknown-linux-musl': 'aarch64-linux-musl-gcc',
84
+ 'loongarch64-unknown-linux-gnu': 'loongarch64-linux-gnu-gcc-13',
85
+ 'riscv64gc-unknown-linux-gnu': 'riscv64-linux-gnu-gcc',
86
+ 'powerpc64le-unknown-linux-gnu': 'powerpc64le-linux-gnu-gcc',
87
+ 's390x-unknown-linux-gnu': 's390x-linux-gnu-gcc'
88
+ };
89
+ function parseTriple(rawTriple) {
90
+ if ('wasm32-wasi' === rawTriple || 'wasm32-wasi-preview1-threads' === rawTriple || rawTriple.startsWith('wasm32-wasip')) return {
91
+ triple: rawTriple,
92
+ platformArchABI: 'wasm32-wasi',
93
+ platform: 'wasi',
94
+ arch: 'wasm32',
95
+ abi: 'wasi'
96
+ };
97
+ const triple = rawTriple.endsWith('eabi') ? `${rawTriple.slice(0, -4)}-eabi` : rawTriple;
98
+ const triples = triple.split('-');
99
+ let cpu;
100
+ let sys;
101
+ let abi = null;
102
+ if (2 === triples.length) [cpu, sys] = triples;
103
+ else [cpu, , sys, abi = null] = triples;
104
+ if (abi && SUB_SYSTEMS.has(abi)) {
105
+ sys = abi;
106
+ abi = null;
107
+ }
108
+ const platform = SysToNodePlatform[sys] ?? sys;
109
+ const arch = CpuToNodeArch[cpu] ?? cpu;
110
+ return {
111
+ triple: rawTriple,
112
+ platformArchABI: abi ? `${platform}-${arch}-${abi}` : `${platform}-${arch}`,
113
+ platform,
114
+ arch,
115
+ abi
116
+ };
117
+ }
118
+ function getSystemDefaultTarget() {
119
+ const host = execSync("rustc -vV", {
120
+ env: process.env
121
+ }).toString('utf8').split('\n').find((line)=>line.startsWith('host: '));
122
+ const triple = host?.slice('host: '.length);
123
+ if (!triple) throw new TypeError("Can not parse target triple from host");
124
+ return parseTriple(triple);
125
+ }
126
+ function getTargetLinker(target) {
127
+ return TARGET_LINKER[target];
128
+ }
129
+ function targetToEnvVar(target) {
130
+ return target.replace(/-/g, '_').toUpperCase();
131
+ }
132
+ const build_debug = debugFactory('build');
133
+ const build_require = createRequire(import.meta.url);
134
+ async function build(options = {}) {
135
+ const builder = new CrossBuilder(options);
136
+ return builder.build();
137
+ }
138
+ function getCrossCompileEnv(options = {}) {
139
+ const builder = new CrossBuilder(options);
140
+ return builder.getEnvs();
141
+ }
142
+ class CrossBuilder {
143
+ options;
144
+ args = [];
145
+ envs = {};
146
+ target;
147
+ cwd;
148
+ constructor(options){
149
+ this.options = options;
150
+ this.target = options.target ? parseTriple(options.target) : process.env.CARGO_BUILD_TARGET ? parseTriple(process.env.CARGO_BUILD_TARGET) : getSystemDefaultTarget();
151
+ this.cwd = options.cwd ?? process.cwd();
152
+ }
153
+ getEnvs() {
154
+ this.pickCrossToolchain();
155
+ this.setEnvs();
156
+ return {
157
+ ...this.envs
158
+ };
159
+ }
160
+ async build() {
161
+ this.pickBinary().setPackage().setFeatures().setTarget().ensureTarget().pickCrossToolchain().setEnvs().setBypassArgs();
162
+ return this.exec();
163
+ }
164
+ ensureTarget() {
165
+ if (this.target.triple) ensureRustTarget(this.target.triple);
166
+ return this;
167
+ }
168
+ pickCrossToolchain() {
169
+ if (false === this.options.useNapiCrossToolChain) return this;
170
+ if (!this.target.triple.includes('linux')) {
171
+ if (true === this.options.useNapiCrossToolChain) build_debug.warn(`Skipping @napi-rs/cross-toolchain because target ${this.target.triple} is not Linux.`);
172
+ return this;
173
+ }
174
+ if (this.options.useCross) build_debug.warn('You are trying to use both `useCross` and `useNapiCross` options, `useCross` will be ignored.');
175
+ if (this.options.crossCompile) build_debug.warn('You are trying to use both `crossCompile` and `useNapiCross` options, `crossCompile` will be ignored.');
176
+ try {
177
+ const { version, download } = build_require('@napi-rs/cross-toolchain');
178
+ const alias = {
179
+ 's390x-unknown-linux-gnu': 's390x-ibm-linux-gnu'
180
+ };
181
+ const toolchainPath = join(homedir(), '.napi-rs', 'cross-toolchain', version, this.target.triple);
182
+ mkdirSync(toolchainPath, {
183
+ recursive: true
184
+ });
185
+ if (existsSync(join(toolchainPath, 'package.json'))) build_debug(`Toolchain ${toolchainPath} exists, skip extracting`);
186
+ else {
187
+ const tarArchive = download(process.arch, this.target.triple);
188
+ tarArchive.unpack(toolchainPath);
189
+ }
190
+ const upperCaseTarget = targetToEnvVar(this.target.triple);
191
+ const crossTargetName = alias[this.target.triple] ?? this.target.triple;
192
+ const linkerEnv = `CARGO_TARGET_${upperCaseTarget}_LINKER`;
193
+ this.setEnvIfNotExists(linkerEnv, join(toolchainPath, 'bin', `${crossTargetName}-gcc`));
194
+ this.setEnvIfNotExists('TARGET_SYSROOT', join(toolchainPath, crossTargetName, 'sysroot'));
195
+ this.setEnvIfNotExists('TARGET_AR', join(toolchainPath, 'bin', `${crossTargetName}-ar`));
196
+ this.setEnvIfNotExists('TARGET_RANLIB', join(toolchainPath, 'bin', `${crossTargetName}-ranlib`));
197
+ this.setEnvIfNotExists('TARGET_READELF', join(toolchainPath, 'bin', `${crossTargetName}-readelf`));
198
+ this.setEnvIfNotExists('TARGET_C_INCLUDE_PATH', join(toolchainPath, crossTargetName, 'sysroot', 'usr', 'include/'));
199
+ this.setEnvIfNotExists('TARGET_CC', join(toolchainPath, 'bin', `${crossTargetName}-gcc`));
200
+ this.setEnvIfNotExists('TARGET_CXX', join(toolchainPath, 'bin', `${crossTargetName}-g++`));
201
+ this.envs[`CXX_${upperCaseTarget}`] = join(toolchainPath, 'bin', `${crossTargetName}-g++`);
202
+ const snakeCaseTarget = this.target.triple.replace(/-/g, '_');
203
+ this.envs[`CC_${snakeCaseTarget}`] = join(toolchainPath, 'bin', `${crossTargetName}-gcc`);
204
+ this.envs[`CXX_${snakeCaseTarget}`] = join(toolchainPath, 'bin', `${crossTargetName}-g++`);
205
+ this.envs[`AR_${snakeCaseTarget}`] = join(toolchainPath, 'bin', `${crossTargetName}-ar`);
206
+ this.setEnvIfNotExists('BINDGEN_EXTRA_CLANG_ARGS', `--sysroot=${this.envs.TARGET_SYSROOT}`);
207
+ if (process.env.TARGET_CC?.startsWith('clang') || process.env.CC?.startsWith('clang') && !process.env.TARGET_CC) {
208
+ const TARGET_CFLAGS = process.env.TARGET_CFLAGS ?? '';
209
+ this.envs.TARGET_CFLAGS = `--sysroot=${this.envs.TARGET_SYSROOT} --gcc-toolchain=${toolchainPath} ${TARGET_CFLAGS}`;
210
+ }
211
+ if (process.env.CXX?.startsWith('clang++') && !process.env.TARGET_CXX || process.env.TARGET_CXX?.startsWith('clang++')) {
212
+ const TARGET_CXXFLAGS = process.env.TARGET_CXXFLAGS ?? '';
213
+ this.envs.TARGET_CXXFLAGS = `--sysroot=${this.envs.TARGET_SYSROOT} --gcc-toolchain=${toolchainPath} ${TARGET_CXXFLAGS}`;
214
+ }
215
+ this.envs.PATH = this.envs.PATH ? `${toolchainPath}/bin:${this.envs.PATH}:${process.env.PATH}` : `${toolchainPath}/bin:${process.env.PATH}`;
216
+ } catch (e) {
217
+ build_debug.warn('Pick cross toolchain failed', e);
218
+ }
219
+ return this;
220
+ }
221
+ pickBinary() {
222
+ if (this.options.crossCompile) if ('win32' === this.target.platform) if ('win32' === process.platform) build_debug.warn('You are trying to cross compile to win32 platform on win32 platform which is unnecessary.');
223
+ else {
224
+ build_debug('Use %i', 'cargo-xwin');
225
+ tryInstallCargoBinary('cargo-xwin', 'xwin');
226
+ this.args.push('xwin', 'build');
227
+ if ('ia32' === this.target.arch) this.envs.XWIN_ARCH = 'x86';
228
+ return this;
229
+ }
230
+ else if ('linux' === this.target.platform && 'linux' === process.platform && this.target.arch === process.arch && function(abi) {
231
+ const glibcVersionRuntime = process.report?.getReport()?.header?.glibcVersionRuntime;
232
+ const libc = glibcVersionRuntime ? 'gnu' : 'musl';
233
+ return abi === libc;
234
+ }(this.target.abi)) build_debug.warn('You are trying to cross compile to linux target on linux platform which is unnecessary.');
235
+ else if ('darwin' === this.target.platform && 'darwin' === process.platform) build_debug.warn('You are trying to cross compile to darwin target on darwin platform which is unnecessary.');
236
+ else {
237
+ build_debug('Use %i', 'cargo-zigbuild');
238
+ tryInstallCargoBinary('cargo-zigbuild', 'zigbuild');
239
+ this.args.push('zigbuild');
240
+ return this;
241
+ }
242
+ this.args.push('build');
243
+ return this;
244
+ }
245
+ setPackage() {
246
+ const args = [];
247
+ if (this.options.package) args.push('--package', this.options.package);
248
+ if (this.options.bin) args.push('--bin', this.options.bin);
249
+ if (args.length) {
250
+ build_debug('Set package flags: ');
251
+ build_debug(' %O', args);
252
+ this.args.push(...args);
253
+ }
254
+ return this;
255
+ }
256
+ setTarget() {
257
+ build_debug('Set compiling target to: ');
258
+ build_debug(' %i', this.target.triple);
259
+ this.args.push('--target', this.target.triple);
260
+ return this;
261
+ }
262
+ setEnvs() {
263
+ let rustflags = process.env.RUSTFLAGS ?? process.env.CARGO_BUILD_RUSTFLAGS ?? '';
264
+ if (this.target.abi?.includes('musl') && !rustflags.includes('target-feature=-crt-static')) rustflags += ' -C target-feature=-crt-static';
265
+ if (this.options.strip && !rustflags.includes('link-arg=-s')) rustflags += ' -C link-arg=-s';
266
+ if (rustflags.length) this.envs.RUSTFLAGS = rustflags;
267
+ const linker = this.options.crossCompile ? void 0 : getTargetLinker(this.target.triple);
268
+ const linkerEnv = `CARGO_TARGET_${targetToEnvVar(this.target.triple)}_LINKER`;
269
+ if (linker && !process.env[linkerEnv] && !this.envs[linkerEnv]) this.envs[linkerEnv] = linker;
270
+ if ('android' === this.target.platform) this.setAndroidEnv();
271
+ if ('wasi' === this.target.platform) this.setWasiEnv();
272
+ if ('openharmony' === this.target.platform) this.setOpenHarmonyEnv();
273
+ if (this.options.env) Object.assign(this.envs, this.options.env);
274
+ build_debug('Set envs: ');
275
+ Object.entries(this.envs).forEach(([k, v])=>{
276
+ build_debug(' %i', `${k}=${v}`);
277
+ });
278
+ return this;
279
+ }
280
+ setAndroidEnv() {
281
+ const { ANDROID_NDK_LATEST_HOME } = process.env;
282
+ if (!ANDROID_NDK_LATEST_HOME) build_debug.warn(`${red('ANDROID_NDK_LATEST_HOME')} environment variable is missing`);
283
+ if ('android' === process.platform) return;
284
+ const targetArch = 'arm' === this.target.arch ? 'armv7a' : 'aarch64';
285
+ const targetPlatform = 'arm' === this.target.arch ? 'androideabi24' : 'android24';
286
+ const hostPlatform = 'darwin' === process.platform ? 'darwin' : 'win32' === process.platform ? 'windows' : 'linux';
287
+ Object.assign(this.envs, {
288
+ CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-android24-clang`,
289
+ CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-androideabi24-clang`,
290
+ TARGET_CC: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang`,
291
+ TARGET_CXX: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang++`,
292
+ TARGET_AR: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/llvm-ar`,
293
+ TARGET_RANLIB: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin/llvm-ranlib`,
294
+ ANDROID_NDK: ANDROID_NDK_LATEST_HOME,
295
+ PATH: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/${hostPlatform}-x86_64/bin${'win32' === process.platform ? ';' : ':'}${process.env.PATH}`
296
+ });
297
+ }
298
+ setWasiEnv() {
299
+ const { WASI_SDK_PATH } = process.env;
300
+ if (WASI_SDK_PATH && existsSync(WASI_SDK_PATH)) {
301
+ this.envs.CARGO_TARGET_WASM32_WASI_PREVIEW1_THREADS_LINKER = join(WASI_SDK_PATH, 'bin', 'wasm-ld');
302
+ this.envs.CARGO_TARGET_WASM32_WASIP1_LINKER = join(WASI_SDK_PATH, 'bin', 'wasm-ld');
303
+ this.envs.CARGO_TARGET_WASM32_WASIP1_THREADS_LINKER = join(WASI_SDK_PATH, 'bin', 'wasm-ld');
304
+ this.envs.CARGO_TARGET_WASM32_WASIP2_LINKER = join(WASI_SDK_PATH, 'bin', 'wasm-ld');
305
+ this.setEnvIfNotExists('TARGET_CC', join(WASI_SDK_PATH, 'bin', 'clang'));
306
+ this.setEnvIfNotExists('TARGET_CXX', join(WASI_SDK_PATH, 'bin', 'clang++'));
307
+ this.setEnvIfNotExists('TARGET_AR', join(WASI_SDK_PATH, 'bin', 'ar'));
308
+ this.setEnvIfNotExists('TARGET_RANLIB', join(WASI_SDK_PATH, 'bin', 'ranlib'));
309
+ this.setEnvIfNotExists('TARGET_CFLAGS', `--target=wasm32-wasi-threads --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj`);
310
+ this.setEnvIfNotExists('TARGET_CXXFLAGS', `--target=wasm32-wasi-threads --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj`);
311
+ this.setEnvIfNotExists("TARGET_LDFLAGS", `-fuse-ld=${WASI_SDK_PATH}/bin/wasm-ld --target=wasm32-wasi-threads`);
312
+ }
313
+ }
314
+ setOpenHarmonyEnv() {
315
+ const { OHOS_SDK_PATH, OHOS_SDK_NATIVE } = process.env;
316
+ const ndkPath = OHOS_SDK_PATH ? `${OHOS_SDK_PATH}/native` : OHOS_SDK_NATIVE;
317
+ if (!ndkPath && 'openharmony' !== process.platform) return void build_debug.warn(`${red('OHOS_SDK_PATH')} or ${red('OHOS_SDK_NATIVE')} environment variable is missing`);
318
+ const linkerName = `CARGO_TARGET_${this.target.triple.toUpperCase().replace(/-/g, '_')}_LINKER`;
319
+ const ranPath = `${ndkPath}/llvm/bin/llvm-ranlib`;
320
+ const arPath = `${ndkPath}/llvm/bin/llvm-ar`;
321
+ const ccPath = `${ndkPath}/llvm/bin/${this.target.triple}-clang`;
322
+ const cxxPath = `${ndkPath}/llvm/bin/${this.target.triple}-clang++`;
323
+ const asPath = `${ndkPath}/llvm/bin/llvm-as`;
324
+ const ldPath = `${ndkPath}/llvm/bin/ld.lld`;
325
+ const stripPath = `${ndkPath}/llvm/bin/llvm-strip`;
326
+ const objDumpPath = `${ndkPath}/llvm/bin/llvm-objdump`;
327
+ const objCopyPath = `${ndkPath}/llvm/bin/llvm-objcopy`;
328
+ const nmPath = `${ndkPath}/llvm/bin/llvm-nm`;
329
+ const binPath = `${ndkPath}/llvm/bin`;
330
+ const libPath = `${ndkPath}/llvm/lib`;
331
+ this.setEnvIfNotExists('LIBCLANG_PATH', libPath);
332
+ this.setEnvIfNotExists('DEP_ATOMIC', 'clang_rt.builtins');
333
+ this.setEnvIfNotExists(linkerName, ccPath);
334
+ this.setEnvIfNotExists('TARGET_CC', ccPath);
335
+ this.setEnvIfNotExists('TARGET_CXX', cxxPath);
336
+ this.setEnvIfNotExists('TARGET_AR', arPath);
337
+ this.setEnvIfNotExists('TARGET_RANLIB', ranPath);
338
+ this.setEnvIfNotExists('TARGET_AS', asPath);
339
+ this.setEnvIfNotExists('TARGET_LD', ldPath);
340
+ this.setEnvIfNotExists('TARGET_STRIP', stripPath);
341
+ this.setEnvIfNotExists('TARGET_OBJDUMP', objDumpPath);
342
+ this.setEnvIfNotExists('TARGET_OBJCOPY', objCopyPath);
343
+ this.setEnvIfNotExists('TARGET_NM', nmPath);
344
+ this.envs.PATH = `${binPath}${'win32' === process.platform ? ';' : ':'}${process.env.PATH}`;
345
+ }
346
+ setFeatures() {
347
+ const args = [];
348
+ if (this.options.allFeatures && this.options.noDefaultFeatures) throw new Error('Cannot specify --all-features and --no-default-features together');
349
+ if (this.options.allFeatures) args.push('--all-features');
350
+ else if (this.options.noDefaultFeatures) args.push('--no-default-features');
351
+ if (this.options.features) args.push('--features', ...this.options.features);
352
+ build_debug('Set features flags: ');
353
+ build_debug(' %O', args);
354
+ this.args.push(...args);
355
+ return this;
356
+ }
357
+ setBypassArgs() {
358
+ if (this.options.release) this.args.push('--release');
359
+ if (this.options.verbose) this.args.push('--verbose');
360
+ if (this.options.targetDir) this.args.push('--target-dir', this.options.targetDir);
361
+ if (this.options.profile) this.args.push('--profile', this.options.profile);
362
+ if (this.options.manifestPath) this.args.push('--manifest-path', this.options.manifestPath);
363
+ if (this.options.cargoArgs?.length) this.args.push(...this.options.cargoArgs);
364
+ return this;
365
+ }
366
+ async exec() {
367
+ build_debug('Start cross-building');
368
+ build_debug(' %i', `cargo ${this.args.join(' ')}`);
369
+ if (this.options.useCross && this.options.crossCompile) throw new Error('`useCross` and `crossCompile` cannot be used together');
370
+ const command = process.env.CARGO ?? (this.options.useCross ? 'cross' : 'cargo');
371
+ return new Promise((resolve, reject)=>{
372
+ const buildProcess = spawn(command, this.args, {
373
+ env: {
374
+ ...process.env,
375
+ ...this.envs
376
+ },
377
+ stdio: 'inherit',
378
+ cwd: this.cwd
379
+ });
380
+ buildProcess.once('exit', (code)=>{
381
+ if (0 === code) {
382
+ build_debug('%i', 'Build completed successfully!');
383
+ resolve({
384
+ success: true,
385
+ target: this.target,
386
+ envs: {
387
+ ...this.envs
388
+ }
389
+ });
390
+ } else reject(new Error(`Build failed with exit code ${code}`));
391
+ });
392
+ buildProcess.once('error', (e)=>{
393
+ reject(new Error(`Build failed with error: ${e.message}`, {
394
+ cause: e
395
+ }));
396
+ });
397
+ });
398
+ }
399
+ setEnvIfNotExists(env, value) {
400
+ if (!process.env[env]) this.envs[env] = value;
401
+ }
402
+ }
403
+ export { TARGET_LINKER, build, log_debug as debug, debugFactory, getCrossCompileEnv, getSystemDefaultTarget, getTargetLinker, parseTriple, targetToEnvVar, tryInstallCargoBinary };
package/dist/log.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ declare module 'obug' {
2
+ interface Debugger {
3
+ info: typeof console.error;
4
+ warn: typeof console.error;
5
+ error: typeof console.error;
6
+ }
7
+ }
8
+ export declare const debugFactory: (namespace: string) => import("obug").Debugger;
9
+ export declare const debug: import("obug").Debugger;
@@ -0,0 +1,25 @@
1
+ export type Platform = NodeJS.Platform | 'wasm' | 'wasi' | 'openharmony';
2
+ type NodeJSArch = 'arm' | 'arm64' | 'ia32' | 'loong64' | 'mips' | 'mipsel' | 'ppc' | 'ppc64' | 'riscv64' | 's390' | 's390x' | 'x32' | 'x64' | 'universal' | 'wasm32';
3
+ export declare const TARGET_LINKER: Record<string, string>;
4
+ export interface Target {
5
+ triple: string;
6
+ platformArchABI: string;
7
+ platform: Platform;
8
+ arch: NodeJSArch;
9
+ abi: string | null;
10
+ }
11
+ /**
12
+ * A triple is a specific format for specifying a target architecture.
13
+ * Triples may be referred to as a target triple which is the architecture for the artifact produced, and the host triple which is the architecture that the compiler is running on.
14
+ * The general format of the triple is `<arch><sub>-<vendor>-<sys>-<abi>` where:
15
+ * - `arch` = The base CPU architecture, for example `x86_64`, `i686`, `arm`, `thumb`, `mips`, etc.
16
+ * - `sub` = The CPU sub-architecture, for example `arm` has `v7`, `v7s`, `v5te`, etc.
17
+ * - `vendor` = The vendor, for example `unknown`, `apple`, `pc`, `nvidia`, etc.
18
+ * - `sys` = The system name, for example `linux`, `windows`, `darwin`, etc. none is typically used for bare-metal without an OS.
19
+ * - `abi` = The ABI, for example `gnu`, `android`, `eabi`, etc.
20
+ */
21
+ export declare function parseTriple(rawTriple: string): Target;
22
+ export declare function getSystemDefaultTarget(): Target;
23
+ export declare function getTargetLinker(target: string): string | undefined;
24
+ export declare function targetToEnvVar(target: string): string;
25
+ export {};
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@xspect-build/cross-build",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ }
10
+ },
11
+ "types": "./dist/index.d.ts",
12
+ "bin": {
13
+ "cross-build": "./dist/cli.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "rslib build",
20
+ "check": "biome check --write",
21
+ "dev": "rslib build --watch",
22
+ "format": "biome format --write",
23
+ "test": "rstest"
24
+ },
25
+ "devDependencies": {
26
+ "@biomejs/biome": "2.3.8",
27
+ "@rslib/core": "^0.18.6",
28
+ "@rstest/core": "^0.7.5",
29
+ "@types/node": "^24.10.4",
30
+ "typescript": "^5.9.3"
31
+ },
32
+ "dependencies": {
33
+ "@napi-rs/cross-toolchain": "^1.0.3",
34
+ "cac": "^6.7.14",
35
+ "colorette": "^2.0.20",
36
+ "obug": "^2.1.1"
37
+ }
38
+ }