diff --git a/packages/playwright-core/src/server/registry/dependencies.ts b/packages/playwright-core/src/server/registry/dependencies.ts index 8d0b47911a9d1..0aac2cf9b67f2 100644 --- a/packages/playwright-core/src/server/registry/dependencies.ts +++ b/packages/playwright-core/src/server/registry/dependencies.ts @@ -21,6 +21,7 @@ import path from 'path'; import { wrapInASCIIBox } from '@utils/ascii'; import { hostPlatform, isOfficiallySupportedPlatform } from '@utils/hostPlatform'; +import { getLinuxDistributionInfoSync, isDebianBasedDistro } from '@utils/linuxUtils'; import { spawnAsync } from '@utils/spawnAsync'; import { getPlaywrightVersion } from '../userAgent'; import { deps } from './nativeDeps'; @@ -90,6 +91,19 @@ export async function installDependenciesWindows(targets: Set, } export async function installDependenciesLinux(targets: Set, dryRun: boolean) { + if (!isDebianBasedDistro()) { + const distroId = getLinuxDistributionInfoSync()?.id || 'this distribution'; + const message = [ + `'install-deps' can only install system dependencies on Debian and Ubuntu.`, + `On ${distroId}, install the libraries Playwright's browsers need using your`, + `distribution's package manager (e.g. zypper, dnf or pacman).`, + ].join('\n'); + if (dryRun) { + console.log(message); // eslint-disable-line no-console + return; + } + throw new Error(message); + } const libraries: string[] = []; const platform = hostPlatform; if (!isOfficiallySupportedPlatform) @@ -229,7 +243,10 @@ export async function validateDependenciesLinux(sdkLanguage: string, linuxLddDir // Check Ubuntu version. const missingPackages = new Set(); - const libraryToPackageNameMapping = deps[hostPlatform] ? { + // Only translate libraries to apt package names on Debian/Ubuntu; on other distributions the + // names would be wrong, so we report the missing libraries instead (see the `else` branch below). + const isDebianBased = isDebianBasedDistro(); + const libraryToPackageNameMapping = isDebianBased && deps[hostPlatform] ? { ...(deps[hostPlatform]?.lib2package || {}), ...MANUAL_LIBRARY_TO_PACKAGE_NAME_UBUNTU, } : {}; @@ -285,13 +302,24 @@ export async function validateDependenciesLinux(sdkLanguage: string, linuxLddDir `<3 Playwright Team`, ]); } else { - // Unhappy path: we either run on unknown distribution, or we failed to resolve all missing - // libraries to package names. + // Unhappy path: we either run on a non-Debian distribution, or we failed to resolve all + // missing libraries to package names. // Print missing libraries only: errorLines.push(...[ `Missing libraries:`, ...[...allMissingDeps].map(dep => ' ' + dep), ]); + if (!isDebianBased) { + const distroId = getLinuxDistributionInfoSync()?.id || 'this distribution'; + errorLines.push(...[ + ``, + `Playwright's automated dependency installer ('install-deps') supports only Debian and`, + `Ubuntu. On ${distroId}, install the packages that provide the libraries listed above`, + `using your distribution's package manager (e.g. zypper, dnf or pacman).`, + ``, + `<3 Playwright Team`, + ]); + } } throw new Error('\n' + wrapInASCIIBox(errorLines.join('\n'), 1)); @@ -313,7 +341,7 @@ async function executablesOrSharedLibraries(directoryPath: string): Promise path.resolve(directoryPath, file)); const allStats = await Promise.all(allPaths.map(aPath => fs.promises.stat(aPath))); - const filePaths = allPaths.filter((aPath, index) => (allStats[index] as any).isFile()); + const filePaths = allPaths.filter((aPath, index) => allStats[index].isFile()); const executablersOrLibraries = (await Promise.all(filePaths.map(async filePath => { const basename = path.basename(filePath).toLowerCase(); diff --git a/packages/playwright-core/src/tools/mcp/browserFactory.ts b/packages/playwright-core/src/tools/mcp/browserFactory.ts index 70cf0878e55d7..db34d9a13acc8 100644 --- a/packages/playwright-core/src/tools/mcp/browserFactory.ts +++ b/packages/playwright-core/src/tools/mcp/browserFactory.ts @@ -18,6 +18,8 @@ import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; +import { getLinuxDistributionInfoSync, isDebianBasedDistro } from '@utils/linuxUtils'; + import { playwright } from '../../inprocess'; import { defaultCacheDirectory } from '../../server/registry/index'; import { testDebug } from './log'; @@ -78,8 +80,7 @@ export interface BrowserContextFactory { function browserInfo(browser: playwrightTypes.Browser, config: FullConfig): BrowserInfo { return { - // eslint-disable-next-line no-restricted-syntax - guid: (browser as any)._guid, + guid: (browser as unknown as { _guid: string })._guid, browserName: config.browser.browserName, launchOptions: config.browser.launchOptions, userDataDir: config.browser.userDataDir @@ -121,8 +122,7 @@ async function createRemoteBrowser(config: FullConfig): Promise // shape. const remote = config.browser.remoteEndpoint!; // `remoteHeaders` is for back-compat, `remoteEndpoint.headers` takes precedence. - // eslint-disable-next-line no-restricted-syntax - const remoteHeaders = (config.browser as any).remoteHeaders as Record | undefined; + const remoteHeaders = (config.browser as { remoteHeaders?: Record }).remoteHeaders; const remoteOptions = typeof remote === 'string' ? { endpoint: remote, headers: remoteHeaders } : { ...remote, headers: { ...remoteHeaders, ...remote.headers } }; @@ -181,11 +181,16 @@ async function createPersistentBrowser(config: FullConfig, clientInfo: ClientInf const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions); const browser = browserContext.browser()!; return browser; - } catch (error: any) { + } catch (error: unknown) { + if (!(error instanceof Error)) + throw error; throwIfExecutableMissing(error, config); if (error.message.includes('cannot open shared object file: No such file or directory')) { const browserName = launchOptions.channel ?? config.browser.browserName; - throw new Error(`Missing system dependencies required to run browser ${browserName}. Install them with: sudo npx playwright install-deps ${browserName}`); + if (isDebianBasedDistro()) + throw new Error(`Missing system dependencies required to run browser ${browserName}. Install them with: sudo npx playwright install-deps ${browserName}`); + const distroId = getLinuxDistributionInfoSync()?.id || 'this distribution'; + throw new Error(`Missing system dependencies required to run browser ${browserName}. On ${distroId}, install the libraries it needs using your distribution's package manager (e.g. zypper, dnf or pacman).`); } if (error.message.includes('ProcessSingleton') || error.message.includes('exitCode=21')) throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); @@ -229,8 +234,8 @@ export function isProfileLocked(userDataDir: string): boolean { const fd = fs.openSync(lockPath, 'r+'); fs.closeSync(fd); return false; - } catch (e: any) { - return e.code !== 'ENOENT'; + } catch (e: unknown) { + return (e as NodeJS.ErrnoException).code !== 'ENOENT'; } } diff --git a/packages/utils/linuxUtils.ts b/packages/utils/linuxUtils.ts index 86d6362588694..168d1a16c842c 100644 --- a/packages/utils/linuxUtils.ts +++ b/packages/utils/linuxUtils.ts @@ -21,9 +21,10 @@ let didFailToReadOSRelease = false; let osRelease: { id: string, version: string, + idLike: string, } | undefined; -export function getLinuxDistributionInfoSync(): { id: string, version: string } | undefined { +export function getLinuxDistributionInfoSync(): { id: string, version: string, idLike: string } | undefined { if (process.platform !== 'linux') return undefined; if (!osRelease && !didFailToReadOSRelease) { @@ -35,6 +36,7 @@ export function getLinuxDistributionInfoSync(): { id: string, version: string } osRelease = { id: fields.get('id') ?? '', version: fields.get('version_id') ?? '', + idLike: fields.get('id_like') ?? '', }; } catch (e) { didFailToReadOSRelease = true; @@ -43,6 +45,19 @@ export function getLinuxDistributionInfoSync(): { id: string, version: string } return osRelease; } +// Distributions whose system dependencies Playwright can install or describe with apt. +// ID_LIKE covers derivatives (e.g. Pop!_OS, Mint, Kali) that set it to "debian"/"ubuntu". +const DEBIAN_BASED_DISTRO_IDS = new Set(['ubuntu', 'debian', 'pop', 'neon', 'tuxedo', 'linuxmint', 'raspbian']); + +export function isDebianBasedDistro(): boolean { + const info = getLinuxDistributionInfoSync(); + if (!info) + return false; + if (DEBIAN_BASED_DISTRO_IDS.has(info.id)) + return true; + return info.idLike.split(' ').some(like => like === 'debian' || like === 'ubuntu'); +} + function parseOSReleaseText(osReleaseText: string): Map { const fields = new Map(); for (const line of osReleaseText.split('\n')) {