diff --git a/install-from-github.js b/install-from-github.js index ac44f4823c..36a4dc4655 100644 --- a/install-from-github.js +++ b/install-from-github.js @@ -27,7 +27,7 @@ try { console.log(`Downloading browsers...`); const { installBrowsersWithProgressBar } = require('./lib/install/installer'); -installBrowsersWithProgressBar(__dirname).catch(e => { +installBrowsersWithProgressBar().catch(e => { console.error(`Failed to install browsers, caused by\n${e.stack}`); process.exit(1); }); diff --git a/packages/common/install.js b/packages/common/install.js index 13c8ccee30..67f7ba312a 100644 --- a/packages/common/install.js +++ b/packages/common/install.js @@ -16,4 +16,4 @@ const { installBrowsersWithProgressBar } = require('./lib/install/installer'); -installBrowsersWithProgressBar(__dirname); +installBrowsersWithProgressBar(); diff --git a/src/cli/driver.ts b/src/cli/driver.ts index 7209898541..c623497bea 100644 --- a/src/cli/driver.ts +++ b/src/cli/driver.ts @@ -24,7 +24,7 @@ import { installBrowsersWithProgressBar } from '../install/installer'; import { Transport } from '../protocol/transport'; import { createPlaywright } from '../server/playwright'; import { gracefullyCloseAll } from '../server/processLauncher'; -import { BrowserName } from '../utils/browserPaths'; +import { BrowserName } from '../utils/registry'; export function printApiJson() { console.log(JSON.stringify(require('../../api.json'))); @@ -54,6 +54,5 @@ export function runServer() { } export async function installBrowsers(browserNames?: BrowserName[]) { - const browsersJsonDir = path.join(__dirname, '..', '..'); - await installBrowsersWithProgressBar(browsersJsonDir, browserNames); + await installBrowsersWithProgressBar(browserNames); } diff --git a/src/install/browserFetcher.ts b/src/install/browserFetcher.ts index c373ad04e0..c6e2d2ab99 100644 --- a/src/install/browserFetcher.ts +++ b/src/install/browserFetcher.ts @@ -23,9 +23,7 @@ import * as ProgressBar from 'progress'; import { getProxyForUrl } from 'proxy-from-env'; import * as URL from 'url'; import * as util from 'util'; -import { assert, getFromENV } from '../utils/utils'; -import * as browserPaths from '../utils/browserPaths'; -import { BrowserName, BrowserPlatform, BrowserDescriptor } from '../utils/browserPaths'; +import { BrowserName, Registry, hostPlatform } from '../utils/registry'; // `https-proxy-agent` v5 is written in Typescript and exposes generated types. // However, as of June 2020, its types are generated with tsconfig that enables @@ -42,88 +40,10 @@ const existsAsync = (path: string): Promise => new Promise(resolve => f export type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void; -function getDownloadHost(browserName: BrowserName, revision: number): string { - const envDownloadHost: { [key: string]: string } = { - chromium: 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST', - firefox: 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST', - webkit: 'PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST', - ffmpeg: 'PLAYWRIGHT_FFMPEG_DOWNLOAD_HOST', - }; - return getFromENV(envDownloadHost[browserName]) || - getFromENV('PLAYWRIGHT_DOWNLOAD_HOST') || - 'https://playwright.azureedge.net'; -} - -function getDownloadUrl(browserName: BrowserName, revision: number, platform: BrowserPlatform): string | undefined { - if (browserName === 'chromium') { - return new Map([ - ['ubuntu18.04', '%s/builds/chromium/%s/chromium-linux.zip'], - ['ubuntu20.04', '%s/builds/chromium/%s/chromium-linux.zip'], - ['mac10.13', '%s/builds/chromium/%s/chromium-mac.zip'], - ['mac10.14', '%s/builds/chromium/%s/chromium-mac.zip'], - ['mac10.15', '%s/builds/chromium/%s/chromium-mac.zip'], - ['mac11', '%s/builds/chromium/%s/chromium-mac.zip'], - ['mac11-arm64', '%s/builds/chromium/%s/chromium-mac-arm64.zip'], - ['win32', '%s/builds/chromium/%s/chromium-win32.zip'], - ['win64', '%s/builds/chromium/%s/chromium-win64.zip'], - ]).get(platform); - } - - if (browserName === 'firefox') { - return new Map([ - ['ubuntu18.04', '%s/builds/firefox/%s/firefox-ubuntu-18.04.zip'], - ['ubuntu20.04', '%s/builds/firefox/%s/firefox-ubuntu-18.04.zip'], - ['mac10.13', '%s/builds/firefox/%s/firefox-mac-10.14.zip'], - ['mac10.14', '%s/builds/firefox/%s/firefox-mac-10.14.zip'], - ['mac10.15', '%s/builds/firefox/%s/firefox-mac-10.14.zip'], - ['mac11', '%s/builds/firefox/%s/firefox-mac-10.14.zip'], - ['mac11-arm64', '%s/builds/firefox/%s/firefox-mac-11.0-arm64.zip'], - ['win32', '%s/builds/firefox/%s/firefox-win32.zip'], - ['win64', '%s/builds/firefox/%s/firefox-win64.zip'], - ]).get(platform); - } - - if (browserName === 'webkit') { - return new Map([ - ['ubuntu18.04', '%s/builds/webkit/%s/webkit-ubuntu-18.04.zip'], - ['ubuntu20.04', '%s/builds/webkit/%s/webkit-ubuntu-20.04.zip'], - ['mac10.13', undefined], - ['mac10.14', '%s/builds/webkit/%s/webkit-mac-10.14.zip'], - ['mac10.15', '%s/builds/webkit/%s/webkit-mac-10.15.zip'], - ['mac11', '%s/builds/webkit/%s/webkit-mac-10.15.zip'], - ['mac11-arm64', '%s/builds/webkit/%s/webkit-mac-11.0-arm64.zip'], - ['win32', '%s/builds/webkit/%s/webkit-win64.zip'], - ['win64', '%s/builds/webkit/%s/webkit-win64.zip'], - ]).get(platform); - } - - if (browserName === 'ffmpeg') { - return new Map([ - ['ubuntu18.04', '%s/builds/ffmpeg/%s/ffmpeg-linux.zip'], - ['ubuntu20.04', '%s/builds/ffmpeg/%s/ffmpeg-linux.zip'], - ['mac10.13', '%s/builds/ffmpeg/%s/ffmpeg-mac.zip'], - ['mac10.14', '%s/builds/ffmpeg/%s/ffmpeg-mac.zip'], - ['mac10.15', '%s/builds/ffmpeg/%s/ffmpeg-mac.zip'], - ['mac11', '%s/builds/ffmpeg/%s/ffmpeg-mac.zip'], - ['mac11-arm64', '%s/builds/ffmpeg/%s/ffmpeg-mac.zip'], - ['win32', '%s/builds/ffmpeg/%s/ffmpeg-win32.zip'], - ['win64', '%s/builds/ffmpeg/%s/ffmpeg-win64.zip'], - ]).get(platform); - } -} - -function revisionURL(browser: BrowserDescriptor, platform = browserPaths.hostPlatform): string { - const revision = parseInt(browser.revision, 10); - const serverHost = getDownloadHost(browser.name, revision); - const urlTemplate = getDownloadUrl(browser.name, revision, platform); - assert(urlTemplate, `ERROR: Playwright does not support ${browser.name} on ${platform}`); - return util.format(urlTemplate, serverHost, browser.revision); -} - -export async function downloadBrowserWithProgressBar(browsersPath: string, browser: BrowserDescriptor): Promise { - const browserPath = browserPaths.browserDirectory(browsersPath, browser); - const progressBarName = `${browser.name} v${browser.revision}`; - if (await existsAsync(browserPath)) { +export async function downloadBrowserWithProgressBar(registry: Registry, browserName: BrowserName): Promise { + const browserDirectory = registry.browserDirectory(browserName); + const progressBarName = `${browserName} v${registry.revision(browserName)}`; + if (await existsAsync(browserDirectory)) { // Already downloaded. return false; } @@ -145,8 +65,8 @@ export async function downloadBrowserWithProgressBar(browsersPath: string, brows progressBar.tick(delta); } - const url = revisionURL(browser); - const zipPath = path.join(os.tmpdir(), `playwright-download-${browser.name}-${browserPaths.hostPlatform}-${browser.revision}.zip`); + const url = registry.downloadURL(browserName); + const zipPath = path.join(os.tmpdir(), `playwright-download-${browserName}-${hostPlatform}-${registry.revision(browserName)}.zip`); try { for (let attempt = 1, N = 3; attempt <= N; ++attempt) { const {error} = await downloadFile(url, zipPath, progress); @@ -161,8 +81,8 @@ export async function downloadBrowserWithProgressBar(browsersPath: string, brows throw error; } } - await extract(zipPath, { dir: browserPath}); - await chmodAsync(browserPaths.executablePath(browserPath, browser)!, 0o755); + await extract(zipPath, { dir: browserDirectory}); + await chmodAsync(registry.executablePath(browserName)!, 0o755); } catch (e) { process.exitCode = 1; throw e; @@ -170,7 +90,7 @@ export async function downloadBrowserWithProgressBar(browsersPath: string, brows if (await existsAsync(zipPath)) await unlinkAsync(zipPath); } - logPolitely(`${progressBarName} downloaded to ${browserPath}`); + logPolitely(`${progressBarName} downloaded to ${browserDirectory}`); return true; } diff --git a/src/install/installer.ts b/src/install/installer.ts index ece657f83d..6dbdcc4aa9 100644 --- a/src/install/installer.ts +++ b/src/install/installer.ts @@ -14,15 +14,14 @@ * limitations under the License. */ -import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import * as util from 'util'; import * as removeFolder from 'rimraf'; import * as lockfile from 'proper-lockfile'; -import * as browserPaths from '../utils/browserPaths'; +import {Registry, allBrowserNames, isBrowserDirectory, BrowserName, registryDirectory} from '../utils/registry'; import * as browserFetcher from './browserFetcher'; -import { getAsBooleanFromENV } from '../utils/utils'; +import { getAsBooleanFromENV, calculateSha1 } from '../utils/utils'; const fsMkdirAsync = util.promisify(fs.mkdir.bind(fs)); const fsReaddirAsync = util.promisify(fs.readdir.bind(fs)); @@ -32,17 +31,18 @@ const fsUnlinkAsync = util.promisify(fs.unlink.bind(fs)); const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const removeFolderAsync = util.promisify(removeFolder); -export async function installBrowsersWithProgressBar(packagePath: string, browserNames?: browserPaths.BrowserName[]) { +const PACKAGE_PATH = path.join(__dirname, '..', '..'); + +export async function installBrowsersWithProgressBar(browserNames: BrowserName[] = allBrowserNames) { // PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD should have a value of 0 or 1 if (getAsBooleanFromENV('PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD')) { browserFetcher.logPolitely('Skipping browsers download because `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` env variable is set'); return false; } - const browsersPath = browserPaths.browsersPath(packagePath); - await fsMkdirAsync(browsersPath, { recursive: true }); - const lockfilePath = path.join(browsersPath, '__dirlock'); - const releaseLock = await lockfile.lock(browsersPath, { + await fsMkdirAsync(registryDirectory, { recursive: true }); + const lockfilePath = path.join(registryDirectory, '__dirlock'); + const releaseLock = await lockfile.lock(registryDirectory, { retries: { retries: 10, // Retry 20 times during 10 minutes with @@ -55,18 +55,18 @@ export async function installBrowsersWithProgressBar(packagePath: string, browse }, lockfilePath, }); - const linksDir = path.join(browsersPath, '.links'); + const linksDir = path.join(registryDirectory, '.links'); try { await fsMkdirAsync(linksDir, { recursive: true }); - await fsWriteFileAsync(path.join(linksDir, sha1(packagePath)), packagePath); - await validateCache(packagePath, browsersPath, linksDir, browserNames); + await fsWriteFileAsync(path.join(linksDir, calculateSha1(PACKAGE_PATH)), PACKAGE_PATH); + await validateCache(linksDir, browserNames); } finally { await releaseLock(); } } -async function validateCache(packagePath: string, browsersPath: string, linksDir: string, browserNames?: browserPaths.BrowserName[]) { +async function validateCache(linksDir: string, browserNames: BrowserName[]) { // 1. Collect used downloads and package descriptors. const usedBrowserPaths: Set = new Set(); for (const fileName of await fsReaddirAsync(linksDir)) { @@ -74,15 +74,19 @@ async function validateCache(packagePath: string, browsersPath: string, linksDir let linkTarget = ''; try { linkTarget = (await fsReadFileAsync(linkPath)).toString(); - const browsersToDownload = await readBrowsersToDownload(linkTarget); - for (const browser of browsersToDownload) { - const usedBrowserPath = browserPaths.browserDirectory(browsersPath, browser); - const browserRevision = parseInt(browser.revision, 10); + const linkRegistry = new Registry(linkTarget); + for (const browserName of allBrowserNames) { + if (!linkRegistry.shouldDownload(browserName)) + continue; + const usedBrowserPath = linkRegistry.browserDirectory(browserName); + const browserRevision = linkRegistry.revision(browserName); // Old browser installations don't have marker file. - const shouldHaveMarkerFile = (browser.name === 'chromium' && browserRevision >= 786218) || - (browser.name === 'firefox' && browserRevision >= 1128) || - (browser.name === 'webkit' && browserRevision >= 1307); - if (!shouldHaveMarkerFile || (await fsExistsAsync(browserPaths.markerFilePath(browsersPath, browser)))) + const shouldHaveMarkerFile = (browserName === 'chromium' && browserRevision >= 786218) || + (browserName === 'firefox' && browserRevision >= 1128) || + (browserName === 'webkit' && browserRevision >= 1307) || + // All new applications have a marker file right away. + (browserName !== 'firefox' && browserName !== 'chromium' && browserName !== 'webkit'); + if (!shouldHaveMarkerFile || (await fsExistsAsync(markerFilePath(usedBrowserPath)))) usedBrowserPaths.add(usedBrowserPath); } } catch (e) { @@ -91,37 +95,29 @@ async function validateCache(packagePath: string, browsersPath: string, linksDir } // 2. Delete all unused browsers. - let downloadedBrowsers = (await fsReaddirAsync(browsersPath)).map(file => path.join(browsersPath, file)); - downloadedBrowsers = downloadedBrowsers.filter(file => browserPaths.isBrowserDirectory(file)); + let downloadedBrowsers = (await fsReaddirAsync(registryDirectory)).map(file => path.join(registryDirectory, file)); + downloadedBrowsers = downloadedBrowsers.filter(file => isBrowserDirectory(file)); const directories = new Set(downloadedBrowsers); - for (const browserPath of usedBrowserPaths) - directories.delete(browserPath); + for (const browserDirectory of usedBrowserPaths) + directories.delete(browserDirectory); for (const directory of directories) { browserFetcher.logPolitely('Removing unused browser at ' + directory); await removeFolderAsync(directory).catch(e => {}); } // 3. Install missing browsers for this package. - const myBrowsersToDownload = await readBrowsersToDownload(packagePath, browserNames); - for (const browser of myBrowsersToDownload) { - await browserFetcher.downloadBrowserWithProgressBar(browsersPath, browser).catch(e => { - throw new Error(`Failed to download ${browser.name}, caused by\n${e.stack}`); + const myRegistry = new Registry(PACKAGE_PATH); + for (const browserName of browserNames) { + if (!myRegistry.shouldDownload(browserName)) + continue; + await browserFetcher.downloadBrowserWithProgressBar(myRegistry, browserName).catch(e => { + throw new Error(`Failed to download ${browserName}, caused by\n${e.stack}`); }); - await fsWriteFileAsync(browserPaths.markerFilePath(browsersPath, browser), ''); + await fsWriteFileAsync(markerFilePath(myRegistry.browserDirectory(browserName)), ''); } } -async function readBrowsersToDownload(packagePath: string, browserNames?: browserPaths.BrowserName[]) { - const browsers = JSON.parse((await fsReadFileAsync(path.join(packagePath, 'browsers.json'))).toString())['browsers'] as browserPaths.BrowserDescriptor[]; - // Older versions do not have "download" field. We assume they need all browsers - // from the list. So we want to skip all browsers that are explicitly marked as "download: false". - return browsers.filter(browser => { - return browserNames ? browserNames.includes(browser.name) : browser.download !== false; - }); +function markerFilePath(browserDirectory: string): string { + return path.join(browserDirectory, 'INSTALLATION_COMPLETE'); } -function sha1(data: string): string { - const sum = crypto.createHash('sha1'); - sum.update(data); - return sum.digest('hex'); -} diff --git a/src/server/android/android.ts b/src/server/android/android.ts index cac265bce5..e80fa81a9f 100644 --- a/src/server/android/android.ts +++ b/src/server/android/android.ts @@ -22,7 +22,6 @@ import * as stream from 'stream'; import * as util from 'util'; import * as ws from 'ws'; import { createGuid, makeWaitForNextTask } from '../../utils/utils'; -import * as browserPaths from '../../utils/browserPaths'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import { BrowserContext, validateBrowserContextOptions } from '../browserContext'; import { ProgressController } from '../progress'; @@ -57,14 +56,10 @@ export interface SocketBackend extends EventEmitter { export class Android { private _backend: Backend; private _devices = new Map(); - readonly _ffmpegPath: string | null; readonly _timeoutSettings: TimeoutSettings; readonly _playwrightOptions: PlaywrightOptions; - constructor(packagePath: string, backend: Backend, playwrightOptions: PlaywrightOptions, ffmpeg: browserPaths.BrowserDescriptor) { - const browsersPath = browserPaths.browsersPath(packagePath); - const browserPath = browserPaths.browserDirectory(browsersPath, ffmpeg); - this._ffmpegPath = browserPaths.executablePath(browserPath, ffmpeg) || null; + constructor(backend: Backend, playwrightOptions: PlaywrightOptions) { this._backend = backend; this._playwrightOptions = playwrightOptions; this._timeoutSettings = new TimeoutSettings(); @@ -276,7 +271,7 @@ export class AndroidDevice extends EventEmitter { }; validateBrowserContextOptions(options, browserOptions); - const browser = await CRBrowser.connect(androidBrowser, browserOptions, this._android._ffmpegPath); + const browser = await CRBrowser.connect(androidBrowser, browserOptions); const controller = new ProgressController(); const defaultContext = browser._defaultContext!; await controller.run(async progress => { diff --git a/src/server/browser.ts b/src/server/browser.ts index ed2736ddb7..39de1a4415 100644 --- a/src/server/browser.ts +++ b/src/server/browser.ts @@ -22,6 +22,7 @@ import { Download } from './download'; import { ProxySettings } from './types'; import { ChildProcess } from 'child_process'; import { RecentLogsCollector } from '../utils/debugLogger'; +import * as registry from '../utils/registry'; export interface BrowserProcess { onclose: ((exitCode: number | null, signal: string | null) => void) | undefined; @@ -32,6 +33,7 @@ export interface BrowserProcess { export type PlaywrightOptions = { contextListeners: ContextListener[], + registry: registry.Registry, isInternal: boolean }; diff --git a/src/server/browserType.ts b/src/server/browserType.ts index e2149141d7..a4908dcdbc 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -19,7 +19,7 @@ import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; import { BrowserContext, normalizeProxySettings, validateBrowserContextOptions } from './browserContext'; -import * as browserPaths from '../utils/browserPaths'; +import * as registry from '../utils/registry'; import { ConnectionTransport } from './transport'; import { BrowserOptions, Browser, BrowserProcess, PlaywrightOptions } from './browser'; import { launchProcess, Env, envArrayToObject } from './processLauncher'; @@ -38,23 +38,18 @@ const existsAsync = (path: string): Promise => new Promise(resolve => f const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-'); export abstract class BrowserType { - private _name: browserPaths.BrowserName; - private _executablePath: string; - private _browserDescriptor: browserPaths.BrowserDescriptor; - readonly _browserPath: string; + private _name: registry.BrowserName; + readonly _registry: registry.Registry; readonly _playwrightOptions: PlaywrightOptions; - constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, playwrightOptions: PlaywrightOptions) { + constructor(browserName: registry.BrowserName, playwrightOptions: PlaywrightOptions) { this._playwrightOptions = playwrightOptions; - this._name = browser.name; - const browsersPath = browserPaths.browsersPath(packagePath); - this._browserDescriptor = browser; - this._browserPath = browserPaths.browserDirectory(browsersPath, browser); - this._executablePath = browserPaths.executablePath(this._browserPath, browser) || ''; + this._name = browserName; + this._registry = playwrightOptions.registry; } executablePath(): string { - return this._executablePath; + return this._registry.executablePath(this._name) || ''; } name(): string { @@ -179,7 +174,7 @@ export abstract class BrowserType { if (!executablePath) { // We can only validate dependencies for bundled browsers. - await validateHostRequirements(this._browserPath, this._browserDescriptor); + await validateHostRequirements(this._registry, this._name); } // Note: it is important to define these variables before launchProcess, so that we don't get diff --git a/src/server/chromium/chromium.ts b/src/server/chromium/chromium.ts index 439ea37f7a..dc6f46c5a9 100644 --- a/src/server/chromium/chromium.ts +++ b/src/server/chromium/chromium.ts @@ -22,7 +22,6 @@ import { kBrowserCloseMessageId } from './crConnection'; import { rewriteErrorMessage } from '../../utils/stackTrace'; import { BrowserType } from '../browserType'; import { ConnectionTransport, ProtocolRequest } from '../transport'; -import * as browserPaths from '../../utils/browserPaths'; import { CRDevTools } from './crDevTools'; import { BrowserOptions, PlaywrightOptions } from '../browser'; import * as types from '../types'; @@ -30,20 +29,16 @@ import { isDebugMode } from '../../utils/utils'; export class Chromium extends BrowserType { private _devtools: CRDevTools | undefined; - private _ffmpegPath: string | null; - constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, ffmpeg: browserPaths.BrowserDescriptor, playwrightOptions: PlaywrightOptions) { - super(packagePath, browser, playwrightOptions); + constructor(playwrightOptions: PlaywrightOptions) { + super('chromium', playwrightOptions); - const browsersPath = browserPaths.browsersPath(packagePath); - const browserPath = browserPaths.browserDirectory(browsersPath, ffmpeg); - this._ffmpegPath = browserPaths.executablePath(browserPath, ffmpeg) || null; if (isDebugMode()) this._devtools = this._createDevTools(); } private _createDevTools() { - return new CRDevTools(path.join(this._browserPath, 'devtools-preferences.json')); + return new CRDevTools(path.join(this._registry.browserDirectory('chromium'), 'devtools-preferences.json')); } async _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise { @@ -52,7 +47,7 @@ export class Chromium extends BrowserType { devtools = this._createDevTools(); await (options as any).__testHookForDevTools(devtools); } - return CRBrowser.connect(transport, options, this._ffmpegPath, devtools); + return CRBrowser.connect(transport, options, devtools); } _rewriteStartupError(error: Error): Error { diff --git a/src/server/chromium/crBrowser.ts b/src/server/chromium/crBrowser.ts index ad3a7e5bb1..aab3e7866c 100644 --- a/src/server/chromium/crBrowser.ts +++ b/src/server/chromium/crBrowser.ts @@ -40,15 +40,14 @@ export class CRBrowser extends Browser { _devtools?: CRDevTools; _isMac = false; private _version = ''; - readonly _ffmpegPath: string | null; private _tracingRecording = false; private _tracingPath: string | null = ''; private _tracingClient: CRSession | undefined; - static async connect(transport: ConnectionTransport, options: BrowserOptions, ffmpegPath: string | null, devtools?: CRDevTools): Promise { + static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise { const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector); - const browser = new CRBrowser(connection, options, ffmpegPath); + const browser = new CRBrowser(connection, options); browser._devtools = devtools; const session = connection.rootSession; const version = await session.send('Browser.getVersion'); @@ -89,9 +88,8 @@ export class CRBrowser extends Browser { return browser; } - constructor(connection: CRConnection, options: BrowserOptions, ffmpegPath: string | null) { + constructor(connection: CRConnection, options: BrowserOptions) { super(options); - this._ffmpegPath = ffmpegPath; this._connection = connection; this._session = this._connection.rootSession; this._connection.on(ConnectionEvents.Disconnected, () => this._didClose()); diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts index 86dbdbac8e..ae3d8e660e 100644 --- a/src/server/chromium/crPage.ts +++ b/src/server/chromium/crPage.ts @@ -811,7 +811,7 @@ class FrameSession { async _startScreencast(screencastId: string, options: types.PageScreencastOptions): Promise { assert(!this._screencastId); - const ffmpegPath = this._crPage._browserContext._browser._ffmpegPath; + const ffmpegPath = this._crPage._browserContext._browser.options.registry.executablePath('ffmpeg'); if (!ffmpegPath) throw new Error('ffmpeg executable was not found'); this._videoRecorder = await VideoRecorder.launch(ffmpegPath, options); diff --git a/src/server/electron/electron.ts b/src/server/electron/electron.ts index 1ff8a131a0..0279f6cf22 100644 --- a/src/server/electron/electron.ts +++ b/src/server/electron/electron.ts @@ -21,7 +21,6 @@ import { CRExecutionContext } from '../chromium/crExecutionContext'; import * as js from '../javascript'; import { Page } from '../page'; import { TimeoutSettings } from '../../utils/timeoutSettings'; -import * as browserPaths from '../../utils/browserPaths'; import { WebSocketTransport } from '../transport'; import * as types from '../types'; import { launchProcess, envArrayToObject } from '../processLauncher'; @@ -125,12 +124,8 @@ export class ElectronApplication extends EventEmitter { export class Electron { private _playwrightOptions: PlaywrightOptions; - private _ffmpegPath: string | null; - constructor(packagePath: string, playwrightOptions: PlaywrightOptions, ffmpeg: browserPaths.BrowserDescriptor) { - const browsersPath = browserPaths.browsersPath(packagePath); - const browserPath = browserPaths.browserDirectory(browsersPath, ffmpeg); - this._ffmpegPath = browserPaths.executablePath(browserPath, ffmpeg) || null; + constructor(playwrightOptions: PlaywrightOptions) { this._playwrightOptions = playwrightOptions; } @@ -188,7 +183,7 @@ export class Electron { protocolLogger: helper.debugProtocolLogger(), browserLogsCollector, }; - const browser = await CRBrowser.connect(chromeTransport, browserOptions, this._ffmpegPath); + const browser = await CRBrowser.connect(chromeTransport, browserOptions); app = new ElectronApplication(browser, nodeConnection); await app._init(); return app; diff --git a/src/server/firefox/firefox.ts b/src/server/firefox/firefox.ts index c0843ae68b..37b8801fa8 100644 --- a/src/server/firefox/firefox.ts +++ b/src/server/firefox/firefox.ts @@ -23,10 +23,14 @@ import { kBrowserCloseMessageId } from './ffConnection'; import { BrowserType } from '../browserType'; import { Env } from '../processLauncher'; import { ConnectionTransport } from '../transport'; -import { BrowserOptions } from '../browser'; +import { BrowserOptions, PlaywrightOptions } from '../browser'; import * as types from '../types'; export class Firefox extends BrowserType { + constructor(playwrightOptions: PlaywrightOptions) { + super('firefox', playwrightOptions); + } + _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise { return FFBrowser.connect(transport, options); } diff --git a/src/server/playwright.ts b/src/server/playwright.ts index 806861c82a..b9779026d1 100644 --- a/src/server/playwright.ts +++ b/src/server/playwright.ts @@ -16,7 +16,6 @@ import * as path from 'path'; import { Tracer } from '../trace/tracer'; -import * as browserPaths from '../utils/browserPaths'; import { Android } from './android/android'; import { AdbBackend } from './android/backendAdb'; import { PlaywrightOptions } from './browser'; @@ -27,6 +26,7 @@ import { serverSelectors } from './selectors'; import { HarTracer } from './supplements/har/harTracer'; import { InspectorController } from './supplements/inspectorController'; import { WebKit } from './webkit/webkit'; +import { Registry } from '../utils/registry'; export class Playwright { readonly selectors = serverSelectors; @@ -37,30 +37,25 @@ export class Playwright { readonly webkit: WebKit; readonly options: PlaywrightOptions; - constructor(isInternal: boolean, packagePath: string, browsers: browserPaths.BrowserDescriptor[]) { + constructor(isInternal: boolean) { this.options = { isInternal, + registry: new Registry(path.join(__dirname, '..', '..')), + contextListeners: isInternal ? [] : [ new InspectorController(), new Tracer(), new HarTracer() ] }; - const chromium = browsers.find(browser => browser.name === 'chromium'); - const ffmpeg = browsers.find(browser => browser.name === 'ffmpeg'); - this.chromium = new Chromium(packagePath, chromium!, ffmpeg!, this.options); - - const firefox = browsers.find(browser => browser.name === 'firefox'); - this.firefox = new Firefox(packagePath, firefox!, this.options); - - const webkit = browsers.find(browser => browser.name === 'webkit'); - this.webkit = new WebKit(packagePath, webkit!, this.options); - - this.electron = new Electron(packagePath, this.options, ffmpeg!); - this.android = new Android(packagePath, new AdbBackend(), this.options, ffmpeg!); + this.chromium = new Chromium(this.options); + this.firefox = new Firefox(this.options); + this.webkit = new WebKit(this.options); + this.electron = new Electron(this.options); + this.android = new Android(new AdbBackend(), this.options); } } export function createPlaywright(isInternal = false) { - return new Playwright(isInternal, path.join(__dirname, '..', '..'), require('../../browsers.json')['browsers']); + return new Playwright(isInternal); } diff --git a/src/server/validateDependencies.ts b/src/server/validateDependencies.ts index 85ac5ad847..1075416d82 100644 --- a/src/server/validateDependencies.ts +++ b/src/server/validateDependencies.ts @@ -19,7 +19,7 @@ import * as path from 'path'; import * as os from 'os'; import { spawn } from 'child_process'; import { getUbuntuVersion } from '../utils/ubuntuVersion'; -import { linuxLddDirectories, windowsExeAndDllDirectories, BrowserDescriptor } from '../utils/browserPaths.js'; +import * as registry from '../utils/registry'; import { printDepsWindowsExecutable } from '../utils/binaryPaths'; const accessAsync = util.promisify(fs.access.bind(fs)); @@ -27,14 +27,14 @@ const checkExecutable = (filePath: string) => accessAsync(filePath, fs.constants const statAsync = util.promisify(fs.stat.bind(fs)); const readdirAsync = util.promisify(fs.readdir.bind(fs)); -export async function validateHostRequirements(browserPath: string, browser: BrowserDescriptor) { +export async function validateHostRequirements(registry: registry.Registry, browserName: registry.BrowserName) { const ubuntuVersion = await getUbuntuVersion(); - if (browser.name === 'firefox' && ubuntuVersion === '16.04') + if (browserName === 'firefox' && ubuntuVersion === '16.04') throw new Error(`Cannot launch firefox on Ubuntu 16.04! Minimum required Ubuntu version for Firefox browser is 18.04`); if (os.platform() === 'linux') - return await validateDependenciesLinux(browserPath, browser); + return await validateDependenciesLinux(registry, browserName); if (os.platform() === 'win32' && os.arch() === 'x64') - return await validateDependenciesWindows(browserPath, browser); + return await validateDependenciesWindows(registry, browserName); } const DL_OPEN_LIBRARIES = { @@ -55,8 +55,8 @@ function isSupportedWindowsVersion(): boolean { return major > 6 || (major === 6 && minor > 1); } -async function validateDependenciesWindows(browserPath: string, browser: BrowserDescriptor) { - const directoryPaths = windowsExeAndDllDirectories(browserPath, browser); +async function validateDependenciesWindows(registry: registry.Registry, browserName: registry.BrowserName) { + const directoryPaths = registry.windowsExeAndDllDirectories(browserName); const lddPaths: string[] = []; for (const directoryPath of directoryPaths) lddPaths.push(...(await executablesOrSharedLibraries(directoryPath))); @@ -116,8 +116,8 @@ async function validateDependenciesWindows(browserPath: string, browser: Browser } } -async function validateDependenciesLinux(browserPath: string, browser: BrowserDescriptor) { - const directoryPaths = linuxLddDirectories(browserPath, browser); +async function validateDependenciesLinux(registry: registry.Registry, browserName: registry.BrowserName) { + const directoryPaths = registry.linuxLddDirectories(browserName); const lddPaths: string[] = []; for (const directoryPath of directoryPaths) lddPaths.push(...(await executablesOrSharedLibraries(directoryPath))); @@ -127,7 +127,7 @@ async function validateDependenciesLinux(browserPath: string, browser: BrowserDe for (const dep of deps) missingDeps.add(dep); } - for (const dep of (await missingDLOPENLibraries(browser))) + for (const dep of (await missingDLOPENLibraries(browserName))) missingDeps.add(dep); if (!missingDeps.size) return; @@ -240,8 +240,8 @@ async function missingFileDependencies(filePath: string, extraLDPaths: string[]) return missingDeps; } -async function missingDLOPENLibraries(browser: BrowserDescriptor): Promise { - const libraries = DL_OPEN_LIBRARIES[browser.name]; +async function missingDLOPENLibraries(browserName: registry.BrowserName): Promise { + const libraries = DL_OPEN_LIBRARIES[browserName]; if (!libraries.length) return []; // NOTE: Using full-qualified path to `ldconfig` since `/sbin` is not part of the diff --git a/src/server/webkit/webkit.ts b/src/server/webkit/webkit.ts index e4430c5485..d192038e3e 100644 --- a/src/server/webkit/webkit.ts +++ b/src/server/webkit/webkit.ts @@ -21,10 +21,14 @@ import * as path from 'path'; import { kBrowserCloseMessageId } from './wkConnection'; import { BrowserType } from '../browserType'; import { ConnectionTransport } from '../transport'; -import { BrowserOptions } from '../browser'; +import { BrowserOptions, PlaywrightOptions } from '../browser'; import * as types from '../types'; export class WebKit extends BrowserType { + constructor(playwrightOptions: PlaywrightOptions) { + super('webkit', playwrightOptions); + } + _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise { return WKBrowser.connect(transport, options); } diff --git a/src/utils/browserPaths.ts b/src/utils/browserPaths.ts deleted file mode 100644 index 912858d838..0000000000 --- a/src/utils/browserPaths.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Copyright 2017 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { execSync } from 'child_process'; -import * as os from 'os'; -import * as path from 'path'; -import { getUbuntuVersionSync } from './ubuntuVersion'; -import { getFromENV } from './utils'; - -export type BrowserName = 'chromium'|'webkit'|'firefox'|'ffmpeg'; -export type BrowserPlatform = 'win32'|'win64'|'mac10.13'|'mac10.14'|'mac10.15'|'mac11'|'mac11-arm64'|'ubuntu18.04'|'ubuntu20.04'; -export type BrowserDescriptor = { - name: BrowserName, - revision: string, - download: boolean, -}; - -export const hostPlatform = ((): BrowserPlatform => { - const platform = os.platform(); - if (platform === 'darwin') { - const [major, minor] = execSync('sw_vers -productVersion', { - stdio: ['ignore', 'pipe', 'ignore'] - }).toString('utf8').trim().split('.').map(x => parseInt(x, 10)); - let arm64 = false; - // BigSur is the first version that might run on Apple Silicon. - if (major >= 11) { - arm64 = execSync('sysctl -in hw.optional.arm64', { - stdio: ['ignore', 'pipe', 'ignore'] - }).toString().trim() === '1'; - } - // We do not want to differentiate between minor big sur releases - // since they don't change core APIs so far. - const macVersion = major === 10 ? `${major}.${minor}` : `${major}`; - const archSuffix = arm64 ? '-arm64' : ''; - return `mac${macVersion}${archSuffix}` as BrowserPlatform; - } - if (platform === 'linux') { - const ubuntuVersion = getUbuntuVersionSync(); - if (parseInt(ubuntuVersion, 10) <= 19) - return 'ubuntu18.04'; - return 'ubuntu20.04'; - } - if (platform === 'win32') - return os.arch() === 'x64' ? 'win64' : 'win32'; - return platform as BrowserPlatform; -})(); - -export function linuxLddDirectories(browserPath: string, browser: BrowserDescriptor): string[] { - if (browser.name === 'chromium') - return [path.join(browserPath, 'chrome-linux')]; - if (browser.name === 'firefox') - return [path.join(browserPath, 'firefox')]; - if (browser.name === 'webkit') { - return [ - path.join(browserPath, 'minibrowser-gtk'), - path.join(browserPath, 'minibrowser-gtk', 'bin'), - path.join(browserPath, 'minibrowser-gtk', 'lib'), - path.join(browserPath, 'minibrowser-wpe'), - path.join(browserPath, 'minibrowser-wpe', 'bin'), - path.join(browserPath, 'minibrowser-wpe', 'lib'), - ]; - } - return []; -} - -export function windowsExeAndDllDirectories(browserPath: string, browser: BrowserDescriptor): string[] { - if (browser.name === 'chromium') - return [path.join(browserPath, 'chrome-win')]; - if (browser.name === 'firefox') - return [path.join(browserPath, 'firefox')]; - if (browser.name === 'webkit') - return [browserPath]; - return []; -} - -export function executablePath(browserPath: string, browser: BrowserDescriptor): string | undefined { - let tokens: string[] | undefined; - if (browser.name === 'chromium') { - tokens = new Map([ - ['ubuntu18.04', ['chrome-linux', 'chrome']], - ['ubuntu20.04', ['chrome-linux', 'chrome']], - ['mac10.13', ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium']], - ['mac10.14', ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium']], - ['mac10.15', ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium']], - ['mac11', ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium']], - ['mac11-arm64', ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium']], - ['win32', ['chrome-win', 'chrome.exe']], - ['win64', ['chrome-win', 'chrome.exe']], - ]).get(hostPlatform); - } - - if (browser.name === 'firefox') { - tokens = new Map([ - ['ubuntu18.04', ['firefox', 'firefox']], - ['ubuntu20.04', ['firefox', 'firefox']], - ['mac10.13', ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox']], - ['mac10.14', ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox']], - ['mac10.15', ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox']], - ['mac11', ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox']], - ['mac11-arm64', ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox']], - ['win32', ['firefox', 'firefox.exe']], - ['win64', ['firefox', 'firefox.exe']], - ]).get(hostPlatform); - } - - if (browser.name === 'webkit') { - tokens = new Map([ - ['ubuntu18.04', ['pw_run.sh']], - ['ubuntu20.04', ['pw_run.sh']], - ['mac10.13', undefined], - ['mac10.14', ['pw_run.sh']], - ['mac10.15', ['pw_run.sh']], - ['mac11', ['pw_run.sh']], - ['mac11-arm64', ['pw_run.sh']], - ['win32', ['Playwright.exe']], - ['win64', ['Playwright.exe']], - ]).get(hostPlatform); - } - if (browser.name === 'ffmpeg') { - tokens = new Map([ - ['ubuntu18.04', ['ffmpeg-linux']], - ['ubuntu20.04', ['ffmpeg-linux']], - ['mac10.13', ['ffmpeg-mac']], - ['mac10.14', ['ffmpeg-mac']], - ['mac10.15', ['ffmpeg-mac']], - ['mac11', ['ffmpeg-mac']], - ['mac11-arm64', ['ffmpeg-mac']], - ['win32', ['ffmpeg-win32.exe']], - ['win64', ['ffmpeg-win64.exe']], - ]).get(hostPlatform); - } - return tokens ? path.join(browserPath, ...tokens) : undefined; -} - -function cacheDirectory() { - if (process.platform === 'linux') - return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); - - if (process.platform === 'darwin') - return path.join(os.homedir(), 'Library', 'Caches'); - - if (process.platform === 'win32') - return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); - throw new Error('Unsupported platform: ' + process.platform); -} - -const defaultBrowsersPath = ((): string | undefined => { - const envDefined = getFromENV('PLAYWRIGHT_BROWSERS_PATH'); - if (envDefined === '0') - return undefined; - return envDefined || path.join(cacheDirectory(), 'ms-playwright'); -})(); - -export function browsersPath(packagePath: string): string { - return defaultBrowsersPath || path.join(packagePath, '.local-browsers'); -} - -export function browserDirectory(browsersPath: string, browser: BrowserDescriptor): string { - return path.join(browsersPath, `${browser.name}-${browser.revision}`); -} - -export function markerFilePath(browsersPath: string, browser: BrowserDescriptor): string { - return path.join(browserDirectory(browsersPath, browser), 'INSTALLATION_COMPLETE'); -} - -export function isBrowserDirectory(browserPath: string): boolean { - const baseName = path.basename(browserPath); - return baseName.startsWith('chromium-') || baseName.startsWith('firefox-') || baseName.startsWith('webkit-') || baseName.startsWith('ffmpeg-'); -} diff --git a/src/utils/registry.ts b/src/utils/registry.ts new file mode 100644 index 0000000000..b638af7138 --- /dev/null +++ b/src/utils/registry.ts @@ -0,0 +1,270 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as util from 'util'; +import { getUbuntuVersionSync } from './ubuntuVersion'; +import { assert, getFromENV } from './utils'; + +export type BrowserName = 'chromium'|'webkit'|'firefox'|'ffmpeg'; +export const allBrowserNames: BrowserName[] = ['chromium', 'webkit', 'firefox', 'ffmpeg']; + +type BrowserPlatform = 'win32'|'win64'|'mac10.13'|'mac10.14'|'mac10.15'|'mac11'|'mac11-arm64'|'ubuntu18.04'|'ubuntu20.04'; +type BrowserDescriptor = { + name: BrowserName, + revision: string, + download: boolean, +}; + +const EXECUTABLE_PATHS = { + chromium: { + 'ubuntu18.04': ['chrome-linux', 'chrome'], + 'ubuntu20.04': ['chrome-linux', 'chrome'], + 'mac10.13': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], + 'mac10.14': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], + 'mac10.15': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], + 'mac11': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], + 'mac11-arm64': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], + 'win32': ['chrome-win', 'chrome.exe'], + 'win64': ['chrome-win', 'chrome.exe'], + }, + firefox: { + 'ubuntu18.04': ['firefox', 'firefox'], + 'ubuntu20.04': ['firefox', 'firefox'], + 'mac10.13': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], + 'mac10.14': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], + 'mac10.15': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], + 'mac11': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], + 'mac11-arm64': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], + 'win32': ['firefox', 'firefox.exe'], + 'win64': ['firefox', 'firefox.exe'], + }, + webkit: { + 'ubuntu18.04': ['pw_run.sh'], + 'ubuntu20.04': ['pw_run.sh'], + 'mac10.13': undefined, + 'mac10.14': ['pw_run.sh'], + 'mac10.15': ['pw_run.sh'], + 'mac11': ['pw_run.sh'], + 'mac11-arm64': ['pw_run.sh'], + 'win32': ['Playwright.exe'], + 'win64': ['Playwright.exe'], + }, + ffmpeg: { + 'ubuntu18.04': ['ffmpeg-linux'], + 'ubuntu20.04': ['ffmpeg-linux'], + 'mac10.13': ['ffmpeg-mac'], + 'mac10.14': ['ffmpeg-mac'], + 'mac10.15': ['ffmpeg-mac'], + 'mac11': ['ffmpeg-mac'], + 'mac11-arm64': ['ffmpeg-mac'], + 'win32': ['ffmpeg-win32.exe'], + 'win64': ['ffmpeg-win64.exe'], + }, +}; + +const DOWNLOAD_URLS = { + chromium: { + 'ubuntu18.04': '%s/builds/chromium/%s/chromium-linux.zip', + 'ubuntu20.04': '%s/builds/chromium/%s/chromium-linux.zip', + 'mac10.13': '%s/builds/chromium/%s/chromium-mac.zip', + 'mac10.14': '%s/builds/chromium/%s/chromium-mac.zip', + 'mac10.15': '%s/builds/chromium/%s/chromium-mac.zip', + 'mac11': '%s/builds/chromium/%s/chromium-mac.zip', + 'mac11-arm64': '%s/builds/chromium/%s/chromium-mac-arm64.zip', + 'win32': '%s/builds/chromium/%s/chromium-win32.zip', + 'win64': '%s/builds/chromium/%s/chromium-win64.zip', + }, + firefox: { + 'ubuntu18.04': '%s/builds/firefox/%s/firefox-ubuntu-18.04.zip', + 'ubuntu20.04': '%s/builds/firefox/%s/firefox-ubuntu-18.04.zip', + 'mac10.13': '%s/builds/firefox/%s/firefox-mac-10.14.zip', + 'mac10.14': '%s/builds/firefox/%s/firefox-mac-10.14.zip', + 'mac10.15': '%s/builds/firefox/%s/firefox-mac-10.14.zip', + 'mac11': '%s/builds/firefox/%s/firefox-mac-10.14.zip', + 'mac11-arm64': '%s/builds/firefox/%s/firefox-mac-11.0-arm64.zip', + 'win32': '%s/builds/firefox/%s/firefox-win32.zip', + 'win64': '%s/builds/firefox/%s/firefox-win64.zip', + }, + webkit: { + 'ubuntu18.04': '%s/builds/webkit/%s/webkit-ubuntu-18.04.zip', + 'ubuntu20.04': '%s/builds/webkit/%s/webkit-ubuntu-20.04.zip', + 'mac10.13': undefined, + 'mac10.14': '%s/builds/webkit/%s/webkit-mac-10.14.zip', + 'mac10.15': '%s/builds/webkit/%s/webkit-mac-10.15.zip', + 'mac11': '%s/builds/webkit/%s/webkit-mac-10.15.zip', + 'mac11-arm64': '%s/builds/webkit/%s/webkit-mac-11.0-arm64.zip', + 'win32': '%s/builds/webkit/%s/webkit-win64.zip', + 'win64': '%s/builds/webkit/%s/webkit-win64.zip', + }, + ffmpeg: { + 'ubuntu18.04': '%s/builds/ffmpeg/%s/ffmpeg-linux.zip', + 'ubuntu20.04': '%s/builds/ffmpeg/%s/ffmpeg-linux.zip', + 'mac10.13': '%s/builds/ffmpeg/%s/ffmpeg-mac.zip', + 'mac10.14': '%s/builds/ffmpeg/%s/ffmpeg-mac.zip', + 'mac10.15': '%s/builds/ffmpeg/%s/ffmpeg-mac.zip', + 'mac11': '%s/builds/ffmpeg/%s/ffmpeg-mac.zip', + 'mac11-arm64': '%s/builds/ffmpeg/%s/ffmpeg-mac.zip', + 'win32': '%s/builds/ffmpeg/%s/ffmpeg-win32.zip', + 'win64': '%s/builds/ffmpeg/%s/ffmpeg-win64.zip', + }, +}; + +export const hostPlatform = ((): BrowserPlatform => { + const platform = os.platform(); + if (platform === 'darwin') { + const [major, minor] = execSync('sw_vers -productVersion', { + stdio: ['ignore', 'pipe', 'ignore'] + }).toString('utf8').trim().split('.').map(x => parseInt(x, 10)); + let arm64 = false; + // BigSur is the first version that might run on Apple Silicon. + if (major >= 11) { + arm64 = execSync('sysctl -in hw.optional.arm64', { + stdio: ['ignore', 'pipe', 'ignore'] + }).toString().trim() === '1'; + } + // We do not want to differentiate between minor big sur releases + // since they don't change core APIs so far. + const macVersion = major === 10 ? `${major}.${minor}` : `${major}`; + const archSuffix = arm64 ? '-arm64' : ''; + return `mac${macVersion}${archSuffix}` as BrowserPlatform; + } + if (platform === 'linux') { + const ubuntuVersion = getUbuntuVersionSync(); + if (parseInt(ubuntuVersion, 10) <= 19) + return 'ubuntu18.04'; + return 'ubuntu20.04'; + } + if (platform === 'win32') + return os.arch() === 'x64' ? 'win64' : 'win32'; + return platform as BrowserPlatform; +})(); + +export const registryDirectory = (() => { + const envDefined = getFromENV('PLAYWRIGHT_BROWSERS_PATH'); + if (envDefined === '0') + return path.join(__dirname, '..', '..', '.local-browsers'); + if (envDefined) + return path.resolve(process.cwd(), envDefined); + return path.join(cacheDirectory(), 'ms-playwright'); +})(); + +export function isBrowserDirectory(browserDirectory: string): boolean { + const baseName = path.basename(browserDirectory); + for (const browserName of allBrowserNames) { + if (baseName.startsWith(browserName + '-')) + return true; + } + return false; +} + +export class Registry { + private _descriptors: BrowserDescriptor[]; + + constructor(packagePath: string) { + const browsersJSON = JSON.parse(fs.readFileSync(path.join(packagePath, 'browsers.json'), 'utf8')); + this._descriptors = browsersJSON['browsers']; + } + + browserDirectory(browserName: BrowserName): string { + const browser = this._descriptors.find(browser => browser.name === browserName); + assert(browser, `ERROR: Playwright does not support ${browserName}`); + return path.join(registryDirectory, `${browser.name}-${browser.revision}`); + } + + revision(browserName: BrowserName): number { + const browser = this._descriptors.find(browser => browser.name === browserName); + assert(browser, `ERROR: Playwright does not support ${browserName}`); + return parseInt(browser.revision, 10); + } + + linuxLddDirectories(browserName: BrowserName): string[] { + const browserDirectory = this.browserDirectory(browserName); + if (browserName === 'chromium') + return [path.join(browserDirectory, 'chrome-linux')]; + if (browserName === 'firefox') + return [path.join(browserDirectory, 'firefox')]; + if (browserName === 'webkit') { + return [ + path.join(browserDirectory, 'minibrowser-gtk'), + path.join(browserDirectory, 'minibrowser-gtk', 'bin'), + path.join(browserDirectory, 'minibrowser-gtk', 'lib'), + path.join(browserDirectory, 'minibrowser-wpe'), + path.join(browserDirectory, 'minibrowser-wpe', 'bin'), + path.join(browserDirectory, 'minibrowser-wpe', 'lib'), + ]; + } + return []; + } + + windowsExeAndDllDirectories(browserName: BrowserName): string[] { + const browserDirectory = this.browserDirectory(browserName); + if (browserName === 'chromium') + return [path.join(browserDirectory, 'chrome-win')]; + if (browserName === 'firefox') + return [path.join(browserDirectory, 'firefox')]; + if (browserName === 'webkit') + return [browserDirectory]; + return []; + } + + executablePath(browserName: BrowserName): string | undefined { + const browserDirectory = this.browserDirectory(browserName); + const tokens = EXECUTABLE_PATHS[browserName][hostPlatform]; + return tokens ? path.join(browserDirectory, ...tokens) : undefined; + } + + downloadURL(browserName: BrowserName): string { + const browser = this._descriptors.find(browser => browser.name === browserName); + assert(browser, `ERROR: Playwright does not support ${browserName}`); + const envDownloadHost: { [key: string]: string } = { + chromium: 'PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST', + firefox: 'PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST', + webkit: 'PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST', + ffmpeg: 'PLAYWRIGHT_FFMPEG_DOWNLOAD_HOST', + }; + const downloadHost = getFromENV(envDownloadHost[browserName]) || + getFromENV('PLAYWRIGHT_DOWNLOAD_HOST') || + 'https://playwright.azureedge.net'; + const urlTemplate = DOWNLOAD_URLS[browserName][hostPlatform]; + assert(urlTemplate, `ERROR: Playwright does not support ${browserName} on ${hostPlatform}`); + return util.format(urlTemplate, downloadHost, browser.revision); + } + + shouldDownload(browserName: BrowserName): boolean { + // Older versions do not have "download" field. We assume they need all browsers + // from the list. So we want to skip all browsers that are explicitly marked as "download: false". + const browser = this._descriptors.find(browser => browser.name === browserName); + return !!browser && browser.download !== false; + } +} + +function cacheDirectory() { + if (process.platform === 'linux') + return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); + + if (process.platform === 'darwin') + return path.join(os.homedir(), 'Library', 'Caches'); + + if (process.platform === 'win32') + return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); + throw new Error('Unsupported platform: ' + process.platform); +} + diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 81afddd670..160e55acf1 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -136,7 +136,7 @@ export function monotonicTime(): number { return seconds * 1000 + (nanoseconds / 1000 | 0) / 1000; } -export function calculateSha1(buffer: Buffer): string { +export function calculateSha1(buffer: Buffer | string): string { const hash = crypto.createHash('sha1'); hash.update(buffer); return hash.digest('hex'); diff --git a/test/screencast.spec.ts b/test/screencast.spec.ts index 73cf4e0c9d..2d5ba19a09 100644 --- a/test/screencast.spec.ts +++ b/test/screencast.spec.ts @@ -19,14 +19,10 @@ import fs from 'fs'; import path from 'path'; import { spawnSync } from 'child_process'; import { PNG } from 'pngjs'; -import * as browserPaths from '../src/utils/browserPaths'; +import { Registry } from '../src/utils/registry'; - -const browsersJSON = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'browsers.json'), 'utf8')); -const ffmpegDescriptor = browsersJSON.browsers.find(({name}) => name === 'ffmpeg'); -const browsersPath = browserPaths.browsersPath(path.join(__dirname, '..')); -const browserPath = browserPaths.browserDirectory(browsersPath, ffmpegDescriptor); -const ffmpeg = browserPaths.executablePath(browserPath, ffmpegDescriptor) || ''; +const registry = new Registry(path.join(__dirname, '..')); +const ffmpeg = registry.executablePath('ffmpeg') || ''; export class VideoPlayer { fileName: string; diff --git a/utils/linux-browser-dependencies/inside_docker/list_dependencies.js b/utils/linux-browser-dependencies/inside_docker/list_dependencies.js index d4e5438c5e..f0423481cd 100644 --- a/utils/linux-browser-dependencies/inside_docker/list_dependencies.js +++ b/utils/linux-browser-dependencies/inside_docker/list_dependencies.js @@ -4,7 +4,7 @@ const fs = require('fs'); const util = require('util'); const path = require('path'); const {spawn} = require('child_process'); -const browserPaths = require('playwright/lib/utils/browserPaths.js'); +const {registryDirectory} = require('playwright/lib/utils/registry.js'); const readdirAsync = util.promisify(fs.readdir.bind(fs)); const readFileAsync = util.promisify(fs.readFile.bind(fs)); @@ -27,12 +27,11 @@ const DL_OPEN_LIBRARIES = { (async () => { console.log('Working on:', await getDistributionName()); console.log('Started at:', currentTime()); - const allBrowsersPath = browserPaths.browsersPath(); - const browserDescriptors = (await readdirAsync(allBrowsersPath)).filter(dir => !dir.startsWith('.')).map(dir => ({ + const browserDescriptors = (await readdirAsync(registryDirectory)).filter(dir => !dir.startsWith('.')).map(dir => ({ // Full browser name, e.g. `webkit-1144` name: dir, // Full patch to browser files - path: path.join(allBrowsersPath, dir), + path: path.join(registryDirectory, dir), // All files that we will try to inspect for missing dependencies. filePaths: [], // All libraries that are missing for the browser. diff --git a/utils/roll_browser.js b/utils/roll_browser.js index 6f47e332a7..1c94a6aa4e 100755 --- a/utils/roll_browser.js +++ b/utils/roll_browser.js @@ -17,6 +17,7 @@ */ const path = require('path'); +const {Registry} = require('../lib/utils/registry'); const fs = require('fs'); const protocolGenerator = require('./protocol-types-generator'); const {execSync} = require('child_process'); @@ -69,14 +70,11 @@ Example: // 3. Download new browser. console.log('\nDownloading new browser...'); const { installBrowsersWithProgressBar } = require('../lib/install/installer'); - await installBrowsersWithProgressBar(ROOT_PATH); + await installBrowsersWithProgressBar(); // 4. Generate types. console.log('\nGenerating protocol types...'); - const browser = { name: browserName, revision }; - const browserPaths = require('../lib/utils/browserPaths'); - const browserDir = browserPaths.browserDirectory(browserPaths.browsersPath(ROOT_PATH), browser); - const executablePath = browserPaths.executablePath(browserDir, browser); + const executablePath = new Registry(ROOT_PATH).executablePath(browserName); await protocolGenerator.generateProtocol(browserName, executablePath).catch(console.warn); // 5. Update docs.