@fireback/cli
Version:
Fireback backend and code gen tools for javascript world
362 lines (313 loc) • 10.7 kB
JavaScript
;
const fs = require("fs");
const os = require("os");
const path = require("path");
const crypto = require("crypto");
const zlib = require("zlib");
const stream = require("stream");
const process = require("process");
const fetch = require("node-fetch");
const HttpsProxyAgent = require("https-proxy-agent");
const ProgressBar = require("progress");
const Proxy = require("proxy-from-env");
const which = require("which");
const helper = require("../js/helper");
const pkgInfo = require("../package.json");
const Logger = require("../js/logger");
const logger = new Logger(getLogStream("stderr"));
const CDN_URL =
process.env.SENTRYCLI_LOCAL_CDNURL ||
process.env.npm_config_sentrycli_cdnurl ||
process.env.SENTRYCLI_CDNURL ||
"https://downloads.sentry-cdn.com/sentry-cli";
function getLogStream(defaultStream) {
const logStream = process.env.SENTRYCLI_LOG_STREAM || defaultStream;
if (logStream === "stdout") {
return process.stdout;
}
if (logStream === "stderr") {
return process.stderr;
}
throw new Error(
`Incorrect SENTRYCLI_LOG_STREAM env variable. Possible values: 'stdout' | 'stderr'`
);
}
function shouldRenderProgressBar() {
const silentFlag = process.argv.some((v) => v === "--silent");
const silentConfig = process.env.npm_config_loglevel === "silent";
const silentEnv = process.env.SENTRYCLI_NO_PROGRESS_BAR;
const ciEnv = process.env.CI === "true" || process.env.CI === "1";
const notTTY = !process.stdout.isTTY;
// If any of possible options is set, skip rendering of progress bar
return !(silentFlag || silentConfig || silentEnv || ciEnv || notTTY);
}
function getDownloadUrl(platform, arch) {
const releasesUrl = `${CDN_URL}/${pkgInfo.version}/sentry-cli`;
let archString = "";
switch (arch) {
case "x64":
archString = "x86_64";
break;
case "x86":
case "ia32":
archString = "i686";
break;
case "arm64":
archString = "aarch64";
break;
case "arm":
archString = "armv7";
break;
default:
archString = arch;
}
switch (platform) {
case "darwin":
return `${releasesUrl}-Darwin-universal`;
case "win32":
// Windows arm machines can run x64 binaries
if (arch === "arm64") {
archString = "x86_64";
}
return `${releasesUrl}-Windows-${archString}.exe`;
case "linux":
case "freebsd":
return `${releasesUrl}-Linux-${archString}`;
default:
return null;
}
}
function createProgressBar(name, total) {
const incorrectTotal = typeof total !== "number" || Number.isNaN(total);
if (incorrectTotal || !shouldRenderProgressBar()) {
return {
tick: () => {},
};
}
const logStream = getLogStream("stdout");
if (logStream.isTTY) {
return new ProgressBar(`fetching ${name} :bar :percent :etas`, {
complete: "█",
incomplete: "░",
width: 20,
total,
});
}
let pct = null;
let current = 0;
return {
tick: (length) => {
current += length;
const next = Math.round((current / total) * 100);
if (next > pct) {
pct = next;
logStream.write(`fetching ${name} ${pct}%\n`);
}
},
};
}
function npmCache() {
const keys = [
"npm_config_cache",
"npm_config_cache_folder",
"npm_config_yarn_offline_mirror",
];
for (let key of [...keys, ...keys.map((k) => k.toUpperCase())]) {
if (process.env[key]) return process.env[key];
}
if (process.env.APPDATA) {
return path.join(process.env.APPDATA, "npm-cache");
}
return path.join(os.homedir(), ".npm");
}
function getCachedPath(url) {
const digest = crypto.createHash("md5").update(url).digest("hex").slice(0, 6);
return path.join(
npmCache(),
"sentry-cli",
`${digest}-${path.basename(url).replace(/[^a-zA-Z0-9.]+/g, "-")}`
);
}
function getTempFile(cached) {
return `${cached}.${process.pid}-${Math.random().toString(16).slice(2)}.tmp`;
}
function validateChecksum(tempPath, name) {
let storedHash;
try {
const checksums = fs.readFileSync(
path.join(__dirname, "../checksums.txt"),
"utf8"
);
const entries = checksums.split("\n");
for (let i = 0; i < entries.length; i++) {
const [key, value] = entries[i].split("=");
if (key === name) {
storedHash = value;
break;
}
}
} catch (e) {
logger.log(
"Checksums are generated when the package is published to npm. They are not available directly in the source repository. Skipping validation."
);
return;
}
if (!storedHash) {
logger.log(`Checksum for ${name} not found, skipping validation.`);
return;
}
const currentHash = crypto
.createHash("sha256")
.update(fs.readFileSync(tempPath))
.digest("hex");
if (storedHash !== currentHash) {
fs.unlinkSync(tempPath);
throw new Error(
`Checksum validation for ${name} failed.\nExpected: ${storedHash}\nReceived: ${currentHash}`
);
} else {
logger.log("Checksum validation passed.");
}
}
async function downloadBinary() {
const arch = os.arch();
const platform = os.platform();
const outputPath = helper.getFallbackBinaryPath();
if (process.env.SENTRYCLI_USE_LOCAL === "1") {
try {
const binPaths = which.sync("sentry-cli", { all: true });
if (!binPaths.length) throw new Error("Binary not found");
const binPath = binPaths[binPaths.length - 1];
logger.log(`Using local binary: ${binPath}`);
fs.copyFileSync(binPath, outputPath);
return Promise.resolve();
} catch (e) {
throw new Error(
"Configured installation of local binary, but it was not found." +
"Make sure that `sentry-cli` executable is available in your $PATH or disable SENTRYCLI_USE_LOCAL env variable."
);
}
}
const downloadUrl = getDownloadUrl(platform, arch);
if (!downloadUrl) {
throw new Error(`Unsupported target ${platform}-${arch}`);
}
const cachedPath = getCachedPath(downloadUrl);
if (fs.existsSync(cachedPath)) {
logger.log(`Using cached binary: ${cachedPath}`);
fs.copyFileSync(cachedPath, outputPath);
return;
}
const proxyUrl = Proxy.getProxyForUrl(downloadUrl);
const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : null;
logger.log(`Downloading from ${downloadUrl}`);
if (proxyUrl) {
logger.log(`Using proxy URL: ${proxyUrl}`);
}
let response;
try {
response = await fetch(downloadUrl, {
agent,
compress: false,
headers: {
"accept-encoding": "gzip, deflate, br",
},
redirect: "follow",
});
} catch (error) {
let errorMsg = `Unable to download sentry-cli binary from ${downloadUrl}.\nError message: ${error.message}`;
if (error.code) {
errorMsg += `\nError code: ${error.code}`;
}
throw new Error(errorMsg);
}
if (!response.ok) {
let errorMsg = `Unable to download sentry-cli binary from ${downloadUrl}.\nServer returned: ${response.status}`;
if (response.statusText) {
errorMsg += ` - ${response.statusText}`;
}
throw new Error(errorMsg);
}
const contentEncoding = response.headers.get("content-encoding");
let decompressor;
if (/\bgzip\b/.test(contentEncoding)) {
decompressor = zlib.createGunzip();
} else if (/\bdeflate\b/.test(contentEncoding)) {
decompressor = zlib.createInflate();
} else if (/\bbr\b/.test(contentEncoding)) {
decompressor = zlib.createBrotliDecompress();
} else {
decompressor = new stream.PassThrough();
}
const name = downloadUrl.match(/.*\/(.*?)$/)[1];
let downloadedBytes = 0;
const totalBytes = parseInt(response.headers.get("content-length"), 10);
const progressBar = createProgressBar(name, totalBytes);
const tempPath = getTempFile(cachedPath);
fs.mkdirSync(path.dirname(tempPath), { recursive: true });
await new Promise((resolve, reject) => {
response.body
.on("error", (e) => reject(e))
.on("data", (chunk) => {
downloadedBytes += chunk.length;
progressBar.tick(chunk.length);
})
.pipe(decompressor)
.pipe(fs.createWriteStream(tempPath, { mode: "0755" }))
.on("error", (e) => reject(e))
.on("close", () => {
if (downloadedBytes >= totalBytes) {
resolve();
} else {
reject(new Error("connection interrupted"));
}
});
});
if (process.env.SENTRYCLI_SKIP_CHECKSUM_VALIDATION !== "1") {
validateChecksum(tempPath, name);
}
fs.copyFileSync(tempPath, cachedPath);
fs.copyFileSync(tempPath, outputPath);
fs.unlinkSync(tempPath);
}
async function checkVersion() {
const output = await helper.execute(["--version"]);
const version = output.replace("sentry-cli ", "").trim();
const expected = pkgInfo.version;
if (version !== expected) {
throw new Error(
`Unexpected sentry-cli version "${version}", expected "${expected}"`
);
}
}
if (process.env.SENTRYCLI_SKIP_DOWNLOAD === "1") {
logger.log(`Skipping download because SENTRYCLI_SKIP_DOWNLOAD=1 detected.`);
process.exit(0);
}
const { packageName: distributionPackageName, subpath: distributionSubpath } =
helper.getDistributionForThisPlatform();
if (distributionPackageName === undefined) {
helper.throwUnsupportedPlatformError();
}
try {
require.resolve(`${distributionPackageName}/${distributionSubpath}`);
// If the `resolve` call succeeds it means a binary was installed successfully via optional dependencies so we can skip the manual postinstall download.
process.exit(0);
} catch (e) {
// Optional dependencies likely didn't get installed - proceed with fallback downloading manually
// Log message inspired by esbuild: https://github.com/evanw/esbuild/blob/914f6080c77cfe32a54888caa51ca6ea13873ce9/lib/npm/node-install.ts#L253
logger.log(
`Sentry CLI failed to locate the "${distributionPackageName}" package after installation!
This can happen if you use an option to disable optional dependencies during installation, like "--no-optional", "--ignore-optional", or "--omit=optional". Sentry CLI uses the "optionalDependencies" package.json feature to install the correct binary for your platform and operating system. This post-install script will now try to work around this by manually downloading the Sentry CLI binary from the Sentry CDN. If this fails, you need to remove the "--no-optional", "--ignore-optional", and "--omit=optional" flags for Sentry CLI to work.`
);
downloadBinary()
.then(() => checkVersion())
.then(() => {
process.exit(0);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
}