Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7a63515
feat(search): bundle ripgrep for local, WSL, and SSH search
nwparker Sep 23, 2026
263a63b
fix(search): address bundled ripgrep review findings
nwparker Sep 23, 2026
63f4dac
chore(search): drop bundled-ripgrep reference doc; assert full packag…
nwparker Sep 23, 2026
c5f0f45
refactor(search): one entry point for spawning the bundled ripgrep
nwparker Sep 23, 2026
bffd427
refactor(search): drop the local install-ripgrep path; enforce the rg…
nwparker Sep 23, 2026
622b315
test(ssh): pin that the cleanup sweep does not wait on the ripgrep up…
nwparker Sep 23, 2026
b435f42
fix(search): derive rg spawn types instead of importing node:child_pr…
nwparker Sep 23, 2026
ede2838
Merge remote-tracking branch 'origin/main' into nwparker/bundle-ripgrep
nwparker Sep 23, 2026
e14d7ef
fix(search): surface an unreachable WSL workspace instead of an empty…
nwparker Sep 23, 2026
5d2152f
fix(search): name the unreachable root in every handler, not three of…
nwparker Sep 23, 2026
8921de1
fix(search): let the error handler own the spawn-failure verdict
nwparker Sep 23, 2026
f29377c
test(search): cover exit code 97 in all four ripgrep close handlers
nwparker Sep 24, 2026
b56ce66
docs(search): stop claiming the close handler always wins the race
nwparker Sep 24, 2026
f79158e
chore(search): ship the jemalloc and libunwind notices the Linux rg n…
nwparker Sep 24, 2026
6059a28
fix(relay): stop spawning a bare rg, name unreachable roots, collect …
nwparker Sep 24, 2026
cf019c2
fix(relay): probe the rg that failed, and close the drive-relative PA…
nwparker Sep 24, 2026
c19f907
Merge remote-tracking branch 'origin/main' into nwparker/bundle-ripgrep
nwparker Sep 24, 2026
11d29fe
test(mobile): repin the session closure past #22452's two shared modules
nwparker Sep 24, 2026
8d6759a
fix(search): preserve remote binaries and complete runtime packaging
nwparker Sep 24, 2026
aa1d940
test(relay): pin the probe's env now that it inherits the relay's PATH
nwparker Sep 24, 2026
763cea4
Merge remote-tracking branch 'origin/main' into nwparker/bundle-ripgrep
nwparker Sep 24, 2026
e506bf3
feat(ssh): collect remote ripgrep builds by reference, not by age
nwparker Sep 24, 2026
29b6534
feat(ssh): collect Windows remotes too, and ship the Rust crate notices
nwparker Sep 24, 2026
0d144cf
fix(search): protect relay cache references and handle failed spawns
nwparker Sep 24, 2026
eb6c375
fix(ripgrep): close review gaps and repair deployment fixtures
nwparker Sep 24, 2026
65d134b
Merge remote-tracking branch 'origin/main' into nwparker/bundle-ripgrep
nwparker Sep 24, 2026
3ebc309
test(mobile): refresh merged session module census
nwparker Sep 24, 2026
6382f9f
fix(ssh): preserve ripgrep caches with empty legacy references
nwparker Sep 24, 2026
f211cbf
test(mobile): assert bundle boundaries instead of global module count
nwparker Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
# Why only ripgrep: the bundled rg ships in every artifact and to SSH remotes, and a bump is a
# one-line pin change (the SSH cache keys on the binary's hash). Other dependencies stay manual.
- package-ecosystem: npm
directory: /
schedule:
interval: monthly
allow:
- dependency-name: '@vscode/ripgrep-universal'
open-pull-requests-limit: 1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,6 @@ tests/e2e/.cross-version-checkouts/

# Generated by config/scripts/sync-anti-slop-plugin.mjs from the pinned oxlint-plugin-anti-slop
.anti-slop-plugin/

