diff --git a/src/cli/cli.ts b/src/cli/cli.ts index fa86bfe8cd..557ff2849d 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -32,46 +32,9 @@ import { BrowserType } from '../client/browserType'; import { BrowserContextOptions, LaunchOptions } from '../client/types'; import { spawn } from 'child_process'; import { registry, Executable } from '../utils/registry'; -import * as utils from '../utils/utils'; - -const SCRIPTS_DIRECTORY = path.join(__dirname, '..', '..', 'bin'); - - -type BrowserChannel = 'chrome-beta'|'chrome'|'msedge'|'msedge-beta'; -const allBrowserChannels: Set = new Set(['chrome-beta', 'chrome', 'msedge', 'msedge-beta']); -const suggestedBrowsersToInstall = ['chromium', 'webkit', 'firefox', ...allBrowserChannels].map(name => `'${name}'`).join(', '); const packageJSON = require('../../package.json'); -const ChannelName = { - 'chrome-beta': 'Google Chrome Beta', - 'chrome': 'Google Chrome', - 'msedge': 'Microsoft Edge', - 'msedge-beta': 'Microsoft Edge Beta', -}; - -const InstallationScriptName = { - 'chrome-beta': { - 'linux': 'reinstall_chrome_beta_linux.sh', - 'darwin': 'reinstall_chrome_beta_mac.sh', - 'win32': 'reinstall_chrome_beta_win.ps1', - }, - 'chrome': { - 'linux': 'reinstall_chrome_stable_linux.sh', - 'darwin': 'reinstall_chrome_stable_mac.sh', - 'win32': 'reinstall_chrome_stable_win.ps1', - }, - 'msedge': { - 'darwin': 'reinstall_msedge_stable_mac.sh', - 'win32': 'reinstall_msedge_stable_win.ps1', - }, - 'msedge-beta': { - 'darwin': 'reinstall_msedge_beta_mac.sh', - 'linux': 'reinstall_msedge_beta_linux.sh', - 'win32': 'reinstall_msedge_beta_win.ps1', - }, -}; - program .version('Version ' + packageJSON.version) .name(process.env.PW_CLI_NAME || 'npx playwright'); @@ -119,29 +82,36 @@ program console.log(' $ debug npm run test'); }); +function suggestedBrowsersToInstall() { + return registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', '); +} + +function checkBrowsersToInstall(args: string[]) { + const faultyArguments: string[] = []; + const executables: Executable[] = []; + for (const arg of args) { + const executable = registry.findExecutable(arg); + if (!executable || executable.installType === 'none') + faultyArguments.push(arg); + else + executables.push(executable); + } + if (faultyArguments.length) { + console.log(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`); + process.exit(1); + } + return executables; +} + program - .command('install [browserType...]') + .command('install [browser...]') .description('ensure browsers necessary for this version of Playwright are installed') - .action(async function(args: any[]) { + .action(async function(args: string[]) { try { - // Install default browsers when invoked without arguments. - if (!args.length) { + if (!args.length) await registry.install(); - return; - } - const binaries = args.map(arg => registry.findExecutable(arg)).filter(b => !!b) as Executable[]; - const browserChannels: Set = new Set(args.filter(browser => allBrowserChannels.has(browser))); - const faultyArguments: string[] = args.filter((browser: any) => !binaries.find(b => b.name === browser) && !browserChannels.has(browser)); - if (faultyArguments.length) { - console.log(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall}`); - process.exit(1); - } - if (browserChannels.has('chrome-beta') || browserChannels.has('chrome') || browserChannels.has('msedge') || browserChannels.has('msedge-beta')) - binaries.push(registry.findExecutable('ffmpeg')!); - if (binaries.length) - await registry.install(binaries); - for (const browserChannel of browserChannels) - await installBrowserChannel(browserChannel); + else + await registry.install(checkBrowsersToInstall(args)); } catch (e) { console.log(`Failed to install browsers\n${e}`); process.exit(1); @@ -153,51 +123,31 @@ program console.log(` Install default browsers.`); console.log(``); console.log(` - $ install chrome firefox`); - console.log(` Install custom browsers, supports ${suggestedBrowsersToInstall}.`); + console.log(` Install custom browsers, supports ${suggestedBrowsersToInstall()}.`); }); -async function installBrowserChannel(channel: BrowserChannel) { - const platform = os.platform(); - const scriptName: (string|undefined) = (InstallationScriptName[channel] as any)[platform]; - if (!scriptName) - throw new Error(`Cannot install ${ChannelName[channel]} on ${platform}`); - - const scriptArgs = []; - if ((channel === 'msedge' || channel === 'msedge-beta') && platform !== 'linux') { - const products = JSON.parse(await utils.fetchData('https://edgeupdates.microsoft.com/api/products')); - const productName = channel === 'msedge' ? 'Stable' : 'Beta'; - const product = products.find((product: any) => product.Product === productName); - const searchConfig = ({ - darwin: {platform: 'MacOS', arch: 'universal', artifact: 'pkg'}, - win32: {platform: 'Windows', arch: os.arch() === 'x64' ? 'x64' : 'x86', artifact: 'msi'}, - } as any)[platform]; - const release = searchConfig ? product.Releases.find((release: any) => release.Platform === searchConfig.platform && release.Architecture === searchConfig.arch) : null; - const artifact = release ? release.Artifacts.find((artifact: any) => artifact.ArtifactName === searchConfig.artifact) : null; - if (artifact) - scriptArgs.push(artifact.Location /* url */); - else - throw new Error(`Cannot install ${ChannelName[channel]} on ${platform}`); - } - - const shell = scriptName.endsWith('.ps1') ? 'powershell.exe' : 'bash'; - const {code} = await utils.spawnAsync(shell, [path.join(SCRIPTS_DIRECTORY, scriptName), ...scriptArgs], { cwd: SCRIPTS_DIRECTORY, stdio: 'inherit' }); - if (code !== 0) - throw new Error(`Failed to install ${ChannelName[channel]}`); -} program - .command('install-deps [browserType...]') + .command('install-deps [browser...]') .description('install dependencies necessary to run browsers (will ask for sudo permissions)') - .action(async function(browserTypes: string[]) { + .action(async function(args: string[]) { try { - // TODO: verify the list and print supported browserTypes in the error message. - const binaries = browserTypes.map(arg => registry.findExecutable(arg)).filter(b => !!b) as Executable[]; - // When passed no arguments, assume default browsers. - await registry.installDeps(browserTypes.length ? binaries : undefined); + if (!args.length) + await registry.installDeps(); + else + await registry.installDeps(checkBrowsersToInstall(args)); } catch (e) { console.log(`Failed to install browser dependencies\n${e}`); process.exit(1); } + }).on('--help', function() { + console.log(``); + console.log(`Examples:`); + console.log(` - $ install-deps`); + console.log(` Install dependecies fro default browsers.`); + console.log(``); + console.log(` - $ install-deps chrome firefox`); + console.log(` Install dependencies for specific browsers, supports ${suggestedBrowsersToInstall()}.`); }); const browsers = [ diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 55aff2d922..b515658fff 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -44,8 +44,8 @@ export abstract class BrowserType extends SdkObject { this._name = browserName; } - executablePath(channel?: string): string { - return registry.findExecutable(this._name).maybeExecutablePath() || ''; + executablePath(): string { + return registry.findExecutable(this._name).executablePath() || ''; } name(): string { @@ -156,21 +156,19 @@ export abstract class BrowserType extends SdkObject { else browserArguments.push(...this._defaultArgs(options, isPersistent, userDataDir)); - const executable = executablePath || this.executablePath(options.channel); - if (!executable) - throw new Error(`No executable path is specified. Pass "executablePath" option directly.`); - if (!(await existsAsync(executable))) { - const errorMessageLines = [`Failed to launch ${this._name} because executable doesn't exist at ${executable}`]; - // If we tried using stock downloaded browser, suggest re-installing playwright. - if (!executablePath) - errorMessageLines.push(`Run "npx playwright install" to install browsers`); - throw new Error(errorMessageLines.join('\n')); + let executable: string; + if (executablePath) { + if (!(await existsAsync(executablePath))) + throw new Error(`Failed to launch ${this._name} because executable doesn't exist at ${executablePath}`); + executable = executablePath; + } else { + const registryExecutable = registry.findExecutable(options.channel || this._name); + if (!registryExecutable || registryExecutable.browserName !== this._name) + throw new Error(`Unsupported ${this._name} channel "${options.channel}"`); + executable = registryExecutable.executablePathOrDie(); + await registryExecutable.validateHostRequirements(); } - // Do not validate dependencies for custom binaries. - if (!executablePath && !options.channel) - await registry.findExecutable(this._name).validateHostRequirements(); - let wsEndpointCallback: ((wsEndpoint: string) => void) | undefined; const shouldWaitForWSListening = options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port')); const waitForWSEndpoint = shouldWaitForWSListening ? new Promise(f => wsEndpointCallback = f) : undefined; diff --git a/src/server/chromium/chromium.ts b/src/server/chromium/chromium.ts index bf5e4c7599..6aacd35459 100644 --- a/src/server/chromium/chromium.ts +++ b/src/server/chromium/chromium.ts @@ -27,13 +27,12 @@ import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../tra import { CRDevTools } from './crDevTools'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import * as types from '../types'; -import { assert, debugMode, headersArrayToObject, removeFolders } from '../../utils/utils'; +import { debugMode, headersArrayToObject, removeFolders } from '../../utils/utils'; import { RecentLogsCollector } from '../../utils/debugLogger'; import { ProgressController } from '../progress'; import { TimeoutSettings } from '../../utils/timeoutSettings'; import { helper } from '../helper'; import { CallMetadata } from '../instrumentation'; -import { findChromiumChannel } from './findChromiumChannel'; import http from 'http'; import { registry } from '../../utils/registry'; @@ -49,20 +48,6 @@ export class Chromium extends BrowserType { this._devtools = this._createDevTools(); } - executablePath(channel?: string): string { - if (channel) { - let executablePath = undefined; - if ((channel as any) === 'chromium-with-symbols') - executablePath = registry.findExecutable('chromium-with-symbols')!.executablePathIfExists(); - else - executablePath = findChromiumChannel(channel); - assert(executablePath, `unsupported chromium channel "${channel}"`); - assert(fs.existsSync(executablePath), `"${channel}" channel is not installed. Try running 'npx playwright install ${channel}'`); - return executablePath; - } - return super.executablePath(channel); - } - async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, sdkLanguage: string, headers?: types.HeadersArray }, timeout?: number) { const controller = new ProgressController(metadata, this); controller.setLogName('browser'); @@ -104,7 +89,7 @@ export class Chromium extends BrowserType { private _createDevTools() { // TODO: this is totally wrong when using channels. - const directory = registry.findExecutable('chromium').directoryIfExists(); + const directory = registry.findExecutable('chromium').directory; return directory ? new CRDevTools(path.join(directory, 'devtools-preferences.json')) : undefined; } diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts index bbe1a41cec..13ace45a20 100644 --- a/src/server/chromium/crPage.ts +++ b/src/server/chromium/crPage.ts @@ -845,10 +845,9 @@ class FrameSession { async _createVideoRecorder(screencastId: string, options: types.PageScreencastOptions): Promise { assert(!this._screencastId); - const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePathIfExists(); - if (!ffmpegPath) - throw new Error('ffmpeg executable was not found'); - if (!canAccessFile(ffmpegPath)) { + const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePath(); + // TODO: use default error message once it's ready. + if (!ffmpegPath || !canAccessFile(ffmpegPath)) { let message: string = ''; switch (this._page._browserContext._options.sdkLanguage) { case 'python': message = 'playwright install ffmpeg'; break; diff --git a/src/server/chromium/findChromiumChannel.ts b/src/server/chromium/findChromiumChannel.ts deleted file mode 100644 index 47cba155f8..0000000000 --- a/src/server/chromium/findChromiumChannel.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. All rights reserved. - * - * 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 path from 'path'; -import { canAccessFile } from '../../utils/utils'; - -function darwin(channel: string): string[] | undefined { - switch (channel) { - case 'chrome': return ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome']; - case 'chrome-beta': return ['/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta']; - case 'chrome-dev': return ['/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev']; - case 'chrome-canary': return ['/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary']; - case 'msedge': return ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge']; - case 'msedge-beta': return ['/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta']; - case 'msedge-dev': return ['/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev']; - case 'msedge-canary': return ['/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary']; - } -} - -function linux(channel: string): string[] | undefined { - switch (channel) { - case 'chrome': return ['/opt/google/chrome/chrome']; - case 'chrome-beta': return ['/opt/google/chrome-beta/chrome']; - case 'chrome-dev': return ['/opt/google/chrome-unstable/chrome']; - case 'msedge-dev': return ['/opt/microsoft/msedge-dev/msedge']; - case 'msedge-beta': return ['/opt/microsoft/msedge-beta/msedge']; - } -} - -function win32(channel: string): string[] | undefined { - let suffix: string | undefined; - switch (channel) { - case 'chrome': suffix = `\\Google\\Chrome\\Application\\chrome.exe`; break; - case 'chrome-beta': suffix = `\\Google\\Chrome Beta\\Application\\chrome.exe`; break; - case 'chrome-dev': suffix = `\\Google\\Chrome Dev\\Application\\chrome.exe`; break; - case 'chrome-canary': suffix = `\\Google\\Chrome SxS\\Application\\chrome.exe`; break; - case 'msedge': suffix = `\\Microsoft\\Edge\\Application\\msedge.exe`; break; - case 'msedge-beta': suffix = `\\Microsoft\\Edge Beta\\Application\\msedge.exe`; break; - case 'msedge-dev': suffix = `\\Microsoft\\Edge Dev\\Application\\msedge.exe`; break; - case 'msedge-canary': suffix = `\\Microsoft\\Edge SxS\\Application\\msedge.exe`; break; - } - if (!suffix) - return; - const prefixes = [ - process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'] - ].filter(Boolean) as string[]; - return prefixes.map(prefix => path.join(prefix, suffix!)); -} - -export function findChromiumChannel(channel: string): string { - let installationPaths: string[] | undefined; - if (process.platform === 'linux') - installationPaths = linux(channel); - else if (process.platform === 'win32') - installationPaths = win32(channel); - else if (process.platform === 'darwin') - installationPaths = darwin(channel); - - if (!installationPaths) - throw new Error(`Chromium distribution '${channel}' is not supported on ${process.platform}`); - - let result: string | undefined; - installationPaths.forEach(chromePath => { - if (canAccessFile(chromePath)) - result = chromePath; - }); - if (result) - return result; - throw new Error(`Chromium distribution is not installed on the system: ${channel}`); -} diff --git a/src/server/firefox/firefox.ts b/src/server/firefox/firefox.ts index 645b5691bd..c648268fe5 100644 --- a/src/server/firefox/firefox.ts +++ b/src/server/firefox/firefox.ts @@ -18,7 +18,6 @@ import * as os from 'os'; import fs from 'fs'; import path from 'path'; -import { assert } from '../../utils/utils'; import { FFBrowser } from './ffBrowser'; import { kBrowserCloseMessageId } from './ffConnection'; import { BrowserType } from '../browserType'; @@ -26,25 +25,12 @@ import { Env } from '../../utils/processLauncher'; import { ConnectionTransport } from '../transport'; import { BrowserOptions, PlaywrightOptions } from '../browser'; import * as types from '../types'; -import { registry } from '../../utils/registry'; export class Firefox extends BrowserType { constructor(playwrightOptions: PlaywrightOptions) { super('firefox', playwrightOptions); } - executablePath(channel?: string): string { - if (channel) { - let executablePath = undefined; - if ((channel as any) === 'firefox-beta') - executablePath = registry.findExecutable('firefox-beta')!.executablePathIfExists(); - assert(executablePath, `unsupported firefox channel "${channel}"`); - assert(fs.existsSync(executablePath), `"${channel}" channel is not installed. Try running 'npx playwright install ${channel}'`); - return executablePath; - } - return super.executablePath(channel); - } - _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise { return FFBrowser.connect(transport, options); } diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts index ef5cf00884..ca2c3da0c4 100644 --- a/src/server/supplements/recorder/recorderApp.ts +++ b/src/server/supplements/recorder/recorderApp.ts @@ -22,7 +22,7 @@ import { EventEmitter } from 'events'; import { internalCallMetadata } from '../../instrumentation'; import type { CallLog, EventData, Mode, Source } from './recorderTypes'; import { BrowserContext } from '../../browserContext'; -import { existsAsync, isUnderTest } from '../../../utils/utils'; +import { isUnderTest } from '../../../utils/utils'; import { installAppIcon } from '../../chromium/crApp'; declare global { @@ -97,8 +97,7 @@ export class RecorderApp extends EventEmitter { let executablePath: string | undefined; if (inspectedContext._browser.options.isChromium) { channel = inspectedContext._browser.options.channel; - const defaultExecutablePath = recorderPlaywright.chromium.executablePath(channel); - if (!(await existsAsync(defaultExecutablePath))) + if (!channel) executablePath = inspectedContext._browser.options.customExecutablePath; } const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', { diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts index f1a4e7af20..0275f58d2d 100644 --- a/src/server/trace/viewer/traceViewer.ts +++ b/src/server/trace/viewer/traceViewer.ts @@ -30,7 +30,6 @@ import { internalCallMetadata } from '../../instrumentation'; import { ProgressController } from '../../progress'; import { BrowserContext } from '../../browserContext'; import { registry } from '../../../utils/registry'; -import { findChromiumChannel } from '../../chromium/findChromiumChannel'; import { installAppIcon } from '../../chromium/crApp'; export class TraceViewer { @@ -140,21 +139,17 @@ export class TraceViewer { // Null means no installation and no channels found. let channel = null; if (traceViewerBrowser === 'chromium') { - if (registry.findExecutable('chromium').executablePathIfExists()) { - // This means we have a browser downloaded. - channel = undefined; - } else { - for (const c of ['chrome', 'msedge']) { - try { - findChromiumChannel(c); - channel = c; - break; - } catch (e) { - } + for (const name of ['chromium', 'chrome', 'msedge']) { + try { + registry.findExecutable(name)!.executablePathOrDie(); + channel = name === 'chromium' ? undefined : name; + break; + } catch (e) { } } if (channel === null) { + // TODO: language-specific error message, or fallback to default error. throw new Error(` ================================================================== Please run 'npx playwright install' to install Playwright browsers diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts index b94ad03e2f..f290f0bf25 100644 --- a/src/utils/dependencies.ts +++ b/src/utils/dependencies.ts @@ -35,7 +35,9 @@ function isSupportedWindowsVersion(): boolean { return major > 6 || (major === 6 && minor > 1); } -export async function installDependenciesWindows(targets: Set<'chromium' | 'firefox' | 'webkit' | 'tools'>) { +export type DependencyGroup = 'chromium' | 'firefox' | 'webkit' | 'tools'; + +export async function installDependenciesWindows(targets: Set) { if (targets.has('chromium')) { const {code} = await utils.spawnAsync('powershell.exe', [path.join(BIN_DIRECTORY, 'install_media_pack.ps1')], { cwd: BIN_DIRECTORY, stdio: 'inherit' }); if (code !== 0) @@ -43,7 +45,7 @@ export async function installDependenciesWindows(targets: Set<'chromium' | 'fire } } -export async function installDependenciesLinux(targets: Set<'chromium' | 'firefox' | 'webkit' | 'tools'>) { +export async function installDependenciesLinux(targets: Set) { const ubuntuVersion = await getUbuntuVersion(); if (ubuntuVersion !== '18.04' && ubuntuVersion !== '20.04' && ubuntuVersion !== '21.04') { console.warn('Cannot install dependencies for this linux distribution!'); // eslint-disable-line no-console diff --git a/src/utils/registry.ts b/src/utils/registry.ts index 360250bdff..7246eaec51 100644 --- a/src/utils/registry.ts +++ b/src/utils/registry.ts @@ -21,11 +21,12 @@ import * as util from 'util'; import * as fs from 'fs'; import lockfile from 'proper-lockfile'; import { getUbuntuVersion } from './ubuntuVersion'; -import { getFromENV, getAsBooleanFromENV, calculateSha1, removeFolders, existsAsync, hostPlatform, canAccessFile } from './utils'; -import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies'; +import { getFromENV, getAsBooleanFromENV, calculateSha1, removeFolders, existsAsync, hostPlatform, canAccessFile, spawnAsync, fetchData } from './utils'; +import { DependencyGroup, installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies'; import { downloadBrowserWithProgressBar, logPolitely } from './browserFetcher'; const PACKAGE_PATH = path.join(__dirname, '..', '..'); +const BIN_PATH = path.join(__dirname, '..', '..', 'bin'); const EXECUTABLE_PATHS = { 'chromium': { @@ -215,21 +216,23 @@ function readDescriptors(packagePath: string) { export type BrowserName = 'chromium' | 'firefox' | 'webkit'; type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-with-symbols'; +type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary'; const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-with-symbols']; export interface Executable { - type: 'browser' | 'tool'; - name: BrowserName | InternalTool; + type: 'browser' | 'tool' | 'channel'; + name: BrowserName | InternalTool | ChromiumChannel; browserName: BrowserName | undefined; - installType: 'download-by-default' | 'download-on-demand'; - maybeExecutablePath(): string | undefined; - executablePathIfExists(): string | undefined; - directoryIfExists(): string | undefined; + installType: 'download-by-default' | 'download-on-demand' | 'install-script' | 'none'; + directory: string | undefined; + executablePathOrDie(): string; + executablePath(): string | undefined; validateHostRequirements(): Promise; } interface ExecutableImpl extends Executable { - _download?: () => Promise; + _install?: () => Promise; + _dependencyGroup?: DependencyGroup; } export class Registry { @@ -237,72 +240,146 @@ export class Registry { constructor(packagePath: string) { const descriptors = readDescriptors(packagePath); - const executablePath = (dir: string, name: keyof typeof EXECUTABLE_PATHS) => { + const findExecutablePath = (dir: string, name: keyof typeof EXECUTABLE_PATHS) => { const tokens = EXECUTABLE_PATHS[name][hostPlatform]; return tokens ? path.join(dir, ...tokens) : undefined; }; - const directoryIfExists = (d: string) => fs.existsSync(d) ? d : undefined; - const executablePathIfExists = (e: string | undefined) => e && canAccessFile(e) ? e : undefined; + const executablePathOrDie = (name: string, e: string | undefined) => { + if (!e) + throw new Error(`${name} is not supported on ${hostPlatform}`); + // TODO: language-specific error message + if (!canAccessFile(e)) + throw new Error(`Executable doesn't exist at ${e}\nRun "npx playwright install ${name}"`); + return e; + }; this._executables = []; const chromium = descriptors.find(d => d.name === 'chromium')!; - const chromiumExecutable = executablePath(chromium.dir, 'chromium'); + const chromiumExecutable = findExecutablePath(chromium.dir, 'chromium'); this._executables.push({ type: 'browser', name: 'chromium', browserName: 'chromium', - directoryIfExists: () => directoryIfExists(chromium.dir), - maybeExecutablePath: () => chromiumExecutable, - executablePathIfExists: () => executablePathIfExists(chromiumExecutable), + directory: chromium.dir, + executablePath: () => chromiumExecutable, + executablePathOrDie: () => executablePathOrDie('chromium', chromiumExecutable), installType: chromium.installByDefault ? 'download-by-default' : 'download-on-demand', validateHostRequirements: () => this._validateHostRequirements('chromium', chromium.dir, ['chrome-linux'], [], ['chrome-win']), - _download: () => this._downloadExecutable(chromium, chromiumExecutable, DOWNLOAD_URLS['chromium'][hostPlatform], 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST'), + _install: () => this._downloadExecutable(chromium, chromiumExecutable, DOWNLOAD_URLS['chromium'][hostPlatform], 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST'), + _dependencyGroup: 'chromium', }); const chromiumWithSymbols = descriptors.find(d => d.name === 'chromium-with-symbols')!; - const chromiumWithSymbolsExecutable = executablePath(chromiumWithSymbols.dir, 'chromium'); + const chromiumWithSymbolsExecutable = findExecutablePath(chromiumWithSymbols.dir, 'chromium'); this._executables.push({ type: 'tool', name: 'chromium-with-symbols', browserName: 'chromium', - directoryIfExists: () => directoryIfExists(chromiumWithSymbols.dir), - maybeExecutablePath: () => chromiumWithSymbolsExecutable, - executablePathIfExists: () => executablePathIfExists(chromiumWithSymbolsExecutable), + directory: chromiumWithSymbols.dir, + executablePath: () => chromiumWithSymbolsExecutable, + executablePathOrDie: () => executablePathOrDie('chromium-with-symbols', chromiumWithSymbolsExecutable), installType: chromiumWithSymbols.installByDefault ? 'download-by-default' : 'download-on-demand', validateHostRequirements: () => this._validateHostRequirements('chromium', chromiumWithSymbols.dir, ['chrome-linux'], [], ['chrome-win']), - _download: () => this._downloadExecutable(chromiumWithSymbols, chromiumWithSymbolsExecutable, DOWNLOAD_URLS['chromium-with-symbols'][hostPlatform], 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST'), + _install: () => this._downloadExecutable(chromiumWithSymbols, chromiumWithSymbolsExecutable, DOWNLOAD_URLS['chromium-with-symbols'][hostPlatform], 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST'), + _dependencyGroup: 'chromium', }); + this._executables.push(this._createChromiumChannel('chrome', { + 'linux': '/opt/google/chrome/chrome', + 'darwin': '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + 'win32': `\\Google\\Chrome\\Application\\chrome.exe`, + }, () => this._installChromiumChannel('chrome', { + 'linux': 'reinstall_chrome_stable_linux.sh', + 'darwin': 'reinstall_chrome_stable_mac.sh', + 'win32': 'reinstall_chrome_stable_win.ps1', + }))); + + this._executables.push(this._createChromiumChannel('chrome-beta', { + 'linux': '/opt/google/chrome-beta/chrome', + 'darwin': '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta', + 'win32': `\\Google\\Chrome Beta\\Application\\chrome.exe`, + }, () => this._installChromiumChannel('chrome-beta', { + 'linux': 'reinstall_chrome_beta_linux.sh', + 'darwin': 'reinstall_chrome_beta_mac.sh', + 'win32': 'reinstall_chrome_beta_win.ps1', + }))); + + this._executables.push(this._createChromiumChannel('chrome-dev', { + 'linux': '/opt/google/chrome-unstable/chrome', + 'darwin': '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev', + 'win32': `\\Google\\Chrome Dev\\Application\\chrome.exe`, + })); + + this._executables.push(this._createChromiumChannel('chrome-canary', { + 'linux': '', + 'darwin': '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + 'win32': `\\Google\\Chrome SxS\\Application\\chrome.exe`, + })); + + this._executables.push(this._createChromiumChannel('msedge', { + 'linux': '', + 'darwin': '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', + 'win32': `\\Microsoft\\Edge\\Application\\msedge.exe`, + }, () => this._installMSEdgeChannel('msedge', { + 'linux': '', + 'darwin': 'reinstall_msedge_stable_mac.sh', + 'win32': 'reinstall_msedge_stable_win.ps1', + }))); + + this._executables.push(this._createChromiumChannel('msedge-beta', { + 'linux': '/opt/microsoft/msedge-beta/msedge', + 'darwin': '/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta', + 'win32': `\\Microsoft\\Edge Beta\\Application\\msedge.exe`, + }, () => this._installMSEdgeChannel('msedge-beta', { + 'darwin': 'reinstall_msedge_beta_mac.sh', + 'linux': 'reinstall_msedge_beta_linux.sh', + 'win32': 'reinstall_msedge_beta_win.ps1', + }))); + + this._executables.push(this._createChromiumChannel('msedge-dev', { + 'linux': '/opt/microsoft/msedge-dev/msedge', + 'darwin': '/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev', + 'win32': `\\Microsoft\\Edge Dev\\Application\\msedge.exe`, + })); + + this._executables.push(this._createChromiumChannel('msedge-canary', { + 'linux': '', + 'darwin': '/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary', + 'win32': `\\Microsoft\\Edge SxS\\Application\\msedge.exe`, + })); + const firefox = descriptors.find(d => d.name === 'firefox')!; - const firefoxExecutable = executablePath(firefox.dir, 'firefox'); + const firefoxExecutable = findExecutablePath(firefox.dir, 'firefox'); this._executables.push({ type: 'browser', name: 'firefox', browserName: 'firefox', - directoryIfExists: () => directoryIfExists(firefox.dir), - maybeExecutablePath: () => firefoxExecutable, - executablePathIfExists: () => executablePathIfExists(firefoxExecutable), + directory: firefox.dir, + executablePath: () => firefoxExecutable, + executablePathOrDie: () => executablePathOrDie('firefox', firefoxExecutable), installType: firefox.installByDefault ? 'download-by-default' : 'download-on-demand', validateHostRequirements: () => this._validateHostRequirements('firefox', firefox.dir, ['firefox'], [], ['firefox']), - _download: () => this._downloadExecutable(firefox, firefoxExecutable, DOWNLOAD_URLS['firefox'][hostPlatform], 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST'), + _install: () => this._downloadExecutable(firefox, firefoxExecutable, DOWNLOAD_URLS['firefox'][hostPlatform], 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST'), + _dependencyGroup: 'firefox', }); const firefoxBeta = descriptors.find(d => d.name === 'firefox-beta')!; - const firefoxBetaExecutable = executablePath(firefoxBeta.dir, 'firefox'); + const firefoxBetaExecutable = findExecutablePath(firefoxBeta.dir, 'firefox'); this._executables.push({ type: 'tool', name: 'firefox-beta', browserName: 'firefox', - directoryIfExists: () => directoryIfExists(firefoxBeta.dir), - maybeExecutablePath: () => firefoxBetaExecutable, - executablePathIfExists: () => executablePathIfExists(firefoxBetaExecutable), + directory: firefoxBeta.dir, + executablePath: () => firefoxBetaExecutable, + executablePathOrDie: () => executablePathOrDie('firefox-beta', firefoxBetaExecutable), installType: firefoxBeta.installByDefault ? 'download-by-default' : 'download-on-demand', validateHostRequirements: () => this._validateHostRequirements('firefox', firefoxBeta.dir, ['firefox'], [], ['firefox']), - _download: () => this._downloadExecutable(firefoxBeta, firefoxBetaExecutable, DOWNLOAD_URLS['firefox-beta'][hostPlatform], 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST'), + _install: () => this._downloadExecutable(firefoxBeta, firefoxBetaExecutable, DOWNLOAD_URLS['firefox-beta'][hostPlatform], 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST'), + _dependencyGroup: 'firefox', }); const webkit = descriptors.find(d => d.name === 'webkit')!; - const webkitExecutable = executablePath(webkit.dir, 'webkit'); + const webkitExecutable = findExecutablePath(webkit.dir, 'webkit'); const webkitLinuxLddDirectories = [ path.join('minibrowser-gtk'), path.join('minibrowser-gtk', 'bin'), @@ -315,29 +392,73 @@ export class Registry { type: 'browser', name: 'webkit', browserName: 'webkit', - directoryIfExists: () => directoryIfExists(webkit.dir), - maybeExecutablePath: () => webkitExecutable, - executablePathIfExists: () => executablePathIfExists(webkitExecutable), + directory: webkit.dir, + executablePath: () => webkitExecutable, + executablePathOrDie: () => executablePathOrDie('webkit', webkitExecutable), installType: webkit.installByDefault ? 'download-by-default' : 'download-on-demand', validateHostRequirements: () => this._validateHostRequirements('webkit', webkit.dir, webkitLinuxLddDirectories, ['libGLESv2.so.2', 'libx264.so'], ['']), - _download: () => this._downloadExecutable(webkit, webkitExecutable, DOWNLOAD_URLS['webkit'][hostPlatform], 'PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST'), + _install: () => this._downloadExecutable(webkit, webkitExecutable, DOWNLOAD_URLS['webkit'][hostPlatform], 'PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST'), + _dependencyGroup: 'webkit', }); const ffmpeg = descriptors.find(d => d.name === 'ffmpeg')!; - const ffmpegExecutable = executablePath(ffmpeg.dir, 'ffmpeg'); + const ffmpegExecutable = findExecutablePath(ffmpeg.dir, 'ffmpeg'); this._executables.push({ type: 'tool', name: 'ffmpeg', browserName: undefined, - directoryIfExists: () => directoryIfExists(ffmpeg.dir), - maybeExecutablePath: () => ffmpegExecutable, - executablePathIfExists: () => executablePathIfExists(ffmpegExecutable), + directory: ffmpeg.dir, + executablePath: () => ffmpegExecutable, + executablePathOrDie: () => executablePathOrDie('ffmpeg', ffmpegExecutable), installType: ffmpeg.installByDefault ? 'download-by-default' : 'download-on-demand', validateHostRequirements: () => Promise.resolve(), - _download: () => this._downloadExecutable(ffmpeg, ffmpegExecutable, DOWNLOAD_URLS['ffmpeg'][hostPlatform], 'PLAYWRIGHT_FFMPEG_DOWNLOAD_HOST'), + _install: () => this._downloadExecutable(ffmpeg, ffmpegExecutable, DOWNLOAD_URLS['ffmpeg'][hostPlatform], 'PLAYWRIGHT_FFMPEG_DOWNLOAD_HOST'), + _dependencyGroup: 'tools', }); } + private _createChromiumChannel(name: ChromiumChannel, lookAt: Record<'linux' | 'darwin' | 'win32', string>, install?: () => Promise): ExecutableImpl { + const executablePath = (shouldThrow: boolean) => { + const suffix = lookAt[process.platform as 'linux' | 'darwin' | 'win32']; + if (!suffix) { + if (shouldThrow) + throw new Error(`Chromium distribution '${name}' is not supported on ${process.platform}`); + return undefined; + } + const prefixes = (process.platform === 'win32' ? [ + process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'] + ].filter(Boolean) : ['']) as string[]; + + for (const prefix of prefixes) { + const executablePath = path.join(prefix, suffix); + if (canAccessFile(executablePath)) + return executablePath; + } + if (!shouldThrow) + return undefined; + + const location = prefixes.length ? ` at ${path.join(prefixes[0], suffix)}` : ``; + // TODO: language-specific error message + const installation = install ? `\nRun "npx playwright install ${name}"` : ''; + throw new Error(`Chromium distribution '${name}' is not found${location}${installation}`); + }; + return { + type: 'channel', + name, + browserName: 'chromium', + directory: undefined, + executablePath: () => executablePath(false), + executablePathOrDie: () => executablePath(true)!, + installType: install ? 'install-script' : 'none', + validateHostRequirements: () => Promise.resolve(), + _install: install, + }; + } + + executables(): Executable[] { + return this._executables; + } + findExecutable(name: BrowserName): Executable; findExecutable(name: string): Executable | undefined; findExecutable(name: string): Executable | undefined { @@ -373,10 +494,10 @@ export class Registry { async installDeps(executablesToInstallDeps?: Executable[]) { const executables = this._addRequirementsAndDedupe(executablesToInstallDeps); - const targets = new Set<'chromium' | 'firefox' | 'webkit' | 'tools'>(); + const targets = new Set(); for (const executable of executables) { - if (executable.browserName) - targets.add(executable.browserName); + if (executable._dependencyGroup) + targets.add(executable._dependencyGroup); } targets.add('tools'); if (os.platform() === 'win32') @@ -414,8 +535,8 @@ export class Registry { // Install browsers for this package. for (const executable of executables) { - if (executable._download) - await executable._download(); + if (executable._install) + await executable._install(); else throw new Error(`ERROR: Playwright does not support installing ${executable.name}`); } @@ -440,6 +561,36 @@ export class Registry { await fs.promises.writeFile(markerFilePath(descriptor.dir), ''); } + private async _installMSEdgeChannel(channel: string, scripts: Record<'linux' | 'darwin' | 'win32', string>) { + const scriptArgs: string[] = []; + if (process.platform !== 'linux') { + const products = JSON.parse(await fetchData('https://edgeupdates.microsoft.com/api/products')); + const productName = channel === 'msedge' ? 'Stable' : 'Beta'; + const product = products.find((product: any) => product.Product === productName); + const searchConfig = ({ + darwin: {platform: 'MacOS', arch: 'universal', artifact: 'pkg'}, + win32: {platform: 'Windows', arch: os.arch() === 'x64' ? 'x64' : 'x86', artifact: 'msi'}, + } as any)[process.platform]; + const release = searchConfig ? product.Releases.find((release: any) => release.Platform === searchConfig.platform && release.Architecture === searchConfig.arch) : null; + const artifact = release ? release.Artifacts.find((artifact: any) => artifact.ArtifactName === searchConfig.artifact) : null; + if (artifact) + scriptArgs.push(artifact.Location /* url */); + else + throw new Error(`Cannot install ${channel} on ${process.platform}`); + } + await this._installChromiumChannel(channel, scripts, scriptArgs); + } + + private async _installChromiumChannel(channel: string, scripts: Record<'linux' | 'darwin' | 'win32', string>, scriptArgs: string[] = []) { + const scriptName = scripts[process.platform as 'linux' | 'darwin' | 'win32']; + if (!scriptName) + throw new Error(`Cannot install ${channel} on ${process.platform}`); + const shell = scriptName.endsWith('.ps1') ? 'powershell.exe' : 'bash'; + const { code } = await spawnAsync(shell, [path.join(BIN_PATH, scriptName), ...scriptArgs], { cwd: BIN_PATH, stdio: 'inherit' }); + if (code !== 0) + throw new Error(`Failed to install ${channel}`); + } + private async _validateInstallationCache(linksDir: string) { // 1. Collect used downloads and package descriptors. const usedBrowserPaths: Set = new Set(); diff --git a/tests/playwright-test/playwright.spec.ts b/tests/playwright-test/playwright.spec.ts index 47259874e8..d0db16aa09 100644 --- a/tests/playwright-test/playwright.spec.ts +++ b/tests/playwright-test/playwright.spec.ts @@ -20,7 +20,7 @@ import path from 'path'; import { spawnSync } from 'child_process'; import { registry } from '../../src/utils/registry'; -const ffmpeg = registry.findExecutable('ffmpeg')!.executablePathIfExists() || ''; +const ffmpeg = registry.findExecutable('ffmpeg')!.executablePath(); export class VideoPlayer { videoWidth: number; diff --git a/tests/video.spec.ts b/tests/video.spec.ts index af01c53e6f..fb8ba95625 100644 --- a/tests/video.spec.ts +++ b/tests/video.spec.ts @@ -21,7 +21,7 @@ import { spawnSync } from 'child_process'; import { PNG } from 'pngjs'; import { registry } from '../src/utils/registry'; -const ffmpeg = registry.findExecutable('ffmpeg')!.executablePathIfExists() || ''; +const ffmpeg = registry.findExecutable('ffmpeg')!.executablePath(); export class VideoPlayer { fileName: string; diff --git a/utils/roll_browser.js b/utils/roll_browser.js index 3247f9bbd1..cf7717b1bd 100755 --- a/utils/roll_browser.js +++ b/utils/roll_browser.js @@ -78,7 +78,7 @@ Example: // 4. Generate types. console.log('\nGenerating protocol types...'); - const executablePath = new Registry(ROOT_PATH).findBinary(binaryName).executablePathIfExists(); + const executablePath = new Registry(ROOT_PATH).findBinary(binaryName).executablePathOrDie(); await protocolGenerator.generateProtocol(browserName, executablePath).catch(console.warn); // 5. Update docs.