feat: strip macOS Gatekeeper quarantine after browser install
Some checks failed
CI / lint (push) Has been cancelled
CI / test (macos-latest) (push) Has been cancelled
CI / test (ubuntu-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
CI / test_docker (push) Has been cancelled

macOS sets com.apple.quarantine on network-downloaded files. On Tahoe
(macOS 26) and later, Gatekeeper enforcement silently blocks launch of
quarantined Chromium binaries, causing confusing "browser failed to
start" errors after a successful install.

After `playwright install` completes on darwin, run
`xattr -dr com.apple.quarantine` against the browser cache directory
(~/Library/Caches/ms-playwright by default, or PLAYWRIGHT_BROWSERS_PATH
when set). Best-effort: errors are logged via testDebug and never
thrown. Skipped on Linux/Windows and when PLAYWRIGHT_BROWSERS_PATH=0
(node_modules install path doesn't get quarantined).
This commit is contained in:
Ryan Malloy 2026-04-25 21:39:13 -06:00
parent 6ae3991efb
commit 4bb1b26137

View File

@ -14,7 +14,8 @@
* limitations under the License.
*/
import { fork } from 'node:child_process';
import { fork, spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -62,6 +63,12 @@ async function runInstall(target: string): Promise<void> {
await runPlaywrightCli(cliPath, ['install', target]);
// macOS: strip Gatekeeper quarantine attribute from freshly downloaded
// browser binaries. Without this, the first launch is silently blocked
// by macOS on a fresh install.
if (process.platform === 'darwin')
await stripDarwinQuarantine();
// Best-effort system-deps install. Only runs when we're already root,
// otherwise skipped silently — users will see Playwright's own missing-lib
// error on the next launch, which tells them exactly what to apt install.
@ -74,6 +81,40 @@ async function runInstall(target: string): Promise<void> {
}
}
/**
* Removes the `com.apple.quarantine` extended attribute from Playwright's
* browser cache directory. macOS sets this on anything downloaded from the
* network; on Tahoe and later it causes Gatekeeper to silently block launch.
*
* Best-effort: errors are logged but never thrown. If `xattr` doesn't exist,
* the cache dir is missing, or the attribute isn't present, we just move on.
*/
async function stripDarwinQuarantine(): Promise<void> {
const cacheDir = process.env.PLAYWRIGHT_BROWSERS_PATH
|| path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright');
// PLAYWRIGHT_BROWSERS_PATH=0 means "install into node_modules" — skip,
// since those binaries weren't downloaded with quarantine in that flow.
if (cacheDir === '0')
return;
testDebug(`stripping quarantine attribute from ${cacheDir}`);
await new Promise<void>(resolve => {
const child = spawn('/usr/bin/xattr', ['-dr', 'com.apple.quarantine', cacheDir], {
stdio: 'pipe',
});
child.on('close', code => {
if (code !== 0)
testDebug(`xattr exited ${code} (non-fatal)`);
resolve();
});
child.on('error', err => {
testDebug(`xattr spawn failed (non-fatal): ${err.message}`);
resolve();
});
});
}
function resolvePlaywrightCli(): string {
try {
const cliUrl = import.meta.resolve('playwright/package.json');