From b0ff4f58ce9f26724827ccd3cb7ed490dd749034 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Thu, 8 Sep 2022 09:05:09 -0700 Subject: [PATCH] feat: implement `--dry-run` for `npx playwright install` (#17158) * feat: implement `--dry-run` for `npx playwright install` The `--dry-run` command prints URLs for browsers to be installed. Demo output: ``` browser: chromium version 106.0.5249.21 Install location: /Users/andreylushnikov/Library/Caches/ms-playwright/chromium-1023 Download url: https://playwright.azureedge.net/builds/chromium/1023/chromium-mac-arm64.zip Download fallback 1: https://playwright-akamai.azureedge.net/builds/chromium/1023/chromium-mac-arm64.zip Download fallback 2: https://playwright-verizon.azureedge.net/builds/chromium/1023/chromium-mac-arm64.zip browser: firefox version 104.0 Install location: /Users/andreylushnikov/Library/Caches/ms-playwright/firefox-1350 Download url: https://playwright.azureedge.net/builds/firefox/1350/firefox-mac-11-arm64.zip Download fallback 1: https://playwright-akamai.azureedge.net/builds/firefox/1350/firefox-mac-11-arm64.zip Download fallback 2: https://playwright-verizon.azureedge.net/builds/firefox/1350/firefox-mac-11-arm64.zip browser: webkit version 16.0 Install location: /Users/andreylushnikov/Library/Caches/ms-playwright/webkit-1714 Download url: https://playwright.azureedge.net/builds/webkit/1714/webkit-mac-12-arm64.zip Download fallback 1: https://playwright-akamai.azureedge.net/builds/webkit/1714/webkit-mac-12-arm64.zip Download fallback 2: https://playwright-verizon.azureedge.net/builds/webkit/1714/webkit-mac-12-arm64.zip browser: ffmpeg Install location: /Users/andreylushnikov/Library/Caches/ms-playwright/ffmpeg-1007 Download url: https://playwright.azureedge.net/builds/ffmpeg/1007/ffmpeg-mac-arm64.zip Download fallback 1: https://playwright-akamai.azureedge.net/builds/ffmpeg/1007/ffmpeg-mac-arm64.zip Download fallback 2: https://playwright-verizon.azureedge.net/builds/ffmpeg/1007/ffmpeg-mac-arm64.zip ``` Fixes #16926 --- packages/playwright-core/src/cli/cli.ts | 42 +++++++------- .../src/server/registry/index.ts | 55 +++++++++++++++---- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/packages/playwright-core/src/cli/cli.ts b/packages/playwright-core/src/cli/cli.ts index effcb0ab7e..fe7486bc55 100755 --- a/packages/playwright-core/src/cli/cli.ts +++ b/packages/playwright-core/src/cli/cli.ts @@ -32,9 +32,7 @@ import type { Page } from '../client/page'; import type { BrowserType } from '../client/browserType'; import type { BrowserContextOptions, LaunchOptions } from '../client/types'; import { spawn } from 'child_process'; -import { getPlaywrightVersion } from '../common/userAgent'; import { wrapInASCIIBox, isLikelyNpxGlobal, assert } from '../utils'; -import { spawnAsync } from '../utils/spawnAsync'; import { launchGridAgent } from '../grid/gridAgent'; import type { GridFactory } from '../grid/gridServer'; import { GridServer } from '../grid/gridServer'; @@ -120,8 +118,9 @@ program .command('install [browser...]') .description('ensure browsers necessary for this version of Playwright are installed') .option('--with-deps', 'install system dependencies for browsers') + .option('--dry-run', 'do not execute installation, only print information') .option('--force', 'force reinstall of stable browser channels') - .action(async function(args: string[], options: { withDeps?: boolean, force?: boolean }) { + .action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean }) { if (isLikelyNpxGlobal()) { console.error(wrapInASCIIBox([ `WARNING: It looks like you are running 'npx playwright install' without first`, @@ -143,27 +142,26 @@ program ].join('\n'), 1)); } try { - if (!args.length) { - const executables = registry.defaultExecutables(); - if (options.withDeps) - await registry.installDeps(executables, false); - await registry.install(executables, false /* forceReinstall */); - } else { - const installDockerImage = args.some(arg => arg === 'docker-image'); - args = args.filter(arg => arg !== 'docker-image'); - if (installDockerImage) { - const imageName = `mcr.microsoft.com/playwright:v${getPlaywrightVersion()}-focal`; - const { code } = await spawnAsync('docker', ['pull', imageName], { stdio: 'inherit' }); - if (code !== 0) { - console.log('Failed to pull docker image'); - process.exit(1); + const hasNoArguments = !args.length; + const executables = hasNoArguments ? registry.defaultExecutables() : checkBrowsersToInstall(args); + if (options.withDeps) + await registry.installDeps(executables, !!options.dryRun); + if (options.dryRun) { + for (const executable of executables) { + const version = executable.browserVersion ? `version ` + executable.browserVersion : ''; + console.log(`browser: ${executable.name}${version ? ' ' + version : ''}`); + console.log(` Install location: ${executable.directory ?? ''}`); + if (executable.downloadURLs?.length) { + const [url, ...fallbacks] = executable.downloadURLs; + console.log(` Download url: ${url}`); + for (let i = 0; i < fallbacks.length; ++i) + console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`); } + console.log(``); } - - const executables = checkBrowsersToInstall(args); - if (options.withDeps) - await registry.installDeps(executables, false); - await registry.install(executables, !!options.force /* forceReinstall */); + } else { + const forceReinstall = hasNoArguments ? false : !!options.force; + await registry.install(executables, forceReinstall); } } catch (e) { console.log(`Failed to install browsers\n${e}`); diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 599df59ff6..a3236a2298 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -310,6 +310,8 @@ export interface Executable { browserName: BrowserName | undefined; installType: 'download-by-default' | 'download-on-demand' | 'install-script' | 'none'; directory: string | undefined; + downloadURLs?: string[], + browserVersion?: string, executablePathOrDie(sdkLanguage: string): string; executablePath(sdkLanguage: string): string | undefined; validateHostRequirements(sdkLanguage: string): Promise; @@ -376,7 +378,9 @@ export class Registry { 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']), - _install: () => this._downloadExecutable(chromium, chromiumExecutable, DOWNLOAD_PATHS['chromium'][hostPlatform], 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST'), + downloadURLs: this._downloadURLs(chromium), + browserVersion: chromium.browserVersion, + _install: () => this._downloadExecutable(chromium, chromiumExecutable), _dependencyGroup: 'chromium', _isHermeticInstallation: true, }); @@ -392,7 +396,9 @@ export class Registry { 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']), - _install: () => this._downloadExecutable(chromiumWithSymbols, chromiumWithSymbolsExecutable, DOWNLOAD_PATHS['chromium-with-symbols'][hostPlatform], 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST'), + downloadURLs: this._downloadURLs(chromiumWithSymbols), + browserVersion: chromiumWithSymbols.browserVersion, + _install: () => this._downloadExecutable(chromiumWithSymbols, chromiumWithSymbolsExecutable), _dependencyGroup: 'chromium', _isHermeticInstallation: true, }); @@ -408,7 +414,9 @@ export class Registry { 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']), - _install: () => this._downloadExecutable(chromiumTipOfTree, chromiumTipOfTreeExecutable, DOWNLOAD_PATHS['chromium-tip-of-tree'][hostPlatform], 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST'), + downloadURLs: this._downloadURLs(chromiumTipOfTree), + browserVersion: chromiumTipOfTree.browserVersion, + _install: () => this._downloadExecutable(chromiumTipOfTree, chromiumTipOfTreeExecutable), _dependencyGroup: 'chromium', _isHermeticInstallation: true, }); @@ -492,7 +500,9 @@ export class Registry { 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']), - _install: () => this._downloadExecutable(firefox, firefoxExecutable, DOWNLOAD_PATHS['firefox'][hostPlatform], 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST'), + downloadURLs: this._downloadURLs(firefox), + browserVersion: firefox.browserVersion, + _install: () => this._downloadExecutable(firefox, firefoxExecutable), _dependencyGroup: 'firefox', _isHermeticInstallation: true, }); @@ -508,7 +518,9 @@ export class Registry { 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']), - _install: () => this._downloadExecutable(firefoxBeta, firefoxBetaExecutable, DOWNLOAD_PATHS['firefox-beta'][hostPlatform], 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST'), + downloadURLs: this._downloadURLs(firefoxBeta), + browserVersion: firefoxBeta.browserVersion, + _install: () => this._downloadExecutable(firefoxBeta, firefoxBetaExecutable), _dependencyGroup: 'firefox', _isHermeticInstallation: true, }); @@ -534,7 +546,9 @@ export class Registry { 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'], ['']), - _install: () => this._downloadExecutable(webkit, webkitExecutable, DOWNLOAD_PATHS['webkit'][hostPlatform], 'PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST'), + downloadURLs: this._downloadURLs(webkit), + browserVersion: webkit.browserVersion, + _install: () => this._downloadExecutable(webkit, webkitExecutable), _dependencyGroup: 'webkit', _isHermeticInstallation: true, }); @@ -550,7 +564,8 @@ export class Registry { executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('ffmpeg', ffmpegExecutable, ffmpeg.installByDefault, sdkLanguage), installType: ffmpeg.installByDefault ? 'download-by-default' : 'download-on-demand', validateHostRequirements: () => Promise.resolve(), - _install: () => this._downloadExecutable(ffmpeg, ffmpegExecutable, DOWNLOAD_PATHS['ffmpeg'][hostPlatform], 'PLAYWRIGHT_FFMPEG_DOWNLOAD_HOST'), + downloadURLs: this._downloadURLs(ffmpeg), + _install: () => this._downloadExecutable(ffmpeg, ffmpegExecutable), _dependencyGroup: 'tools', _isHermeticInstallation: true, }); @@ -727,17 +742,33 @@ export class Registry { } } - private async _downloadExecutable(descriptor: BrowsersJSONDescriptor, executablePath: string | undefined, downloadPathTemplate: string | undefined, downloadHostEnv: string) { - if (!downloadPathTemplate || !executablePath) - throw new Error(`ERROR: Playwright does not support ${descriptor.name} on ${hostPlatform}`); - if (hostPlatform === 'generic-linux' || hostPlatform === 'generic-linux-arm64') - logPolitely('BEWARE: your OS is not officially supported by Playwright; downloading fallback build.'); + private _downloadURLs(descriptor: BrowsersJSONDescriptor): string[] { + const downloadPathTemplate: string|undefined = (DOWNLOAD_PATHS as any)[descriptor.name][hostPlatform]; + if (!downloadPathTemplate) + return []; const downloadPath = util.format(downloadPathTemplate, descriptor.revision); let downloadURLs = PLAYWRIGHT_CDN_MIRRORS.map(mirror => `${mirror}/${downloadPath}`) ; + let downloadHostEnv; + if (descriptor.name.startsWith('chromium')) + downloadHostEnv = 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST'; + else if (descriptor.name.startsWith('firefox')) + downloadHostEnv = 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST'; + else if (descriptor.name.startsWith('webkit')) + downloadHostEnv = 'PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST'; + const customHostOverride = (downloadHostEnv && getFromENV(downloadHostEnv)) || getFromENV('PLAYWRIGHT_DOWNLOAD_HOST'); if (customHostOverride) downloadURLs = [`${customHostOverride}/${downloadPath}`]; + return downloadURLs; + } + + private async _downloadExecutable(descriptor: BrowsersJSONDescriptor, executablePath: string | undefined) { + const downloadURLs = this._downloadURLs(descriptor); + if (!downloadURLs.length || !executablePath) + throw new Error(`ERROR: Playwright does not support ${descriptor.name} on ${hostPlatform}`); + if (hostPlatform === 'generic-linux' || hostPlatform === 'generic-linux-arm64') + logPolitely('BEWARE: your OS is not officially supported by Playwright; downloading fallback build.'); const displayName = descriptor.name.split('-').map(word => { return word === 'ffmpeg' ? 'FFMPEG' : word.charAt(0).toUpperCase() + word.slice(1);