UNPKG

10.5 kBJavaScriptView Raw
1/* eslint-disable no-console */
2import _ from 'lodash';
3import {system, fs} from '@appium/support';
4import axios from 'axios';
5import {exec} from 'teen_process';
6import semver from 'semver';
7import findUp from 'find-up';
8import {getDefaultsForSchema, getAllArgSpecs} from './schema/schema';
9
10const npmPackage = fs.readPackageJsonFrom(__dirname);
11
12const APPIUM_VER = npmPackage.version;
13const ENGINES = /** @type {Record<string,string>} */ (npmPackage.engines);
14const MIN_NODE_VERSION = ENGINES.node;
15const MIN_NPM_VERSION = ENGINES.npm;
16
17const GIT_META_ROOT = '.git';
18const GIT_BINARY = `git${system.isWindows() ? '.exe' : ''}`;
19const GITHUB_API = 'https://api.github.com/repos/appium/appium';
20
21/**
22 * @type {import('appium/types').BuildInfo}
23 */
24const BUILD_INFO = {
25 version: APPIUM_VER,
26};
27
28function getNodeVersion() {
29 return /** @type {import('semver').SemVer} */ (semver.coerce(process.version));
30}
31
32/**
33 * Returns version of `npm`
34 * @returns {Promise<string>}
35 */
36async function getNpmVersion() {
37 const {stdout} = await exec(system.isWindows() ? 'npm.cmd' : 'npm', ['--version']);
38 return stdout.trim();
39}
40
41/**
42 * @param {boolean} [useGithubApiFallback]
43 */
44async function updateBuildInfo(useGithubApiFallback = false) {
45 const sha = await getGitRev(useGithubApiFallback);
46 if (!sha) {
47 return;
48 }
49 BUILD_INFO['git-sha'] = sha;
50 const buildTimestamp = await getGitTimestamp(sha, useGithubApiFallback);
51 if (buildTimestamp) {
52 BUILD_INFO.built = buildTimestamp;
53 }
54}
55
56/**
57 * Finds the Git metadata dir (see `GIT_META_ROOT`)
58 *
59 * This is needed because Appium cannot assume `package.json` and `.git` are in the same
60 * directory. Monorepos, see?
61 * @returns {Promise<string|undefined>} Path to dir or `undefined` if not found
62 */
63async function findGitRoot() {
64 return await findUp(GIT_META_ROOT, {cwd: rootDir, type: 'directory'});
65}
66
67/**
68 * @param {boolean} [useGithubApiFallback]
69 * @returns {Promise<string?>}
70 */
71async function getGitRev(useGithubApiFallback = false) {
72 const gitRoot = await findGitRoot();
73 if (gitRoot) {
74 try {
75 const {stdout} = await exec(GIT_BINARY, ['rev-parse', 'HEAD'], {
76 cwd: gitRoot,
77 });
78 return stdout.trim();
79 } catch (ign) {}
80 }
81
82 if (!useGithubApiFallback) {
83 return null;
84 }
85
86 // If the package folder is not a valid git repository
87 // then fetch the corresponding tag info from GitHub
88 try {
89 return (
90 await axios.get(`${GITHUB_API}/git/refs/tags/appium@${APPIUM_VER}`, {
91 headers: {
92 'User-Agent': `Appium ${APPIUM_VER}`,
93 },
94 })
95 ).data?.object?.sha;
96 } catch (ign) {}
97 return null;
98}
99
100/**
101 * @param {string} commitSha
102 * @param {boolean} [useGithubApiFallback]
103 * @returns {Promise<string?>}
104 */
105async function getGitTimestamp(commitSha, useGithubApiFallback = false) {
106 const gitRoot = await findGitRoot();
107 if (gitRoot) {
108 try {
109 const {stdout} = await exec(GIT_BINARY, ['show', '-s', '--format=%ci', commitSha], {
110 cwd: gitRoot,
111 });
112 return stdout.trim();
113 } catch (ign) {}
114 }
115
116 if (!useGithubApiFallback) {
117 return null;
118 }
119
120 try {
121 return (
122 await axios.get(`${GITHUB_API}/git/tags/${commitSha}`, {
123 headers: {
124 'User-Agent': `Appium ${APPIUM_VER}`,
125 },
126 })
127 ).data?.tagger?.date;
128 } catch (ign) {}
129 return null;
130}
131
132/**
133 * Mutable object containing Appium build information. By default it
134 * only contains the Appium version, but is updated with the build timestamp
135 * and git commit hash asynchronously as soon as `updateBuildInfo` is called
136 * and succeeds.
137 * @returns {import('appium/types').BuildInfo}
138 */
139function getBuildInfo() {
140 return BUILD_INFO;
141}
142
143function checkNodeOk() {
144 const version = getNodeVersion();
145 if (!semver.satisfies(version, MIN_NODE_VERSION)) {
146 throw new Error(
147 `Node version must be at least ${MIN_NODE_VERSION}; current is ${version.version}`
148 );
149 }
150}
151
152export async function checkNpmOk() {
153 const npmVersion = await getNpmVersion();
154 if (!semver.satisfies(npmVersion, MIN_NPM_VERSION)) {
155 throw new Error(
156 `npm version must be at least ${MIN_NPM_VERSION}; current is ${npmVersion}. Run "npm install -g npm" to upgrade.`
157 );
158 }
159}
160
161function warnNodeDeprecations() {
162 /**
163 * Uncomment this section to get node version deprecation warnings
164 * Also add test cases to config-specs.js to cover the cases added
165 **/
166 // const version = getNodeVersion();
167 // if (version.major < 8) {
168 // logger.warn(`Appium support for versions of node < ${version.major} has been ` +
169 // 'deprecated and will be removed in a future version. Please ' +
170 // 'upgrade!');
171 // }
172}
173
174async function showBuildInfo() {
175 await updateBuildInfo(true);
176 console.log(JSON.stringify(getBuildInfo())); // eslint-disable-line no-console
177}
178
179/**
180 * Returns k/v pairs of server arguments which are _not_ the defaults.
181 * @param {Args} parsedArgs
182 * @returns {Args}
183 */
184function getNonDefaultServerArgs(parsedArgs) {
185 /**
186 * Flattens parsed args into a single level object for comparison with
187 * flattened defaults across server args and extension args.
188 * @param {Args} args
189 * @returns {Record<string, { value: any, argSpec: ArgSpec }>}
190 */
191 const flatten = (args) => {
192 const argSpecs = getAllArgSpecs();
193 const flattened = _.reduce(
194 [...argSpecs.values()],
195 (acc, argSpec) => {
196 if (_.has(args, argSpec.dest)) {
197 acc[argSpec.dest] = {value: _.get(args, argSpec.dest), argSpec};
198 }
199 return acc;
200 },
201 /** @type {Record<string, { value: any, argSpec: ArgSpec }>} */ ({})
202 );
203
204 return flattened;
205 };
206
207 const args = flatten(parsedArgs);
208
209 // hopefully these function names are descriptive enough
210 const typesDiffer = /** @param {string} dest */ (dest) =>
211 typeof args[dest].value !== typeof defaultsFromSchema[dest];
212
213 const defaultValueIsArray = /** @param {string} dest */ (dest) =>
214 _.isArray(defaultsFromSchema[dest]);
215
216 const argsValueIsArray = /** @param {string} dest */ (dest) => _.isArray(args[dest].value);
217
218 const arraysDiffer = /** @param {string} dest */ (dest) =>
219 _.gt(_.size(_.difference(args[dest].value, defaultsFromSchema[dest])), 0);
220
221 const valuesDiffer = /** @param {string} dest */ (dest) =>
222 args[dest].value !== defaultsFromSchema[dest];
223
224 const defaultIsDefined = /** @param {string} dest */ (dest) =>
225 !_.isUndefined(defaultsFromSchema[dest]);
226
227 // note that `_.overEvery` is like an "AND", and `_.overSome` is like an "OR"
228
229 const argValueNotArrayOrArraysDiffer = _.overSome([_.negate(argsValueIsArray), arraysDiffer]);
230
231 const defaultValueNotArrayAndValuesDiffer = _.overEvery([
232 _.negate(defaultValueIsArray),
233 valuesDiffer,
234 ]);
235
236 /**
237 * This used to be a hideous conditional, but it's broken up into a hideous function instead.
238 * hopefully this makes things a little more understandable.
239 * - checks if the default value is defined
240 * - if so, and the default is not an array:
241 * - ensures the types are the same
242 * - ensures the values are equal
243 * - if so, and the default is an array:
244 * - ensures the args value is an array
245 * - ensures the args values do not differ from the default values
246 * @type {(dest: string) => boolean}
247 */
248 const isNotDefault = _.overEvery([
249 defaultIsDefined,
250 _.overSome([
251 typesDiffer,
252 _.overEvery([defaultValueIsArray, argValueNotArrayOrArraysDiffer]),
253 defaultValueNotArrayAndValuesDiffer,
254 ]),
255 ]);
256
257 const defaultsFromSchema = getDefaultsForSchema(true);
258
259 return _.reduce(
260 _.pickBy(args, (__, key) => isNotDefault(key)),
261 // explodes the flattened object back into nested one
262 (acc, {value, argSpec}) => _.set(acc, argSpec.dest, value),
263 /** @type {Args} */ ({})
264 );
265}
266
267/**
268 * Compacts an object for {@link showConfig}:
269 * 1. Removes `subcommand` key/value
270 * 2. Removes `undefined` values
271 * 3. Removes empty objects (but not `false` values)
272 * Does not operate recursively.
273 */
274const compactConfig = _.partial(
275 _.omitBy,
276 _,
277 (value, key) =>
278 key === 'subcommand' || _.isUndefined(value) || (_.isObject(value) && _.isEmpty(value))
279);
280
281/**
282 * Shows a breakdown of the current config after CLI params, config file loaded & defaults applied.
283 *
284 * The actual shape of `preConfigParsedArgs` and `defaults` does not matter for the purposes of this function,
285 * but it's intended to be called with values of type {@link ParsedArgs} and `DefaultValues<true>`, respectively.
286 *
287 * @param {Partial<ParsedArgs>} nonDefaultPreConfigParsedArgs - Parsed CLI args (or param to `init()`) before config & defaults applied
288 * @param {import('./config-file').ReadConfigFileResult} configResult - Result of attempting to load a config file. _Must_ be normalized
289 * @param {Partial<ParsedArgs>} defaults - Configuration defaults from schemas
290 * @param {ParsedArgs} parsedArgs - Entire parsed args object
291 */
292function showConfig(nonDefaultPreConfigParsedArgs, configResult, defaults, parsedArgs) {
293 console.log('Appium Configuration\n');
294 console.log('from defaults:\n');
295 console.dir(compactConfig(defaults));
296 if (configResult.config) {
297 console.log(`\nfrom config file at ${configResult.filepath}:\n`);
298 console.dir(compactConfig(configResult.config));
299 } else {
300 console.log(`\n(no configuration file loaded)`);
301 }
302 const compactedNonDefaultPreConfigArgs = compactConfig(nonDefaultPreConfigParsedArgs);
303 if (_.isEmpty(compactedNonDefaultPreConfigArgs)) {
304 console.log(`\n(no CLI parameters provided)`);
305 } else {
306 console.log('\nvia CLI or function call:\n');
307 console.dir(compactedNonDefaultPreConfigArgs);
308 }
309 console.log('\nfinal configuration:\n');
310 console.dir(compactConfig(parsedArgs));
311}
312
313/**
314 * @param {string} tmpDir
315 */
316async function validateTmpDir(tmpDir) {
317 try {
318 await fs.mkdirp(tmpDir);
319 } catch (e) {
320 throw new Error(
321 `We could not ensure that the temp dir you specified ` +
322 `(${tmpDir}) exists. Please make sure it's writeable.`
323 );
324 }
325}
326
327const rootDir = fs.findRoot(__dirname);
328
329export {
330 getBuildInfo,
331 checkNodeOk,
332 showBuildInfo,
333 warnNodeDeprecations,
334 validateTmpDir,
335 getNonDefaultServerArgs,
336 getGitRev,
337 APPIUM_VER,
338 updateBuildInfo,
339 showConfig,
340 rootDir,
341};
342
343/**
344 * @typedef {import('appium/types').ParsedArgs} ParsedArgs
345 * @typedef {import('appium/types').Args} Args
346 * @typedef {import('./schema/arg-spec').ArgSpec} ArgSpec
347 */