install.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. 'use strict';
  2. const fs = require('fs');
  3. const http = require('http');
  4. const os = require('os');
  5. const path = require('path');
  6. const crypto = require('crypto');
  7. const zlib = require('zlib');
  8. const stream = require('stream');
  9. const process = require('process');
  10. const HttpsProxyAgent = require('https-proxy-agent');
  11. const fetch = require('node-fetch');
  12. const ProgressBar = require('progress');
  13. const Proxy = require('proxy-from-env');
  14. // NOTE: Can be dropped in favor of `fs.mkdirSync(path, { recursive: true })` once we stop supporting Node 8.x
  15. const mkdirp = require('mkdirp');
  16. const which = require('which');
  17. const helper = require('./helper');
  18. const pkgInfo = require('../package.json');
  19. const Logger = require('./logger');
  20. function getLogStream(defaultStream) {
  21. const logStream = process.env.SENTRYCLI_LOG_STREAM || defaultStream;
  22. if (logStream === 'stdout') {
  23. return process.stdout;
  24. }
  25. if (logStream === 'stderr') {
  26. return process.stderr;
  27. }
  28. throw new Error(
  29. `Incorrect SENTRYCLI_LOG_STREAM env variable. Possible values: 'stdout' | 'stderr'`
  30. );
  31. }
  32. const ttyLogger = new Logger(getLogStream('stderr'));
  33. const CDN_URL =
  34. process.env.SENTRYCLI_LOCAL_CDNURL ||
  35. process.env.npm_config_sentrycli_cdnurl ||
  36. process.env.SENTRYCLI_CDNURL ||
  37. 'https://downloads.sentry-cdn.com/sentry-cli';
  38. function shouldRenderProgressBar() {
  39. const silentFlag = process.argv.some((v) => v === '--silent');
  40. const silentConfig = process.env.npm_config_loglevel === 'silent';
  41. // Leave `SENTRY_NO_PROGRESS_BAR` for backwards compatibility
  42. const silentEnv = process.env.SENTRYCLI_NO_PROGRESS_BAR || process.env.SENTRY_NO_PROGRESS_BAR;
  43. const ciEnv = process.env.CI === 'true';
  44. // If any of possible options is set, skip rendering of progress bar
  45. return !(silentFlag || silentConfig || silentEnv || ciEnv);
  46. }
  47. function getDownloadUrl(platform, arch) {
  48. const releasesUrl = `${CDN_URL}/${pkgInfo.version}/sentry-cli`;
  49. let archString = '';
  50. switch (arch) {
  51. case 'x64':
  52. archString = 'x86_64';
  53. break;
  54. case 'x86':
  55. case 'ia32':
  56. archString = 'i686';
  57. break;
  58. case 'arm64':
  59. archString = 'aarch64';
  60. break;
  61. case 'arm':
  62. archString = 'armv7';
  63. break;
  64. default:
  65. archString = arch;
  66. }
  67. switch (platform) {
  68. case 'darwin':
  69. return `${releasesUrl}-Darwin-universal`;
  70. case 'win32':
  71. return `${releasesUrl}-Windows-${archString}.exe`;
  72. case 'linux':
  73. case 'freebsd':
  74. return `${releasesUrl}-Linux-${archString}`;
  75. default:
  76. return null;
  77. }
  78. }
  79. function createProgressBar(name, total) {
  80. const incorrectTotal = typeof total !== 'number' || Number.isNaN(total);
  81. if (incorrectTotal || !shouldRenderProgressBar()) {
  82. return {
  83. tick: () => {},
  84. };
  85. }
  86. const logStream = getLogStream('stdout');
  87. if (logStream.isTTY) {
  88. return new ProgressBar(`fetching ${name} :bar :percent :etas`, {
  89. complete: '█',
  90. incomplete: '░',
  91. width: 20,
  92. total,
  93. });
  94. }
  95. let pct = null;
  96. let current = 0;
  97. return {
  98. tick: (length) => {
  99. current += length;
  100. const next = Math.round((current / total) * 100);
  101. if (next > pct) {
  102. pct = next;
  103. logStream.write(`fetching ${name} ${pct}%\n`);
  104. }
  105. },
  106. };
  107. }
  108. function npmCache() {
  109. const env = process.env;
  110. return (
  111. env.npm_config_cache ||
  112. env.npm_config_cache_folder ||
  113. env.npm_config_yarn_offline_mirror ||
  114. (env.APPDATA ? path.join(env.APPDATA, 'npm-cache') : path.join(os.homedir(), '.npm'))
  115. );
  116. }
  117. function getCachedPath(url) {
  118. const digest = crypto.createHash('md5').update(url).digest('hex').slice(0, 6);
  119. return path.join(
  120. npmCache(),
  121. 'sentry-cli',
  122. `${digest}-${path.basename(url).replace(/[^a-zA-Z0-9.]+/g, '-')}`
  123. );
  124. }
  125. function getTempFile(cached) {
  126. return `${cached}.${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
  127. }
  128. function validateChecksum(tempPath, name, logger) {
  129. let storedHash;
  130. try {
  131. const checksums = fs.readFileSync(path.join(__dirname, '../checksums.txt'), 'utf8');
  132. const entries = checksums.split('\n');
  133. for (let i = 0; i < entries.length; i++) {
  134. const [key, value] = entries[i].split('=');
  135. if (key === name) {
  136. storedHash = value;
  137. break;
  138. }
  139. }
  140. } catch (e) {
  141. logger.log(
  142. 'Checksums are generated when the package is published to npm. They are not available directly in the source repository. Skipping validation.'
  143. );
  144. return;
  145. }
  146. if (!storedHash) {
  147. logger.log(`Checksum for ${name} not found, skipping validation.`);
  148. return;
  149. }
  150. const currentHash = crypto.createHash('sha256').update(fs.readFileSync(tempPath)).digest('hex');
  151. if (storedHash !== currentHash) {
  152. fs.unlinkSync(tempPath);
  153. throw new Error(
  154. `Checksum validation for ${name} failed.\nExpected: ${storedHash}\nReceived: ${currentHash}`
  155. );
  156. } else {
  157. logger.log('Checksum validation passed.');
  158. }
  159. }
  160. function checkVersion() {
  161. return helper.execute(['--version']).then((output) => {
  162. const version = output.replace('sentry-cli ', '').trim();
  163. const expected = process.env.SENTRYCLI_LOCAL_CDNURL ? 'DEV' : pkgInfo.version;
  164. if (version !== expected) {
  165. throw new Error(`Unexpected sentry-cli version "${version}", expected "${expected}"`);
  166. }
  167. });
  168. }
  169. function downloadBinary(logger = ttyLogger) {
  170. if (process.env.SENTRYCLI_SKIP_DOWNLOAD === '1') {
  171. logger.log(`Skipping download because SENTRYCLI_SKIP_DOWNLOAD=1 detected.`);
  172. return Promise.resolve();
  173. }
  174. const arch = os.arch();
  175. const platform = os.platform();
  176. const outputPath = helper.getPath();
  177. if (process.env.SENTRYCLI_USE_LOCAL === '1') {
  178. try {
  179. const binPath = which.sync('sentry-cli');
  180. logger.log(`Using local binary: ${binPath}`);
  181. fs.copyFileSync(binPath, outputPath);
  182. return Promise.resolve();
  183. } catch (e) {
  184. throw new Error(
  185. 'Configured installation of local binary, but it was not found.' +
  186. 'Make sure that `sentry-cli` executable is available in your $PATH or disable SENTRYCLI_USE_LOCAL env variable.'
  187. );
  188. }
  189. }
  190. const downloadUrl = getDownloadUrl(platform, arch);
  191. if (!downloadUrl) {
  192. return Promise.reject(new Error(`Unsupported target ${platform}-${arch}`));
  193. }
  194. const cachedPath = getCachedPath(downloadUrl);
  195. if (fs.existsSync(cachedPath)) {
  196. logger.log(`Using cached binary: ${cachedPath}`);
  197. fs.copyFileSync(cachedPath, outputPath);
  198. return Promise.resolve();
  199. }
  200. const proxyUrl = Proxy.getProxyForUrl(downloadUrl);
  201. const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : null;
  202. logger.log(`Downloading from ${downloadUrl}`);
  203. if (proxyUrl) {
  204. logger.log(`Using proxy URL: ${proxyUrl}`);
  205. }
  206. return fetch(downloadUrl, {
  207. agent,
  208. compress: false,
  209. headers: {
  210. 'accept-encoding': 'gzip, deflate, br',
  211. },
  212. redirect: 'follow',
  213. })
  214. .then((response) => {
  215. if (!response.ok) {
  216. throw new Error(
  217. `Unable to download sentry-cli binary from ${downloadUrl}.\nServer returned ${response.status}: ${response.statusText}.`
  218. );
  219. }
  220. const contentEncoding = response.headers.get('content-encoding');
  221. let decompressor;
  222. if (/\bgzip\b/.test(contentEncoding)) {
  223. decompressor = zlib.createGunzip();
  224. } else if (/\bdeflate\b/.test(contentEncoding)) {
  225. decompressor = zlib.createInflate();
  226. } else if (/\bbr\b/.test(contentEncoding)) {
  227. decompressor = zlib.createBrotliDecompress();
  228. } else {
  229. decompressor = new stream.PassThrough();
  230. }
  231. const name = downloadUrl.match(/.*\/(.*?)$/)[1];
  232. const total = parseInt(response.headers.get('content-length'), 10);
  233. const progressBar = createProgressBar(name, total);
  234. const tempPath = getTempFile(cachedPath);
  235. mkdirp.sync(path.dirname(tempPath));
  236. return new Promise((resolve, reject) => {
  237. response.body
  238. .on('error', (e) => reject(e))
  239. .on('data', (chunk) => progressBar.tick(chunk.length))
  240. .pipe(decompressor)
  241. .pipe(fs.createWriteStream(tempPath, { mode: '0755' }))
  242. .on('error', (e) => reject(e))
  243. .on('close', () => resolve());
  244. }).then(() => {
  245. if (process.env.SENTRYCLI_SKIP_CHECKSUM_VALIDATION !== '1') {
  246. validateChecksum(tempPath, name, logger);
  247. }
  248. fs.copyFileSync(tempPath, cachedPath);
  249. fs.copyFileSync(tempPath, outputPath);
  250. fs.unlinkSync(tempPath);
  251. });
  252. })
  253. .then(() => {
  254. return checkVersion();
  255. })
  256. .catch((error) => {
  257. if (error instanceof fetch.FetchError) {
  258. throw new Error(
  259. `Unable to download sentry-cli binary from ${downloadUrl}.\nError code: ${error.code}`
  260. );
  261. } else {
  262. throw error;
  263. }
  264. });
  265. }
  266. module.exports = {
  267. downloadBinary,
  268. };