diff --git a/docs/browsers-api/browsers.installedbrowser.md b/docs/browsers-api/browsers.installedbrowser.md index 48ad1a90fab02..b8879c5a95b14 100644 --- a/docs/browsers-api/browsers.installedbrowser.md +++ b/docs/browsers-api/browsers.installedbrowser.md @@ -23,3 +23,10 @@ The constructor for this class is marked as internal. Third-party code should no | executablePath | readonly | string | | | path | readonly | string | Path to the root of the installation folder. Use [computeExecutablePath()](./browsers.computeexecutablepath.md) to get the path to the executable binary. | | platform | | [BrowserPlatform](./browsers.browserplatform.md) | | + +## Methods + +| Method | Modifiers | Description | +| ----------------------------------------------------------------------- | --------- | ----------- | +| [readMetadata()](./browsers.installedbrowser.readmetadata.md) | | | +| [writeMetadata(metadata)](./browsers.installedbrowser.writemetadata.md) | | | diff --git a/docs/browsers-api/browsers.installedbrowser.readmetadata.md b/docs/browsers-api/browsers.installedbrowser.readmetadata.md new file mode 100644 index 0000000000000..aa1e4d8248c01 --- /dev/null +++ b/docs/browsers-api/browsers.installedbrowser.readmetadata.md @@ -0,0 +1,17 @@ +--- +sidebar_label: InstalledBrowser.readMetadata +--- + +# InstalledBrowser.readMetadata() method + +#### Signature: + +```typescript +class InstalledBrowser { + readMetadata(): Metadata; +} +``` + +**Returns:** + +Metadata diff --git a/docs/browsers-api/browsers.installedbrowser.writemetadata.md b/docs/browsers-api/browsers.installedbrowser.writemetadata.md new file mode 100644 index 0000000000000..4c0ae5d0c88f4 --- /dev/null +++ b/docs/browsers-api/browsers.installedbrowser.writemetadata.md @@ -0,0 +1,23 @@ +--- +sidebar_label: InstalledBrowser.writeMetadata +--- + +# InstalledBrowser.writeMetadata() method + +#### Signature: + +```typescript +class InstalledBrowser { + writeMetadata(metadata: Metadata): void; +} +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | ----------- | +| metadata | Metadata | | + +**Returns:** + +void diff --git a/docs/browsers-api/browsers.installoptions.md b/docs/browsers-api/browsers.installoptions.md index 2e90d814db48d..23278c6f09b47 100644 --- a/docs/browsers-api/browsers.installoptions.md +++ b/docs/browsers-api/browsers.installoptions.md @@ -12,12 +12,13 @@ export interface InstallOptions ## Properties -| Property | Modifiers | Type | Description | Default | -| ------------------------ | --------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| baseUrl | optional | string | Determines the host that will be used for downloading. |

Either

- https://storage.googleapis.com/chrome-for-testing-public or - https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central

| -| browser | | [Browser](./browsers.browser.md) | Determines which browser to install. | | -| buildId | | string | Determines which buildId to download. BuildId should uniquely identify binaries and they are used for caching. | | -| cacheDir | | string | Determines the path to download browsers to. | | -| downloadProgressCallback | optional | (downloadedBytes: number, totalBytes: number) => void | Provides information about the progress of the download. | | -| platform | optional | [BrowserPlatform](./browsers.browserplatform.md) | Determines which platform the browser will be suited for. | **Auto-detected.** | -| unpack | optional | boolean | Whether to unpack and install browser archives. | true | +| Property | Modifiers | Type | Description | Default | +| ------------------------ | --------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| baseUrl | optional | string | Determines the host that will be used for downloading. |

Either

- https://storage.googleapis.com/chrome-for-testing-public or - https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central

