diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 8a00626d19..5f2767d3f3 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -160,6 +160,10 @@ program } else { const forceReinstall = hasNoArguments ? false : !!options.force; await registry.install(executables, forceReinstall); + await registry.validateHostRequirementsForExecutablesIfNeeded(executables, process.env.PW_LANG_NAME || 'javascript').catch((e: Error) => { + e.name = 'Playwright Host validation warning'; + console.error(e); + }); } } catch (e) { console.log(`Failed to install browsers\n${e}`); diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 6efbc3be82..7540e40090 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -180,7 +180,7 @@ export abstract class BrowserType extends SdkObject { if (!registryExecutable || registryExecutable.browserName !== this._name) throw new Error(`Unsupported ${this._name} channel "${options.channel}"`); executable = registryExecutable.executablePathOrDie(this.attribution.playwright.options.sdkLanguage); - await registryExecutable.validateHostRequirements(this.attribution.playwright.options.sdkLanguage); + await registry.validateHostRequirementsForExecutablesIfNeeded([registryExecutable], this.attribution.playwright.options.sdkLanguage); } const waitForWSEndpoint = (options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port'))) ? new ManualPromise() : undefined; diff --git a/packages/playwright-core/src/server/registry/dependencies.ts b/packages/playwright-core/src/server/registry/dependencies.ts index 1a7b849374..44f56f3d2f 100644 --- a/packages/playwright-core/src/server/registry/dependencies.ts +++ b/packages/playwright-core/src/server/registry/dependencies.ts @@ -230,17 +230,17 @@ export async function validateDependenciesLinux(sdkLanguage: string, linuxLddDir const pwVersion = getPlaywrightVersion(); const requiredDockerImage = dockerInfo.dockerImageName.replace(dockerInfo.driverVersion, pwVersion); errorLines.push(...[ - `This is most likely due to docker image version not matching Playwright version:`, - `- Playwright: ${pwVersion}`, - `- Docker: ${dockerInfo.driverVersion}`, + `This is most likely due to Docker image version not matching Playwright version:`, + `- Playwright : ${pwVersion}`, + `- Docker image: ${dockerInfo.driverVersion}`, ``, `Either:`, - `- (recommended) use docker image "${requiredDockerImage}"`, - `- (alternative 1) run the following command inside docker to install missing dependencies:`, + `- (recommended) use Docker image "${requiredDockerImage}"`, + `- (alternative 1) run the following command inside Docker to install missing dependencies:`, ``, ` ${maybeSudo}${buildPlaywrightCLICommand(sdkLanguage, 'install-deps')}`, ``, - `- (alternative 2) use apt inside docker:`, + `- (alternative 2) use apt inside Docker:`, ``, ` ${maybeSudo}apt-get install ${[...missingPackages].join('\\\n ')}`, ``, diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 2b3663a47d..8c3fe31287 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -20,7 +20,6 @@ import path from 'path'; import * as util from 'util'; import * as fs from 'fs'; import { lockfile } from '../../utilsBundle'; -import { getLinuxDistributionInfo } from '../../utils/linuxUtils'; import { fetchData } from '../../utils/network'; import { getEmbedderName } from '../../utils/userAgent'; import { getFromENV, getAsBooleanFromENV, calculateSha1, wrapInASCIIBox, getPackageManagerExecCommand } from '../../utils'; @@ -32,6 +31,7 @@ import { transformCommandsForRoot, dockerVersion, readDockerVersionSync } from ' import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies'; import { downloadBrowserWithProgressBar, logPolitely } from './browserFetcher'; export { writeDockerVersion } from './dependencies'; +import { debugLogger } from '../../common/debugLogger'; const PACKAGE_PATH = path.join(__dirname, '..', '..', '..'); const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin'); @@ -382,7 +382,7 @@ export interface Executable { browserVersion?: string, executablePathOrDie(sdkLanguage: string): string; executablePath(sdkLanguage: string): string | undefined; - validateHostRequirements(sdkLanguage: string): Promise; + _validateHostRequirements(sdkLanguage: string): Promise; } interface ExecutableImpl extends Executable { @@ -445,7 +445,7 @@ export class Registry { executablePath: () => chromiumExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium', chromiumExecutable, chromium.installByDefault, sdkLanguage), installType: chromium.installByDefault ? 'download-by-default' : 'download-on-demand', - validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'chromium', chromium.dir, ['chrome-linux'], [], ['chrome-win']), + _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'chromium', chromium.dir, ['chrome-linux'], [], ['chrome-win']), downloadURLs: this._downloadURLs(chromium), browserVersion: chromium.browserVersion, _install: () => this._downloadExecutable(chromium, chromiumExecutable), @@ -463,7 +463,7 @@ export class Registry { executablePath: () => chromiumWithSymbolsExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-with-symbols', chromiumWithSymbolsExecutable, chromiumWithSymbols.installByDefault, sdkLanguage), installType: chromiumWithSymbols.installByDefault ? 'download-by-default' : 'download-on-demand', - validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'chromium', chromiumWithSymbols.dir, ['chrome-linux'], [], ['chrome-win']), + _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'chromium', chromiumWithSymbols.dir, ['chrome-linux'], [], ['chrome-win']), downloadURLs: this._downloadURLs(chromiumWithSymbols), browserVersion: chromiumWithSymbols.browserVersion, _install: () => this._downloadExecutable(chromiumWithSymbols, chromiumWithSymbolsExecutable), @@ -481,7 +481,7 @@ export class Registry { executablePath: () => chromiumTipOfTreeExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-tip-of-tree', chromiumTipOfTreeExecutable, chromiumTipOfTree.installByDefault, sdkLanguage), installType: chromiumTipOfTree.installByDefault ? 'download-by-default' : 'download-on-demand', - validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'chromium', chromiumTipOfTree.dir, ['chrome-linux'], [], ['chrome-win']), + _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'chromium', chromiumTipOfTree.dir, ['chrome-linux'], [], ['chrome-win']), downloadURLs: this._downloadURLs(chromiumTipOfTree), browserVersion: chromiumTipOfTree.browserVersion, _install: () => this._downloadExecutable(chromiumTipOfTree, chromiumTipOfTreeExecutable), @@ -567,7 +567,7 @@ export class Registry { executablePath: () => firefoxExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('firefox', firefoxExecutable, firefox.installByDefault, sdkLanguage), installType: firefox.installByDefault ? 'download-by-default' : 'download-on-demand', - validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'firefox', firefox.dir, ['firefox'], [], ['firefox']), + _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'firefox', firefox.dir, ['firefox'], [], ['firefox']), downloadURLs: this._downloadURLs(firefox), browserVersion: firefox.browserVersion, _install: () => this._downloadExecutable(firefox, firefoxExecutable), @@ -585,7 +585,7 @@ export class Registry { executablePath: () => firefoxAsanExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('firefox-asan', firefoxAsanExecutable, firefoxAsan.installByDefault, sdkLanguage), installType: firefoxAsan.installByDefault ? 'download-by-default' : 'download-on-demand', - validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'firefox', firefoxAsan.dir, ['firefox'], [], ['firefox']), + _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'firefox', firefoxAsan.dir, ['firefox'], [], ['firefox']), downloadURLs: this._downloadURLs(firefoxAsan), browserVersion: firefoxAsan.browserVersion, _install: () => this._downloadExecutable(firefoxAsan, firefoxAsanExecutable), @@ -603,7 +603,7 @@ export class Registry { executablePath: () => firefoxBetaExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('firefox-beta', firefoxBetaExecutable, firefoxBeta.installByDefault, sdkLanguage), installType: firefoxBeta.installByDefault ? 'download-by-default' : 'download-on-demand', - validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'firefox', firefoxBeta.dir, ['firefox'], [], ['firefox']), + _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'firefox', firefoxBeta.dir, ['firefox'], [], ['firefox']), downloadURLs: this._downloadURLs(firefoxBeta), browserVersion: firefoxBeta.browserVersion, _install: () => this._downloadExecutable(firefoxBeta, firefoxBetaExecutable), @@ -631,7 +631,7 @@ export class Registry { executablePath: () => webkitExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('webkit', webkitExecutable, webkit.installByDefault, sdkLanguage), installType: webkit.installByDefault ? 'download-by-default' : 'download-on-demand', - validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'webkit', webkit.dir, webkitLinuxLddDirectories, ['libGLESv2.so.2', 'libx264.so'], ['']), + _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'webkit', webkit.dir, webkitLinuxLddDirectories, ['libGLESv2.so.2', 'libx264.so'], ['']), downloadURLs: this._downloadURLs(webkit), browserVersion: webkit.browserVersion, _install: () => this._downloadExecutable(webkit, webkitExecutable), @@ -649,7 +649,7 @@ export class Registry { executablePath: () => ffmpegExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('ffmpeg', ffmpegExecutable, ffmpeg.installByDefault, sdkLanguage), installType: ffmpeg.installByDefault ? 'download-by-default' : 'download-on-demand', - validateHostRequirements: () => Promise.resolve(), + _validateHostRequirements: () => Promise.resolve(), downloadURLs: this._downloadURLs(ffmpeg), _install: () => this._downloadExecutable(ffmpeg, ffmpegExecutable), _dependencyGroup: 'tools', @@ -664,7 +664,7 @@ export class Registry { executablePath: () => undefined, executablePathOrDie: () => '', installType: 'download-on-demand', - validateHostRequirements: () => Promise.resolve(), + _validateHostRequirements: () => Promise.resolve(), downloadURLs: this._downloadURLs(android), _install: () => this._downloadExecutable(android), _dependencyGroup: 'tools', @@ -704,7 +704,7 @@ export class Registry { executablePath: (sdkLanguage: string) => executablePath(sdkLanguage, false), executablePathOrDie: (sdkLanguage: string) => executablePath(sdkLanguage, true)!, installType: install ? 'install-script' : 'none', - validateHostRequirements: () => Promise.resolve(), + _validateHostRequirements: () => Promise.resolve(), _isHermeticInstallation: false, _install: install, }; @@ -735,14 +735,6 @@ export class Registry { } private async _validateHostRequirements(sdkLanguage: string, browserName: BrowserName, browserDirectory: string, linuxLddDirectories: string[], dlOpenLibraries: string[], windowsExeAndDllDirectories: string[]) { - if (getAsBooleanFromENV('PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS')) { - process.stderr.write('Skipping host requirements validation logic because `PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS` env variable is set.\n'); - return; - } - const distributionInfo = await getLinuxDistributionInfo(); - if (browserName === 'firefox' && distributionInfo?.id === 'ubuntu' && distributionInfo?.version === '16.04') - throw new Error(`Cannot launch Firefox on Ubuntu 16.04! Minimum required Ubuntu version for Firefox browser is 20.04`); - if (os.platform() === 'linux') return await validateDependenciesLinux(sdkLanguage, linuxLddDirectories.map(d => path.join(browserDirectory, d)), dlOpenLibraries); if (os.platform() === 'win32' && os.arch() === 'x64') @@ -859,6 +851,37 @@ export class Registry { }; } + async validateHostRequirementsForExecutablesIfNeeded(executables: Executable[], sdkLanguage: string) { + if (getAsBooleanFromENV('PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS')) { + process.stderr.write('Skipping host requirements validation logic because `PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS` env variable is set.\n'); + return; + } + for (const executable of executables) + await this._validateHostRequirementsForExecutableIfNeeded(executable, sdkLanguage); + } + + private async _validateHostRequirementsForExecutableIfNeeded(executable: Executable, sdkLanguage: string) { + const kMaximumReValidationPeriod = 30 * 24 * 60 * 60 * 1000; // 30 days + // Executable does not require validation. + if (!executable.directory) + return; + const markerFile = path.join(executable.directory, 'DEPENDENCIES_VALIDATED'); + // Executable is already validated. + if (await fs.promises.stat(markerFile).then(stat => (Date.now() - stat.mtime.getTime()) < kMaximumReValidationPeriod).catch(() => false)) + return; + + debugLogger.log('install', `validating host requirements for "${executable.name}"`); + try { + await executable._validateHostRequirements(sdkLanguage); + debugLogger.log('install', `validation passed for ${executable.name}`); + } catch (error) { + debugLogger.log('install', `validation failed for ${executable.name}`); + throw error; + } + + await fs.promises.writeFile(markerFile, '').catch(() => {}); + } + private _downloadURLs(descriptor: BrowsersJSONDescriptor): string[] { const paths = (DOWNLOAD_PATHS as any)[descriptor.name]; const downloadPathTemplate: string|undefined = paths[hostPlatform] || paths['']; diff --git a/tests/installation/fixture-scripts/validate-dependencies.js b/tests/installation/fixture-scripts/validate-dependencies.js deleted file mode 100644 index b63d4a9357..0000000000 --- a/tests/installation/fixture-scripts/validate-dependencies.js +++ /dev/null @@ -1,8 +0,0 @@ -const playwright = require('playwright'); - -process.env.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = 1; - -(async () => { - const browser = await playwright.chromium.launch(); - await browser.close(); -})(); \ No newline at end of file diff --git a/tests/installation/validate-dependencies.spec.ts b/tests/installation/validate-dependencies.spec.ts index 247628c307..59a0cd5d21 100644 --- a/tests/installation/validate-dependencies.spec.ts +++ b/tests/installation/validate-dependencies.spec.ts @@ -15,17 +15,96 @@ */ import { test, expect } from './npmTest'; -test('validate dependencies', async ({ exec }) => { - await exec('npm i playwright'); - await exec('npx playwright install chromium'); +test.use({ isolateBrowsers: true }); - await test.step('default (on)', async () => { - const result1 = await exec('node validate-dependencies.js'); - expect(result1).toContain(`PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS`); +test('should validate dependencies correctly if skipped during install', async ({ exec, writeFiles }) => { + await exec('npm i playwright'); + + await writeFiles({ + 'test.js': `const { chromium } = require('playwright'); + (async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.close(); + await browser.close(); + })();`, }); - await test.step('disabled (off)', async () => { + const result = await exec('npx playwright install chromium', { + env: { + PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS: '1', + DEBUG: 'pw:install', + } + }); + expect(result).toContain(`Skipping host requirements validation logic because`); + + await test.step('should skip dependency validation for a custom executablePath', async () => { const result2 = await exec('node validate-dependencies-skip-executable-path.js'); expect(result2).not.toContain(`PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS`); }); + + await test.step('should skip dependency validation on launch if env var is passed', async () => { + const result = await exec('node test.js', { + env: { + PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS: '1', + } + }); + expect(result).toContain(`Skipping host requirements validation logic because`); + }); + + await test.step('should validate dependencies (skipped during install)', async () => { + const result = await exec('node test.js', { + env: { + DEBUG: 'pw:install', + }, + }); + expect(result).toContain(`validating host requirements for "chromium"`); + expect(result).not.toContain(`validating host requirements for "firefox"`); + expect(result).not.toContain(`validating host requirements for "webkit"`); + }); + + await test.step('should validate dependencies for a second launch (skipped during install)', async () => { + const result = await exec('node test.js', { + env: { + DEBUG: 'pw:install', + }, + }); + expect(result).toContain(`validating host requirements for "chromium"`); + expect(result).not.toContain(`validating host requirements for "firefox"`); + expect(result).not.toContain(`validating host requirements for "webkit"`); + }); +}); + +test('should not validate dependencies on launch if validated during install', async ({ exec, writeFiles }) => { + await exec('npm i playwright'); + + await writeFiles({ + 'test.js': `const { chromium } = require('playwright'); + (async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.close(); + await browser.close(); + })();`, + }); + + const result = await exec('npx playwright install chromium', { + env: { + DEBUG: 'pw:install', + } + }); + expect(result).toContain(`validating host requirements for "chromium"`); + expect(result).not.toContain(`validating host requirements for "firefox"`); + expect(result).not.toContain(`validating host requirements for "webkit"`); + + await test.step('should not validate dependencies on launch if already validated', async () => { + const result = await exec('node test.js', { + env: { + DEBUG: 'pw:install', + }, + }); + expect(result).not.toContain(`validating host requirements for "chromium"`); + expect(result).not.toContain(`validating host requirements for "firefox"`); + expect(result).not.toContain(`validating host requirements for "webkit"`); + }); });