feat: allow downloading Firefox channels other than nightly (#12051)

Co-authored-by: Nikolay Vitkov <nvitkov@chromium.org>
This commit is contained in:
Alex Rudenko 2024-03-13 14:44:56 +01:00 committed by GitHub
parent ef7a9eac16
commit e4cc2f9ee9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 341 additions and 54 deletions

View File

@ -222,7 +222,31 @@ export class CLI {
);
yargs.example(
'$0 install firefox',
'Install the latest available build of the Firefox browser.'
'Install the latest nightly available build of the Firefox browser.'
);
yargs.example(
'$0 install firefox@stable',
'Install the latest stable build of the Firefox browser.'
);
yargs.example(
'$0 install firefox@beta',
'Install the latest beta build of the Firefox browser.'
);
yargs.example(
'$0 install firefox@devedition',
'Install the latest devedition build of the Firefox browser.'
);
yargs.example(
'$0 install firefox@esr',
'Install the latest ESR build of the Firefox browser.'
);
yargs.example(
'$0 install firefox@nightly',
'Install the latest nightly build of the Firefox browser.'
);
yargs.example(
'$0 install firefox@stable_111.0.1',
'Install a specific version of the Firefox browser.'
);
yargs.example(
'$0 install firefox --platform mac',
@ -395,7 +419,7 @@ export function makeProgressCallback(
return (downloadedBytes: number, totalBytes: number) => {
if (!progressBar) {
progressBar = new ProgressBar(
`Downloading ${browser} r${buildId} - ${toMegabytes(
`Downloading ${browser} ${buildId} - ${toMegabytes(
totalBytes
)} [:bar] :percent :etas `,
{

View File

@ -54,28 +54,36 @@ export const versionComparators = {
export {Browser, BrowserPlatform, ChromeReleaseChannel};
/**
* @public
* @internal
*/
export async function resolveBuildId(
async function resolveBuildIdForBrowserTag(
browser: Browser,
platform: BrowserPlatform,
tag: string
tag: BrowserTag
): Promise<string> {
switch (browser) {
case Browser.FIREFOX:
switch (tag as BrowserTag) {
switch (tag) {
case BrowserTag.LATEST:
return await firefox.resolveBuildId('FIREFOX_NIGHTLY');
return await firefox.resolveBuildId(firefox.FirefoxChannel.NIGHTLY);
case BrowserTag.BETA:
return await firefox.resolveBuildId(firefox.FirefoxChannel.BETA);
case BrowserTag.NIGHTLY:
return await firefox.resolveBuildId(firefox.FirefoxChannel.NIGHTLY);
case BrowserTag.DEVEDITION:
return await firefox.resolveBuildId(
firefox.FirefoxChannel.DEVEDITION
);
case BrowserTag.STABLE:
return await firefox.resolveBuildId(firefox.FirefoxChannel.STABLE);
case BrowserTag.ESR:
return await firefox.resolveBuildId(firefox.FirefoxChannel.ESR);
case BrowserTag.CANARY:
case BrowserTag.DEV:
case BrowserTag.STABLE:
throw new Error(
`${tag} is not supported for ${browser}. Use 'latest' instead.`
);
throw new Error(`${tag.toUpperCase()} is not available for Firefox`);
}
case Browser.CHROME: {
switch (tag as BrowserTag) {
switch (tag) {
case BrowserTag.LATEST:
return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
case BrowserTag.BETA:
@ -86,13 +94,11 @@ export async function resolveBuildId(
return await chrome.resolveBuildId(ChromeReleaseChannel.DEV);
case BrowserTag.STABLE:
return await chrome.resolveBuildId(ChromeReleaseChannel.STABLE);
default:
const result = await chrome.resolveBuildId(tag);
if (result) {
return result;
}
case BrowserTag.NIGHTLY:
case BrowserTag.DEVEDITION:
case BrowserTag.ESR:
throw new Error(`${tag.toUpperCase()} is not available for Chrome`);
}
return tag;
}
case Browser.CHROMEDRIVER: {
switch (tag) {
@ -105,13 +111,13 @@ export async function resolveBuildId(
return await chromedriver.resolveBuildId(ChromeReleaseChannel.DEV);
case BrowserTag.STABLE:
return await chromedriver.resolveBuildId(ChromeReleaseChannel.STABLE);
default:
const result = await chromedriver.resolveBuildId(tag);
if (result) {
return result;
}
case BrowserTag.NIGHTLY:
case BrowserTag.DEVEDITION:
case BrowserTag.ESR:
throw new Error(
`${tag.toUpperCase()} is not available for ChromeDriver`
);
}
return tag;
}
case Browser.CHROMEHEADLESSSHELL: {
switch (tag) {
@ -132,29 +138,68 @@ export async function resolveBuildId(
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.STABLE
);
default:
const result = await chromeHeadlessShell.resolveBuildId(tag);
if (result) {
return result;
}
case BrowserTag.NIGHTLY:
case BrowserTag.DEVEDITION:
case BrowserTag.ESR:
throw new Error(`${tag} is not available for chrome-headless-shell`);
}
return tag;
}
case Browser.CHROMIUM:
switch (tag as BrowserTag) {
switch (tag) {
case BrowserTag.LATEST:
return await chromium.resolveBuildId(platform);
case BrowserTag.BETA:
case BrowserTag.NIGHTLY:
case BrowserTag.CANARY:
case BrowserTag.DEV:
case BrowserTag.DEVEDITION:
case BrowserTag.BETA:
case BrowserTag.STABLE:
case BrowserTag.ESR:
throw new Error(
`${tag} is not supported for ${browser}. Use 'latest' instead.`
`${tag} is not supported for Chromium. Use 'latest' instead.`
);
}
}
// We assume the tag is the buildId if it didn't match any keywords.
return tag;
}
/**
* @public
*/
export async function resolveBuildId(
browser: Browser,
platform: BrowserPlatform,
tag: string
): Promise<string> {
const browserTag = tag as BrowserTag;
if (Object.values(BrowserTag).includes(browserTag)) {
return await resolveBuildIdForBrowserTag(browser, platform, browserTag);
}
switch (browser) {
case Browser.FIREFOX:
return tag;
case Browser.CHROME:
const chromeResult = await chrome.resolveBuildId(tag);
if (chromeResult) {
return chromeResult;
}
return tag;
case Browser.CHROMEDRIVER:
const chromeDriverResult = await chromedriver.resolveBuildId(tag);
if (chromeDriverResult) {
return chromeDriverResult;
}
return tag;
case Browser.CHROMEHEADLESSSHELL:
const chromeHeadlessShellResult =
await chromeHeadlessShell.resolveBuildId(tag);
if (chromeHeadlessShellResult) {
return chromeHeadlessShellResult;
}
return tag;
case Browser.CHROMIUM:
return tag;
}
}
/**

View File

@ -11,7 +11,7 @@ import {getJSON} from '../httpUtil.js';
import {BrowserPlatform, type ProfileOptions} from './types.js';
function archive(platform: BrowserPlatform, buildId: string): string {
function archiveNightly(platform: BrowserPlatform, buildId: string): string {
switch (platform) {
case BrowserPlatform.LINUX:
return `firefox-${buildId}.en-US.${platform}-x86_64.tar.bz2`;
@ -24,48 +24,146 @@ function archive(platform: BrowserPlatform, buildId: string): string {
}
}
function archive(platform: BrowserPlatform, buildId: string): string {
switch (platform) {
case BrowserPlatform.LINUX:
return `firefox-${buildId}.tar.bz2`;
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return `Firefox ${buildId}.dmg`;
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return `Firefox Setup ${buildId}.exe`;
}
}
function platformName(platform: BrowserPlatform): string {
switch (platform) {
case BrowserPlatform.LINUX:
return `linux-x86_64`;
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return `mac`;
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return platform;
}
}
function parseBuildId(buildId: string): [FirefoxChannel, string] {
for (const value of Object.values(FirefoxChannel)) {
if (buildId.startsWith(value + '_')) {
buildId = buildId.substring(value.length + 1);
return [value, buildId];
}
}
// Older versions do not have channel as the prefix.«
return [FirefoxChannel.NIGHTLY, buildId];
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
buildId: string,
baseUrl = 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central'
baseUrl?: string
): string {
return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
const [channel, resolvedBuildId] = parseBuildId(buildId);
switch (channel) {
case FirefoxChannel.NIGHTLY:
baseUrl ??=
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central';
break;
case FirefoxChannel.DEVEDITION:
baseUrl ??= 'https://archive.mozilla.org/pub/devedition/releases';
break;
case FirefoxChannel.BETA:
case FirefoxChannel.STABLE:
case FirefoxChannel.ESR:
baseUrl ??= 'https://archive.mozilla.org/pub/firefox/releases';
break;
}
switch (channel) {
case FirefoxChannel.NIGHTLY:
return `${baseUrl}/${resolveDownloadPath(platform, resolvedBuildId).join('/')}`;
case FirefoxChannel.DEVEDITION:
case FirefoxChannel.BETA:
case FirefoxChannel.STABLE:
case FirefoxChannel.ESR:
return `${baseUrl}/${resolvedBuildId}/${platformName(platform)}/en-US/${archive(platform, resolvedBuildId)}`;
}
}
export function resolveDownloadPath(
platform: BrowserPlatform,
buildId: string
): string[] {
return [archive(platform, buildId)];
return [archiveNightly(platform, buildId)];
}
export function relativeExecutablePath(
platform: BrowserPlatform,
_buildId: string
buildId: string
): string {
switch (platform) {
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox');
case BrowserPlatform.LINUX:
return path.join('firefox', 'firefox');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('firefox', 'firefox.exe');
const [channel] = parseBuildId(buildId);
switch (channel) {
case FirefoxChannel.NIGHTLY:
switch (platform) {
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return path.join(
'Firefox Nightly.app',
'Contents',
'MacOS',
'firefox'
);
case BrowserPlatform.LINUX:
return path.join('firefox', 'firefox');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('firefox', 'firefox.exe');
}
case FirefoxChannel.BETA:
case FirefoxChannel.DEVEDITION:
case FirefoxChannel.ESR:
case FirefoxChannel.STABLE:
switch (platform) {
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return path.join('Firefox.app', 'Contents', 'MacOS', 'firefox');
case BrowserPlatform.LINUX:
return path.join('firefox', 'firefox');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('core', 'firefox.exe');
}
}
}
export enum FirefoxChannel {
STABLE = 'stable',
ESR = 'esr',
DEVEDITION = 'devedition',
BETA = 'beta',
NIGHTLY = 'nightly',
}
export async function resolveBuildId(
channel: 'FIREFOX_NIGHTLY' = 'FIREFOX_NIGHTLY'
channel: FirefoxChannel = FirefoxChannel.NIGHTLY
): Promise<string> {
const channelToVersionKey = {
[FirefoxChannel.ESR]: 'FIREFOX_ESR',
[FirefoxChannel.STABLE]: 'LATEST_FIREFOX_VERSION',
[FirefoxChannel.DEVEDITION]: 'FIREFOX_DEVEDITION',
[FirefoxChannel.BETA]: 'FIREFOX_DEVEDITION',
[FirefoxChannel.NIGHTLY]: 'FIREFOX_NIGHTLY',
};
const versions = (await getJSON(
new URL('https://product-details.mozilla.org/1.0/firefox_versions.json')
)) as Record<string, string>;
const version = versions[channel];
const version = versions[channelToVersionKey[channel]];
if (!version) {
throw new Error(`Channel ${channel} is not found.`);
}
return version;
return channel + '_' + version;
}
export async function createProfile(options: ProfileOptions): Promise<void> {

View File

@ -36,9 +36,12 @@ export enum BrowserPlatform {
*/
export enum BrowserTag {
CANARY = 'canary',
NIGHTLY = 'nightly',
BETA = 'beta',
DEV = 'dev',
DEVEDITION = 'devedition',
STABLE = 'stable',
ESR = 'esr',
LATEST = 'latest',
}

View File

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {exec as execChildProcess} from 'child_process';
import {exec as execChildProcess, spawnSync} from 'child_process';
import {createReadStream} from 'fs';
import {mkdir, readdir} from 'fs/promises';
import * as path from 'path';
@ -30,6 +30,18 @@ export async function unpackArchive(
} else if (archivePath.endsWith('.dmg')) {
await mkdir(folderPath);
await installDMG(archivePath, folderPath);
} else if (archivePath.endsWith('.exe')) {
// Firefox on Windows.
const result = spawnSync(archivePath, [`/ExtractDir=${folderPath}`], {
env: {
__compat_layer: 'RunAsInvoker',
},
});
if (result.status !== 0) {
throw new Error(
`Failed to extract ${archivePath} to ${folderPath}: ${result.output}`
);
}
} else {
throw new Error(`Unsupported archive format: ${archivePath}`);
}

View File

@ -138,6 +138,7 @@ describe('Chrome install', () => {
});
it('falls back to the chrome-for-testing dashboard URLs if URL is not available', async function () {
this.timeout(60000);
const expectedOutputPath = path.join(
tmpDir,
'chrome',

View File

@ -18,7 +18,7 @@ import {
} from '../../../lib/cjs/browser-data/firefox.js';
describe('Firefox', () => {
it('should resolve download URLs', () => {
it('should resolve download URLs for Nightly', () => {
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.LINUX, '111.0a1'),
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.linux-x86_64.tar.bz2'
@ -41,6 +41,75 @@ describe('Firefox', () => {
);
});
it('should resolve download URLs for beta', () => {
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.LINUX, 'beta_115.0b8'),
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/linux-x86_64/en-US/firefox-115.0b8.tar.bz2'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC, 'beta_115.0b8'),
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/mac/en-US/Firefox 115.0b8.dmg'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC_ARM, 'beta_115.0b8'),
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/mac/en-US/Firefox 115.0b8.dmg'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN32, 'beta_115.0b8'),
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/win32/en-US/Firefox Setup 115.0b8.exe'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN64, 'beta_115.0b8'),
'https://archive.mozilla.org/pub/firefox/releases/115.0b8/win64/en-US/Firefox Setup 115.0b8.exe'
);
});
it('should resolve download URLs for stable', () => {
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.LINUX, 'stable_111.0.1'),
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/linux-x86_64/en-US/firefox-111.0.1.tar.bz2'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC, 'stable_111.0.1'),
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/mac/en-US/Firefox 111.0.1.dmg'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC_ARM, 'stable_111.0.1'),
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/mac/en-US/Firefox 111.0.1.dmg'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN32, 'stable_111.0.1'),
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/win32/en-US/Firefox Setup 111.0.1.exe'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN64, 'stable_111.0.1'),
'https://archive.mozilla.org/pub/firefox/releases/111.0.1/win64/en-US/Firefox Setup 111.0.1.exe'
);
});
it('should resolve download URLs for devedition', () => {
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.LINUX, 'devedition_115.0b8'),
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/linux-x86_64/en-US/firefox-115.0b8.tar.bz2'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC, 'devedition_115.0b8'),
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/mac/en-US/Firefox 115.0b8.dmg'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC_ARM, 'devedition_115.0b8'),
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/mac/en-US/Firefox 115.0b8.dmg'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN32, 'devedition_115.0b8'),
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/win32/en-US/Firefox Setup 115.0b8.exe'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN64, 'devedition_115.0b8'),
'https://archive.mozilla.org/pub/devedition/releases/115.0b8/win64/en-US/Firefox Setup 115.0b8.exe'
);
});
it('should resolve executable paths', () => {
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.LINUX, '111.0a1'),

View File

@ -5,6 +5,8 @@
*/
import assert from 'assert';
import {spawnSync} from 'child_process';
import {existsSync} from 'fs';
import {readdir} from 'fs/promises';
import {platform} from 'os';
import {join} from 'path';
@ -49,3 +51,36 @@ import {readAsset} from './util.js';
});
}
);
describe('Firefox download', () => {
configureSandbox({
dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
env: cwd => {
return {
PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
PUPPETEER_SKIP_DOWNLOAD: 'true',
};
},
});
it('can download Firefox stable', async function () {
assert.ok(!existsSync(join(this.sandbox, '.cache', 'puppeteer')));
const result = spawnSync(
'npx',
['puppeteer', 'browsers', 'install', 'firefox@stable'],
{
// npx is not found without the shell flag on Windows.
shell: process.platform === 'win32',
cwd: this.sandbox,
env: {
...process.env,
PUPPETEER_CACHE_DIR: join(this.sandbox, '.cache', 'puppeteer'),
},
}
);
assert.strictEqual(result.status, 0);
const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
assert.equal(files.length, 1);
assert.equal(files[0], 'firefox');
});
});