| +| browser | | [Browser](./browsers.browser.md) | Determines which browser to install. | | +| buildId | | string | Determines which buildId to download. BuildId should uniquely identify binaries and they are used for caching. | | +| buildIdAlias | optional | string | An alias for the provided buildId. It will be used to maintain local metadata to support aliases in the launch command. | | +| cacheDir | | string | Determines the path to download browsers to. | | +| downloadProgressCallback | optional | (downloadedBytes: number, totalBytes: number) => void | Provides information about the progress of the download. | | +| platform | optional | [BrowserPlatform](./browsers.browserplatform.md) | Determines which platform the browser will be suited for. | **Auto-detected.** | +| unpack | optional | boolean | Whether to unpack and install browser archives. | true | diff --git a/packages/browsers/src/CLI.ts b/packages/browsers/src/CLI.ts index 255f5545b4849..063540d97ceda 100644 --- a/packages/browsers/src/CLI.ts +++ b/packages/browsers/src/CLI.ts @@ -238,6 +238,7 @@ export class CLI { } args.browser.buildId = pinnedVersion; } + const originalBuildId = args.browser.buildId; args.browser.buildId = await resolveBuildId( args.browser.name, args.platform, @@ -253,6 +254,10 @@ export class CLI { args.browser.buildId ), baseUrl: args.baseUrl, + buildIdAlias: + originalBuildId !== args.browser.buildId + ? originalBuildId + : undefined, }); console.log( `${args.browser.name}@${ diff --git a/packages/browsers/src/Cache.ts b/packages/browsers/src/Cache.ts index 13b465835a61b..e6b574d9ddd41 100644 --- a/packages/browsers/src/Cache.ts +++ b/packages/browsers/src/Cache.ts @@ -8,13 +8,18 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; +import debug from 'debug'; + import { Browser, type BrowserPlatform, executablePathByBrowser, + getVersionComparator, } from './browser-data/browser-data.js'; import {detectBrowserPlatform} from './detectPlatform.js'; +const debugCache = debug('puppeteer:browsers:cache'); + /** * @public */ @@ -57,6 +62,14 @@ export class InstalledBrowser { this.buildId ); } + + readMetadata(): Metadata { + return this.#cache.readMetadata(this.browser); + } + + writeMetadata(metadata: Metadata): void { + this.#cache.writeMetadata(this.browser, metadata); + } } /** @@ -80,6 +93,11 @@ export interface ComputeExecutablePathOptions { buildId: string; } +export interface Metadata { + // Maps an alias (canary/latest/dev/etc.) to a buildId. + aliases: Record; +} + /** * The cache used by Puppeteer relies on the following structure: * @@ -112,6 +130,39 @@ export class Cache { return path.join(this.#rootDir, browser); } + metadataFile(browser: Browser): string { + return path.join(this.browserRoot(browser), '.metadata'); + } + + readMetadata(browser: Browser): Metadata { + const metatadaPath = this.metadataFile(browser); + if (!fs.existsSync(metatadaPath)) { + return {aliases: {}}; + } + // TODO: add type-safe parsing. + const data = JSON.parse(fs.readFileSync(metatadaPath, 'utf8')); + if (typeof data !== 'object') { + throw new Error('.metadata is not an object'); + } + return data; + } + + writeMetadata(browser: Browser, metadata: Metadata): void { + const metatadaPath = this.metadataFile(browser); + fs.mkdirSync(path.dirname(metatadaPath), {recursive: true}); + fs.writeFileSync(metatadaPath, JSON.stringify(metadata, null, 2)); + } + + resolveAlias(browser: Browser, alias: string): string | undefined { + const metadata = this.readMetadata(browser); + if (alias === 'latest') { + return Object.values(metadata.aliases || {}) + .sort(getVersionComparator(browser)) + .at(-1); + } + return metadata.aliases[alias]; + } + installationDir( browser: Browser, platform: BrowserPlatform, @@ -134,6 +185,12 @@ export class Cache { platform: BrowserPlatform, buildId: string ): void { + const metadata = this.readMetadata(browser); + for (const alias of Object.keys(metadata.aliases)) { + if (metadata.aliases[alias] === buildId) { + delete metadata.aliases[alias]; + } + } fs.rmSync(this.installationDir(browser, platform, buildId), { force: true, recursive: true, @@ -180,6 +237,12 @@ export class Cache { `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` ); } + try { + options.buildId = + this.resolveAlias(options.browser, options.buildId) ?? options.buildId; + } catch { + debugCache('could not read .metadata file for the browser'); + } const installationDir = this.installationDir( options.browser, options.platform, diff --git a/packages/browsers/src/install.ts b/packages/browsers/src/install.ts index 1fc94506accd2..e78b2c346161a 100644 --- a/packages/browsers/src/install.ts +++ b/packages/browsers/src/install.ts @@ -62,6 +62,13 @@ export interface InstallOptions { * binaries and they are used for caching. */ buildId: string; + /** + * An alias for the provided `buildId`. It will be used to maintain local + * metadata to support aliases in the `launch` command. + * + * @example 'canary' + */ + buildIdAlias?: string; /** * Provides information about the progress of the download. */ @@ -233,17 +240,23 @@ async function installUrl( } finally { debugTimeEnd('extract'); } + const installedBrowser = new InstalledBrowser( + cache, + options.browser, + options.buildId, + options.platform + ); + if (options.buildIdAlias) { + const metadata = installedBrowser.readMetadata(); + metadata.aliases[options.buildIdAlias] = options.buildId; + installedBrowser.writeMetadata(metadata); + } + return installedBrowser; } finally { if (existsSync(archivePath)) { await unlink(archivePath); } } - return new InstalledBrowser( - cache, - options.browser, - options.buildId, - options.platform - ); } /** diff --git a/packages/browsers/test/src/Cache.spec.ts b/packages/browsers/test/src/Cache.spec.ts new file mode 100644 index 0000000000000..e8a3fb9619abd --- /dev/null +++ b/packages/browsers/test/src/Cache.spec.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2024 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {Browser, Cache} from '../../lib/cjs/main.js'; + +describe('Cache', () => { + let tmpDir = '/tmp/puppeteer-browsers-test'; + let cache: Cache; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + cache = new Cache(tmpDir); + }); + + afterEach(() => { + cache.clear(); + }); + + it('return empty metadata if .metadata file does not exist', async function () { + assert.deepStrictEqual(cache.readMetadata(Browser.CHROME), { + aliases: {}, + }); + }); + + it('throw an error if .metadata is malformed', async function () { + // @ts-expect-error wrong type on purpose; + cache.writeMetadata(Browser.CHROME, 'metadata'); + assert.throws(() => { + return cache.readMetadata(Browser.CHROME); + }, new Error(`.metadata is not an object`)); + }); + + it('writes and reads .metadata', async function () { + cache.writeMetadata(Browser.CHROME, { + aliases: { + canary: '123.0.0.0', + }, + }); + assert.deepStrictEqual(cache.readMetadata(Browser.CHROME), { + aliases: { + canary: '123.0.0.0', + }, + }); + + assert.deepStrictEqual( + cache.resolveAlias(Browser.CHROME, 'canary'), + '123.0.0.0' + ); + }); + + it('resolves latest', async function () { + cache.writeMetadata(Browser.CHROME, { + aliases: { + canary: '115.0.5789', + stable: '114.0.5789', + }, + }); + + assert.deepStrictEqual( + cache.resolveAlias(Browser.CHROME, 'latest'), + '115.0.5789' + ); + }); +}); diff --git a/packages/puppeteer/src/node/install.ts b/packages/puppeteer/src/node/install.ts index 5b2cbe1887951..1af3105ee9da7 100644 --- a/packages/puppeteer/src/node/install.ts +++ b/packages/puppeteer/src/node/install.ts @@ -74,6 +74,8 @@ export async function downloadBrowser(): Promise { buildId, downloadProgressCallback: makeProgressCallback(browser, buildId), baseUrl: downloadBaseUrl, + buildIdAlias: + buildId !== unresolvedBuildId ? unresolvedBuildId : undefined, }) .then(result => { logPolitely( @@ -112,6 +114,10 @@ export async function downloadBrowser(): Promise { shellBuildId ), baseUrl: downloadBaseUrl, + buildIdAlias: + shellBuildId !== unresolvedShellBuildId + ? unresolvedShellBuildId + : undefined, }) .then(result => { logPolitely(