diff --git a/.github/workflows/tests_rhel.yml b/.github/workflows/tests_rhel.yml new file mode 100644 index 0000000000000..97cea27778a32 --- /dev/null +++ b/.github/workflows/tests_rhel.yml @@ -0,0 +1,44 @@ +name: RHEL / Rocky Linux 9 (community) + +on: + push: + branches: + - main + - release-* + pull_request: + paths: + - packages/utils/hostPlatform.ts + - packages/utils/linuxUtils.ts + - packages/playwright-core/src/server/registry/nativeDeps.ts + - packages/playwright-core/src/server/registry/dependencies.ts + - packages/playwright-core/src/server/registry/index.ts + +jobs: + rhel9-chromium: + name: Rocky Linux 9 — Chromium smoke test + runs-on: ubuntu-latest + container: + image: rockylinux:9 + + steps: + - uses: actions/checkout@v6 + + - name: Enable Node.js 20 module stream + run: | + dnf module enable -y nodejs:20 + dnf install -y nodejs + + - name: Install npm dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Install OS-level Chromium dependencies + run: npx playwright install-deps chromium + + - name: Install Chromium browser + run: npx playwright install chromium + + - name: Chromium headless smoke test + run: node utils/linux-browser-dependencies/inside_docker/rhel9-smoke-test.js diff --git a/packages/playwright-core/src/server/registry/dependencies.ts b/packages/playwright-core/src/server/registry/dependencies.ts index 48e96f58c900a..92719aa21799c 100644 --- a/packages/playwright-core/src/server/registry/dependencies.ts +++ b/packages/playwright-core/src/server/registry/dependencies.ts @@ -20,7 +20,8 @@ import os from 'os'; import path from 'path'; import { wrapInASCIIBox } from '@utils/ascii'; -import { hostPlatform, isOfficiallySupportedPlatform } from '@utils/hostPlatform'; +import { hostPlatform, isOfficiallySupportedPlatform, isRhelFamilyDistro } from '@utils/hostPlatform'; +import { getLinuxDistributionInfoSync } from '@utils/linuxUtils'; import { spawnAsync } from '@utils/spawnAsync'; import { getPlaywrightVersion } from '../userAgent'; import { deps } from './nativeDeps'; @@ -92,8 +93,18 @@ export async function installDependenciesWindows(targets: Set, export async function installDependenciesLinux(targets: Set, dryRun: boolean) { const libraries: string[] = []; const platform = hostPlatform; - if (!isOfficiallySupportedPlatform) - console.warn(`BEWARE: your OS is not officially supported by Playwright; installing dependencies for ${platform} as a fallback.`); // eslint-disable-line no-console + if (!isOfficiallySupportedPlatform) { + if (platform === '') { + // Provide an actionable message for RHEL 8 users who fell through to . + const distroInfo = getLinuxDistributionInfoSync(); + if (distroInfo && isRhelFamilyDistro(distroInfo) && parseInt(distroInfo.version, 10) < 9) + throw new Error('RHEL/Rocky Linux 8 and earlier are not supported by Playwright. Please upgrade to RHEL 9+ or use a supported platform.'); + } + if (platform.startsWith('rhel')) + console.warn(`BEWARE: your OS is not officially supported by Playwright; installing dependencies for ${platform} on a best-effort basis.`); // eslint-disable-line no-console + else + console.warn(`BEWARE: your OS is not officially supported by Playwright; installing dependencies for ${platform} as a fallback.`); // eslint-disable-line no-console + } for (const target of targets) { const info = deps[platform]; if (!info) { @@ -105,12 +116,37 @@ export async function installDependenciesLinux(targets: Set, dr const uniqueLibraries = Array.from(new Set(libraries)); if (!dryRun) console.log(`Installing dependencies...`); // eslint-disable-line no-console - const commands: string[] = []; - commands.push('apt-get update'); - commands.push(['apt-get', 'install', '-y', '--no-install-recommends', - ...uniqueLibraries, - ].join(' ')); - const { command, args, elevatedPermissions } = await transformCommandsForRoot(commands); + if (platform.startsWith('rhel')) { + await installDependenciesLinuxRhel(uniqueLibraries, dryRun); + } else { + const commands: string[] = []; + commands.push('apt-get update'); + commands.push(['apt-get', 'install', '-y', '--no-install-recommends', + ...uniqueLibraries, + ].join(' ')); + const { command, args, elevatedPermissions } = await transformCommandsForRoot(commands); + if (dryRun) { + console.log(`${command} ${quoteProcessArgs(args).join(' ')}`); // eslint-disable-line no-console + return; + } + if (elevatedPermissions) + console.log('Switching to root user to install dependencies...'); // eslint-disable-line no-console + const child = childProcess.spawn(command, args, { stdio: 'inherit' }); + await new Promise((resolve, reject) => { + child.on('exit', (code: number) => code === 0 ? resolve() : reject(new Error(`Installation process exited with code: ${code}`))); + child.on('error', reject); + }); + } +} + +async function installDependenciesLinuxRhel(libraries: string[], dryRun: boolean) { + const dnfOrYum = await findDnfOrYum(); + const weakDepsFlag = dnfOrYum === 'dnf' ? ' --setopt=install_weak_deps=False' : ''; + const commands = [ + `${dnfOrYum} install -y epel-release`, + `${dnfOrYum} install -y${weakDepsFlag} ${libraries.join(' ')}`, + ]; + const { command, args, elevatedPermissions } = await transformCommandsForRoot(commands); if (dryRun) { console.log(`${command} ${quoteProcessArgs(args).join(' ')}`); // eslint-disable-line no-console return; @@ -119,11 +155,21 @@ export async function installDependenciesLinux(targets: Set, dr console.log('Switching to root user to install dependencies...'); // eslint-disable-line no-console const child = childProcess.spawn(command, args, { stdio: 'inherit' }); await new Promise((resolve, reject) => { - child.on('exit', (code: number) => code === 0 ? resolve() : reject(new Error(`Installation process exited with code: ${code}`))); + child.on('exit', (code: number) => code === 0 ? resolve() : reject(new Error(`Installation process exited with code: ${code}. Check that EPEL is accessible and that all required packages are available for your architecture.`))); child.on('error', reject); }); } +async function findDnfOrYum(): Promise { + const dnfResult = await spawnAsync('which', ['dnf'], {}); + if (dnfResult.code === 0) + return 'dnf'; + const yumResult = await spawnAsync('which', ['yum'], {}); + if (yumResult.code === 0) + return 'yum'; + throw new Error("Neither 'dnf' nor 'yum' found on PATH. Please install one and retry."); +} + export async function validateDependenciesWindows(sdkLanguage: string, windowsExeAndDllDirectories: string[]) { const directoryPaths = windowsExeAndDllDirectories; const lddPaths: string[] = []; @@ -208,7 +254,7 @@ export async function validateDependenciesLinux(sdkLanguage: string, linuxLddDir const libraryToPackageNameMapping = deps[hostPlatform] ? { ...(deps[hostPlatform]?.lib2package || {}), - ...MANUAL_LIBRARY_TO_PACKAGE_NAME_UBUNTU, + ...(hostPlatform.startsWith('rhel') ? {} : MANUAL_LIBRARY_TO_PACKAGE_NAME_UBUNTU), } : {}; // Translate missing dependencies to package names to install with apt. for (const missingDep of missingDeps) { @@ -220,6 +266,9 @@ export async function validateDependenciesLinux(sdkLanguage: string, linuxLddDir } const maybeSudo = process.getuid?.() && os.platform() !== 'win32' ? 'sudo ' : ''; + const isRhel = hostPlatform.startsWith('rhel'); + const pkgMgr = isRhel ? 'dnf' : 'apt'; + const pkgInstallCmd = isRhel ? 'dnf install' : 'apt-get install'; const dockerInfo = readDockerVersionSync(); const errorLines = [ `Host system is missing dependencies to run browsers.`, @@ -242,9 +291,9 @@ export async function validateDependenciesLinux(sdkLanguage: string, linuxLddDir ``, ` ${maybeSudo}${buildPlaywrightCLICommand(sdkLanguage, 'install-deps')}`, ``, - `- (alternative 2) use apt inside Docker:`, + `- (alternative 2) use ${pkgMgr} inside Docker:`, ``, - ` ${maybeSudo}apt-get install ${[...missingPackages].join('\\\n ')}`, + ` ${maybeSudo}${pkgInstallCmd} ${[...missingPackages].join('\\\n ')}`, ``, `<3 Playwright Team`, ]); @@ -256,8 +305,8 @@ export async function validateDependenciesLinux(sdkLanguage: string, linuxLddDir ``, ` ${maybeSudo}${buildPlaywrightCLICommand(sdkLanguage, 'install-deps')}`, ``, - `Alternatively, use apt:`, - ` ${maybeSudo}apt-get install ${[...missingPackages].join('\\\n ')}`, + `Alternatively, use ${pkgMgr}:`, + ` ${maybeSudo}${pkgInstallCmd} ${[...missingPackages].join('\\\n ')}`, ``, `<3 Playwright Team`, ]); diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 31e1b9cb90e23..6160da4a38000 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -159,6 +159,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', 'debian13-x64': cftUrl('linux64/chrome-linux64.zip'), 'debian13-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', + 'rhel9-x64': cftUrl('linux64/chrome-linux64.zip'), + 'rhel9-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', 'mac10.13': cftUrl('mac-x64/chrome-mac-x64.zip'), 'mac10.14': cftUrl('mac-x64/chrome-mac-x64.zip'), 'mac10.15': cftUrl('mac-x64/chrome-mac-x64.zip'), @@ -192,6 +194,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip', 'debian13-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), 'debian13-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip', + 'rhel9-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), + 'rhel9-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip', 'mac10.13': undefined, 'mac10.14': undefined, 'mac10.15': undefined, @@ -225,6 +229,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', 'debian13-x64': cftUrl('linux64/chrome-linux64.zip'), 'debian13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', + 'rhel9-x64': cftUrl('linux64/chrome-linux64.zip'), + 'rhel9-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', 'mac10.13': cftUrl('mac-x64/chrome-mac-x64.zip'), 'mac10.14': cftUrl('mac-x64/chrome-mac-x64.zip'), 'mac10.15': cftUrl('mac-x64/chrome-mac-x64.zip'), @@ -258,6 +264,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', 'debian13-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), 'debian13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', + 'rhel9-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), + 'rhel9-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', 'mac10.13': undefined, 'mac10.14': undefined, 'mac10.15': undefined, @@ -291,6 +299,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/firefox/%s/firefox-debian-12-arm64.zip', 'debian13-x64': 'builds/firefox/%s/firefox-debian-13.zip', 'debian13-arm64': 'builds/firefox/%s/firefox-debian-13-arm64.zip', + 'rhel9-x64': undefined, + 'rhel9-arm64': undefined, 'mac10.13': 'builds/firefox/%s/firefox-mac.zip', 'mac10.14': 'builds/firefox/%s/firefox-mac.zip', 'mac10.15': 'builds/firefox/%s/firefox-mac.zip', @@ -324,6 +334,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-12-arm64.zip', 'debian13-x64': 'builds/firefox-beta/%s/firefox-beta-debian-12.zip', 'debian13-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-12-arm64.zip', + 'rhel9-x64': undefined, + 'rhel9-arm64': undefined, 'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac.zip', 'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac.zip', 'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac.zip', @@ -357,6 +369,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/webkit/%s/webkit-debian-12-arm64.zip', 'debian13-x64': 'builds/webkit/%s/webkit-debian-13.zip', 'debian13-arm64': 'builds/webkit/%s/webkit-debian-13-arm64.zip', + 'rhel9-x64': undefined, + 'rhel9-arm64': undefined, 'mac10.13': undefined, 'mac10.14': undefined, 'mac10.15': undefined, @@ -390,6 +404,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', 'debian13-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', 'debian13-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', + 'rhel9-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', + 'rhel9-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', 'mac10.13': 'builds/ffmpeg/%s/ffmpeg-mac.zip', 'mac10.14': 'builds/ffmpeg/%s/ffmpeg-mac.zip', 'mac10.15': 'builds/ffmpeg/%s/ffmpeg-mac.zip', @@ -423,6 +439,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': undefined, 'debian13-x64': undefined, 'debian13-arm64': undefined, + 'rhel9-x64': undefined, + 'rhel9-arm64': undefined, 'mac10.13': undefined, 'mac10.14': undefined, 'mac10.15': undefined, @@ -456,6 +474,8 @@ const DOWNLOAD_PATHS: Record = { 'debian12-arm64': 'builds/android/%s/android.zip', 'debian13-x64': 'builds/android/%s/android.zip', 'debian13-arm64': 'builds/android/%s/android.zip', + 'rhel9-x64': undefined, + 'rhel9-arm64': undefined, 'mac10.13': 'builds/android/%s/android.zip', 'mac10.14': 'builds/android/%s/android.zip', 'mac10.15': 'builds/android/%s/android.zip', diff --git a/packages/playwright-core/src/server/registry/nativeDeps.ts b/packages/playwright-core/src/server/registry/nativeDeps.ts index 5748b7b34d2a2..2b64891a30943 100644 --- a/packages/playwright-core/src/server/registry/nativeDeps.ts +++ b/packages/playwright-core/src/server/registry/nativeDeps.ts @@ -1281,3 +1281,63 @@ deps['debian13-arm64'] = { ...deps['debian13-x64'].lib2package, }, }; + +// Generated by: utils/linux-browser-dependencies/run.sh rockylinux:9 +deps['rhel9-x64'] = { + tools: [], + firefox: [], + webkit: [], + chromium: [ + 'alsa-lib', + 'at-spi2-atk', + 'at-spi2-core', + 'atk', + 'cairo', + 'cups-libs', + 'dbus-libs', + 'libX11', + 'libXcomposite', + 'libXdamage', + 'libXext', + 'libXfixes', + 'libXrandr', + 'libxcb', + 'libxkbcommon', + 'mesa-libgbm', + 'nspr', + 'nss', + 'nss-util', + 'pango', + ], + lib2package: { + 'libasound.so.2': 'alsa-lib', + 'libatk-1.0.so.0': 'atk', + 'libatk-bridge-2.0.so.0': 'at-spi2-atk', + 'libatspi.so.0': 'at-spi2-core', + 'libcairo.so.2': 'cairo', + 'libcups.so.2': 'cups-libs', + 'libdbus-1.so.3': 'dbus-libs', + 'libgbm.so.1': 'mesa-libgbm', + 'libnspr4.so': 'nspr', + 'libnss3.so': 'nss', + 'libnssutil3.so': 'nss-util', + 'libpango-1.0.so.0': 'pango', + 'libsmime3.so': 'nss', + 'libX11.so.6': 'libX11', + 'libxcb.so.1': 'libxcb', + 'libXcomposite.so.1': 'libXcomposite', + 'libXdamage.so.1': 'libXdamage', + 'libXext.so.6': 'libXext', + 'libXfixes.so.3': 'libXfixes', + 'libxkbcommon.so.0': 'libxkbcommon', + 'libXrandr.so.2': 'libXrandr', + }, +}; + +deps['rhel9-arm64'] = { + tools: [], + firefox: [], + webkit: [], + chromium: [...deps['rhel9-x64'].chromium], + lib2package: { ...deps['rhel9-x64'].lib2package }, +}; diff --git a/packages/utils/hostPlatform.ts b/packages/utils/hostPlatform.ts index 990907a0cc879..742ece6beb96f 100644 --- a/packages/utils/hostPlatform.ts +++ b/packages/utils/hostPlatform.ts @@ -35,6 +35,7 @@ export type HostPlatform = 'win64' | 'debian11-x64' | 'debian11-arm64' | 'debian12-x64' | 'debian12-arm64' | 'debian13-x64' | 'debian13-arm64' | + 'rhel9-x64' | 'rhel9-arm64' | ''; function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupportedPlatform: boolean } { @@ -74,58 +75,78 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte if (platform === 'linux') { if (!['x64', 'arm64'].includes(os.arch())) return { hostPlatform: '', isOfficiallySupportedPlatform: false }; - - const archSuffix = '-' + os.arch(); - const distroInfo = getLinuxDistributionInfoSync(); - - // Pop!_OS is ubuntu-based and has the same versions. - // KDE Neon is ubuntu-based and has the same versions. - // TUXEDO OS is ubuntu-based and has the same versions. - if (distroInfo?.id === 'ubuntu' || distroInfo?.id === 'pop' || distroInfo?.id === 'neon' || distroInfo?.id === 'tuxedo') { - const isUbuntu = distroInfo?.id === 'ubuntu'; - const version = distroInfo?.version; - const major = parseInt(distroInfo.version, 10); - if (major < 20) - return { hostPlatform: ('ubuntu18.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; - if (major < 22) - return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '20.04' }; - if (major < 24) - return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '22.04' }; - if (major < 26) - return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '24.04' }; - return { hostPlatform: ('ubuntu' + distroInfo.version + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; - } - // Linux Mint is ubuntu-based but does not have the same versions - if (distroInfo?.id === 'linuxmint') { - const mintMajor = parseInt(distroInfo.version, 10); - if (mintMajor <= 20) - return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; - if (mintMajor === 21) - return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; - return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; - } - if (distroInfo?.id === 'debian' || distroInfo?.id === 'raspbian') { - const isOfficiallySupportedPlatform = distroInfo?.id === 'debian'; - if (distroInfo?.version === '11') - return { hostPlatform: ('debian11' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - if (distroInfo?.version === '12') - return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - if (distroInfo?.version === '13') - return { hostPlatform: ('debian13' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - // use most recent supported release for 'debian testing' and 'unstable'. - // they never include a numeric version entry in /etc/os-release. - if (distroInfo?.version === '') - return { hostPlatform: ('debian13' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - } - return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; + return calculateLinuxPlatform(getLinuxDistributionInfoSync(), os.arch() as LinuxArch); } if (platform === 'win32') return { hostPlatform: 'win64', isOfficiallySupportedPlatform: true }; return { hostPlatform: '', isOfficiallySupportedPlatform: false }; } +const RHEL_IDS = new Set(['rocky', 'rhel', 'almalinux', 'centos', 'ol']); + +export function isRhelFamilyDistro(distroInfo: { id: string, idLike: string }): boolean { + return RHEL_IDS.has(distroInfo.id) || + (distroInfo.id !== 'fedora' && distroInfo.idLike.split(' ').some(id => RHEL_IDS.has(id))); +} + +type LinuxArch = 'x64' | 'arm64'; + export const { hostPlatform, isOfficiallySupportedPlatform } = calculatePlatform(); +export function calculateLinuxPlatform(distroInfo: { id: string, version: string, idLike: string } | undefined, arch: LinuxArch): { hostPlatform: HostPlatform, isOfficiallySupportedPlatform: boolean } { + const archSuffix = '-' + arch; + // Pop!_OS is ubuntu-based and has the same versions. + // KDE Neon is ubuntu-based and has the same versions. + // TUXEDO OS is ubuntu-based and has the same versions. + if (distroInfo?.id === 'ubuntu' || distroInfo?.id === 'pop' || distroInfo?.id === 'neon' || distroInfo?.id === 'tuxedo') { + const isUbuntu = distroInfo?.id === 'ubuntu'; + const version = distroInfo?.version; + const major = parseInt(distroInfo.version, 10); + if (major < 20) + return { hostPlatform: ('ubuntu18.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; + if (major < 22) + return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '20.04' }; + if (major < 24) + return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '22.04' }; + if (major < 26) + return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '24.04' }; + return { hostPlatform: ('ubuntu' + distroInfo.version + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; + } + // Linux Mint is ubuntu-based but does not have the same versions + if (distroInfo?.id === 'linuxmint') { + const mintMajor = parseInt(distroInfo.version, 10); + if (mintMajor <= 20) + return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; + if (mintMajor === 21) + return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; + return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; + } + if (distroInfo?.id === 'debian' || distroInfo?.id === 'raspbian') { + const isOfficiallySupportedPlatform = distroInfo?.id === 'debian'; + if (distroInfo?.version === '11') + return { hostPlatform: ('debian11' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; + if (distroInfo?.version === '12') + return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; + if (distroInfo?.version === '13') + return { hostPlatform: ('debian13' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; + // use most recent supported release for 'debian testing' and 'unstable'. + // they never include a numeric version entry in /etc/os-release. + if (distroInfo?.version === '') + return { hostPlatform: ('debian13' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; + } + // RHEL-family: Rocky Linux, RHEL, AlmaLinux, CentOS Stream, Oracle Linux. + // Fedora is explicitly excluded — its packages are incompatible. + if (distroInfo && isRhelFamilyDistro(distroInfo)) { + const major = parseInt(distroInfo!.version, 10); + if (major >= 9) + return { hostPlatform: ('rhel9' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; + // RHEL 8 and earlier: fall through to so the unsupported-platform + // warning fires. The RHEL 8 message is emitted by installDependenciesLinux. + return { hostPlatform: '', isOfficiallySupportedPlatform: false }; + } + return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; +} + export type ShortPlatform = 'mac-x64' | 'mac-arm64' | 'linux-x64' | 'linux-arm64' | 'win-x64' | ''; function toShortPlatform(hostPlatform: HostPlatform): ShortPlatform { diff --git a/packages/utils/linuxUtils.ts b/packages/utils/linuxUtils.ts index 86d6362588694..080246213fe49 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; diff --git a/tests/library/utils/hostPlatform.spec.ts b/tests/library/utils/hostPlatform.spec.ts new file mode 100644 index 0000000000000..0e6b4a46049f0 --- /dev/null +++ b/tests/library/utils/hostPlatform.spec.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from '@playwright/test'; +import { calculateLinuxPlatform } from '../../../packages/utils/hostPlatform'; + +test.describe('calculateLinuxPlatform - RHEL family detection', () => { + test('rocky linux 9 maps to rhel9-x64', () => { + const result = calculateLinuxPlatform({ id: 'rocky', version: '9', idLike: 'rhel centos fedora' }, 'x64'); + expect(result.hostPlatform).toBe('rhel9-x64'); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); + + test('rhel 9.3 maps to rhel9-x64', () => { + const result = calculateLinuxPlatform({ id: 'rhel', version: '9.3', idLike: '' }, 'x64'); + expect(result.hostPlatform).toBe('rhel9-x64'); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); + + test('almalinux 9.1 maps to rhel9-x64', () => { + const result = calculateLinuxPlatform({ id: 'almalinux', version: '9.1', idLike: 'rhel centos fedora' }, 'x64'); + expect(result.hostPlatform).toBe('rhel9-x64'); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); + + test('centos 9 maps to rhel9-x64', () => { + const result = calculateLinuxPlatform({ id: 'centos', version: '9', idLike: 'rhel fedora' }, 'x64'); + expect(result.hostPlatform).toBe('rhel9-x64'); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); + + test('fedora 40 does NOT map to rhel9-*', () => { + const result = calculateLinuxPlatform({ id: 'fedora', version: '40', idLike: '' }, 'x64'); + expect(result.hostPlatform).not.toContain('rhel9'); + }); + + test('unknown distro with id_like containing rhel maps to rhel9-x64 for version >= 9', () => { + const result = calculateLinuxPlatform({ id: 'unknown', version: '9', idLike: 'rhel centos fedora' }, 'x64'); + expect(result.hostPlatform).toBe('rhel9-x64'); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); + + test('rocky linux 8.9 does NOT map to rhel9-* (falls to unknown)', () => { + const result = calculateLinuxPlatform({ id: 'rocky', version: '8.9', idLike: 'rhel centos fedora' }, 'x64'); + expect(result.hostPlatform).toBe(''); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); + + test('rocky linux 9 on arm64 maps to rhel9-arm64', () => { + const result = calculateLinuxPlatform({ id: 'rocky', version: '9', idLike: 'rhel centos fedora' }, 'arm64'); + expect(result.hostPlatform).toBe('rhel9-arm64'); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); + + test('fedora with id_like rhel does NOT map to rhel9-* (fedora excluded by id check)', () => { + const result = calculateLinuxPlatform({ id: 'fedora', version: '9', idLike: 'rhel' }, 'x64'); + expect(result.hostPlatform).not.toContain('rhel9'); + }); + + test('oracle linux 9.3 maps to rhel9-x64', () => { + const result = calculateLinuxPlatform({ id: 'ol', version: '9.3', idLike: '' }, 'x64'); + expect(result.hostPlatform).toBe('rhel9-x64'); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); + + test('undefined distroInfo falls through to ubuntu24.04 default', () => { + const result = calculateLinuxPlatform(undefined, 'x64'); + expect(result.hostPlatform).toBe('ubuntu24.04-x64'); + expect(result.isOfficiallySupportedPlatform).toBe(false); + }); +}); diff --git a/utils/linux-browser-dependencies/inside_docker/list_dependencies.js b/utils/linux-browser-dependencies/inside_docker/list_dependencies.js index 8dcee31b6879a..cecf6f532e428 100644 --- a/utils/linux-browser-dependencies/inside_docker/list_dependencies.js +++ b/utils/linux-browser-dependencies/inside_docker/list_dependencies.js @@ -1,285 +1,29 @@ #!/usr/bin/env node -const fs = require('fs'); -const util = require('util'); -const path = require('path'); -const {spawn} = require('child_process'); -const {registryDirectory} = require('playwright-core/lib/coreBundle').registry; +const { run, runCommand } = require('./list_dependencies_base'); -const readdirAsync = util.promisify(fs.readdir.bind(fs)); -const readFileAsync = util.promisify(fs.readFile.bind(fs)); +run({ findPackages, pickPackage }).catch(err => { + console.error(err); + process.exit(1); +}); -const readline = require('readline'); - -// These libraries are accessed dynamically by browsers using `dlopen` system call and -// thus have to be installed in the system. -// -// Tip: to assess which libraries are getting opened dynamically, one can use `strace`: -// -// strace -f -e trace=open,openat -// -const DL_OPEN_LIBRARIES = { - chromium: [], - firefox: [], - webkit: [ 'libGLESv2.so.2' ], -}; - -(async () => { - console.log('Working on:', await getDistributionName()); - console.log('Started at:', currentTime()); - const browserDescriptors = (await readdirAsync(registryDirectory)).filter(dir => !dir.startsWith('.')).map(dir => ({ - // Full browser name, e.g. `webkit-1144` - name: dir, - // Full patch to browser files - path: path.join(registryDirectory, dir), - // All files that we will try to inspect for missing dependencies. - filePaths: [], - // All libraries that are missing for the browser. - missingLibraries: new Set(), - // All packages required for the browser. - requiredPackages: new Set(), - // Libraries that we didn't find a package. - unresolvedLibraries: new Set(), - })); - - // Collect all missing libraries for all browsers. - const allMissingLibraries = new Set(); - for (const descriptor of browserDescriptors) { - // Browser vendor, can be `webkit`, `firefox` or `chromium` - const vendor = descriptor.name.split('-')[0]; - for (const library of (DL_OPEN_LIBRARIES[vendor] || [])) { - descriptor.missingLibraries.add(library); - allMissingLibraries.add(library); - } - - const {stdout} = await runCommand('find', [descriptor.path, '-type', 'f']); - descriptor.filePaths = stdout.trim().split('\n').map(f => f.trim()).filter(filePath => !filePath.toLowerCase().endsWith('.sh')); - await Promise.all(descriptor.filePaths.map(async filePath => { - const missingLibraries = await missingFileDependencies(filePath); - for (const library of missingLibraries) { - descriptor.missingLibraries.add(library); - allMissingLibraries.add(library); - } - })); - } - - const libraryToPackage = new Map(); - const ambiguityLibraries = new Map(); - - // Map missing libraries to packages that could be installed to fulfill the dependency. - console.log(`Finding packages for ${allMissingLibraries.size} missing libraries...`); - - for (let i = 0, array = [...allMissingLibraries].sort(); i < allMissingLibraries.size; ++i) { - const library = array[i]; - const packages = await findPackages(library); - - const progress = `${i + 1}/${allMissingLibraries.size}`; - console.log(`${progress.padStart(7)}: ${library} => ${JSON.stringify(packages)}`); - - if (!packages.length) { - const browsersWithMissingLibrary = browserDescriptors.filter(d => d.missingLibraries.has(library)).map(d => d.name).join(', '); - const PADDING = ''.padStart(7) + ' '; - console.log(PADDING + `ERROR: failed to resolve '${library}' required by ${browsersWithMissingLibrary}`); - } else if (packages.length === 1) { - libraryToPackage.set(library, packages[0]); - } else { - ambiguityLibraries.set(library, packages); - } - } - - console.log(''); - console.log(`Picking packages for ${ambiguityLibraries.size} libraries that have multiple package candidates`); - // Pick packages to install to fulfill missing libraries. - // - // This is a 2-step process: - // 1. Pick easy libraries by filtering out debug, test and dev packages. - // 2. After that, pick packages that we already picked before. - - // Step 1: pick libraries that are easy to pick. - const totalAmbiguityLibraries = ambiguityLibraries.size; - for (const [library, packages] of ambiguityLibraries) { - const package = pickPackage(library, packages); - if (!package) - continue; - libraryToPackage.set(library, package); - ambiguityLibraries.delete(library); - const progress = `${totalAmbiguityLibraries - ambiguityLibraries.size}/${totalAmbiguityLibraries}`; - console.log(`${progress.padStart(7)}: ${library} => ${package}`); - console.log(''.padStart(9) + `(note) packages are ${JSON.stringify(packages)}`); - } - // 2nd pass - prefer packages that we already picked. - const allUsedPackages = new Set(libraryToPackage.values()); - for (const [library, packages] of ambiguityLibraries) { - const package = packages.find(package => allUsedPackages.has(package)); - if (!package) - continue; - libraryToPackage.set(library, package); - ambiguityLibraries.delete(library); - const progress = `${totalAmbiguityLibraries - ambiguityLibraries.size}/${totalAmbiguityLibraries}`; - console.log(`${progress.padStart(7)}: ${library} => ${package}`); - console.log(''.padStart(9) + `(note) packages are ${JSON.stringify(packages)}`); - } - - // 3rd pass - prompt user to resolve ambiguity. - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - const promptAsync = (question) => new Promise(resolve => rl.question(question, resolve)); - - // Report all ambiguities that were failed to resolve. - for (const [library, packages] of ambiguityLibraries) { - const question = [ - `Pick a package for '${library}':`, - ...packages.map((package, index) => ` (${index + 1}) ${package}`), - 'Enter number: ', - ].join('\n'); - - const answer = await promptAsync(question); - const index = parseInt(answer, 10) - 1; - if (isNaN(index) || (index < 0) || (index >= packages.length)) { - console.error(`ERROR: unknown index "${answer}". Must be a number between 1 and ${packages.length}`); - process.exit(1); - } - const package = packages[index]; - - ambiguityLibraries.delete(library); - libraryToPackage.set(library, package); - console.log(answer); - console.log(`- ${library} => ${package}`); - } - rl.close(); - - // For each browser build a list of packages to install. - for (const descriptor of browserDescriptors) { - for (const library of descriptor.missingLibraries) { - const package = libraryToPackage.get(library); - if (package) - descriptor.requiredPackages.add(package); - else - descriptor.unresolvedLibraries.add(library); - } - } - - // Formatting results. - console.log(''); - console.log(`----- Library to package name mapping -----`); - console.log('{'); - const sortedEntries = [...libraryToPackage.entries()].sort((a, b) => a[0].localeCompare(b[0])); - for (const [library, package] of sortedEntries) - console.log(` "${library}": "${package}",`); - console.log('}'); - - // Packages and unresolved libraries for every browser - for (const descriptor of browserDescriptors) { - console.log(''); - console.log(`======= ${descriptor.name}: required packages =======`); - const requiredPackages = [...descriptor.requiredPackages].sort(); - console.log(JSON.stringify(requiredPackages, null, 2)); - console.log(''); - console.log(`------- ${descriptor.name}: unresolved libraries -------`); - const unresolvedLibraries = [...descriptor.unresolvedLibraries].sort(); - console.log(JSON.stringify(unresolvedLibraries, null, 2)); - } - - const status = browserDescriptors.some(d => d.unresolvedLibraries.size) ? 'FAILED' : 'SUCCESS'; - console.log(` - ==================== - ${status} - ==================== - `); -})(); +async function findPackages(libraryName) { + const {stdout} = await runCommand('apt-file', ['search', libraryName]); + if (!stdout.trim()) + return []; + const libs = stdout.trim().split('\n').map(line => line.split(':')[0]); + return [...new Set(libs)]; +} function pickPackage(library, packages) { // Step 1: try to filter out debug, test and dev packages. - packages = packages.filter(package => !package.endsWith('-dbg') && !package.endsWith('-test') && !package.endsWith('-dev') && !package.endsWith('-mesa')); + packages = packages.filter(p => !p.endsWith('-dbg') && !p.endsWith('-test') && !p.endsWith('-dev') && !p.endsWith('-mesa')); if (packages.length === 1) return packages[0]; // Step 2: use library name to filter packages with the same name. const prefix = library.split(/[-.]/).shift().toLowerCase(); - packages = packages.filter(package => package.toLowerCase().startsWith(prefix)); + packages = packages.filter(p => p.toLowerCase().startsWith(prefix)); if (packages.length === 1) return packages[0]; return null; } - -async function findPackages(libraryName) { - const {stdout} = await runCommand('apt-file', ['search', libraryName]); - if (!stdout.trim()) - return []; - const libs = stdout.trim().split('\n').map(line => line.split(':')[0]); - return [...new Set(libs)]; -} - -async function fileDependencies(filePath) { - const {stdout, code} = await lddAsync(filePath); - if (code !== 0) - return []; - const deps = stdout.split('\n').map(line => { - line = line.trim(); - const missing = line.includes('not found'); - const name = line.split('=>')[0].trim(); - return {name, missing}; - }); - return deps; -} - -async function missingFileDependencies(filePath) { - const deps = await fileDependencies(filePath); - return deps.filter(dep => dep.missing).map(dep => dep.name); -} - -async function lddAsync(filePath) { - let LD_LIBRARY_PATH = []; - // Some shared objects inside browser sub-folders link against libraries that - // ship with the browser. We consider these to be included, so we want to account - // for them in the LD_LIBRARY_PATH. - for (let dirname = path.dirname(filePath); dirname !== '/'; dirname = path.dirname(dirname)) - LD_LIBRARY_PATH.push(dirname); - return await runCommand('ldd', [filePath], { - cwd: path.dirname(filePath), - env: { - ...process.env, - LD_LIBRARY_PATH: LD_LIBRARY_PATH.join(':'), - }, - }); -} - -function runCommand(command, args, options = {}) { - const childProcess = spawn(command, args, options); - - return new Promise((resolve) => { - let stdout = ''; - let stderr = ''; - childProcess.stdout.on('data', data => stdout += data); - childProcess.stderr.on('data', data => stderr += data); - childProcess.on('close', (code) => { - resolve({stdout, stderr, code}); - }); - }); -} - -async function getDistributionName() { - const osReleaseText = await readFileAsync('/etc/os-release', 'utf8'); - const fields = new Map(); - for (const line of osReleaseText.split('\n')) { - const tokens = line.split('='); - const name = tokens.shift(); - let value = tokens.join('=').trim(); - if (value.startsWith('"') && value.endsWith('"')) - value = value.substring(1, value.length - 1); - if (!name) - continue; - fields.set(name.toLowerCase(), value); - } - return fields.get('pretty_name') || ''; -} - -function currentTime() { - const date = new Date(); - const dateTimeFormat = new Intl.DateTimeFormat('en', { year: 'numeric', month: 'short', day: '2-digit' }); - const [{ value: month },,{ value: day },,{ value: year }] = dateTimeFormat .formatToParts(date ); - return `${month} ${day}, ${year}`; -} - diff --git a/utils/linux-browser-dependencies/inside_docker/list_dependencies_base.js b/utils/linux-browser-dependencies/inside_docker/list_dependencies_base.js new file mode 100644 index 0000000000000..0b4cef0defe67 --- /dev/null +++ b/utils/linux-browser-dependencies/inside_docker/list_dependencies_base.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const util = require('util'); +const path = require('path'); +const {spawn} = require('child_process'); +const {registryDirectory} = require('playwright-core/lib/coreBundle').registry; + +const readdirAsync = util.promisify(fs.readdir.bind(fs)); +const readFileAsync = util.promisify(fs.readFile.bind(fs)); + +const readline = require('readline'); + +// These libraries are accessed dynamically by browsers using `dlopen` system call and +// thus have to be installed in the system. +// +// Tip: to assess which libraries are getting opened dynamically, one can use `strace`: +// +// strace -f -e trace=open,openat +// +const DL_OPEN_LIBRARIES = { + chromium: [], + firefox: [], + webkit: [ 'libGLESv2.so.2' ], +}; + +/** + * @param {{ findPackages: (library: string) => Promise, pickPackage: (library: string, packages: string[]) => string | null }} opts + */ +async function run({ findPackages, pickPackage }) { + console.log('Working on:', await getDistributionName()); + console.log('Started at:', currentTime()); + const browserDescriptors = (await readdirAsync(registryDirectory)).filter(dir => !dir.startsWith('.')).map(dir => ({ + // Full browser name, e.g. `webkit-1144` + name: dir, + // Full patch to browser files + path: path.join(registryDirectory, dir), + // All files that we will try to inspect for missing dependencies. + filePaths: [], + // All libraries that are missing for the browser. + missingLibraries: new Set(), + // All packages required for the browser. + requiredPackages: new Set(), + // Libraries that we didn't find a package. + unresolvedLibraries: new Set(), + })); + + // Collect all missing libraries for all browsers. + const allMissingLibraries = new Set(); + for (const descriptor of browserDescriptors) { + // Browser vendor, can be `webkit`, `firefox` or `chromium` + const vendor = descriptor.name.split('-')[0]; + for (const library of (DL_OPEN_LIBRARIES[vendor] || [])) { + descriptor.missingLibraries.add(library); + allMissingLibraries.add(library); + } + + const {stdout} = await runCommand('find', [descriptor.path, '-type', 'f']); + descriptor.filePaths = stdout.trim().split('\n').map(f => f.trim()).filter(filePath => !filePath.toLowerCase().endsWith('.sh')); + await Promise.all(descriptor.filePaths.map(async filePath => { + const missingLibraries = await missingFileDependencies(filePath); + for (const library of missingLibraries) { + descriptor.missingLibraries.add(library); + allMissingLibraries.add(library); + } + })); + } + + const libraryToPackage = new Map(); + const ambiguityLibraries = new Map(); + + // Map missing libraries to packages that could be installed to fulfill the dependency. + console.log(`Finding packages for ${allMissingLibraries.size} missing libraries...`); + + for (let i = 0, array = [...allMissingLibraries].sort(); i < allMissingLibraries.size; ++i) { + const library = array[i]; + const packages = await findPackages(library); + + const progress = `${i + 1}/${allMissingLibraries.size}`; + console.log(`${progress.padStart(7)}: ${library} => ${JSON.stringify(packages)}`); + + if (!packages.length) { + const browsersWithMissingLibrary = browserDescriptors.filter(d => d.missingLibraries.has(library)).map(d => d.name).join(', '); + const PADDING = ''.padStart(7) + ' '; + console.log(PADDING + `ERROR: failed to resolve '${library}' required by ${browsersWithMissingLibrary}`); + } else if (packages.length === 1) { + libraryToPackage.set(library, packages[0]); + } else { + ambiguityLibraries.set(library, packages); + } + } + + console.log(''); + console.log(`Picking packages for ${ambiguityLibraries.size} libraries that have multiple package candidates`); + // Pick packages to install to fulfill missing libraries. + // + // This is a 2-step process: + // 1. Pick easy libraries by filtering out debug, test and dev packages. + // 2. After that, pick packages that we already picked before. + + // Step 1: pick libraries that are easy to pick. + const totalAmbiguityLibraries = ambiguityLibraries.size; + for (const [library, packages] of ambiguityLibraries) { + const pkg = pickPackage(library, packages); + if (!pkg) + continue; + libraryToPackage.set(library, pkg); + ambiguityLibraries.delete(library); + const progress = `${totalAmbiguityLibraries - ambiguityLibraries.size}/${totalAmbiguityLibraries}`; + console.log(`${progress.padStart(7)}: ${library} => ${pkg}`); + console.log(''.padStart(9) + `(note) packages are ${JSON.stringify(packages)}`); + } + // 2nd pass - prefer packages that we already picked. + const allUsedPackages = new Set(libraryToPackage.values()); + for (const [library, packages] of ambiguityLibraries) { + const pkg = packages.find(p => allUsedPackages.has(p)); + if (!pkg) + continue; + libraryToPackage.set(library, pkg); + ambiguityLibraries.delete(library); + const progress = `${totalAmbiguityLibraries - ambiguityLibraries.size}/${totalAmbiguityLibraries}`; + console.log(`${progress.padStart(7)}: ${library} => ${pkg}`); + console.log(''.padStart(9) + `(note) packages are ${JSON.stringify(packages)}`); + } + + // 3rd pass - prompt user to resolve ambiguity. + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + const promptAsync = (question) => new Promise(resolve => rl.question(question, resolve)); + + // Report all ambiguities that were failed to resolve. + for (const [library, packages] of ambiguityLibraries) { + const question = [ + `Pick a package for '${library}':`, + ...packages.map((p, index) => ` (${index + 1}) ${p}`), + 'Enter number: ', + ].join('\n'); + + const answer = await promptAsync(question); + const index = parseInt(answer, 10) - 1; + if (isNaN(index) || (index < 0) || (index >= packages.length)) { + console.error(`ERROR: unknown index "${answer}". Must be a number between 1 and ${packages.length}`); + process.exit(1); + } + const pkg = packages[index]; + + ambiguityLibraries.delete(library); + libraryToPackage.set(library, pkg); + console.log(answer); + console.log(`- ${library} => ${pkg}`); + } + rl.close(); + + // For each browser build a list of packages to install. + for (const descriptor of browserDescriptors) { + for (const library of descriptor.missingLibraries) { + const pkg = libraryToPackage.get(library); + if (pkg) + descriptor.requiredPackages.add(pkg); + else + descriptor.unresolvedLibraries.add(library); + } + } + + // Formatting results. + console.log(''); + console.log(`----- Library to package name mapping -----`); + console.log('{'); + const sortedEntries = [...libraryToPackage.entries()].sort((a, b) => a[0].localeCompare(b[0])); + for (const [library, pkg] of sortedEntries) + console.log(` "${library}": "${pkg}",`); + console.log('}'); + + // Packages and unresolved libraries for every browser + for (const descriptor of browserDescriptors) { + console.log(''); + console.log(`======= ${descriptor.name}: required packages =======`); + const requiredPackages = [...descriptor.requiredPackages].sort(); + console.log(JSON.stringify(requiredPackages, null, 2)); + console.log(''); + console.log(`------- ${descriptor.name}: unresolved libraries -------`); + const unresolvedLibraries = [...descriptor.unresolvedLibraries].sort(); + console.log(JSON.stringify(unresolvedLibraries, null, 2)); + } + + const status = browserDescriptors.some(d => d.unresolvedLibraries.size) ? 'FAILED' : 'SUCCESS'; + console.log(` + ==================== + ${status} + ==================== + `); +} + +async function fileDependencies(filePath) { + const {stdout, code} = await lddAsync(filePath); + if (code !== 0) + return []; + const deps = stdout.split('\n').map(line => { + line = line.trim(); + const missing = line.includes('not found'); + const name = line.split('=>')[0].trim(); + return {name, missing}; + }); + return deps; +} + +async function missingFileDependencies(filePath) { + const deps = await fileDependencies(filePath); + return deps.filter(dep => dep.missing).map(dep => dep.name); +} + +async function lddAsync(filePath) { + let LD_LIBRARY_PATH = []; + // Some shared objects inside browser sub-folders link against libraries that + // ship with the browser. We consider these to be included, so we want to account + // for them in the LD_LIBRARY_PATH. + for (let dirname = path.dirname(filePath); dirname !== '/'; dirname = path.dirname(dirname)) + LD_LIBRARY_PATH.push(dirname); + return await runCommand('ldd', [filePath], { + cwd: path.dirname(filePath), + env: { + ...process.env, + LD_LIBRARY_PATH: LD_LIBRARY_PATH.join(':'), + }, + }); +} + +function runCommand(command, args, options = {}) { + const childProcess = spawn(command, args, options); + + return new Promise((resolve) => { + let stdout = ''; + let stderr = ''; + childProcess.stdout.on('data', data => stdout += data); + childProcess.stderr.on('data', data => stderr += data); + childProcess.on('close', (code) => { + resolve({stdout, stderr, code}); + }); + }); +} + +async function getDistributionName() { + const osReleaseText = await readFileAsync('/etc/os-release', 'utf8'); + const fields = new Map(); + for (const line of osReleaseText.split('\n')) { + const tokens = line.split('='); + const name = tokens.shift(); + let value = tokens.join('=').trim(); + if (value.startsWith('"') && value.endsWith('"')) + value = value.substring(1, value.length - 1); + if (!name) + continue; + fields.set(name.toLowerCase(), value); + } + return fields.get('pretty_name') || ''; +} + +function currentTime() { + const date = new Date(); + const dateTimeFormat = new Intl.DateTimeFormat('en', { year: 'numeric', month: 'short', day: '2-digit' }); + const [{ value: month },,{ value: day },,{ value: year }] = dateTimeFormat.formatToParts(date); + return `${month} ${day}, ${year}`; +} + +module.exports = { run, runCommand }; diff --git a/utils/linux-browser-dependencies/inside_docker/list_dependencies_rhel.js b/utils/linux-browser-dependencies/inside_docker/list_dependencies_rhel.js new file mode 100644 index 0000000000000..a9a4402a0874c --- /dev/null +++ b/utils/linux-browser-dependencies/inside_docker/list_dependencies_rhel.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const { run, runCommand } = require('./list_dependencies_base'); + +run({ findPackages, pickPackage }).catch(err => { + console.error(err); + process.exit(1); +}); + +async function findPackages(libraryName) { + // dnf repoquery --whatprovides '' lists packages providing a file matching the pattern. + const {stdout} = await runCommand('dnf', ['repoquery', '--setopt=install_weak_deps=False', '-q', '--whatprovides', libraryName]); + if (!stdout.trim()) + return []; + // Output format: name-version-release.arch or name.arch + const pkgs = stdout.trim().split('\n').map(line => { + // Strip epoch, version, release, arch: keep just the package name + return line.trim().replace(/-[0-9].*$/, '').replace(/\.[^.]+$/, ''); + }).filter(Boolean); + return [...new Set(pkgs)]; +} + +function pickPackage(library, packages) { + packages = packages.filter(p => !p.endsWith('-debuginfo') && !p.endsWith('-devel') && !p.endsWith('-tests')); + if (packages.length === 1) + return packages[0]; + const prefix = library.split(/[-.]/).shift().toLowerCase(); + packages = packages.filter(p => p.toLowerCase().startsWith(prefix)); + if (packages.length === 1) + return packages[0]; + return null; +} diff --git a/utils/linux-browser-dependencies/inside_docker/process_rhel.sh b/utils/linux-browser-dependencies/inside_docker/process_rhel.sh new file mode 100755 index 0000000000000..058f6992ba475 --- /dev/null +++ b/utils/linux-browser-dependencies/inside_docker/process_rhel.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -e +set +x + +# Install Node.js 20 via dnf module stream (playwright requires Node >= 18) +dnf module enable -y nodejs:20 +dnf install -y nodejs + +# Install playwright-core +mkdir /root/tmp && cd /root/tmp && npm init -y && npm i /root/hostfolder/playwright-core.tar.gz && npx playwright-core install chromium + +cp /root/hostfolder/inside_docker/list_dependencies_rhel.js /root/tmp/list_dependencies_rhel.js + +FILENAME="RUN_RESULT" +if [[ -n $1 ]]; then + FILENAME=$1 +fi +node list_dependencies_rhel.js | tee "/root/hostfolder/$FILENAME" diff --git a/utils/linux-browser-dependencies/inside_docker/rhel9-smoke-test.js b/utils/linux-browser-dependencies/inside_docker/rhel9-smoke-test.js new file mode 100644 index 0000000000000..a97f26ed89221 --- /dev/null +++ b/utils/linux-browser-dependencies/inside_docker/rhel9-smoke-test.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +const { chromium } = require('playwright-core'); +(async () => { + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + await page.goto('data:text/html,Rocky Linux 9

Playwright on Rocky Linux 9

'); + const title = await page.title(); + if (title !== 'Rocky Linux 9') + throw new Error(`Unexpected title: ${title}`); + console.log('title:', title); + await browser.close(); + console.log('Chromium smoke test PASSED'); +})().catch(e => { console.error(e); process.exit(1); }); diff --git a/utils/linux-browser-dependencies/run.sh b/utils/linux-browser-dependencies/run.sh index 70971793b1c46..47fda975775bc 100755 --- a/utils/linux-browser-dependencies/run.sh +++ b/utils/linux-browser-dependencies/run.sh @@ -32,5 +32,10 @@ cd "$(dirname "$0")" # image. node ../../utils/pack_package.js playwright-core ./playwright-core.tar.gz -docker run --platform linux/amd64 -v $PWD:/root/hostfolder --rm -it "$1" /root/hostfolder/inside_docker/process.sh "$2" +IMAGE="$1" +if [[ "$IMAGE" == *rocky* ]] || [[ "$IMAGE" == *rhel* ]] || [[ "$IMAGE" == *centos* ]] || [[ "$IMAGE" == *almalinux* ]] || [[ "$IMAGE" == *oraclelinux* ]]; then + docker run --platform linux/amd64 -v $PWD:/root/hostfolder --rm -it "$IMAGE" /root/hostfolder/inside_docker/process_rhel.sh "$2" +else + docker run --platform linux/amd64 -v $PWD:/root/hostfolder --rm -it "$IMAGE" /root/hostfolder/inside_docker/process.sh "$2" +fi