From 2f0f9508b690f6a4d17b28a86405cfb84ae7635e Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 18 Sep 2024 09:50:17 +0200 Subject: [PATCH 1/8] feat: Add geckodriver download script --- lib/logger.js | 2 +- lib/utils.js | 100 +++++++++++- package.json | 15 ++ scripts/install-geckodriver.mjs | 275 ++++++++++++++++++++++++++++++++ 4 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 scripts/install-geckodriver.mjs diff --git a/lib/logger.js b/lib/logger.js index b9533de..d82cf95 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,5 +1,5 @@ import { logger } from 'appium/support'; -const log = logger.getLogger('GeckoDriver'); +export const log = logger.getLogger('GeckoDriver'); export default log; diff --git a/lib/utils.js b/lib/utils.js index 08c1e6d..ac1e552 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,9 @@ import _ from 'lodash'; +import { fs, net, zip, tempDir } from 'appium/support'; +import tar from 'tar-stream'; +import zlib from 'node:zlib'; +import B from 'bluebird'; +import path from 'node:path'; const GECKO_CAP_PREFIXES = ['moz:']; // https://www.w3.org/TR/webdriver/#capabilities @@ -13,7 +18,7 @@ const STANDARD_CAPS = [ 'unhandledPromptBehavior', ]; -function formatCapsForServer (caps) { +export function formatCapsForServer (caps) { const result = {}; if (caps.browserName) { result.browserName = 'firefox'; @@ -30,4 +35,95 @@ function formatCapsForServer (caps) { return result; } -export { formatCapsForServer }; +/** + * + * @param {string} srcUrl + * @param {string} dstPath + * @returns {Promise} + */ +export async function downloadToFile(srcUrl, dstPath) { + await net.downloadFile(srcUrl, dstPath); +} + +/** + * + * @param {string} p + * @returns {Promise} + */ +export async function mkdirp(p) { + await fs.mkdirp(p); +} + +/** + * + * @param {string} srcAcrhive + * @param {string} fileToExtract + * @param {string} dstPath + * @returns {Promise} + */ +export async function extractFileFromTarGz(srcAcrhive, fileToExtract, dstPath) { + const chunks = []; + const extract = tar.extract(); + const extractPromise = new B((resolve, reject) => { + extract.on('entry', (header, stream, next) => { + if (header.name === fileToExtract) { + stream.on('data', (chunk) => { + chunks.push(chunk); + }); + } + stream.on('end', function() { + next(); + }); + stream.resume(); + }); + extract.once('error', reject); + extract.once('finish', async () => { + if (chunks.length) { + try { + await fs.writeFile(dstPath, Buffer.concat(chunks)); + } catch (e) { + return reject(e); + } + } else { + return reject( + new Error(`The file '${fileToExtract}' could not be found in the '${srcAcrhive}' archive`) + ); + } + resolve(); + }); + }); + + fs.createReadStream(srcAcrhive) + .pipe(zlib.createGunzip()) + .pipe(extract); + + await extractPromise; +} + +/** + * + * @param {string} srcAcrhive + * @param {string} fileToExtract + * @param {string} dstPath + * @returns {Promise} + */ +export async function extractFileFromZip(srcAcrhive, fileToExtract, dstPath) { + let didFindEntry = false; + await zip.readEntries(srcAcrhive, async ({entry, extractEntryTo}) => { + if (didFindEntry || entry.fileName !== fileToExtract) { + return; + } + didFindEntry = true; + + const tmpRoot = await tempDir.openDir(); + try { + await extractEntryTo(tmpRoot); + await fs.mv(path.resolve(tmpRoot, entry.fileName), dstPath); + } finally { + await fs.rimraf(tmpRoot); + } + }); + if (!didFindEntry) { + throw new Error(`The file '${fileToExtract}' could not be found in the '${srcAcrhive}' archive`); + } +} diff --git a/package.json b/package.json index 1d9a598..f7b9dd6 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "Android", "Windows" ], + "scripts": { + "install-geckodriver": "./scripts/install-geckodriver.mjs" + }, "mainClass": "GeckoDriver" }, "main": "./build/index.js", @@ -42,16 +45,28 @@ "directories": { "lib": "lib" }, + "files": [ + "index.js", + "lib", + "build", + "CHANGELOG.md", + "LICENSE", + "npm-shrinkwrap.json", + "scripts/*.mjs" + ], "peerDependencies": { "appium": "^2.4.1" }, "dependencies": { "appium-adb": "^12.0.3", "asyncbox": "^3.0.0", + "axios": "^1.7.7", "bluebird": "^3.5.1", "lodash": "^4.17.4", "portscanner": "2.2.0", + "semver": "^7.6.3", "source-map-support": "^0.x", + "tar-stream": "^3.1.7", "teen_process": "^2.0.0" }, "scripts": { diff --git a/scripts/install-geckodriver.mjs b/scripts/install-geckodriver.mjs new file mode 100644 index 0000000..0508e26 --- /dev/null +++ b/scripts/install-geckodriver.mjs @@ -0,0 +1,275 @@ +import axios from 'axios'; +import semver from 'semver'; +import _ from 'lodash'; +import path from 'node:path'; +import { tmpdir } from 'node:os'; +import { log } from '../build/lib/logger.js'; +import { + downloadToFile, + mkdirp, + extractFileFromTarGz, + extractFileFromZip, +} from '../build/lib/utils.js'; +import fs from 'node:fs/promises'; + +const OWNER = 'mozilla'; +const REPO = 'geckodriver'; +const API_ROOT = `https://api.github.com/repos/${OWNER}/${REPO}`; +const API_VERSION_HEADER = {'X-GitHub-Api-Version': '2022-11-28'}; +const API_TIMEOUT_MS = 45 * 1000; +const STABLE_VERSION = 'stable'; +const EXT_TAR_GZ = '.tar.gz'; +const EXT_ZIP = '.zip'; +const EXT_REGEXP = new RegExp(`(${_.escapeRegExp(EXT_TAR_GZ)}|${_.escapeRegExp(EXT_ZIP)})$`); +const ARCHIVE_NAME_PREFIX = 'geckodriver-v'; +const ARCH_MAPPING = Object.freeze({ + ia32: '32', + x64: '64', + arm64: 'aarch64', +}); +const PLATFORM_MAPPING = Object.freeze({ + win32: 'win', + darwin: 'macos', + linux: 'linux', +}); + +/** + * + * @param {import('axios').AxiosResponseHeaders} headers + * @returns {string|null} + */ +function parseNextPageUrl(headers) { + if (!headers.link) { + return null; + } + + for (const part of headers.link.split(';')) { + const [rel, pageUrl] = part.split(',').map(_.trim); + if (rel === 'rel="next"' && pageUrl) { + return pageUrl.replace(/^<|>$/g, ''); + } + } + return null; +} + +/** + * @returns {Promise<[string, boolean]>} + */ +async function prepareDestinationFolder() { + let dstRoot; + switch (process.platform) { + case 'win32': + dstRoot = path.join(process.env.LOCALAPPDATA, 'Mozilla'); + break; + case 'linux': + case 'darwin': + dstRoot = path.join('/usr', 'local', 'bin'); + break; + default: + throw new Error( + `GeckoDriver does not support the ${process.platform} platform. ` + + `Only Linux, Windows and macOS are supported.` + ); + } + await mkdirp(dstRoot); + const pathParts = process.env.PATH ? process.env.PATH.split(path.delimiter) : []; + const isInPath = pathParts + .map((pp) => path.normalize(pp)) + .some((pp) => pp === path.normalize(dstRoot)); + return [dstRoot, isInPath]; +} + +/** + * https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#list-releases + * + * @returns {Promise[]} */ + const allReleases = []; + let currentUrl = `${API_ROOT}/releases`; + do { + const {data, headers} = await axios.get(currentUrl, { + timeout: API_TIMEOUT_MS, + headers: { ...API_VERSION_HEADER } + }); + allReleases.push(...data); + currentUrl = parseNextPageUrl(headers); + } while (currentUrl); + /** @type {ReleaseInfo[]} */ + const result = []; + for (const releaseInfo of allReleases) { + const isDraft = !!releaseInfo.draft; + const isPrerelease = !!releaseInfo.prerelease; + const version = semver.coerce(releaseInfo.tag_name?.replace(/^v/, '')); + if (!version) { + continue; + } + /** @type {ReleaseAsset[]} */ + const releaseAssets = []; + for (const asset of (releaseInfo.assets ?? [])) { + const assetName = asset?.name; + const downloadUrl = asset?.browser_download_url; + if ( + !_.startsWith(assetName, ARCHIVE_NAME_PREFIX) + || !(_.endsWith(assetName, EXT_TAR_GZ) || _.endsWith(assetName, EXT_ZIP)) + || !downloadUrl + ) { + continue; + } + releaseAssets.push({ + name: assetName, + url: downloadUrl, + }); + } + result.push({ + version, + isDraft, + isPrerelease, + assets: releaseAssets, + }); + } + return result; +} + +/** + * @param {ReleaseInfo[]} releases + * @param {string} version + * @returns {ReleaseInfo} + */ +function selectRelease(releases, version) { + if (version === STABLE_VERSION) { + const stableReleasesAsc = releases + .filter(({isDraft, isPrerelease}) => !isDraft && !isPrerelease) + .toSorted((a, b) => a.version.compare(b.version)); + const dstRelease = _.last(stableReleasesAsc); + if (!dstRelease) { + throw new Error(`Cannot find any stable GeckoDriver release: ${JSON.stringify(releases)}`); + } + return dstRelease; + } + const coercedVersion = semver.coerce(version); + if (!coercedVersion) { + throw new Error(`The provided version string '${version}' cannot be coerced to a valid SemVer representation`); + } + const dstRelease = releases.find((r) => r.version.compare(coercedVersion) === 0); + if (!dstRelease) { + throw new Error( + `The provided version string '${version}' cannot be matched to any available GeckoDriver releases: ` + + JSON.stringify(releases) + ); + } + return dstRelease; +} + +/** + * + * @param {ReleaseInfo} release + * @returns {ReleaseAsset} + */ +function selectAsset(release) { + if (_.isEmpty(release.assets)) { + throw new Error(`GeckoDriver v${release.version} does not contain any matching releases`); + } + /** @type {ReleaseAsset[]} */ + const candidates = []; + const dstPlatform = PLATFORM_MAPPING[process.platform]; + const dstArch = ARCH_MAPPING[process.arch]; + // Try to find an exact match + for (const asset of release.assets) { + if (!dstPlatform || !_.includes(asset.name, `-${dstPlatform}`)) { + continue; + } + const nameWoExt = asset.name.replace(EXT_REGEXP, ''); + log.info(`${nameWoExt} -> ${dstPlatform} :: ${dstArch}`); + if ( + (dstArch === 'aarch64' && _.endsWith(nameWoExt, `-${dstArch}`)) + || (['64', '32'].includes(dstArch) && _.endsWith(nameWoExt, `${dstArch}.`)) + ) { + candidates.push(asset); + } + } + // If no exact match has been been found then try a loose one + if (_.isEmpty(candidates)) { + for (const asset of release.assets) { + const nameWoExt = asset.name.replace(EXT_REGEXP, ''); + if (dstPlatform && _.endsWith(nameWoExt, `-${dstPlatform}`)) { + candidates.push(asset); + } + if (dstArch === '64' && _.endsWith(nameWoExt, `-${dstPlatform}32`)) { + candidates.push(asset); + } + } + } + if (!_.isEmpty(candidates)) { + return candidates[0]; + } + throw new Error( + `GeckoDriver v${release.version} does not contain any release matching the ` + + `current OS architecture ${process.arch}. Available packages: ${release.assets.map(({name}) => name)}` + ); +} + +/** + * + * @param {string} version + * @returns {Promise} + */ +async function installGeckodriver(version) { + log.debug(`Retrieving releases from ${API_ROOT}`); + const releases = await listReleases(); + if (!releases.length) { + throw new Error(`Cannot retrieve any valid GeckoDriver releases from GitHub`); + } + log.debug(`Retrieved ${releases.length} GitHub releases`); + const release = selectRelease(releases, version); + const asset = selectAsset(release); + + const [dstFolder, isInPath] = await prepareDestinationFolder(); + if (!isInPath) { + log.warning( + `The folder '${dstFolder}' is not present in the PATH environment variable. ` + + `Please add it there manually before starting a session.` + ); + } + + const archiveName = asset.name.replace(EXT_REGEXP, ''); + const archivePath = path.join( + tmpdir(), + `${archiveName}_${(Math.random() + 1).toString(36).substring(7)}${asset.name.replace(archiveName, '')}` + ); + log.info(`Will download and install v${release.version} from ${asset.url}`); + try { + await downloadToFile(asset.url, archivePath); + let executablePath; + if (archivePath.endsWith(EXT_TAR_GZ)) { + executablePath = path.join(dstFolder, 'geckodriver'); + await extractFileFromTarGz(archivePath, path.basename(executablePath), executablePath); + } else { + // .zip is only used for Windows + executablePath = path.join(dstFolder, 'geckodriver.exe'); + await extractFileFromZip(archivePath, path.basename(executablePath), executablePath); + } + log.info(`The driver is now available at '${executablePath}'`); + } finally { + try { + await fs.unlink(archivePath); + } catch (ign) {} + } +} + +(async () => await installGeckodriver(process.argv[2] ?? STABLE_VERSION))(); + +/** + * @typedef {Object} ReleaseAsset + * @property {string} name + * @property {string} url + */ + +/** + * @typedef {Object} ReleaseInfo + * @property {import('semver').SemVer} version + * @property {boolean} isDraft + * @property {boolean} isPrerelease + * @property {ReleaseAsset[]} assets + */ From 2b1834830bb499538050ec878145d06e51041954 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 18 Sep 2024 11:11:17 +0200 Subject: [PATCH 2/8] Update CI --- .github/workflows/functional-test.yml | 9 ++------- README.md | 18 ++++++++++++++++++ scripts/install-geckodriver.mjs | 13 +++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 8c0905f..2ccd591 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -34,13 +34,6 @@ jobs: name: Start X virtual frame buffer - run: sudo apt-get install firefox name: Install Firefox - - run: | - latest_release="$(curl https://api.github.com/repos/mozilla/geckodriver/releases/latest | jq -r .name)" - curl -L "https://github.com/mozilla/geckodriver/releases/download/v${latest_release}/geckodriver-v${latest_release}-linux64.tar.gz" \ - --output /tmp/geckodriver.tar.gz - tar -xzf /tmp/geckodriver.tar.gz - sudo mv geckodriver /usr/bin/ - name: Deploy Gecko driver - run: | npm install -g appium npm install @@ -50,6 +43,8 @@ jobs: pushd "$cwd" cd ~ appium driver install --source=local "$cwd" + appium driver run gecko install-geckodriver + export PATH="$PATH:/usr/local/bin" nohup appium server \ --port=$APPIUM_TEST_SERVER_PORT \ --address=$APPIUM_TEST_SERVER_HOST \ diff --git a/README.md b/README.md index fb7753a..27d3b01 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ Under the hood this driver is a wrapper/proxy over `geckodriver` binary. Check t It is mandatory to have both Firefox browser installed and the geckodriver binary downloaded on the platform where automated tests are going to be executed. Firefox could be downloaded from the [official download site](https://www.mozilla.org/en-GB/firefox/all/) and the driver binary could be retrieved from the GitHub [releases page](https://github.com/mozilla/geckodriver/releases). The binary must be put into one of the folders included to PATH environment variable. On macOS it also might be necessary to run `xattr -cr ""` to avoid [notarization](https://firefox-source-docs.mozilla.org/testing/geckodriver/Notarization.html) issues. +Since driver version 1.4.0 the geckodriver binary deployment could be automated via the +[install-geckodriver](#install-geckodriver) driver script. + Then you need to decide where the automated test is going to be executed. Gecko driver supports the following target platforms: - macOS - Windows @@ -56,6 +59,21 @@ setWindowRect | See https://www.w3.org/TR/webdriver/#capabilities timeouts | See https://www.w3.org/TR/webdriver/#capabilities unhandledPromptBehavior | See https://www.w3.org/TR/webdriver/#capabilities +## Scripts + +### install-geckodriver + +This script is used to install the given or latest stable version of Geckodriver server from +the [GitHub releases](https://github.com/mozilla/geckodriver/release) page. +Run `appium driver run gecko install-geckodriver `, where `optional_version` +must be either valid Geckodriver version number or should not be present (the latest stable version is used then). +By default, the script will download and unpack the binary into `/usr/local/bin/geckodriver` +on macOS and Linux or into `%LOCALAPPDATA%\Mozilla\geckodriver.exe` on Windows. +You must also make sure the `%LOCALAPPDATA%\Mozilla` (Windows) or `/usr/local/bin/` (Linux & macOS) +folder is present in the PATH environment variable before +starting an actual automation session. The deployment script should also show a warning message if +it is unable to find the parent folder in the PATH folders list. + ## Example ```python diff --git a/scripts/install-geckodriver.mjs b/scripts/install-geckodriver.mjs index 0508e26..b2de1fc 100644 --- a/scripts/install-geckodriver.mjs +++ b/scripts/install-geckodriver.mjs @@ -11,6 +11,7 @@ import { extractFileFromZip, } from '../build/lib/utils.js'; import fs from 'node:fs/promises'; +import { exec } from 'teen_process'; const OWNER = 'mozilla'; const REPO = 'geckodriver'; @@ -33,6 +34,17 @@ const PLATFORM_MAPPING = Object.freeze({ linux: 'linux', }); +/** + * + * @param {string} dstPath + * @returns {Promise} + */ +async function clearNotarization(dstPath) { + if (process.platform === 'darwin') { + await exec('xattr', ['-cr', dstPath]); + } +} + /** * * @param {import('axios').AxiosResponseHeaders} headers @@ -250,6 +262,7 @@ async function installGeckodriver(version) { executablePath = path.join(dstFolder, 'geckodriver.exe'); await extractFileFromZip(archivePath, path.basename(executablePath), executablePath); } + await clearNotarization(executablePath); log.info(`The driver is now available at '${executablePath}'`); } finally { try { From 13ccd8ae20aa2ae44a49b13002cdfd58a3ea96ca Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 18 Sep 2024 11:20:29 +0200 Subject: [PATCH 3/8] Tune --- scripts/install-geckodriver.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/install-geckodriver.mjs b/scripts/install-geckodriver.mjs index b2de1fc..04222fd 100644 --- a/scripts/install-geckodriver.mjs +++ b/scripts/install-geckodriver.mjs @@ -187,16 +187,17 @@ function selectAsset(release) { const candidates = []; const dstPlatform = PLATFORM_MAPPING[process.platform]; const dstArch = ARCH_MAPPING[process.arch]; + log.info(`Operating system: ${process.platform}@${process.arch}`); // Try to find an exact match for (const asset of release.assets) { if (!dstPlatform || !_.includes(asset.name, `-${dstPlatform}`)) { continue; } const nameWoExt = asset.name.replace(EXT_REGEXP, ''); - log.info(`${nameWoExt} -> ${dstPlatform} :: ${dstArch}`); if ( (dstArch === 'aarch64' && _.endsWith(nameWoExt, `-${dstArch}`)) - || (['64', '32'].includes(dstArch) && _.endsWith(nameWoExt, `${dstArch}.`)) + || ('64' === dstArch && _.endsWith(nameWoExt, `${dstArch}.`)) + || ('32' === dstArch && _.endsWith(nameWoExt, `${dstArch}.`)) ) { candidates.push(asset); } From 1dc4cf728e584f542c771e46d2764c13f088cac8 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 18 Sep 2024 11:22:09 +0200 Subject: [PATCH 4/8] Remove dot --- scripts/install-geckodriver.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install-geckodriver.mjs b/scripts/install-geckodriver.mjs index 04222fd..ed5bd53 100644 --- a/scripts/install-geckodriver.mjs +++ b/scripts/install-geckodriver.mjs @@ -196,8 +196,8 @@ function selectAsset(release) { const nameWoExt = asset.name.replace(EXT_REGEXP, ''); if ( (dstArch === 'aarch64' && _.endsWith(nameWoExt, `-${dstArch}`)) - || ('64' === dstArch && _.endsWith(nameWoExt, `${dstArch}.`)) - || ('32' === dstArch && _.endsWith(nameWoExt, `${dstArch}.`)) + || ('64' === dstArch && _.endsWith(nameWoExt, `${dstArch}`)) + || ('32' === dstArch && _.endsWith(nameWoExt, `${dstArch}`)) ) { candidates.push(asset); } From 8c6f678fb3085a49e883e7e19ad552d4ede3ede5 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 18 Sep 2024 11:22:32 +0200 Subject: [PATCH 5/8] Simplify --- scripts/install-geckodriver.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install-geckodriver.mjs b/scripts/install-geckodriver.mjs index ed5bd53..f332747 100644 --- a/scripts/install-geckodriver.mjs +++ b/scripts/install-geckodriver.mjs @@ -196,8 +196,8 @@ function selectAsset(release) { const nameWoExt = asset.name.replace(EXT_REGEXP, ''); if ( (dstArch === 'aarch64' && _.endsWith(nameWoExt, `-${dstArch}`)) - || ('64' === dstArch && _.endsWith(nameWoExt, `${dstArch}`)) - || ('32' === dstArch && _.endsWith(nameWoExt, `${dstArch}`)) + || ('64' === dstArch && _.endsWith(nameWoExt, dstArch)) + || ('32' === dstArch && _.endsWith(nameWoExt, dstArch)) ) { candidates.push(asset); } From 164ee09354e52b2531119c38c88978e0d7e384a0 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 18 Sep 2024 11:26:08 +0200 Subject: [PATCH 6/8] moar --- scripts/install-geckodriver.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/install-geckodriver.mjs b/scripts/install-geckodriver.mjs index f332747..fc29d5a 100644 --- a/scripts/install-geckodriver.mjs +++ b/scripts/install-geckodriver.mjs @@ -205,11 +205,14 @@ function selectAsset(release) { // If no exact match has been been found then try a loose one if (_.isEmpty(candidates)) { for (const asset of release.assets) { - const nameWoExt = asset.name.replace(EXT_REGEXP, ''); - if (dstPlatform && _.endsWith(nameWoExt, `-${dstPlatform}`)) { - candidates.push(asset); + if (!dstPlatform || !_.includes(asset.name, `-${dstPlatform}`)) { + continue; } - if (dstArch === '64' && _.endsWith(nameWoExt, `-${dstPlatform}32`)) { + const nameWoExt = asset.name.replace(EXT_REGEXP, ''); + if ( + _.endsWith(nameWoExt, `-${dstPlatform}`) + || (dstArch === '64' && _.endsWith(nameWoExt, `-${dstPlatform}32`)) + ) { candidates.push(asset); } } From c727fd8452a176b4f6e6eecfd5baed579790c222 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 18 Sep 2024 11:29:47 +0200 Subject: [PATCH 7/8] fix --- scripts/install-geckodriver.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/install-geckodriver.mjs b/scripts/install-geckodriver.mjs index fc29d5a..5f57717 100644 --- a/scripts/install-geckodriver.mjs +++ b/scripts/install-geckodriver.mjs @@ -196,8 +196,7 @@ function selectAsset(release) { const nameWoExt = asset.name.replace(EXT_REGEXP, ''); if ( (dstArch === 'aarch64' && _.endsWith(nameWoExt, `-${dstArch}`)) - || ('64' === dstArch && _.endsWith(nameWoExt, dstArch)) - || ('32' === dstArch && _.endsWith(nameWoExt, dstArch)) + || (['64', '32'].includes(dstArch) && _.endsWith(nameWoExt, `-${dstPlatform}${dstArch}`)) ) { candidates.push(asset); } From 45b2b9f516ef49f7403fabf61caff628d7cd3faa Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 18 Sep 2024 11:53:05 +0200 Subject: [PATCH 8/8] no duplication --- scripts/install-geckodriver.mjs | 47 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/scripts/install-geckodriver.mjs b/scripts/install-geckodriver.mjs index 5f57717..2d4f573 100644 --- a/scripts/install-geckodriver.mjs +++ b/scripts/install-geckodriver.mjs @@ -183,41 +183,40 @@ function selectAsset(release) { if (_.isEmpty(release.assets)) { throw new Error(`GeckoDriver v${release.version} does not contain any matching releases`); } - /** @type {ReleaseAsset[]} */ - const candidates = []; const dstPlatform = PLATFORM_MAPPING[process.platform]; const dstArch = ARCH_MAPPING[process.arch]; log.info(`Operating system: ${process.platform}@${process.arch}`); - // Try to find an exact match - for (const asset of release.assets) { - if (!dstPlatform || !_.includes(asset.name, `-${dstPlatform}`)) { - continue; - } - const nameWoExt = asset.name.replace(EXT_REGEXP, ''); - if ( - (dstArch === 'aarch64' && _.endsWith(nameWoExt, `-${dstArch}`)) - || (['64', '32'].includes(dstArch) && _.endsWith(nameWoExt, `-${dstPlatform}${dstArch}`)) - ) { - candidates.push(asset); - } - } - // If no exact match has been been found then try a loose one - if (_.isEmpty(candidates)) { + /** @type {(filterFunc: (string) => boolean) => null|ReleaseAsset} */ + const findAssetMatch = (filterFunc) => { for (const asset of release.assets) { if (!dstPlatform || !_.includes(asset.name, `-${dstPlatform}`)) { continue; } const nameWoExt = asset.name.replace(EXT_REGEXP, ''); - if ( - _.endsWith(nameWoExt, `-${dstPlatform}`) - || (dstArch === '64' && _.endsWith(nameWoExt, `-${dstPlatform}32`)) - ) { - candidates.push(asset); + if (filterFunc(nameWoExt)) { + return asset; } } + return null; + }; + + // Try to find an exact match + const exactMatch = findAssetMatch( + (nameWoExt) => + (dstArch === 'aarch64' && _.endsWith(nameWoExt, `-${dstArch}`)) + || (['64', '32'].includes(dstArch) && _.endsWith(nameWoExt, `-${dstPlatform}${dstArch}`)) + ); + if (exactMatch) { + return exactMatch; } - if (!_.isEmpty(candidates)) { - return candidates[0]; + // If no exact match has been been found then try a loose one + const looseMatch = findAssetMatch( + (nameWoExt) => + _.endsWith(nameWoExt, `-${dstPlatform}`) + || (dstArch === '64' && _.endsWith(nameWoExt, `-${dstPlatform}32`)) + ); + if (looseMatch) { + return looseMatch; } throw new Error( `GeckoDriver v${release.version} does not contain any release matching the ` +