# Generated by the CI unit-test sequencer (and by reproducing a shard locally).
ci-shards/
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh
- **Windows terminal shells**: `--shell` picks the shell a terminal _is_; `--command` is typed into whatever shell the host spawned, so a shell choice routed through `command` silently becomes a child process. See [`docs/reference/windows-terminal-shell-selection.md`](./docs/reference/windows-terminal-shell-selection.md).
- **Windows setup scripts**: the setup/issue-command runner is a `.cmd` batch file unless the script starts with a `#!` line — never derive that from the user's terminal-shell preference, and never launch a `.cmd` runner with a bare `cmd.exe /c` from a Git Bash pane (MSYS rewrites the `/c`). See [`docs/reference/windows-setup-shell.md`](./docs/reference/windows-setup-shell.md).
- **Windows child processes**: start them through `runProcess`/`spawnProcess` in `src/shared/child-process/` — never `child_process` directly. It pins `windowsHide`, refuses `shell: true`, and encodes `.cmd`/`.bat` arguments so neither `CommandLineToArgvW` nor `cmd.exe` mangles them. A ratchet test fails on any new direct import. Recognised npm/pnpm `.cmd` shims are resolved to their real target so the spawn skips `cmd.exe` entirely; see [`docs/reference/windows-cmd-shim-resolution.md`](./docs/reference/windows-cmd-shim-resolution.md) before adding a shim shape or debugging one.
- **Ripgrep**: Orca bundles `rg` for every platform, WSL, and SSH remotes. Spawn it through `spawnBundledRipgrep` (main) or `resolveRelayRipgrepCommand` (relay), never a bare `'rg'` — Windows resolves a bare name in the spawn cwd before PATH. Don't add git/readdir fallbacks locally; the relay's chain exists only for hosts an upload never reached.
- **Windows process enumeration**: read the table through `src/main/windows/windows-process-table.ts`, never by forking `powershell.exe`. See [`docs/reference/windows-process-enumeration.md`](./docs/reference/windows-process-enumeration.md).
- **Windows MSYS/Git Bash panes**: their children break away from the per-PTY job unless it is created without `JOB_OBJECT_LIMIT_BREAKAWAY_OK`, and a `conpty.node` built before that fix passes every existing gate. Before changing the per-PTY job or debugging `windows-msys-job.win32.test.ts`, read [`docs/reference/windows-msys-job-breakaway.md`](./docs/reference/windows-msys-job-breakaway.md).
- **Windows daemon-host relocation**: the terminal daemon runs from a copy of the app runtime under `%LOCALAPPDATA%`, which is what survives an auto-update. Before touching that copy, its exe name, or the NSIS uninstall macro, read [`docs/reference/windows-daemon-host-relocation.md`](./docs/reference/windows-daemon-host-relocation.md).
Expand Down
73 changes: 73 additions & 0 deletions config/bundled-ripgrep-resources.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const { chmodSync, existsSync } = require('node:fs')
const { join } = require('node:path')

// Why every relay platform in every artifact: SSH deploys upload the remote host's rg from the
// local bundle, and WSL runs the Linux build from the Windows install directory.
const BUNDLED_RIPGREP_PLATFORMS = [
'linux-x64',
'linux-arm64',
'darwin-x64',
'darwin-arm64',
'win32-x64',
'win32-arm64'
]
const RIPGREP_PACKAGE_BIN_DIR = 'node_modules/@vscode/ripgrep-universal/bin'
const RIPGREP_RESOURCE_DIR = 'ripgrep'

function ripgrepBinaryName(platform) {
return platform.startsWith('win32-') ? 'rg.exe' : 'rg'
}

const bundledRipgrepExtraResources = [
{
from: RIPGREP_PACKAGE_BIN_DIR,
to: RIPGREP_RESOURCE_DIR,
filter: BUNDLED_RIPGREP_PLATFORMS.map((platform) => `${platform}/**`)
},
// Why: the binaries statically link PCRE2, and on Linux musl, jemalloc and LLVM libunwind --
// all of which require their notice on binary redistribution. Whole dir, so notices can be added.
{ from: 'resources/licenses/ripgrep', to: `${RIPGREP_RESOURCE_DIR}/licenses` }
]

// Why: codesign would try to sign the Linux/Windows builds; they are inert data on macOS.
const bundledRipgrepMacSignIgnore = ['/ripgrep/(linux|win32)-']

