chore: move working with browser channels to Registry Executables (#7581)

This commit is contained in:
Dmitry Gozman 2021-07-13 19:03:49 -07:00 committed by GitHub
parent 34777853f7
commit 0742cb9076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 273 additions and 291 deletions

View File

@ -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<BrowserChannel> = 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<BrowserChannel> = 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 = [

View File

@ -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<string>(f => wsEndpointCallback = f) : undefined;

View File

@ -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;
}

View File

@ -845,10 +845,9 @@ class FrameSession {
async _createVideoRecorder(screencastId: string, options: types.PageScreencastOptions): Promise<void> {
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;

View File

@ -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}`);
}

View File

@ -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<FFBrowser> {
return FFBrowser.connect(transport, options);
}

View File

@ -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(), '', {

View File

@ -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

View File

@ -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<DependencyGroup>) {
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<DependencyGroup>) {
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

View File

@ -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<void>;
}
interface ExecutableImpl extends Executable {
_download?: () => Promise<void>;
_install?: () => Promise<void>;
_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<void>): 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<DependencyGroup>();
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<string> = new Set();

View File

@ -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;

View File

@ -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;

View File

@ -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.