@napi-rs/cli 3.8.6 → 3.9.1

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.
Files changed (34) hide show
  1. package/dist/cli.js +9085 -8737
  2. package/dist/index.cjs +9092 -8744
  3. package/dist/index.d.cts +37 -17
  4. package/dist/index.d.ts +37 -17
  5. package/dist/index.js +9085 -8737
  6. package/docs/build.md +36 -34
  7. package/package.json +4 -5
  8. package/src/api/__tests__/__snapshots__/templates.spec.ts.md +9 -13
  9. package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
  10. package/src/api/__tests__/build.spec.ts +284 -0
  11. package/src/api/__tests__/create-npm-dirs.spec.ts +4 -3
  12. package/src/api/__tests__/templates.spec.ts +84 -1
  13. package/src/api/build.ts +181 -7
  14. package/src/api/create-npm-dirs.ts +2 -1
  15. package/src/api/new.ts +13 -18
  16. package/src/api/pre-publish.ts +5 -10
  17. package/src/api/rename.ts +10 -21
  18. package/src/api/templates/js-binding.ts +7 -9
  19. package/src/api/templates/load-wasi-template.ts +20 -17
  20. package/src/api/templates/wasi-worker-template.ts +36 -31
  21. package/src/commands/__tests__/build.spec.ts +24 -0
  22. package/src/commands/build.ts +1 -1
  23. package/src/def/build.ts +30 -4
  24. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +18 -28
  25. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
  26. package/src/utils/__tests__/misc.spec.ts +4 -0
  27. package/src/utils/__tests__/serialize.spec.ts +55 -0
  28. package/src/utils/__tests__/target.spec.ts +221 -0
  29. package/src/utils/__tests__/typegen.spec.ts +115 -0
  30. package/src/utils/index.ts +1 -0
  31. package/src/utils/misc.ts +4 -5
  32. package/src/utils/serialize.ts +47 -0
  33. package/src/utils/target.ts +150 -1
  34. package/src/utils/typegen.ts +241 -30