// Why: electron-builder only warns on a missing extraResources source.
function assertBundledRipgrepInstalled(projectDir = join(__dirname, '..')) {
const missing = BUNDLED_RIPGREP_PLATFORMS.filter(
(platform) =>
!existsSync(join(projectDir, RIPGREP_PACKAGE_BIN_DIR, platform, ripgrepBinaryName(platform)))
)
if (missing.length > 0) {
throw new Error(
`@vscode/ripgrep-universal is missing binaries for ${missing.join(', ')}; run pnpm install.`
)
}
}

function finalizePackagedRipgrep(resourcesDir) {
for (const platform of BUNDLED_RIPGREP_PLATFORMS) {
const binaryPath = join(
resourcesDir,
RIPGREP_RESOURCE_DIR,
platform,
ripgrepBinaryName(platform)
)
if (!existsSync(binaryPath)) {
throw new Error(`Packaged app is missing bundled ripgrep: ${binaryPath}`)
}
// Why: the upstream tarball's exec bits are not guaranteed after copying.
chmodSync(binaryPath, 0o755)
}
}

module.exports = {
BUNDLED_RIPGREP_PLATFORMS,
RIPGREP_PACKAGE_BIN_DIR,
RIPGREP_RESOURCE_DIR,
ripgrepBinaryName,
assertBundledRipgrepInstalled,
bundledRipgrepExtraResources,
bundledRipgrepMacSignIgnore,
finalizePackagedRipgrep
}
10 changes: 10 additions & 0 deletions config/electron-builder.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ const {
assertMobileWebBundleBuilt
} = require('./scripts/verify-packaged-mobile-web-bundle.cjs')
const { verifyPackagedPluginResources } = require('./scripts/verify-packaged-plugin-resources.cjs')
const {
assertBundledRipgrepInstalled,
bundledRipgrepExtraResources,
bundledRipgrepMacSignIgnore,
finalizePackagedRipgrep
} = require('./bundled-ripgrep-resources.cjs')
const {
verifyPackagedWindowsNodePty
} = require('./scripts/verify-packaged-node-pty-job-ownership.cjs')
Expand Down Expand Up @@ -105,6 +111,7 @@ const emojiShortcodeDatasetResource = {
}
const commonExtraResources = [
relayExtraResource,
...bundledRipgrepExtraResources,
bundledPluginResources,
skillFreshnessResources,
emojiShortcodeDatasetResource
Expand Down Expand Up @@ -297,6 +304,7 @@ module.exports = {
// so a test can point the guard at a scratch bundle instead of needing the repo's out/ built.
beforePack: (context, mobileWebBundleDir = MOBILE_WEB_BUNDLE_DIR) => {
assertPackagedNativeVariantsInstalled(context.electronPlatformName, context.arch)
assertBundledRipgrepInstalled()
assertMobileWebBundleBuilt(mobileWebBundleDir)
},
afterPack: async (context) => {
Expand Down Expand Up @@ -384,6 +392,7 @@ module.exports = {
// Why: inspect electron-builder's real output so a broken extraResources
// mapping fails packaging before bundled content reaches users.
verifyPackagedPluginResources(resourcesDir)
finalizePackagedRipgrep(resourcesDir)
chmodUnixCliLaunchers(resourcesDir, context.electronPlatformName)
chmodMacServeSimHelpers(resourcesDir, context.electronPlatformName)
for (const filename of readdirSync(resourcesDir)) {
Expand Down Expand Up @@ -482,6 +491,7 @@ module.exports = {
icon: 'resources/build/icon.icns',
entitlements: 'resources/build/entitlements.mac.plist',
entitlementsInherit: 'resources/build/entitlements.mac.plist',
signIgnore: bundledRipgrepMacSignIgnore,
extendInfo: {
NSAppleEventsUsageDescription:
'Orca allows terminal-launched developer tools to automate local apps when you request it.',
Expand Down
38 changes: 21 additions & 17 deletions config/scripts/build-orcad.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,23 @@
*/
import { fork, spawnSync } from 'node:child_process'
import { build } from 'esbuild'
import { createHash } from 'node:crypto'
import {
chmodSync,
copyFileSync,
existsSync,
cpSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { arch, platform, tmpdir } from 'node:os'
import { join } from 'node:path'
import process from 'node:process'
import {
ORCAD_VERSION,
ORCAD_VERSION_FILENAME,
orcadArtifactFilenames
ORCAD_RIPGREP_ARTIFACTS
} from '../../src/shared/orcad-artifacts.ts'
import { computeOrcadFullVersion } from './orcad-artifact-version.mjs'

const ROOT = join(import.meta.dirname, '..', '..')
const OUT_DIR = join(ROOT, 'out', 'orcad')
Expand Down Expand Up @@ -83,6 +81,23 @@ copyFileSync(AGENT_BROWSER_SOURCE, AGENT_BROWSER_OUTPUT)
if (process.platform !== 'win32') {
chmodSync(AGENT_BROWSER_OUTPUT, 0o755)
}
// Why every platform: an SSH deployment can target a different host than the build machine.
for (const artifact of ORCAD_RIPGREP_ARTIFACTS) {
const [, ripgrepPlatform, ripgrepName] = artifact.split('/')
const outputDir = join(OUT_DIR, 'ripgrep', ripgrepPlatform)
mkdirSync(outputDir, { recursive: true })
const outputPath = join(outputDir, ripgrepName)
copyFileSync(
join(ROOT, 'node_modules', '@vscode', 'ripgrep-universal', 'bin', ripgrepPlatform, ripgrepName),
outputPath
)
if (!ripgrepPlatform.startsWith('win32-')) {
chmodSync(outputPath, 0o755)
}
}
cpSync(join(ROOT, 'resources', 'licenses', 'ripgrep'), join(OUT_DIR, 'ripgrep', 'licenses'), {
recursive: true
})

/** Why one call per child and not one `outdir` build: esbuild mirrors each entry's source
* directory under `outdir`, and both children must land flat beside orcad.js — that is where
Expand Down Expand Up @@ -246,18 +261,7 @@ if (graphErrors.length > 0) {
// already-`.install-complete` dir is never re-uploaded. The deploy would silently run stale
// bytes while reporting the new version.
if (process.exitCode !== 1) {
const hash = createHash('sha256')
for (const filename of orcadArtifactFilenames()) {
const artifactPath = join(OUT_DIR, filename)
if (!existsSync(artifactPath)) {
throw new Error(
`orcad declares ${filename} in ORCAD_ARTIFACTS but never emitted it. Add the build ` +
'step, or drop it from src/shared/orcad-artifacts.ts.'
)
}
hash.update(readFileSync(artifactPath))
}
const fullVersion = `${ORCAD_VERSION}+${hash.digest('hex').slice(0, 12)}`
const fullVersion = computeOrcadFullVersion(OUT_DIR)
writeFileSync(join(OUT_DIR, ORCAD_VERSION_FILENAME), fullVersion)
console.log(
`[build-orcad] ok — ${fullVersion}, ${(output.bytes / 1024 / 1024).toFixed(2)} MB, ${Object.keys(output.inputs).length} modules, zero electron and node:sqlite imports.`
Expand Down
12 changes: 12 additions & 0 deletions config/scripts/electron-builder-runtime-resources.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ describe('packaged runtime resources', () => {
join(resourcesDir, 'plugins', 'launch'),
{ recursive: true }
)
await seedBundledRipgrep(resourcesDir)

const unpackedMainDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'main')
await mkdir(unpackedMainDir, { recursive: true })
Expand Down Expand Up @@ -476,6 +477,7 @@ describe('packaged runtime resources', () => {
join(resourcesDir, 'plugins', 'launch'),
{ recursive: true }
)
await seedBundledRipgrep(resourcesDir)
await mkdir(join(resourcesDir, 'node_modules', 'zod', 'src'), { recursive: true })
// Why: afterPack now fails hard when the unpacked daemon entry is
// missing, so the fixture must carry one like a real package layout.
Expand Down Expand Up @@ -528,6 +530,16 @@ describe('packaged runtime resources', () => {
// Why source-anchored: the bundler renames a createRequire()'d require, so
// verifyPackagedMainRuntimeDeps' `require("x")` scan cannot see these specifiers — packaging
// stays green while the packaged app throws MODULE_NOT_FOUND the first time the path runs.
// Why stubs: afterPack only checks each platform binary exists; non-ELF bytes skip the glibc scan.
async function seedBundledRipgrep(resourcesDir) {
const { BUNDLED_RIPGREP_PLATFORMS } = require('../bundled-ripgrep-resources.cjs')
for (const platform of BUNDLED_RIPGREP_PLATFORMS) {
const dir = join(resourcesDir, 'ripgrep', platform)
await mkdir(dir, { recursive: true })
await writeFile(join(dir, platform.startsWith('win32-') ? 'rg.exe' : 'rg'), '', 'utf8')
}
}

function collectLazyRequireSpecifiers(directory, found = new Map()) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const entryPath = join(directory, entry.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,6 @@ const MERMAID_PACKAGE = 'node_modules/mermaid/'
* modules 4221 -> 4219 (-2)
* local modules 1035 -> 1033 (-2)
*/
const SESSION_ROUTE_MODULES = 4219

/** What the page enters this route through once the route is a switch with a `.web.tsx` sibling. */
const ROUTE_ENTRY = [
'app/h/[hostId]/session/[worktreeId].web.tsx',
Expand Down Expand Up @@ -523,10 +521,8 @@ describeClosure(
const { modules } = await mobileWebAppRouteClosure(SESSION_ROUTE)
// The engine is here, as the one artifact the loader imports.
expect(artifactModules(modules)).toHaveLength(1)
// And the package's own file tree is not, anywhere: it is inside that artifact. Meaningful
// only beside the line above, which is why the two sit together.
// Package sources stay inside that artifact; unrelated module counts are not this boundary.
expect(packageModules(modules)).toEqual([])
expect(modules).toHaveLength(SESSION_ROUTE_MODULES)

const download = await mobileWebAppRouteChunkClosure(SESSION_ROUTE)
// The fence: nothing of the engine is reachable from the route's own chunk by an import
Expand Down
19 changes: 19 additions & 0 deletions config/scripts/orcad-artifact-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createHash } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { ORCAD_VERSION, orcadArtifactFilenames } from '../../src/shared/orcad-artifacts.ts'

export function computeOrcadFullVersion(artifactDir) {
const hash = createHash('sha256')
for (const filename of orcadArtifactFilenames()) {
const artifactPath = join(artifactDir, filename)
if (!existsSync(artifactPath)) {
throw new Error(
`orcad declares ${filename} in ORCAD_ARTIFACTS but never emitted it. Add the build ` +
'step, or drop it from src/shared/orcad-artifacts.ts.'
)
}
hash.update(readFileSync(artifactPath))
}
return `${ORCAD_VERSION}+${hash.digest('hex').slice(0, 12)}`
}
30 changes: 30 additions & 0 deletions config/scripts/orcad-artifact-version.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
ORCAD_RIPGREP_ARTIFACTS,
orcadArtifactFilenames
} from '../../src/shared/orcad-artifacts.ts'
import { computeOrcadFullVersion } from './orcad-artifact-version.mjs'

describe('standalone runtime version', () => {
it('changes when a shipped search binary changes and rejects a missing binary', () => {
const dir = mkdtempSync(join(tmpdir(), 'orcad-version-'))
try {
for (const filename of orcadArtifactFilenames()) {
const path = join(dir, filename)
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, filename)
}
const before = computeOrcadFullVersion(dir)
const binary = join(dir, ORCAD_RIPGREP_ARTIFACTS[0])
writeFileSync(binary, 'updated binary')
expect(computeOrcadFullVersion(dir)).not.toBe(before)
rmSync(binary)
expect(() => computeOrcadFullVersion(dir)).toThrow(ORCAD_RIPGREP_ARTIFACTS[0])
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
})
3 changes: 2 additions & 1 deletion config/scripts/pr-e2e-gate-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,8 @@ describe('PR E2E gate contract', () => {
)
}
for (const source of [
'src/main/ipc/rg-availability.ts',
'src/main/ripgrep/bundled-ripgrep-path.ts',
'src/shared/bundled-ripgrep.ts',
'src/shared/ripgrep-process-availability.ts'
]) {
expect(selectPrE2eSpecs([source]), source).toEqual([
Expand Down
2 changes: 1 addition & 1 deletion config/scripts/pr-e2e-source-routing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const PR_E2E_SOURCE_ROUTES = [
specs: ['tests/e2e/paired-quick-open-large-tree.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/main\/ipc\/(?:filesystem-(?:list-files|search-file-paths)|rg-availability)\.ts|src\/main\/providers\/(?:filesystem-provider-contract|ssh-filesystem-provider(?:-capabilities)?)\.ts|src\/main\/runtime\/(?:orca-runtime-files|rpc\/methods\/files)\.ts|src\/relay\/(?:fs-handler(?:-install-rg|-list-files|-ripgrep-fallback)?|fs-list-files-fallback-chain)\.ts|src\/renderer\/src\/(?:components\/(?:QuickOpen|quick-open-file-list|quick-open-search)\.tsx?|runtime\/(?:runtime-file-client|runtime-legacy-quick-open-inventory)\.ts)|src\/shared\/(?:quick-open-(?:install-rg|path-search|transport-budget)|ripgrep-process-availability)\.ts)$/.test(
/^(?:src\/main\/ipc\/filesystem-(?:list-files|search-file-paths)\.ts|src\/main\/ripgrep\/bundled-ripgrep-path\.ts|src\/main\/providers\/(?:filesystem-provider-contract|ssh-filesystem-provider(?:-capabilities)?)\.ts|src\/main\/runtime\/(?:orca-runtime-files|rpc\/methods\/files)\.ts|src\/relay\/(?:fs-handler(?:-install-rg|-list-files|-ripgrep-fallback)?|fs-list-files-fallback-chain|relay-bundled-ripgrep)\.ts|src\/renderer\/src\/(?:components\/(?:QuickOpen|quick-open-file-list|quick-open-search)\.tsx?|runtime\/(?:runtime-file-client|runtime-legacy-quick-open-inventory)\.ts)|src\/shared\/(?:quick-open-(?:install-rg|path-search|transport-budget)|ripgrep-process-availability|bundled-ripgrep)\.ts)$/.test(
file
)
},
Expand Down
17 changes: 13 additions & 4 deletions config/scripts/verify-linux-glibc-floor.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,11 @@ function declaredArchFromPath(filePath) {
return match ? ARCH_BY_TOKEN[match[1].toLowerCase()] : null
}

function findArchViolation(filePath, targetArch) {
function findArchViolation(filePath, targetArch, rootDir) {
// A path that names an architecture is judged against that name, so a per-arch vendored package
// is fine while `bin/linux-arm64-.../node-pty.node` holding an x86-64 binary is still caught.
const declared = declaredArchFromPath(filePath)
// Why relative: the arm64 slice's own dir (`linux-arm64-unpacked`) must not declare every file arm64.
const declared = declaredArchFromPath(rootDir ? relative(rootDir, filePath) : filePath)
const expectedArch = declared ?? targetArch
const expected = ELF_MACHINE_BY_ARCH[expectedArch]
if (expected === undefined) {
Expand Down Expand Up @@ -373,7 +374,15 @@ function readDynamicInfo(filePath, objdumpPath) {

/** Imported (undefined) dynamic symbols from `objdump -T` (fail-closed). */
function readImportedSymbols(filePath, objdumpPath) {
return parseImportedSymbols(runObjdump(objdumpPath, '-T', filePath))
try {
return parseImportedSymbols(runObjdump(objdumpPath, '-T', filePath))
} catch (error) {
// Why: a statically linked binary (bundled ripgrep) has no dynamic symbol table to import from.
if (error instanceof Error && error.message.includes('not a dynamic object')) {
return new Set()
}
throw error
}
}

/**
Expand Down Expand Up @@ -403,7 +412,7 @@ function verifyLinuxGlibcFloor(rootDir, options = {}) {
// Why before the glibc pass: a wrong-architecture binary's symbol versions are valid but
// meaningless, so reporting a floor violation for it would send the reader down the wrong path.
const archOffenders = binaries
.map((filePath) => ({ filePath, violation: findArchViolation(filePath, targetArch) }))
.map((filePath) => ({ filePath, violation: findArchViolation(filePath, targetArch, rootDir) }))
.filter(({ violation }) => violation !== null)
if (archOffenders.length > 0) {
const detail = archOffenders
Expand Down
Loading
Loading