mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Merge branch 'dev' into promptless/document-developer-tools
This commit is contained in:
commit
df8e67195e
@ -1,9 +1,10 @@
|
||||
import { Command } from "commander";
|
||||
import { execFileSync, spawn } from "child_process";
|
||||
import { execFileSync, execSync, spawn } from "child_process";
|
||||
import extract from "extract-zip";
|
||||
import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { createInterface } from "readline";
|
||||
import { Readable } from "stream";
|
||||
import { pipeline } from "stream/promises";
|
||||
import { fileURLToPath } from "url";
|
||||
@ -389,7 +390,7 @@ export function formatDuration(seconds: number): string {
|
||||
|
||||
// --- Dependency preflight ---------------------------------------------------
|
||||
|
||||
type BinarySpec = { name: string, install: string };
|
||||
type BinarySpec = { name: string, linuxPkg: string, macPkg: string };
|
||||
|
||||
function commandExists(bin: string): boolean {
|
||||
try {
|
||||
@ -415,13 +416,17 @@ export function platformInstallHint(linuxPkg: string, macPkg: string): string {
|
||||
}
|
||||
|
||||
function bin(name: string, linuxPkg: string, macPkg: string): BinarySpec {
|
||||
return { name, install: platformInstallHint(linuxPkg, macPkg) };
|
||||
return { name, linuxPkg, macPkg };
|
||||
}
|
||||
|
||||
function installHint(b: BinarySpec): string {
|
||||
return platformInstallHint(b.linuxPkg, b.macPkg);
|
||||
}
|
||||
|
||||
function requireBinaries(commandName: string, bins: BinarySpec[]): void {
|
||||
const missing = bins.filter((b) => !commandExists(b.name));
|
||||
if (missing.length === 0) return;
|
||||
const lines = missing.map((b) => ` - ${b.name} → ${b.install}`);
|
||||
const lines = missing.map((b) => ` - ${b.name} → ${installHint(b)}`);
|
||||
throw new CliError(
|
||||
`\`stack emulator ${commandName}\` requires the following missing binaries:\n${lines.join("\n")}`,
|
||||
);
|
||||
@ -431,10 +436,110 @@ function warnIfMissing(commandName: string, bins: BinarySpec[]): void {
|
||||
const missing = bins.filter((b) => !commandExists(b.name));
|
||||
if (missing.length === 0) return;
|
||||
for (const b of missing) {
|
||||
console.warn(`[stack emulator ${commandName}] optional dep '${b.name}' missing — feature degraded. Install: ${b.install}`);
|
||||
console.warn(`[stack emulator ${commandName}] optional dep '${b.name}' missing — feature degraded. Install: ${installHint(b)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmPrompt(question: string): Promise<boolean> {
|
||||
if (!process.stdin.isTTY) {
|
||||
throw new CliError("Cannot prompt for confirmation: stdin is not a TTY. Install the missing dependencies manually and retry.");
|
||||
}
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
return await new Promise((resolvePromise) => {
|
||||
rl.question(`${question} [y/N] `, (answer) => {
|
||||
rl.close();
|
||||
resolvePromise(/^y(es)?$/i.test(answer.trim()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureDepsForPull(arch: "arm64" | "amd64"): Promise<void> {
|
||||
const allBins = [archSpecificQemuBin(arch), ...commonVmBins(), bin("zstd", "zstd", "zstd")];
|
||||
const missingBins = allBins.filter((b) => !commandExists(b.name));
|
||||
const firmwareMissing = arch === "arm64" && !aarch64FirmwareAvailable();
|
||||
if (missingBins.length === 0 && !firmwareMissing) return;
|
||||
|
||||
const platform = process.platform;
|
||||
// Auto-install targets macOS (brew) and Debian/Ubuntu-family Linux
|
||||
// (apt-get). On other distros or platforms, fall back to the standard
|
||||
// per-binary install hints.
|
||||
const linuxHasApt = platform === "linux" && commandExists("apt-get");
|
||||
if (platform !== "darwin" && !linuxHasApt) {
|
||||
preflightForVmStart("pull", arch);
|
||||
return;
|
||||
}
|
||||
|
||||
// In non-interactive environments (CI, piped stdin) we cannot prompt, so
|
||||
// surface the standard per-binary install hints instead of erroring with
|
||||
// only a TTY complaint.
|
||||
if (!process.stdin.isTTY) {
|
||||
preflightForVmStart("pull", arch);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("The emulator needs the following dependencies that aren't installed:");
|
||||
for (const b of missingBins) console.log(` - ${b.name}`);
|
||||
if (firmwareMissing) console.log(" - aarch64 UEFI firmware");
|
||||
console.log();
|
||||
|
||||
const pkgs = new Set<string>();
|
||||
for (const b of missingBins) {
|
||||
pkgs.add(platform === "darwin" ? b.macPkg : b.linuxPkg);
|
||||
}
|
||||
// macOS qemu formula bundles the aarch64 firmware; Linux needs a separate package.
|
||||
if (firmwareMissing && platform === "linux") pkgs.add("qemu-efi-aarch64");
|
||||
// Edge case: on macOS arm64, firmware can be missing while all binaries
|
||||
// are present (e.g. a partial qemu install). Reinstalling `qemu` recreates
|
||||
// the bundled firmware files.
|
||||
if (firmwareMissing && platform === "darwin") pkgs.add("qemu");
|
||||
const pkgList = Array.from(pkgs).sort();
|
||||
if (pkgList.length === 0) {
|
||||
preflightForVmStart("pull", arch);
|
||||
return;
|
||||
}
|
||||
|
||||
const brewMissing = platform === "darwin" && !commandExists("brew");
|
||||
console.log("Proposed install plan:");
|
||||
if (brewMissing) {
|
||||
console.log(" - install Homebrew by running the official installer:");
|
||||
console.log(" /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"");
|
||||
console.log(" (executes remote code from raw.githubusercontent.com — review https://brew.sh if unsure)");
|
||||
}
|
||||
if (platform === "darwin") console.log(` - brew install ${pkgList.join(" ")}`);
|
||||
else console.log(` - sudo apt-get update && sudo apt-get install -y ${pkgList.join(" ")}`);
|
||||
console.log();
|
||||
|
||||
const ok = await confirmPrompt("Proceed with install?");
|
||||
if (!ok) {
|
||||
throw new CliError("Dependency install declined. Install the missing packages manually and retry.");
|
||||
}
|
||||
|
||||
if (brewMissing) {
|
||||
console.log("\nInstalling Homebrew...");
|
||||
execSync('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', {
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
console.log("\nInstalling packages...");
|
||||
if (platform === "darwin") {
|
||||
// After a fresh Homebrew bootstrap, `brew` lives at /opt/homebrew/bin
|
||||
// (Apple Silicon) or /usr/local/bin (Intel); the installer only updates
|
||||
// shell profiles, not the current process's PATH, so resolve it by
|
||||
// absolute path when needed.
|
||||
const brewBin = commandExists("brew")
|
||||
? "brew"
|
||||
: existsSync("/opt/homebrew/bin/brew")
|
||||
? "/opt/homebrew/bin/brew"
|
||||
: "/usr/local/bin/brew";
|
||||
execFileSync(brewBin, ["install", ...pkgList], { stdio: "inherit" });
|
||||
} else {
|
||||
execFileSync("sudo", ["apt-get", "update"], { stdio: "inherit" });
|
||||
execFileSync("sudo", ["apt-get", "install", "-y", ...pkgList], { stdio: "inherit" });
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
function aarch64FirmwareAvailable(): boolean {
|
||||
return AARCH64_FIRMWARE_PATHS.some((p) => existsSync(p));
|
||||
}
|
||||
@ -512,6 +617,9 @@ export function registerEmulatorCommand(program: Command) {
|
||||
.option("--skip-snapshot", "Download only the qcow2; skip the one-time local snapshot capture")
|
||||
.action(async (opts: { arch?: string, repo?: string, branch?: string, tag?: string, pr?: string, run?: string, skipSnapshot?: boolean }) => {
|
||||
const arch = resolveArch(opts.arch);
|
||||
if (!opts.skipSnapshot) {
|
||||
await ensureDepsForPull(arch);
|
||||
}
|
||||
const repo = opts.repo ?? DEFAULT_REPO;
|
||||
|
||||
if (opts.run || opts.pr) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user