@@ -1,13 +1,49 @@
1
+ import { realpathSync } from 'node:fs'
2
+ import { chmod, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
1
3
  import os from 'os'
4
+ import { join } from 'node:path'
2
5
 
3
6
  import test from 'ava'
4
7
 
5
8
  import {
9
+ wasiLibcHasNewFutexAbi,
6
10
  parseTriple,
7
11
  getSystemDefaultTarget,
12
+ rustBundledWasiLibc,
13
+ wasiSdkMajorVersion,
8
14
  AVAILABLE_TARGETS,
9
15
  } from '../target.js'
10
16
 
17
+ async function withWasiSdkDir(
18
+ setup: (wasiSdkPath: string) => Promise<void>,
19
+ assertion: (wasiSdkPath: string) => void,
20
+ ) {
21
+ const wasiSdkPath = await mkdtemp(join(os.tmpdir(), 'napi-rs-wasi-sdk-'))
22
+ try {
23
+ await setup(wasiSdkPath)
24
+ assertion(wasiSdkPath)
25
+ } finally {
26
+ await rm(wasiSdkPath, { recursive: true, force: true })
27
+ }
28
+ }
29
+
30
+ async function writeVersionFile(wasiSdkPath: string, content: string) {
31
+ await writeFile(join(wasiSdkPath, 'VERSION'), content)
32
+ }
33
+
34
+ async function writeVersionHeader(wasiSdkPath: string, content: string) {
35
+ const headerDir = join(
36
+ wasiSdkPath,
37
+ 'share',
38
+ 'wasi-sysroot',
39
+ 'include',
40
+ 'wasm32-wasip1-threads',
41
+ 'wasi',
42
+ )
43
+ await mkdir(headerDir, { recursive: true })
44
+ await writeFile(join(headerDir, 'version.h'), content)
45
+ }
46
+
11
47
  test('should parse triple correctly', (t) => {
12
48
  t.snapshot(AVAILABLE_TARGETS.map(parseTriple))
13
49
  })
@@ -17,3 +53,188 @@ test('should get system default target correctly', (t) => {
17
53
 
18
54
  t.is(target.platform, os.platform())
19
55
  })
56
+
57
+ test('should read the wasi-sdk major version from VERSION', async (t) => {
58
+ await withWasiSdkDir(
59
+ (wasiSdkPath) => writeVersionFile(wasiSdkPath, '34.0\n'),
60
+ (wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 34),
61
+ )
62
+ })
63
+
64
+ test('should ignore the build suffix in VERSION', async (t) => {
65
+ await withWasiSdkDir(
66
+ (wasiSdkPath) => writeVersionFile(wasiSdkPath, '33.0+m\n'),
67
+ (wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 33),
68
+ )
69
+ })
70
+
71
+ test('should read a pre wasi-sdk 30 VERSION', async (t) => {
72
+ await withWasiSdkDir(
73
+ (wasiSdkPath) => writeVersionFile(wasiSdkPath, '27.0\n'),
74
+ (wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 27),
75
+ )
76
+ })
77
+
78
+ test('should fall back to the wasi/version.h header', async (t) => {
79
+ await withWasiSdkDir(
80
+ (wasiSdkPath) =>
81
+ writeVersionHeader(
82
+ wasiSdkPath,
83
+ '#define __wasi_sdk_major__ 30\n#define __wasi_sdk_minor__ 0\n',
84
+ ),
85
+ (wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 30),
86
+ )
87
+ })
88
+
89
+ test('should prefer VERSION over the wasi/version.h header', async (t) => {
90
+ await withWasiSdkDir(
91
+ async (wasiSdkPath) => {
92
+ await writeVersionFile(wasiSdkPath, '34.0\n')
93
+ await writeVersionHeader(wasiSdkPath, '#define __wasi_sdk_major__ 33\n')
94
+ },
95
+ (wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), 34),
96
+ )
97
+ })
98
+
99
+ test('should return null when no version signal exists', async (t) => {
100
+ await withWasiSdkDir(
101
+ () => Promise.resolve(),
102
+ (wasiSdkPath) => t.is(wasiSdkMajorVersion(wasiSdkPath), null),
103
+ )
104
+ })
105
+
106
+ test('should return null instead of throwing on a malformed VERSION', async (t) => {
107
+ await withWasiSdkDir(
108
+ (wasiSdkPath) => writeVersionFile(wasiSdkPath, 'not a version at all\n'),
109
+ (wasiSdkPath) => {
110
+ t.notThrows(() => wasiSdkMajorVersion(wasiSdkPath))
111
+ t.is(wasiSdkMajorVersion(wasiSdkPath), null)
112
+ },
113
+ )
114
+ })
115
+
116
+ test('should return null when the wasi-sdk path does not exist', (t) => {
117
+ t.is(wasiSdkMajorVersion(join(os.tmpdir(), 'napi-rs-missing-wasi-sdk')), null)
118
+ })
119
+
120
+ test('should detect the new futex ABI from a wasi-libc archive', async (t) => {
121
+ const dir = await mkdtemp(join(os.tmpdir(), 'napi-rs-wasi-libc-'))
122
+ try {
123
+ // wasi-libc moved the futex helpers into `futex.c` when it dropped the
124
+ // unused `int op` parameter. `__wait.c` survives either way, so only the
125
+ // presence of `futex.c` separates the two ABIs.
126
+ const newAbi = join(dir, 'libc-new.a')
127
+ const legacyAbi = join(dir, 'libc-legacy.a')
128
+ await writeFile(newAbi, '!<arch>\n__wait.c.obj\nfutex.c.obj\n')
129
+ await writeFile(
130
+ legacyAbi,
131
+ '!<arch>\n__wait.c.obj\n__wasilibc_busywait.c.obj\n',
132
+ )
133
+
134
+ t.true(wasiLibcHasNewFutexAbi(newAbi))
135
+ t.false(wasiLibcHasNewFutexAbi(legacyAbi))
136
+ } finally {
137
+ await rm(dir, { recursive: true, force: true })
138
+ }
139
+ })
140
+
141
+ test('should return null when the wasi-libc archive is unavailable', (t) => {
142
+ t.is(wasiLibcHasNewFutexAbi(null), null)
143
+ t.is(
144
+ wasiLibcHasNewFutexAbi(join(os.tmpdir(), 'napi-rs-missing-libc.a')),
145
+ null,
146
+ )
147
+ })
148
+
149
+ // A fake `rustc` must be an executable file, and `execFileSync` cannot run a
150
+ // Windows `.cmd` shim without a shell. The behaviour under test does not
151
+ // depend on the platform, so cover it on POSIX only.
152
+ const posixOnly = process.platform === 'win32' ? test.serial.skip : test.serial
153
+
154
+ /**
155
+ * Installs a `rustc` that reports its own working directory as the sysroot,
156
+ * so a test can tell which directory the probe ran in.
157
+ */
158
+ async function withFakeRustc(
159
+ envVar: 'RUSTC' | 'CARGO_BUILD_RUSTC',
160
+ body: (dir: string) => Promise<void> | void,
161
+ ) {
162
+ const dir = await mkdtemp(join(os.tmpdir(), 'napi-rs-fake-rustc-'))
163
+ const rustc = join(dir, 'rustc')
164
+ // `$PWD` comes from the inherited environment, so ask the kernel for the
165
+ // real working directory. `-P` also resolves the `/var` symlink on macOS.
166
+ await writeFile(rustc, '#!/bin/sh\necho "$(pwd -P)/sysroot"\n')
167
+ await chmod(rustc, 0o755)
168
+ const previous = process.env[envVar]
169
+ process.env[envVar] = rustc
170
+ try {
171
+ await body(dir)
172
+ } finally {
173
+ if (previous === undefined) {
174
+ delete process.env[envVar]
175
+ } else {
176
+ process.env[envVar] = previous
177
+ }
178
+ }
179
+ }
180
+
181
+ posixOnly('probes the Rust sysroot from the build cwd', async (t) => {
182
+ await withFakeRustc('RUSTC', async (dir) => {
183
+ const caller = join(dir, 'caller')
184
+ const project = join(dir, 'project')
185
+ await mkdir(caller)
186
+ await mkdir(project)
187
+
188
+ // `rustc` is a rustup shim, so the directory it runs in picks the
189
+ // toolchain. Cargo is spawned with the build cwd; the probe must match it.
190
+ t.is(
191
+ rustBundledWasiLibc('wasm32-wasip1-threads', project),
192
+ join(
193
+ realpathSync(project),
194
+ 'sysroot',
195
+ 'lib',
196
+ 'rustlib',
197
+ 'wasm32-wasip1-threads',
198
+ 'lib',
199
+ 'self-contained',
200
+ 'libc.a',
201
+ ),
202
+ )
203
+ t.not(
204
+ rustBundledWasiLibc('wasm32-wasip1-threads', caller),
205
+ rustBundledWasiLibc('wasm32-wasip1-threads', project),
206
+ )
207
+ })
208
+ })
209
+
210
+ posixOnly('falls back to CARGO_BUILD_RUSTC when RUSTC is unset', async (t) => {
211
+ const previousRustc = process.env.RUSTC
212
+ delete process.env.RUSTC
213
+ try {
214
+ await withFakeRustc('CARGO_BUILD_RUSTC', (dir) => {
215
+ t.true(
216
+ rustBundledWasiLibc('wasm32-wasip1-threads', dir)?.startsWith(
217
+ realpathSync(dir),
218
+ ),
219
+ )
220
+ })
221
+ } finally {
222
+ if (previousRustc !== undefined) {
223
+ process.env.RUSTC = previousRustc
224
+ }
225
+ }
226
+ })
227
+
228
+ posixOnly('returns null when the compiler cannot be run', (t) => {
229
+ const previous = process.env.RUSTC
230
+ process.env.RUSTC = join(os.tmpdir(), 'napi-rs-no-such-rustc')
231
+ try {
232
+ t.is(rustBundledWasiLibc('wasm32-wasip1-threads'), null)
233
+ } finally {
234
+ if (previous === undefined) {
235
+ delete process.env.RUSTC
236
+ } else {
237
+ process.env.RUSTC = previous
238
+ }
239
+ }
240
+ })
@@ -1,3 +1,5 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
1
3
  import { join } from 'path'
