appium-geckodriver 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/build/lib/commands/find.d.ts +1 -1
- package/build/lib/commands/find.d.ts.map +1 -1
- package/build/lib/commands/find.js +3 -6
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/constants.js +2 -5
- package/build/lib/constants.js.map +1 -1
- package/build/lib/desired-caps.js +3 -6
- package/build/lib/desired-caps.js.map +1 -1
- package/build/lib/doctor/optional-checks.js +18 -22
- package/build/lib/doctor/optional-checks.js.map +1 -1
- package/build/lib/doctor/required-checks.js +8 -12
- package/build/lib/doctor/required-checks.js.map +1 -1
- package/build/lib/doctor/utils.js +4 -7
- package/build/lib/doctor/utils.js.map +1 -1
- package/build/lib/driver.d.ts +4 -4
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +18 -55
- package/build/lib/driver.js.map +1 -1
- package/build/lib/gecko.d.ts +1 -1
- package/build/lib/gecko.d.ts.map +1 -1
- package/build/lib/gecko.js +26 -34
- package/build/lib/gecko.js.map +1 -1
- package/build/lib/index.d.ts +1 -1
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +3 -6
- package/build/lib/index.js.map +1 -1
- package/build/lib/logger.js +3 -6
- package/build/lib/logger.js.map +1 -1
- package/build/lib/method-map.js +1 -4
- package/build/lib/method-map.js.map +1 -1
- package/build/lib/utils.js +20 -30
- package/build/lib/utils.js.map +1 -1
- package/lib/commands/find.ts +2 -2
- package/lib/desired-caps.ts +1 -1
- package/lib/doctor/required-checks.ts +1 -1
- package/lib/driver.ts +7 -7
- package/lib/gecko.ts +3 -3
- package/lib/index.ts +1 -1
- package/lib/logger.ts +1 -1
- package/lib/method-map.ts +1 -1
- package/lib/utils.ts +2 -2
- package/npm-shrinkwrap.json +12 -9
- package/package.json +12 -10
- package/scripts/install-geckodriver.mjs +382 -216
|
@@ -1,279 +1,445 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import axios from 'axios';
|
|
2
3
|
import * as semver from 'semver';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import { tmpdir
|
|
5
|
-
import {
|
|
5
|
+
import {homedir, tmpdir} from 'node:os';
|
|
6
|
+
import {constants as fsConstants} from 'node:fs';
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import {exec} from 'teen_process';
|
|
9
|
+
import {Command} from 'commander';
|
|
10
|
+
import {log} from '../build/lib/logger.js';
|
|
6
11
|
import {
|
|
7
12
|
downloadToFile,
|
|
8
13
|
mkdirp,
|
|
9
14
|
extractFileFromTarGz,
|
|
10
15
|
extractFileFromZip,
|
|
11
16
|
} from '../build/lib/utils.js';
|
|
12
|
-
import fs from 'node:fs/promises';
|
|
13
|
-
import { exec } from 'teen_process';
|
|
14
17
|
|
|
15
|
-
const OWNER = 'mozilla';
|
|
16
|
-
const REPO = 'geckodriver';
|
|
17
|
-
const API_ROOT = `https://api.github.com/repos/${OWNER}/${REPO}`;
|
|
18
|
-
const API_VERSION_HEADER = {'X-GitHub-Api-Version': '2022-11-28'};
|
|
19
|
-
const API_TIMEOUT_MS = 45 * 1000;
|
|
20
18
|
const STABLE_VERSION = 'stable';
|
|
21
19
|
const EXT_TAR_GZ = '.tar.gz';
|
|
22
20
|
const EXT_ZIP = '.zip';
|
|
23
21
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
24
22
|
const EXT_REGEXP = new RegExp(`(${escapeRegExp(EXT_TAR_GZ)}|${escapeRegExp(EXT_ZIP)})$`);
|
|
25
|
-
const ARCHIVE_NAME_PREFIX = 'geckodriver-v';
|
|
26
|
-
const ARCH_MAPPING = Object.freeze({
|
|
27
|
-
ia32: '32',
|
|
28
|
-
x64: '64',
|
|
29
|
-
arm64: 'aarch64',
|
|
30
|
-
});
|
|
31
|
-
const PLATFORM_MAPPING = Object.freeze({
|
|
32
|
-
win32: 'win',
|
|
33
|
-
darwin: 'macos',
|
|
34
|
-
linux: 'linux',
|
|
35
|
-
});
|
|
36
23
|
|
|
37
24
|
/**
|
|
38
|
-
*
|
|
39
|
-
* @param {string} dstPath
|
|
40
|
-
* @returns {Promise<void>}
|
|
25
|
+
* Fetches and selects Geckodriver releases from GitHub.
|
|
41
26
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
}
|
|
27
|
+
class GeckodriverReleaseCatalog {
|
|
28
|
+
static OWNER = 'mozilla';
|
|
29
|
+
static REPO = 'geckodriver';
|
|
30
|
+
static API_ROOT = `https://api.github.com/repos/${GeckodriverReleaseCatalog.OWNER}/${GeckodriverReleaseCatalog.REPO}`;
|
|
31
|
+
static API_VERSION_HEADER = {'X-GitHub-Api-Version': '2022-11-28'};
|
|
32
|
+
static API_TIMEOUT_MS = 45 * 1000;
|
|
33
|
+
static ARCHIVE_NAME_PREFIX = 'geckodriver-v';
|
|
34
|
+
static ARCH_MAPPING = Object.freeze({
|
|
35
|
+
ia32: '32',
|
|
36
|
+
x64: '64',
|
|
37
|
+
arm64: 'aarch64',
|
|
38
|
+
});
|
|
39
|
+
static PLATFORM_MAPPING = Object.freeze({
|
|
40
|
+
win32: 'win',
|
|
41
|
+
darwin: 'macos',
|
|
42
|
+
linux: 'linux',
|
|
43
|
+
});
|
|
44
|
+
static SUPPORTED_PLATFORMS = Object.keys(GeckodriverReleaseCatalog.PLATFORM_MAPPING).join(', ');
|
|
45
|
+
static SUPPORTED_ARCHITECTURES = Object.keys(GeckodriverReleaseCatalog.ARCH_MAPPING).join(', ');
|
|
47
46
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
/**
|
|
48
|
+
* https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#list-releases
|
|
49
|
+
*
|
|
50
|
+
* @returns {Promise<ReleaseInfo[]>}
|
|
51
|
+
*/
|
|
52
|
+
async listReleases() {
|
|
53
|
+
/** @type {Record<string, any>[]} */
|
|
54
|
+
const allReleases = [];
|
|
55
|
+
let currentUrl = `${GeckodriverReleaseCatalog.API_ROOT}/releases`;
|
|
56
|
+
do {
|
|
57
|
+
const {data, headers} = await axios.get(currentUrl, {
|
|
58
|
+
timeout: GeckodriverReleaseCatalog.API_TIMEOUT_MS,
|
|
59
|
+
headers: {...GeckodriverReleaseCatalog.API_VERSION_HEADER},
|
|
60
|
+
});
|
|
61
|
+
allReleases.push(...data);
|
|
62
|
+
currentUrl = this.#parseNextPageUrl(headers);
|
|
63
|
+
} while (currentUrl);
|
|
64
|
+
|
|
65
|
+
/** @type {ReleaseInfo[]} */
|
|
66
|
+
const result = [];
|
|
67
|
+
for (const releaseInfo of allReleases) {
|
|
68
|
+
const isDraft = !!releaseInfo.draft;
|
|
69
|
+
const isPrerelease = !!releaseInfo.prerelease;
|
|
70
|
+
const version = semver.coerce(releaseInfo.tag_name?.replace(/^v/, ''));
|
|
71
|
+
if (!version) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
/** @type {ReleaseAsset[]} */
|
|
75
|
+
const releaseAssets = [];
|
|
76
|
+
for (const asset of releaseInfo.assets ?? []) {
|
|
77
|
+
const assetName = asset?.name;
|
|
78
|
+
const downloadUrl = asset?.browser_download_url;
|
|
79
|
+
if (
|
|
80
|
+
!assetName?.startsWith(GeckodriverReleaseCatalog.ARCHIVE_NAME_PREFIX)
|
|
81
|
+
|| !(assetName?.endsWith(EXT_TAR_GZ) || assetName?.endsWith(EXT_ZIP))
|
|
82
|
+
|| !downloadUrl
|
|
83
|
+
) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
releaseAssets.push({
|
|
87
|
+
name: assetName,
|
|
88
|
+
url: downloadUrl,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
result.push({
|
|
92
|
+
version,
|
|
93
|
+
isDraft,
|
|
94
|
+
isPrerelease,
|
|
95
|
+
assets: releaseAssets,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
56
99
|
}
|
|
57
100
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
101
|
+
/**
|
|
102
|
+
* @param {ReleaseInfo[]} releases
|
|
103
|
+
* @param {string} version
|
|
104
|
+
* @returns {ReleaseInfo}
|
|
105
|
+
*/
|
|
106
|
+
selectRelease(releases, version) {
|
|
107
|
+
if (version === STABLE_VERSION) {
|
|
108
|
+
const stableReleasesAsc = releases
|
|
109
|
+
.filter(({isDraft, isPrerelease}) => !isDraft && !isPrerelease)
|
|
110
|
+
.toSorted((a, b) => a.version.compare(b.version));
|
|
111
|
+
const dstRelease = stableReleasesAsc.at(-1);
|
|
112
|
+
if (!dstRelease) {
|
|
113
|
+
throw new Error(`Cannot find any stable GeckoDriver release: ${JSON.stringify(releases)}`);
|
|
114
|
+
}
|
|
115
|
+
return dstRelease;
|
|
62
116
|
}
|
|
117
|
+
const coercedVersion = semver.coerce(version);
|
|
118
|
+
if (!coercedVersion) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`The provided version string '${version}' cannot be coerced to a valid SemVer representation`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const dstRelease = releases.find((r) => r.version.compare(coercedVersion) === 0);
|
|
124
|
+
if (!dstRelease) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`The provided version string '${version}' cannot be matched to any available GeckoDriver releases: ` +
|
|
127
|
+
JSON.stringify(releases)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return dstRelease;
|
|
63
131
|
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
132
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
case 'darwin':
|
|
78
|
-
dstRoot = path.join('/usr', 'local', 'bin');
|
|
79
|
-
break;
|
|
80
|
-
default:
|
|
133
|
+
/**
|
|
134
|
+
* @param {ReleaseInfo} release
|
|
135
|
+
* @returns {ReleaseAsset}
|
|
136
|
+
*/
|
|
137
|
+
selectAsset(release) {
|
|
138
|
+
if (release.assets.length === 0) {
|
|
139
|
+
throw new Error(`GeckoDriver v${release.version} does not contain any matching releases`);
|
|
140
|
+
}
|
|
141
|
+
const dstPlatform = GeckodriverReleaseCatalog.PLATFORM_MAPPING[process.platform];
|
|
142
|
+
if (!dstPlatform) {
|
|
81
143
|
throw new Error(
|
|
82
144
|
`GeckoDriver does not support the ${process.platform} platform. ` +
|
|
83
|
-
`
|
|
145
|
+
`Supported platforms: ${GeckodriverReleaseCatalog.SUPPORTED_PLATFORMS}.`
|
|
84
146
|
);
|
|
85
147
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
148
|
+
const dstArch = GeckodriverReleaseCatalog.ARCH_MAPPING[process.arch];
|
|
149
|
+
if (!dstArch) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`GeckoDriver does not support the ${process.arch} architecture. ` +
|
|
152
|
+
`Supported architectures: ${GeckodriverReleaseCatalog.SUPPORTED_ARCHITECTURES}.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
log.info(`Operating system: ${process.platform}@${process.arch}`);
|
|
93
156
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const isPrerelease = !!releaseInfo.prerelease;
|
|
116
|
-
const version = semver.coerce(releaseInfo.tag_name?.replace(/^v/, ''));
|
|
117
|
-
if (!version) {
|
|
118
|
-
continue;
|
|
157
|
+
/** @type {(filterFunc: (string) => boolean) => null|ReleaseAsset} */
|
|
158
|
+
const findAssetMatch = (filterFunc) => {
|
|
159
|
+
for (const asset of release.assets) {
|
|
160
|
+
if (!asset.name.includes(`-${dstPlatform}`)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const nameWoExt = asset.name.replace(EXT_REGEXP, '');
|
|
164
|
+
if (filterFunc(nameWoExt)) {
|
|
165
|
+
return asset;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const exactMatch = findAssetMatch(
|
|
172
|
+
(nameWoExt) =>
|
|
173
|
+
(dstArch === 'aarch64' && nameWoExt.endsWith(`-${dstArch}`))
|
|
174
|
+
|| (['64', '32'].includes(dstArch) && nameWoExt.endsWith(`-${dstPlatform}${dstArch}`))
|
|
175
|
+
);
|
|
176
|
+
if (exactMatch) {
|
|
177
|
+
return exactMatch;
|
|
119
178
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
179
|
+
const looseMatch = findAssetMatch(
|
|
180
|
+
(nameWoExt) =>
|
|
181
|
+
nameWoExt.endsWith(`-${dstPlatform}`)
|
|
182
|
+
|| (dstArch === '64' && nameWoExt.endsWith(`-${dstPlatform}32`))
|
|
183
|
+
);
|
|
184
|
+
if (looseMatch) {
|
|
185
|
+
return looseMatch;
|
|
186
|
+
}
|
|
187
|
+
throw new Error(
|
|
188
|
+
`GeckoDriver v${release.version} does not contain any release matching the ` +
|
|
189
|
+
`current OS architecture ${process.arch}. Available packages: ${release.assets.map(({name}) => name)}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {import('axios').AxiosResponseHeaders} headers
|
|
195
|
+
* @returns {string|null}
|
|
196
|
+
*/
|
|
197
|
+
#parseNextPageUrl(headers) {
|
|
198
|
+
if (!headers.link) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const linkPart of headers.link.split(',')) {
|
|
203
|
+
const [pageUrl, rel] = linkPart.split(';').map((item) => item.trim());
|
|
204
|
+
if (rel === 'rel="next"' && pageUrl) {
|
|
205
|
+
return pageUrl.replace(/^<|>$/g, '');
|
|
131
206
|
}
|
|
132
|
-
releaseAssets.push({
|
|
133
|
-
name: assetName,
|
|
134
|
-
url: downloadUrl,
|
|
135
|
-
});
|
|
136
207
|
}
|
|
137
|
-
|
|
138
|
-
version,
|
|
139
|
-
isDraft,
|
|
140
|
-
isPrerelease,
|
|
141
|
-
assets: releaseAssets,
|
|
142
|
-
});
|
|
208
|
+
return null;
|
|
143
209
|
}
|
|
144
|
-
return result;
|
|
145
210
|
}
|
|
146
211
|
|
|
147
212
|
/**
|
|
148
|
-
*
|
|
149
|
-
* @param {string} version
|
|
150
|
-
* @returns {ReleaseInfo}
|
|
213
|
+
* Resolves and validates Geckodriver installation paths.
|
|
151
214
|
*/
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!dstRelease) {
|
|
159
|
-
throw new Error(`Cannot find any stable GeckoDriver release: ${JSON.stringify(releases)}`);
|
|
160
|
-
}
|
|
161
|
-
return dstRelease;
|
|
215
|
+
class GeckodriverInstallPath {
|
|
216
|
+
/**
|
|
217
|
+
* @returns {string}
|
|
218
|
+
*/
|
|
219
|
+
static #getExecutableName() {
|
|
220
|
+
return process.platform === 'win32' ? 'geckodriver.exe' : 'geckodriver';
|
|
162
221
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} [explicitDestDir]
|
|
225
|
+
* @returns {Promise<{path: string, onPath: boolean}>}
|
|
226
|
+
*/
|
|
227
|
+
async resolve(explicitDestDir) {
|
|
228
|
+
const dstRoot = explicitDestDir
|
|
229
|
+
? path.resolve(explicitDestDir)
|
|
230
|
+
: await this.#resolveDefault();
|
|
231
|
+
if (!(await this.#isInstallable(dstRoot))) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`The destination directory '${dstRoot}' is not writable or already contains ` +
|
|
234
|
+
`a non-overwritable geckodriver binary`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
path: dstRoot,
|
|
239
|
+
onPath: this.#isOnPath(dstRoot),
|
|
240
|
+
};
|
|
173
241
|
}
|
|
174
|
-
return dstRelease;
|
|
175
|
-
}
|
|
176
242
|
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
243
|
+
/**
|
|
244
|
+
* @param {string} dstRoot
|
|
245
|
+
* @returns {boolean}
|
|
246
|
+
*/
|
|
247
|
+
#isOnPath(dstRoot) {
|
|
248
|
+
const pathParts = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
|
|
249
|
+
return pathParts
|
|
250
|
+
.map((pp) => path.normalize(pp))
|
|
251
|
+
.some((pp) => pp === path.normalize(dstRoot));
|
|
185
252
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} dstRoot
|
|
256
|
+
* @returns {Promise<boolean>}
|
|
257
|
+
*/
|
|
258
|
+
async #isInstallable(dstRoot) {
|
|
259
|
+
const executablePath = path.join(dstRoot, GeckodriverInstallPath.#getExecutableName());
|
|
260
|
+
try {
|
|
261
|
+
await mkdirp(dstRoot);
|
|
262
|
+
await fs.access(dstRoot, fsConstants.W_OK);
|
|
263
|
+
try {
|
|
264
|
+
const stats = await fs.lstat(executablePath);
|
|
265
|
+
if (!stats.isFile()) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
await fs.access(executablePath, fsConstants.W_OK);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code !== 'ENOENT') {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const probePath = path.join(dstRoot, `.geckodriver-install-probe-${process.pid}`);
|
|
274
|
+
await fs.writeFile(probePath, '');
|
|
275
|
+
await fs.unlink(probePath);
|
|
194
276
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
277
|
+
return true;
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @param {string[]} candidates
|
|
285
|
+
* @returns {Promise<string>}
|
|
286
|
+
*/
|
|
287
|
+
async #selectFromCandidates(candidates) {
|
|
288
|
+
for (const candidate of candidates) {
|
|
289
|
+
if (await this.#isInstallable(candidate)) {
|
|
290
|
+
return candidate;
|
|
198
291
|
}
|
|
199
292
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Try to find an exact match
|
|
204
|
-
const exactMatch = findAssetMatch(
|
|
205
|
-
(nameWoExt) =>
|
|
206
|
-
(dstArch === 'aarch64' && nameWoExt.endsWith(`-${dstArch}`))
|
|
207
|
-
|| (['64', '32'].includes(dstArch) && nameWoExt.endsWith(`-${dstPlatform}${dstArch}`))
|
|
208
|
-
);
|
|
209
|
-
if (exactMatch) {
|
|
210
|
-
return exactMatch;
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Could not find a writable installation directory. Tried: ${candidates.join(', ')}`
|
|
295
|
+
);
|
|
211
296
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @returns {Promise<string>}
|
|
300
|
+
*/
|
|
301
|
+
async #resolveDefault() {
|
|
302
|
+
switch (process.platform) {
|
|
303
|
+
case 'win32':
|
|
304
|
+
return path.join(process.env.LOCALAPPDATA, 'Mozilla');
|
|
305
|
+
case 'linux':
|
|
306
|
+
case 'darwin':
|
|
307
|
+
return this.#selectFromCandidates([
|
|
308
|
+
path.join('/usr', 'local', 'bin'),
|
|
309
|
+
path.join(homedir(), '.local', 'bin'),
|
|
310
|
+
]);
|
|
311
|
+
default:
|
|
312
|
+
throw new Error(
|
|
313
|
+
`GeckoDriver does not support the ${process.platform} platform. ` +
|
|
314
|
+
`Supported platforms: ${GeckodriverReleaseCatalog.SUPPORTED_PLATFORMS}.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
220
317
|
}
|
|
221
|
-
throw new Error(
|
|
222
|
-
`GeckoDriver v${release.version} does not contain any release matching the ` +
|
|
223
|
-
`current OS architecture ${process.arch}. Available packages: ${release.assets.map(({name}) => name)}`
|
|
224
|
-
);
|
|
225
318
|
}
|
|
226
319
|
|
|
227
320
|
/**
|
|
228
|
-
*
|
|
229
|
-
* @param {string} version
|
|
230
|
-
* @returns {Promise<void>}
|
|
321
|
+
* Downloads and installs Geckodriver binaries.
|
|
231
322
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
323
|
+
class GeckodriverInstaller {
|
|
324
|
+
/**
|
|
325
|
+
* @param {GeckodriverReleaseCatalog} [catalog]
|
|
326
|
+
* @param {GeckodriverInstallPath} [installPath]
|
|
327
|
+
*/
|
|
328
|
+
constructor(
|
|
329
|
+
catalog = new GeckodriverReleaseCatalog(),
|
|
330
|
+
installPath = new GeckodriverInstallPath(),
|
|
331
|
+
) {
|
|
332
|
+
this.catalog = catalog;
|
|
333
|
+
this.installPath = installPath;
|
|
237
334
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param {string} version
|
|
338
|
+
* @param {{destDir?: string}} [options]
|
|
339
|
+
* @returns {Promise<void>}
|
|
340
|
+
*/
|
|
341
|
+
async install(version, options = {}) {
|
|
342
|
+
log.debug(`Retrieving releases from ${GeckodriverReleaseCatalog.API_ROOT}`);
|
|
343
|
+
const releases = await this.catalog.listReleases();
|
|
344
|
+
if (!releases.length) {
|
|
345
|
+
throw new Error(`Cannot retrieve any valid GeckoDriver releases from GitHub`);
|
|
346
|
+
}
|
|
347
|
+
log.debug(`Retrieved ${releases.length} GitHub releases`);
|
|
348
|
+
|
|
349
|
+
const release = this.catalog.selectRelease(releases, version);
|
|
350
|
+
const asset = this.catalog.selectAsset(release);
|
|
351
|
+
const {path: dstFolder, onPath} = await this.installPath.resolve(options.destDir);
|
|
352
|
+
|
|
353
|
+
if (!onPath) {
|
|
354
|
+
log.warning(
|
|
355
|
+
`The folder '${dstFolder}' is not present in the PATH environment variable. ` +
|
|
356
|
+
`Please add it there manually before starting a session.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const archiveName = asset.name.replace(EXT_REGEXP, '');
|
|
361
|
+
const archivePath = path.join(
|
|
362
|
+
tmpdir(),
|
|
363
|
+
`${archiveName}_${(Math.random() + 1).toString(36).substring(7)}${asset.name.replace(archiveName, '')}`
|
|
247
364
|
);
|
|
365
|
+
log.info(`Will download and install v${release.version} from ${asset.url}`);
|
|
366
|
+
try {
|
|
367
|
+
await downloadToFile(asset.url, archivePath);
|
|
368
|
+
const executablePath = await this.#deployBinary(archivePath, dstFolder);
|
|
369
|
+
await this.#clearNotarization(executablePath);
|
|
370
|
+
log.info(`The driver is now available at '${executablePath}'`);
|
|
371
|
+
} finally {
|
|
372
|
+
try {
|
|
373
|
+
await fs.unlink(archivePath);
|
|
374
|
+
} catch {}
|
|
375
|
+
}
|
|
248
376
|
}
|
|
249
377
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
await downloadToFile(asset.url, archivePath);
|
|
258
|
-
let executablePath;
|
|
378
|
+
/**
|
|
379
|
+
* @param {string} archivePath
|
|
380
|
+
* @param {string} dstFolder
|
|
381
|
+
* @returns {Promise<string>}
|
|
382
|
+
*/
|
|
383
|
+
async #deployBinary(archivePath, dstFolder) {
|
|
259
384
|
if (archivePath.endsWith(EXT_TAR_GZ)) {
|
|
260
|
-
executablePath = path.join(dstFolder, 'geckodriver');
|
|
385
|
+
const executablePath = path.join(dstFolder, 'geckodriver');
|
|
261
386
|
await extractFileFromTarGz(archivePath, path.basename(executablePath), executablePath);
|
|
262
|
-
|
|
263
|
-
// .zip is only used for Windows
|
|
264
|
-
executablePath = path.join(dstFolder, 'geckodriver.exe');
|
|
265
|
-
await extractFileFromZip(archivePath, path.basename(executablePath), executablePath);
|
|
387
|
+
return executablePath;
|
|
266
388
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
await fs.unlink(archivePath);
|
|
272
|
-
} catch {}
|
|
389
|
+
const executablePath = path.join(dstFolder, 'geckodriver.exe');
|
|
390
|
+
await extractFileFromZip(archivePath, path.basename(executablePath), executablePath);
|
|
391
|
+
return executablePath;
|
|
273
392
|
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* @param {string} dstPath
|
|
396
|
+
* @returns {Promise<void>}
|
|
397
|
+
*/
|
|
398
|
+
async #clearNotarization(dstPath) {
|
|
399
|
+
if (process.platform === 'darwin') {
|
|
400
|
+
await exec('xattr', ['-cr', dstPath]);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* CLI with Commander.js
|
|
407
|
+
*/
|
|
408
|
+
async function main() {
|
|
409
|
+
const installer = new GeckodriverInstaller();
|
|
410
|
+
const program = new Command();
|
|
411
|
+
|
|
412
|
+
program
|
|
413
|
+
.name('appium driver run gecko install-geckodriver')
|
|
414
|
+
.description('Download and install Geckodriver from GitHub releases')
|
|
415
|
+
.argument('[version]', 'Geckodriver version to install (default: latest stable)', STABLE_VERSION)
|
|
416
|
+
.option('-d, --dest <path>', 'Destination directory for the geckodriver binary')
|
|
417
|
+
.addHelpText(
|
|
418
|
+
'after',
|
|
419
|
+
`
|
|
420
|
+
EXAMPLES:
|
|
421
|
+
# Install the latest stable Geckodriver to the default location
|
|
422
|
+
appium driver run gecko install-geckodriver
|
|
423
|
+
|
|
424
|
+
# Install a specific version
|
|
425
|
+
appium driver run gecko install-geckodriver 0.36.0
|
|
426
|
+
|
|
427
|
+
# Install to a custom directory
|
|
428
|
+
appium driver run gecko install-geckodriver --dest ~/.local/bin
|
|
429
|
+
|
|
430
|
+
NOTE:
|
|
431
|
+
On macOS and Linux, the default location is the first writable directory among:
|
|
432
|
+
/usr/local/bin and ~/.local/bin.
|
|
433
|
+
On Windows, the default location is %LOCALAPPDATA%\\Mozilla.`
|
|
434
|
+
)
|
|
435
|
+
.action(async (version, options) => {
|
|
436
|
+
await installer.install(version, {destDir: options.dest});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await program.parseAsync(process.argv);
|
|
274
440
|
}
|
|
275
441
|
|
|
276
|
-
(async () => await
|
|
442
|
+
(async () => await main())();
|
|
277
443
|
|
|
278
444
|
/**
|
|
279
445
|
* @typedef {Object} ReleaseAsset
|