360 lines
11 KiB
JavaScript
360 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const _ = require('lodash');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const chalk = require('chalk');
|
|
const debug = require('debug')('cypress:cli');
|
|
const {
|
|
Listr
|
|
} = require('listr2');
|
|
const Promise = require('bluebird');
|
|
const logSymbols = require('log-symbols');
|
|
const {
|
|
stripIndent
|
|
} = require('common-tags');
|
|
const fs = require('../fs');
|
|
const download = require('./download');
|
|
const util = require('../util');
|
|
const state = require('./state');
|
|
const unzip = require('./unzip');
|
|
const logger = require('../logger');
|
|
const {
|
|
throwFormErrorText,
|
|
errors
|
|
} = require('../errors');
|
|
const verbose = require('../VerboseRenderer');
|
|
const {
|
|
buildInfo,
|
|
version
|
|
} = require('../../package.json');
|
|
function _getBinaryUrlFromBuildInfo(arch, {
|
|
commitSha,
|
|
commitBranch
|
|
}) {
|
|
return `https://cdn.cypress.io/beta/binary/${version}/${os.platform()}-${arch}/${commitBranch}-${commitSha}/cypress.zip`;
|
|
}
|
|
const alreadyInstalledMsg = () => {
|
|
if (!util.isPostInstall()) {
|
|
logger.log(stripIndent`
|
|
Skipping installation:
|
|
|
|
Pass the ${chalk.yellow('--force')} option if you'd like to reinstall anyway.
|
|
`);
|
|
}
|
|
};
|
|
const displayCompletionMsg = () => {
|
|
// check here to see if we are globally installed
|
|
if (util.isInstalledGlobally()) {
|
|
// if we are display a warning
|
|
logger.log();
|
|
logger.warn(stripIndent`
|
|
${logSymbols.warning} Warning: It looks like you\'ve installed Cypress globally.
|
|
|
|
This will work, but it'\s not recommended.
|
|
|
|
The recommended way to install Cypress is as a devDependency per project.
|
|
|
|
You should probably run these commands:
|
|
|
|
- ${chalk.cyan('npm uninstall -g cypress')}
|
|
- ${chalk.cyan('npm install --save-dev cypress')}
|
|
`);
|
|
return;
|
|
}
|
|
logger.log();
|
|
logger.log('You can now open Cypress by running:', chalk.cyan(path.join('node_modules', '.bin', 'cypress'), 'open'));
|
|
logger.log();
|
|
logger.log(chalk.grey('https://on.cypress.io/installing-cypress'));
|
|
logger.log();
|
|
};
|
|
const downloadAndUnzip = ({
|
|
version,
|
|
installDir,
|
|
downloadDir
|
|
}) => {
|
|
const progress = {
|
|
throttle: 100,
|
|
onProgress: null
|
|
};
|
|
const downloadDestination = path.join(downloadDir, `cypress-${process.pid}.zip`);
|
|
const rendererOptions = getRendererOptions();
|
|
|
|
// let the user know what version of cypress we're downloading!
|
|
logger.log(`Installing Cypress ${chalk.gray(`(version: ${version})`)}`);
|
|
logger.log();
|
|
const tasks = new Listr([{
|
|
options: {
|
|
title: util.titleize('Downloading Cypress')
|
|
},
|
|
task: (ctx, task) => {
|
|
// as our download progresses indicate the status
|
|
progress.onProgress = progessify(task, 'Downloading Cypress');
|
|
return download.start({
|
|
version,
|
|
downloadDestination,
|
|
progress
|
|
}).then(redirectVersion => {
|
|
if (redirectVersion) version = redirectVersion;
|
|
debug(`finished downloading file: ${downloadDestination}`);
|
|
}).then(() => {
|
|
// save the download destination for unzipping
|
|
util.setTaskTitle(task, util.titleize(chalk.green('Downloaded Cypress')), rendererOptions.renderer);
|
|
});
|
|
}
|
|
}, unzipTask({
|
|
progress,
|
|
zipFilePath: downloadDestination,
|
|
installDir,
|
|
rendererOptions
|
|
}), {
|
|
options: {
|
|
title: util.titleize('Finishing Installation')
|
|
},
|
|
task: (ctx, task) => {
|
|
const cleanup = () => {
|
|
debug('removing zip file %s', downloadDestination);
|
|
return fs.removeAsync(downloadDestination);
|
|
};
|
|
return cleanup().then(() => {
|
|
debug('finished installation in', installDir);
|
|
util.setTaskTitle(task, util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir)), rendererOptions.renderer);
|
|
});
|
|
}
|
|
}], {
|
|
rendererOptions
|
|
});
|
|
|
|
// start the tasks!
|
|
return Promise.resolve(tasks.run());
|
|
};
|
|
const validateOS = () => {
|
|
return util.getPlatformInfo().then(platformInfo => {
|
|
return platformInfo.match(/(win32-x64|linux-x64|linux-arm64|darwin-x64|darwin-arm64)/);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Returns the version to install - either a string like `1.2.3` to be fetched
|
|
* from the download server or a file path or HTTP URL.
|
|
*/
|
|
function getVersionOverride({
|
|
arch,
|
|
envVarVersion,
|
|
buildInfo
|
|
}) {
|
|
// let this environment variable reset the binary version we need
|
|
if (envVarVersion) {
|
|
return envVarVersion;
|
|
}
|
|
if (buildInfo && !buildInfo.stable) {
|
|
logger.log(chalk.yellow(stripIndent`
|
|
${logSymbols.warning} Warning: You are installing a pre-release build of Cypress.
|
|
|
|
Bugs may be present which do not exist in production builds.
|
|
|
|
This build was created from:
|
|
* Commit SHA: ${buildInfo.commitSha}
|
|
* Commit Branch: ${buildInfo.commitBranch}
|
|
* Commit Timestamp: ${buildInfo.commitDate}
|
|
`));
|
|
logger.log();
|
|
return _getBinaryUrlFromBuildInfo(arch, buildInfo);
|
|
}
|
|
}
|
|
function getEnvVarVersion() {
|
|
if (!util.getEnv('CYPRESS_INSTALL_BINARY')) return;
|
|
|
|
// because passed file paths are often double quoted
|
|
// and might have extra whitespace around, be robust and trim the string
|
|
const trimAndRemoveDoubleQuotes = true;
|
|
const envVarVersion = util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes);
|
|
debug('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion);
|
|
return envVarVersion;
|
|
}
|
|
const start = async (options = {}) => {
|
|
debug('installing with options %j', options);
|
|
const envVarVersion = getEnvVarVersion();
|
|
if (envVarVersion === '0') {
|
|
debug('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install');
|
|
logger.log(stripIndent`
|
|
${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`);
|
|
logger.log();
|
|
return;
|
|
}
|
|
_.defaults(options, {
|
|
force: false,
|
|
buildInfo
|
|
});
|
|
if (util.getEnv('CYPRESS_CACHE_FOLDER')) {
|
|
const envCache = util.getEnv('CYPRESS_CACHE_FOLDER');
|
|
logger.log(stripIndent`
|
|
${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)}
|
|
|
|
Previous installs of Cypress may not be found.
|
|
`);
|
|
logger.log();
|
|
}
|
|
const pkgVersion = util.pkgVersion();
|
|
const arch = await util.getRealArch();
|
|
const versionOverride = getVersionOverride({
|
|
arch,
|
|
envVarVersion,
|
|
buildInfo: options.buildInfo
|
|
});
|
|
const versionToInstall = versionOverride || pkgVersion;
|
|
debug('version in package.json is %s, version to install is %s', pkgVersion, versionToInstall);
|
|
const installDir = state.getVersionDir(pkgVersion, options.buildInfo);
|
|
const cacheDir = state.getCacheDir();
|
|
const binaryDir = state.getBinaryDir(pkgVersion);
|
|
if (!(await validateOS())) {
|
|
return throwFormErrorText(errors.invalidOS)();
|
|
}
|
|
await fs.ensureDirAsync(cacheDir).catch({
|
|
code: 'EACCES'
|
|
}, err => {
|
|
return throwFormErrorText(errors.invalidCacheDirectory)(stripIndent`
|
|
Failed to access ${chalk.cyan(cacheDir)}:
|
|
|
|
${err.message}
|
|
`);
|
|
});
|
|
const binaryPkg = await state.getBinaryPkgAsync(binaryDir);
|
|
const binaryVersion = await state.getBinaryPkgVersion(binaryPkg);
|
|
const shouldInstall = () => {
|
|
if (!binaryVersion) {
|
|
debug('no binary installed under cli version');
|
|
return true;
|
|
}
|
|
logger.log();
|
|
logger.log(stripIndent`
|
|
Cypress ${chalk.green(binaryVersion)} is installed in ${chalk.cyan(installDir)}
|
|
`);
|
|
logger.log();
|
|
if (options.force) {
|
|
debug('performing force install over existing binary');
|
|
return true;
|
|
}
|
|
if (binaryVersion === versionToInstall || !util.isSemver(versionToInstall)) {
|
|
// our version matches, tell the user this is a noop
|
|
alreadyInstalledMsg();
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// noop if we've been told not to download
|
|
if (!shouldInstall()) {
|
|
return debug('Not downloading or installing binary');
|
|
}
|
|
if (envVarVersion) {
|
|
logger.log(chalk.yellow(stripIndent`
|
|
${logSymbols.warning} Warning: Forcing a binary version different than the default.
|
|
|
|
The CLI expected to install version: ${chalk.green(pkgVersion)}
|
|
|
|
Instead we will install version: ${chalk.green(versionToInstall)}
|
|
|
|
These versions may not work properly together.
|
|
`));
|
|
logger.log();
|
|
}
|
|
const getLocalFilePath = async () => {
|
|
// see if version supplied is a path to a binary
|
|
if (await fs.pathExistsAsync(versionToInstall)) {
|
|
return path.extname(versionToInstall) === '.zip' ? versionToInstall : false;
|
|
}
|
|
const possibleFile = util.formAbsolutePath(versionToInstall);
|
|
debug('checking local file', possibleFile, 'cwd', process.cwd());
|
|
|
|
// if this exists return the path to it
|
|
// else false
|
|
if ((await fs.pathExistsAsync(possibleFile)) && path.extname(possibleFile) === '.zip') {
|
|
return possibleFile;
|
|
}
|
|
return false;
|
|
};
|
|
const pathToLocalFile = await getLocalFilePath();
|
|
if (pathToLocalFile) {
|
|
const absolutePath = path.resolve(versionToInstall);
|
|
debug('found local file at', absolutePath);
|
|
debug('skipping download');
|
|
const rendererOptions = getRendererOptions();
|
|
return new Listr([unzipTask({
|
|
progress: {
|
|
throttle: 100,
|
|
onProgress: null
|
|
},
|
|
zipFilePath: absolutePath,
|
|
installDir,
|
|
rendererOptions
|
|
})], {
|
|
rendererOptions
|
|
}).run();
|
|
}
|
|
if (options.force) {
|
|
debug('Cypress already installed at', installDir);
|
|
debug('but the installation was forced');
|
|
}
|
|
debug('preparing to download and unzip version ', versionToInstall, 'to path', installDir);
|
|
const downloadDir = os.tmpdir();
|
|
await downloadAndUnzip({
|
|
version: versionToInstall,
|
|
installDir,
|
|
downloadDir
|
|
});
|
|
|
|
// delay 1 sec for UX, unless we are testing
|
|
await Promise.delay(1000);
|
|
displayCompletionMsg();
|
|
};
|
|
module.exports = {
|
|
start,
|
|
_getBinaryUrlFromBuildInfo
|
|
};
|
|
const unzipTask = ({
|
|
zipFilePath,
|
|
installDir,
|
|
progress,
|
|
rendererOptions
|
|
}) => {
|
|
return {
|
|
options: {
|
|
title: util.titleize('Unzipping Cypress')
|
|
},
|
|
task: (ctx, task) => {
|
|
// as our unzip progresses indicate the status
|
|
progress.onProgress = progessify(task, 'Unzipping Cypress');
|
|
return unzip.start({
|
|
zipFilePath,
|
|
installDir,
|
|
progress
|
|
}).then(() => {
|
|
util.setTaskTitle(task, util.titleize(chalk.green('Unzipped Cypress')), rendererOptions.renderer);
|
|
});
|
|
}
|
|
};
|
|
};
|
|
const progessify = (task, title) => {
|
|
// return higher order function
|
|
return (percentComplete, remaining) => {
|
|
percentComplete = chalk.white(` ${percentComplete}%`);
|
|
|
|
// pluralize seconds remaining
|
|
remaining = chalk.gray(`${remaining}s`);
|
|
util.setTaskTitle(task, util.titleize(title, percentComplete, remaining), getRendererOptions().renderer);
|
|
};
|
|
};
|
|
|
|
// if we are running in CI then use
|
|
// the verbose renderer else use
|
|
// the default
|
|
const getRendererOptions = () => {
|
|
let renderer = util.isCi() ? verbose : 'default';
|
|
if (logger.logLevel() === 'silent') {
|
|
renderer = 'silent';
|
|
}
|
|
return {
|
|
renderer
|
|
};
|
|
}; |