2
4
  import { fileURLToPath } from 'url'
3
5
 
@@ -5,6 +7,24 @@ import test from 'ava'
5
7
 
6
8
  import { correctStringIdent, processTypeDef } from '../typegen.js'
7
9
 
10
+ async function processInlineTypeDef(
11
+ lines: Array<Record<string, unknown>>,
12
+ constEnum = true,
13
+ runtimeStringEnum = false,
14
+ ) {
15
+ const dir = await mkdtemp(join(tmpdir(), 'napi-typegen-'))
16
+ const file = join(dir, 'defs')
17
+ await writeFile(
18
+ file,
19
+ lines.map((line) => JSON.stringify(line)).join('\n') + '\n',
20
+ )
21
+ try {
22
+ return await processTypeDef(file, constEnum, runtimeStringEnum)
23
+ } finally {
24
+ await rm(dir, { recursive: true, force: true })
25
+ }
26
+ }
27
+
8
28
  test('should ident string correctly', (t) => {
9
29
  const input = `
10
30
  /**
@@ -83,3 +103,98 @@ test('runtimeStringEnum is a no-op when constEnum is set', async (t) => {
83
103
 
84
104
  t.snapshot(dts)
85
105
  })
106
+
107
+ test('simple type aliases stay on one line', async (t) => {
108
+ const { dts } = await processInlineTypeDef([
109
+ { kind: 'type', name: 'CustomU32', def: 'number' },
110
+ ])
111
+ t.true(dts.includes('export type CustomU32 = number'))
112
+ t.false(/export type CustomU32 =\s*\n/.test(dts))
113
+ })
114
+
115
+ test('empty classes do not contain a blank body', async (t) => {
116
+ const { dts } = await processInlineTypeDef([
117
+ { kind: 'struct', name: 'Blake2BKey', def: '' },
118
+ ])
119
+ t.true(dts.includes('export declare class Blake2BKey {}'))
120
+ t.false(dts.includes('class Blake2BKey {\n\n}'))
121
+ })
122
+
123
+ test('no-const-enum string enums emit a spaced union', async (t) => {
124
+ const { dts } = await processTypeDef(flagFixture, false, false)
125
+ t.true(dts.includes("export type Status = 'Active' | 'Inactive'"))
126
+ t.false(dts.includes("'Active'|"))
127
+ })
128
+
129
+ test('no-const-enum string enums keep commas inside variant docs', async (t) => {
130
+ const { dts } = await processInlineTypeDef(
131
+ [
132
+ {
133
+ kind: 'string_enum',
134
+ name: 'Status',
135
+ def: "/** First, documented variant */\n Active = 'Active',\n Inactive = 'Inactive'",
136
+ },
137
+ ],
138
+ false,
139
+ false,
140
+ )
141
+ t.true(dts.includes("export type Status = 'Active' | 'Inactive'"))
142
+ t.false(dts.includes('/** First |'))
143
+ })
144
+
145
+ test('no-const-enum string enums keep Unicode variant names', async (t) => {
146
+ const { dts } = await processInlineTypeDef(
147
+ [
148
+ {
149
+ kind: 'string_enum',
150
+ name: 'Status',
151
+ def: "成功 = '成功',\n Failed = 'Failed'",
152
+ },
153
+ ],
154
+ false,
155
+ false,
156
+ )
157
+ t.true(dts.includes("export type Status = '成功' | 'Failed'"))
158
+ })
159
+
160
+ test('no-const-enum string enums keep quoted JS property names', async (t) => {
161
+ const { dts } = await processInlineTypeDef(
162
+ [
163
+ {
164
+ kind: 'string_enum',
165
+ name: 'Header',
166
+ def: "'content-type' = 'content-type',\n Plain = 'plain'",
167
+ },
168
+ ],
169
+ false,
170
+ false,
171
+ )
172
+ t.true(dts.includes("export type Header = 'content-type' | 'plain'"))
173
+ })
174
+
175
+ test('no-const-enum string enums keep commas and escapes inside values', async (t) => {
176
+ const { dts } = await processInlineTypeDef(
177
+ [
178
+ {
179
+ kind: 'string_enum',
180
+ name: 'Label',
181
+ def: "Csv = 'a,b',\n Quoted = 'say \\'hi\\'',\n Count = 2",
182
+ },
183
+ ],
184
+ false,
185
+ false,
186
+ )
187
+ t.true(dts.includes("export type Label = 'a,b' | 'say \\'hi\\'' | 2"))
188
+ })
189
+
190
+ test('correctStringIdent keeps nested object-type fields indented', (t) => {
191
+ const input = `export declare function bufferComplexOverride(value: {
192
+ Buffer(): "line"
193
+ mapped: { [Buffer in keyof T]: T[Buffer] }
194
+ external: Buffer
195
+ }): void`
196
+ const formatted = correctStringIdent(input, 0)
197
+ t.true(formatted.includes(' mapped: { [Buffer in keyof T]: T[Buffer] }'))
198
+ t.true(formatted.includes(' external: Buffer'))
199
+ t.false(formatted.includes('\nmapped:'))
200
+ })
@@ -1,5 +1,6 @@
1
1
  export * from './log.js'
2
2
  export * from './misc.js'
3
+ export * from './serialize.js'
3
4
  export * from './target.js'
4
5
  export * from './version.js'
5
6
  export * from './metadata.js'
package/src/utils/misc.ts CHANGED
@@ -44,6 +44,7 @@ import { setTimeout as delay } from 'node:timers/promises'
44
44
 
45
45
  import pkgJson from '../../package.json' with { type: 'json' }
46
46
  import { debug } from './log.js'
47
+ import { serializeJson } from './serialize.js'
47
48
 
48
49
  export const readFileAsync = readFile
49
50
  export const writeFileAsync = writeFile
@@ -132,7 +133,8 @@ interface ReconciliationReclaimState {
132
133
  }
133
134
 
134
135
  type ReconciliationMetadataOwner =
135
- ReconciliationLockOwner | ReconciliationReclaimOwner
136
+ | ReconciliationLockOwner
137
+ | ReconciliationReclaimOwner
136
138
 
137
139
  interface ReconciliationCandidateState {
138
140
  owner: ReconciliationMetadataOwner
@@ -6153,10 +6155,7 @@ export async function updatePackageJson(
6153
6155
  return
6154
6156
  }
6155
6157
  const old = JSON.parse(await readFileAsync(path, 'utf8'))
6156
- await writeFileAsync(
6157
- path,
6158
- JSON.stringify(mergePackageJson(old, partial), null, 2),
6159
- )
6158
+ await writeFileAsync(path, serializeJson(mergePackageJson(old, partial)))
6160
6159
  }
6161
6160
 
6162
6161
  export const CLI_VERSION = pkgJson.version
@@ -0,0 +1,47 @@
1
+ import { stringify as stringifyToml } from '@std/toml'
2
+ import { dump as yamlDump } from 'js-yaml'
3
+
4
+ /**
5
+ * Canonical JSON for files the CLI writes (package.json, napi config).
6
+ *
7
+ * Always 2-space indent and a POSIX trailing newline. Callers must not
8
+ * add another `\n` or omit one — every JSON writer uses this helper.
9
+ */
10
+ export function serializeJson(value: unknown): string {
11
+ return `${JSON.stringify(value, null, 2)}\n`
12
+ }
13
+
14
+ /**
15
+ * Canonical TOML for files the CLI rewrites (Cargo.toml).
16
+ *
17
+ * `@std/toml` stringify inserts a leading blank line and drops comments.
18
+ * We cannot preserve comments through parse→stringify. We only strip the
19
+ * leading blank and keep exactly one trailing newline — no regex rewrite
20
+ * of array contents, which would split commas inside quoted strings.
21
+ */
22
+ export function serializeToml(value: Record<string, unknown>): string {
23
+ let serialized = stringifyToml(value)
24
+ if (serialized.startsWith('\n')) {
25
+ serialized = serialized.slice(1)
26
+ }
27
+ if (!serialized.endsWith('\n')) {
28
+ serialized += '\n'
29
+ }
30
+ return serialized
31
+ }
32
+
33
+ /**
34
+ * Canonical YAML for files the CLI rewrites (GitHub Actions workflows).
35
+ *
36
+ * Matches `napi new` / `napi rename` dump options and always ends with a
37
+ * trailing newline. Blank lines from the original document are not
38
+ * preserved — js-yaml dump cannot round-trip those.
39
+ */
40
+ export function serializeYaml(value: unknown): string {
41
+ const serialized = yamlDump(value, {
42
+ lineWidth: -1,
43
+ noRefs: true,
44
+ sortKeys: false,
45
+ })
46
+ return serialized.endsWith('\n') ? serialized : `${serialized}\n`
47
+ }
@@ -1,4 +1,6 @@
1
- import { execSync } from 'node:child_process'
1
+ import { execFileSync, execSync } from 'node:child_process'
2
+ import { readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
2
4
 
3
5
  export type Platform = NodeJS.Platform | 'wasm' | 'wasi' | 'openharmony'
4
6
 
@@ -156,6 +158,153 @@ export function wasiTargetHasThreads(
156
158
  return getWasiTarget(target)?.flavor === 'threads'
157
159
  }
158
160
 
161
+ function readTextFileOrNull(path: string): string | null {
162
+ try {
163
+ return readFileSync(path, 'utf8')
164
+ } catch {
165
+ return null
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Detect the major version of a wasi-sdk installation.
171
+ *
172
+ * wasi-libc dropped the unused `int op` parameter from
173
+ * `__wasilibc_futex_wait_atomic_wait` and `__wasilibc_futex_wait_maybe_busy`,
174
+ * and wasi-sdk 34 is the first release that ships the 3-argument signature.
175
+ * Static archives compiled against the two signatures cannot be mixed, so the
176
+ * emnapi archives have to be picked by wasi-sdk version, not by target triple
177
+ * alone.
178
+ *
179
+ * `<wasiSdkPath>/VERSION` is the primary signal: every release ships it and
180
+ * its first line is the version (`27.0`, `33.0+m`, `34.0`, ...). The
181
+ * `wasi/version.h` header carrying `__wasi_sdk_major__` only appears from
182
+ * wasi-sdk 30 onwards, so it stays a fallback for trees without a `VERSION`.
183
+ *
184
+ * Returns `null` when neither signal is readable or parseable. Detection must
185
+ * never throw: an unknown wasi-sdk degrades to the legacy archives instead of
186
+ * failing the build.
187
+ */
188
+ export function wasiSdkMajorVersion(wasiSdkPath: string): number | null {
189
+ const version = readTextFileOrNull(join(wasiSdkPath, 'VERSION'))
190
+ if (version) {
191
+ // `33.0+m` and friends carry a build suffix, so only the leading integer
192
+ // of the first line is meaningful.
193
+ const major = /^\s*(\d+)/.exec(version.split('\n', 1)[0])
194
+ if (major) {
195
+ return Number(major[1])
196
+ }
197
+ }
198
+ const versionHeader = readTextFileOrNull(
199
+ join(
200
+ wasiSdkPath,
201
+ 'share',
202
+ 'wasi-sysroot',
203
+ 'include',
204
+ 'wasm32-wasip1-threads',
205
+ 'wasi',
206
+ 'version.h',
207
+ ),
208
+ )
209
+ if (versionHeader) {
210
+ const major = /^\s*#\s*define\s+__wasi_sdk_major__\s+(\d+)/m.exec(
211
+ versionHeader,
212
+ )
213
+ if (major) {
214
+ return Number(major[1])
215
+ }
216
+ }
217
+ return null
218
+ }
219
+
220
+ /**
221
+ * Archive member that only exists in a wasi-libc carrying the 3-argument
222
+ * futex ABI.
223
+ *
224
+ * wasi-libc moved the wasi-threads futex helpers out of `__wait.c` and into a
225
+ * new `futex.c` in the same change that dropped the unused `int op` parameter
226
+ * (WebAssembly/wasi-libc#846). `__wait.c` still exists afterwards for other
227
+ * symbols, so the presence of `futex.c` — not the absence of `__wait.c` — is
228
+ * what separates the two ABIs.
229
+ */
230
+ const NEW_FUTEX_ABI_ARCHIVE_MEMBER = 'futex.c.obj'
231
+
232
+ /**
233
+ * Path to the wasi-libc that cargo links when no wasi-sdk is configured.
234
+ *
235
+ * Without `WASI_SDK_PATH` the target links through `rust-lld` against the
236
+ * wasi-libc bundled with the Rust standard library, so that copy — not a
237
+ * wasi-sdk — decides the futex ABI.
238
+ *
239
+ * `cwd` must be the directory Cargo is spawned in, because that is what
240
+ * selects the toolchain.
241
+ *
242
+ * Returns `null` when `rustc` cannot be queried or the target is not
243
+ * installed. Never throws: an undetectable sysroot degrades to the legacy
244
+ * archives.
245
+ */
246
+ export function rustBundledWasiLibc(
247
+ wasiTarget: string,
248
+ cwd?: string,
249
+ ): string | null {
250
+ let sysroot: string
251
+ try {
252
+ // `rustc` is a rustup shim: it resolves `rust-toolchain.toml` and any
253
+ // directory override from its working directory. Cargo is spawned with
254
+ // the build cwd, so the probe must use the same one or it can read a
255
+ // different toolchain's wasi-libc than the one that gets linked.
256
+ // `RUSTC` and `CARGO_BUILD_RUSTC` bypass the shim entirely; Cargo gives
257
+ // `RUSTC` precedence, so do the same. `build.rustc` from the project's
258
+ // `.cargo/config.toml` stays out of reach without invoking cargo itself.
259
+ sysroot = execFileSync(
260
+ process.env.RUSTC ?? process.env.CARGO_BUILD_RUSTC ?? 'rustc',
261
+ ['--print', 'sysroot'],
262
+ {
263
+ cwd,
264
+ env: process.env,
265
+ stdio: ['ignore', 'pipe', 'ignore'],
266
+ },
267
+ )
268
+ .toString('utf8')
269
+ .trim()
270
+ } catch {
271
+ return null
272
+ }
273
+ if (!sysroot) {
274
+ return null
275
+ }
276
+ return join(
277
+ sysroot,
278
+ 'lib',
279
+ 'rustlib',
280
+ wasiTarget,
281
+ 'lib',
282
+ 'self-contained',
283
+ 'libc.a',
284
+ )
285
+ }
286
+
287
+ /**
288
+ * Whether a wasi-libc archive carries the 3-argument futex ABI.
289
+ *
290
+ * Returns `null` when the archive cannot be read, so callers can tell
291
+ * "definitely the legacy ABI" apart from "could not tell".
292
+ */
293
+ export function wasiLibcHasNewFutexAbi(
294
+ libcArchivePath: string | null,
295
+ ): boolean | null {
296
+ if (!libcArchivePath) {
297
+ return null
298
+ }
299
+ let archive: Buffer
300
+ try {
301
+ archive = readFileSync(libcArchivePath)
302
+ } catch {
303
+ return null
304
+ }
305
+ return archive.includes(NEW_FUTEX_ABI_ARCHIVE_MEMBER, 0, 'latin1')
306
+ }
307
+
159
308
  /**
160
309
  * A triple is a specific format for specifying a target architecture.
161
310
  * 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.