fix(macos): run the frida rebuild with the vendored node on PATH; compare real paths

Two defects that only macOS arm64 can see.

`npm rebuild frida` spawns the package install script in a shell, and that script
resolves `node` from PATH. On the machines this bundle targets there is no `node`
on PATH at all, so the rebuild died with `sh: node: command not found`, reported
`fridaBindings` as unavailable, and skipped — which never fails a run. The
vendored runtime is now prepended to PATH for that single spawn, and on a Mac
with no node it produces `FRIDA_OK 3`.

`samePath()` compared `path.resolve()` spellings, but Node resolves a module URL
through its real path: a checkout under `/var/folders/...` yields
`/private/var/...`, while an explicit `--root /var/folders/...` keeps what it was
given. A mirrored run then decided it was not mirrored, ignored `mirror.json` and
looked for the manifest only the mirror step creates. Both sides are now
canonicalised, with a fallback for a directory that does not exist yet.

The macOS CI workflow that exercised these is deliberately not part of this
branch: it cannot obtain a runner in this environment, and a permanently red
check on main is worse than no check. It lands as its own PR.
This commit is contained in:
bogatkin82-jpg authored and czcdr committed 2026-11-13 20:04:09 +08:00
1 parent b3eb2b4d53
commit 2b5f0d1abc
2 files changed
+50 -6

No files matched your search

+39 -4
View File
@@ -35,7 +35,7 @@
*/
import { spawnSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { appendFileSync, chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
import { appendFileSync, chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import { homedir } from "node:os";
@@ -48,10 +48,33 @@ const PLUGIN_ROOT = path.resolve(HERE, "..");
const IS_WINDOWS = process.platform === "win32";
const EXE = IS_WINDOWS ? ".exe" : "";
/**
* Two spellings of one directory are the same directory, not two.
*
* `path.resolve` alone is not enough on macOS: Node resolves a module's own URL
* through its real path, so `fileURLToPath(import.meta.url)` for a checkout under
* `/var/folders/...` (and any other symlinked location) yields `/private/var/...`,
* while an explicitly passed `--root /var/folders/...` keeps the spelling it was
* given. `samePath` then decided the run was not a mirrored one, ignored
* `mirror.json`, and looked for the manifest the mirror step was supposed to
* create — the BOM/mirror regression test in scripts/setup.test.mjs failed for
* exactly that reason on macOS while passing on Linux, where /tmp is not a symlink.
*/
function canonicalPath(value) {
const resolved = path.resolve(value);
try {
return realpathSync(resolved);
} catch {
// A path that does not exist yet (the bundle root on a first run) has no real
// path to compare; the resolved spelling is still the best answer available.
return resolved;
}
}
/** Windows treats two spellings of one path as equal; the mirror checks must too. */
function samePath(left, right) {
const a = path.resolve(left);
const b = path.resolve(right);
const a = canonicalPath(left);
const b = canonicalPath(right);
return IS_WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
}
@@ -1984,10 +2007,22 @@ function ensureFridaBindings(nodeRuntime, npmCli, installRoot) {
log(` frida has no native binding at ${bindingFile}; running its own install script via npm rebuild frida`);
}
// `npm rebuild` runs the package's own install script in a shell, and that
// script resolves `node` from PATH — not from the interpreter npm itself is
// running under. On the machines this bundle exists for there is no `node` on
// PATH at all (that is why it vendors one), so the rebuild died with
// `npm rebuild frida exited 127: sh: node: command not found` and the binding
// was reported unavailable even where prebuild-install could have fetched a
// matching prebuilt binary. Put the vendored runtime first for this one spawn.
const rebuild = spawnSync(
nodeRuntime,
[npmCli, "rebuild", "frida", "--prefix", installRoot, "--loglevel", "error"],
{ cwd: installRoot, encoding: "utf8", timeout: 1800000 },
{
cwd: installRoot,
encoding: "utf8",
timeout: 1800000,
env: { ...process.env, PATH: `${path.dirname(nodeRuntime)}${path.delimiter}${process.env.PATH ?? ""}` },
},
);
// 2) Hash the result BEFORE importing it too — a rebuild can produce a binding
+11 -2
View File
@@ -13,7 +13,7 @@
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -230,7 +230,16 @@ test("a mirrored shim takes its bundle root from its own location, not DSH_HOME"
const mirror = path.join(root, "plugins", "rea-bundle");
mkdirSync(path.join(mirror, "bin"), { recursive: true });
copyFileSync(SHIM_SOURCE, path.join(mirror, "bin", "rea-shim.mjs"));
assert.equal(shimBundleRoot(path.join(mirror, "bin", "rea-shim.mjs"), { DSH_HOME: path.join(root, "elsewhere") }), mirror);
// The shim derives its root from its own module URL, which Node resolves
// through the real path: on macOS `tmpdir()` is /var/folders/... and that is a
// symlink to /private/var/folders/..., so the reported root is the canonical
// spelling while the fixture path is not. Compare the same thing the shim
// reports rather than failing on the spelling — the other assertions in this
// test are about which of the two roots wins, not about how it is spelled.
assert.equal(
shimBundleRoot(path.join(mirror, "bin", "rea-shim.mjs"), { DSH_HOME: path.join(root, "elsewhere") }),
realpathSync(mirror),
);
} finally {
rmTreeQuiet(root);
}