import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageDir = path.resolve(__dirname, ".."); const cacheDir = path.join(packageDir, ".cache", "playwright-system-libs"); const browsersCacheDir = path.join(packageDir, ".cache", "ms-playwright"); const debDir = path.join(cacheDir, "deb"); const extractedDir = path.join(cacheDir, "root"); const libraryDirs = [ path.join(extractedDir, "usr", "lib", "x86_64-linux-gnu"), path.join(extractedDir, "lib", "x86_64-linux-gnu"), ]; const systemLibraryDirs = [ "/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu", "/usr/lib", "/lib", ]; const linuxRuntimePackages = [ { debPrefix: "libatk1.0-0t64_", specs: ["libatk1.0-0t64"] }, { debPrefix: "libatk-bridge2.0-0t64_", specs: ["libatk-bridge2.0-0t64"] }, { debPrefix: "libatspi2.0-0t64_", specs: ["libatspi2.0-0t64"] }, { debPrefix: "libxcomposite1_", specs: ["libxcomposite1"] }, { debPrefix: "libxdamage1_", specs: ["libxdamage1"] }, { debPrefix: "libxfixes3_", specs: ["libxfixes3"] }, { debPrefix: "libxrandr2_", specs: ["libxrandr2"] }, { debPrefix: "libxkbcommon0_", specs: ["libxkbcommon0"] }, { debPrefix: "libasound2t64_", specs: ["libasound2t64", "libasound2t64=1.2.11-1build2"], }, { debPrefix: "libgbm1_", specs: ["libgbm1", "libgbm1=24.0.5-1ubuntu1"], }, { debPrefix: "libdrm2_", specs: ["libdrm2", "libdrm2=2.4.120-2build1"], }, { debPrefix: "libwayland-server0_", specs: ["libwayland-server0"] }, { debPrefix: "libxcb-randr0_", specs: ["libxcb-randr0"] }, { debPrefix: "libxi6_", specs: ["libxi6"] }, ]; const requiredLibraries = [ "libatk-1.0.so.0", "libatk-bridge-2.0.so.0", "libatspi.so.0", "libXcomposite.so.1", "libXdamage.so.1", "libXfixes.so.3", "libXrandr.so.2", "libasound.so.2", "libgbm.so.1", "libxkbcommon.so.0", "libdrm.so.2", "libwayland-server.so.0", "libxcb-randr.so.0", "libXi.so.6", ]; function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd ?? packageDir, env: options.env ?? process.env, stdio: options.stdio ?? "inherit", encoding: "utf8", }); if (result.status !== 0) { const commandLine = [command, ...args].join(" "); const error = new Error(`Command failed (${result.status}): ${commandLine}`); error.stdout = result.stdout ?? ""; error.stderr = result.stderr ?? ""; throw error; } return result; } function tryRun(command, args, options = {}) { return spawnSync(command, args, { cwd: options.cwd ?? packageDir, env: options.env ?? process.env, stdio: options.stdio ?? "pipe", encoding: "utf8", }); } function libraryPresentInDirs(name, dirs) { return dirs.some((dir) => existsSync(path.join(dir, name))); } function cachedLibraryPresent(name) { return libraryPresentInDirs(name, libraryDirs); } function systemLibraryPresent(name) { return libraryPresentInDirs(name, systemLibraryDirs); } function headlessShellPresent() { if (!existsSync(browsersCacheDir)) { return false; } for (const entry of readdirSync(browsersCacheDir)) { if (!entry.startsWith("chromium_headless_shell-")) { continue; } const executablePath = path.join( browsersCacheDir, entry, "chrome-headless-shell-linux64", "chrome-headless-shell", ); if (existsSync(executablePath)) { return true; } } return false; } function ensurePlaywrightBrowser(env) { if (headlessShellPresent()) { return; } mkdirSync(browsersCacheDir, { recursive: true }); run("pnpm", ["exec", "playwright", "install", "--only-shell", "chromium"], { env, }); } function ensureBrowserRuntimeLibraries() { if (process.platform !== "linux") { return []; } if (requiredLibraries.every(systemLibraryPresent)) { return []; } if (requiredLibraries.every(cachedLibraryPresent)) { return libraryDirs.filter(existsSync); } mkdirSync(debDir, { recursive: true }); rmSync(extractedDir, { recursive: true, force: true }); mkdirSync(extractedDir, { recursive: true }); for (const pkg of linuxRuntimePackages) { let debFile = readdirSync(debDir).find((entry) => entry.startsWith(pkg.debPrefix) && entry.endsWith(".deb")); if (!debFile) { let lastFailure = null; for (const spec of pkg.specs) { const result = tryRun("apt-get", ["download", spec], { cwd: debDir }); if (result.status === 0) { debFile = readdirSync(debDir).find( (entry) => entry.startsWith(pkg.debPrefix) && entry.endsWith(".deb"), ); lastFailure = null; break; } lastFailure = result; } if (!debFile) { const failureText = lastFailure ? [lastFailure.stdout, lastFailure.stderr].filter(Boolean).join("\n") : "unknown apt-get failure"; throw new Error(`Unable to download ${pkg.debPrefix}: ${failureText}`.trim()); } } run("dpkg-deb", ["-x", path.join(debDir, debFile), extractedDir], { stdio: "inherit" }); } const missing = requiredLibraries.filter((library) => !cachedLibraryPresent(library)); if (missing.length > 0) { throw new Error(`Missing extracted browser runtime libraries: ${missing.join(", ")}`); } return libraryDirs.filter(existsSync); } function main() { const extraLibraryDirs = ensureBrowserRuntimeLibraries(); const env = { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browsersCacheDir, LD_LIBRARY_PATH: [...extraLibraryDirs, process.env.LD_LIBRARY_PATH] .filter(Boolean) .join(":"), }; ensurePlaywrightBrowser(env); run("pnpm", ["exec", "playwright", "test", "--project=chromium", "--workers=1"], { env, }); } main();