@apollo/rover
Version:
The new Apollo CLI
384 lines (334 loc) • 10.8 kB
JavaScript
;
const libc = require("detect-libc");
const os = require("os");
const tar = require("tar");
const { Console } = require("node:console");
const { existsSync, mkdirSync, rmSync } = require("fs");
const { join } = require("path");
const { spawnSync } = require("child_process");
const { Readable } = require("stream");
const { ProxyAgent, fetch } = require("undici");
const error = (msg) => {
console.error(msg);
process.exit(1);
};
const { version } = require("./package.json");
const fs = require("fs");
const name = `rover`;
const supportedPlatforms = [
{
TYPE: "Windows_NT",
ARCHITECTURE: "x64",
RUST_TARGET: "x86_64-pc-windows-msvc",
BINARY_NAME: `${name}-${version}.exe`,
RAW_NAME: `${name}.exe`,
},
{
TYPE: "Linux",
ARCHITECTURE: "x64",
RUST_TARGET: "x86_64-unknown-linux-gnu",
BINARY_NAME: `${name}-${version}`,
RAW_NAME: `${name}`,
},
{
TYPE: "Linux",
ARCHITECTURE: "arm64",
RUST_TARGET: "aarch64-unknown-linux-gnu",
BINARY_NAME: `${name}-${version}`,
RAW_NAME: `${name}`,
},
{
TYPE: "Darwin",
ARCHITECTURE: "x64",
RUST_TARGET: "x86_64-apple-darwin",
BINARY_NAME: `${name}-${version}`,
RAW_NAME: `${name}`,
},
{
TYPE: "Darwin",
ARCHITECTURE: "arm64",
RUST_TARGET: "aarch64-apple-darwin",
BINARY_NAME: `${name}-${version}`,
RAW_NAME: `${name}`,
},
];
const getPlatform = (type = os.type(), architecture = os.arch()) => {
for (let supportedPlatform of supportedPlatforms) {
if (
type === supportedPlatform.TYPE &&
architecture === supportedPlatform.ARCHITECTURE
) {
if (supportedPlatform.TYPE === "Linux") {
let musl_warning =
"Downloading musl binary that does not include `rover supergraph compose`.";
if (libc.isNonGlibcLinuxSync()) {
console.warn(
"This operating system does not support dynamic linking to glibc.",
);
console.warn(musl_warning);
supportedPlatform.RUST_TARGET = "x86_64-unknown-linux-musl";
} else {
let libc_version = libc.versionSync();
let split_libc_version = libc_version.split(".");
let libc_major_version = split_libc_version[0];
let libc_minor_version = split_libc_version[1];
let min_major_version = 2;
let min_minor_version = 17;
if (
libc_major_version < min_major_version ||
libc_minor_version < min_minor_version
) {
console.warn(
`This operating system needs glibc >= ${min_major_version}.${min_minor_version}, but only has ${libc_version} installed.`,
);
console.warn(musl_warning);
supportedPlatform.RUST_TARGET = "x86_64-unknown-linux-musl";
}
}
}
return supportedPlatform;
}
}
const stderr = new Console(process.stderr);
stderr.log(
`Platform with type "${type}" and architecture "${architecture}" is not supported by ${name}.`,
);
stderr.table(supportedPlatforms);
process.exit(1);
};
const DEFAULT_PORTS = { "http:": 80, "https:": 443 };
/*! Copyright (c) 2014-present Matt Zabriskie & Collaborators - MIT License */
const parseNoProxyEntry = (entry) => {
let entryHost = entry;
let entryPort = 0;
if (entryHost.charAt(0) === "[") {
const bracketIndex = entryHost.indexOf("]");
if (bracketIndex !== -1) {
const host = entryHost.slice(1, bracketIndex);
const rest = entryHost.slice(bracketIndex + 1);
if (rest.charAt(0) === ":" && /^\d+$/.test(rest.slice(1))) {
entryPort = Number.parseInt(rest.slice(1), 10);
}
return [host, entryPort];
}
}
const firstColon = entryHost.indexOf(":");
const lastColon = entryHost.lastIndexOf(":");
if (
firstColon !== -1 &&
firstColon === lastColon &&
/^\d+$/.test(entryHost.slice(lastColon + 1))
) {
entryPort = Number.parseInt(entryHost.slice(lastColon + 1), 10);
entryHost = entryHost.slice(0, lastColon);
}
return [entryHost, entryPort];
};
/*! Copyright (c) 2014-present Matt Zabriskie & Collaborators - MIT License */
const normalizeNoProxyHost = (hostname) => {
if (!hostname) return hostname;
if (
hostname.charAt(0) === "[" &&
hostname.charAt(hostname.length - 1) === "]"
) {
hostname = hostname.slice(1, -1);
}
return hostname.replace(/\.+$/, "");
};
/*! Copyright (c) 2014-present Matt Zabriskie & Collaborators - MIT License */
const shouldBypassProxy = (requestURL) => {
const noProxy = (
process.env.no_proxy ||
process.env.NO_PROXY ||
""
).toLowerCase();
if (!noProxy) return false;
if (noProxy === "*") return true;
const port =
Number.parseInt(requestURL.port, 10) ||
DEFAULT_PORTS[requestURL.protocol] ||
0;
const hostname = normalizeNoProxyHost(requestURL.hostname.toLowerCase());
return noProxy.split(/[\s,]+/).some((entry) => {
if (!entry) return false;
let [entryHost, entryPort] = parseNoProxyEntry(entry);
entryHost = normalizeNoProxyHost(entryHost);
if (!entryHost) return false;
if (entryPort && entryPort !== port) return false;
if (entryHost.charAt(0) === "*") {
entryHost = entryHost.slice(1);
}
if (entryHost.charAt(0) === ".") {
return hostname.endsWith(entryHost);
}
return hostname === entryHost;
});
};
const getProxyEnv = (requestURL) => {
if (shouldBypassProxy(requestURL)) return null;
if (requestURL.protocol === "http:") {
return process.env.HTTP_PROXY || process.env.http_proxy || null;
}
if (requestURL.protocol === "https:") {
return process.env.HTTPS_PROXY || process.env.https_proxy || null;
}
return null;
};
/*! Copyright (c) 2019 Avery Harnish - MIT License */
class Binary {
constructor(name, raw_name, url, installDirectory) {
let errors = [];
if (typeof url !== "string") {
errors.push("url must be a string");
} else {
try {
new URL(url);
} catch (e) {
errors.push(e);
}
}
if (name && typeof name !== "string") {
errors.push("name must be a string");
}
if (!name) {
errors.push("You must specify the name of your binary");
}
if (errors.length > 0) {
let errorMsg =
"One or more of the parameters you passed to the Binary constructor are invalid:\n";
errors.forEach((error) => {
errorMsg += error;
});
errorMsg +=
'\n\nCorrect usage: new Binary("my-binary", "https://example.com/binary/download.tar.gz")';
error(errorMsg);
}
this.url = url;
this.name = name;
this.raw_name = raw_name;
this.installDirectory = installDirectory;
if (!existsSync(this.installDirectory)) {
mkdirSync(this.installDirectory, { recursive: true });
}
this.binaryPath = join(this.installDirectory, this.name);
}
exists() {
return existsSync(this.binaryPath);
}
install(suppressLogs = false) {
if (this.exists()) {
if (!suppressLogs) {
console.error(
`${this.name} is already installed, skipping installation.`,
);
}
return Promise.resolve();
}
if (existsSync(this.installDirectory)) {
rmSync(this.installDirectory, { recursive: true });
}
mkdirSync(this.installDirectory, { recursive: true });
if (!suppressLogs) {
console.error(`Downloading release from ${this.url}`);
}
const proxyUrl = getProxyEnv(new URL(this.url));
const agent = proxyUrl ? new ProxyAgent(proxyUrl) : null;
const fetchPromise = fetch(this.url, agent ? { dispatcher: agent } : {});
return fetchPromise
.then((res) => {
if (!res.ok) {
throw new Error(
`Failed to download binary: HTTP ${res.status} ${res.statusText}`,
);
}
return new Promise((resolve, reject) => {
const sink = Readable.fromWeb(res.body).pipe(
tar.x({ strip: 1, C: this.installDirectory }),
);
sink.on("finish", () => resolve());
sink.on("error", (err) => reject(err));
});
})
.then(() => {
fs.renameSync(
join(this.installDirectory, this.raw_name),
this.binaryPath,
);
if (!suppressLogs) {
console.error(`${this.name} has been installed!`);
}
})
.finally(() => agent && agent.close())
.catch((e) => {
error(`Error fetching release: ${e.message}`);
});
}
run() {
const promise = !this.exists() ? this.install(true) : Promise.resolve();
promise
.then(() => {
const [, , ...args] = process.argv;
const options = { cwd: process.cwd(), stdio: "inherit" };
const result = spawnSync(this.binaryPath, args, options);
if (result.error) {
error(result.error);
}
process.exit(result.status);
})
.catch((e) => {
error(e.message);
process.exit(1);
});
}
}
const getBinary = (overrideInstallDirectory, platform = getPlatform()) => {
const download_host =
process.env.npm_config_apollo_rover_download_host ||
process.env.APOLLO_ROVER_DOWNLOAD_HOST ||
"https://rover.apollo.dev";
// the url for this binary is constructed from values in `package.json`
// https://rover.apollo.dev/tar/rover/x86_64-unknown-linux-gnu/vx.x.x
const url = `${download_host}/tar/${name}/${platform.RUST_TARGET}/v${version}`;
const { dirname } = require("path");
const appDir = dirname(require.main.filename);
let installDirectory = join(appDir, "binary");
if (overrideInstallDirectory != null && overrideInstallDirectory !== "") {
installDirectory = overrideInstallDirectory;
}
let binary = new Binary(
platform.BINARY_NAME,
platform.RAW_NAME,
url,
installDirectory,
);
// setting this allows us to extract supergraph plugins to the proper directory
// the variable itself is read in Rust code
process.env.APOLLO_NODE_MODULES_BIN_DIR = binary.installDirectory;
return binary;
};
const install = (suppressLogs = false) => {
const binary = getBinary();
// these messages are duplicated in `src/command/install/mod.rs`
// for the curl installer.
if (!suppressLogs) {
console.error(
"If you would like to disable Rover's anonymized usage collection, you can set APOLLO_TELEMETRY_DISABLED=true",
);
console.error(
"You can check out our documentation at https://go.apollo.dev/r/docs.",
);
}
return binary.install(suppressLogs);
};
const run = () => {
const binary = getBinary();
binary.run();
};
module.exports = {
install,
run,
getBinary,
getPlatform,
getProxyEnv,
shouldBypassProxy,
};