Switch setup.ts to Alpine rootfs, fix remote deployment

- setup.ts now downloads Alpine Linux minirootfs instead of Ubuntu squashfs
- Installs Alpine packages (openssh, python3, curl, ca-certificates) in chroot
- Fixes install script failing on non-Alpine base rootfs (adduser syntax)
- Clean up unused imports and lint warnings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 15:21:06 +00:00
parent d149319090
commit b613c2db6f
5 changed files with 68 additions and 39 deletions

View File

@@ -36,70 +36,104 @@ export async function runSetup() {
if (existsSync(CONFIG.baseRootfs)) {
log("Base rootfs already exists, skipping download.");
} else {
log("Downloading rootfs...");
log("Downloading Alpine Linux minirootfs...");
// Find latest rootfs key from S3 listing
const arch = execFileSync("uname", ["-m"], { encoding: "utf-8" }).trim();
const alpineTar = `${CONFIG.baseDir}/alpine-minirootfs.tar.gz`;
// Find latest Alpine version
const listing = execFileSync(
"curl",
["-fsSL", CONFIG.assets.rootfsListUrl],
["-fsSL", "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/" + arch + "/"],
{ encoding: "utf-8", timeout: 30_000 }
);
const keys = [...listing.matchAll(/<Key>([^<]+)<\/Key>/g)].map(
(m) => m[1]
const match = listing.match(
new RegExp(`alpine-minirootfs-[\\d.]+-${arch}\\.tar\\.gz`, "g")
);
const rootfsKey = keys.sort().pop();
if (!rootfsKey) throw new Error("Could not find rootfs in S3 listing");
if (!match || match.length === 0)
throw new Error("Could not find Alpine minirootfs");
const filename = match.sort().pop()!;
const squashfsPath = `${CONFIG.baseDir}/rootfs.squashfs`;
download(`${CONFIG.assets.rootfsBaseUrl}/${rootfsKey}`, squashfsPath);
log("Rootfs downloaded. Converting squashfs to ext4...");
download(
`https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/${arch}/${filename}`,
alpineTar
);
log("Alpine downloaded. Building ext4 rootfs...");
// Convert squashfs to ext4
const squashMount = "/tmp/fireclaw-squash";
const ext4Mount = "/tmp/fireclaw-ext4";
mkdirSync(squashMount, { recursive: true });
// Create ext4 image and unpack Alpine
const ext4Mount = "/tmp/fireclaw-alpine";
mkdirSync(ext4Mount, { recursive: true });
try {
execFileSync(
"sudo",
["mount", "-t", "squashfs", squashfsPath, squashMount],
{ stdio: "pipe" }
);
execFileSync("truncate", ["-s", "1G", CONFIG.baseRootfs], {
execFileSync("truncate", ["-s", "256M", CONFIG.baseRootfs], {
stdio: "pipe",
});
execFileSync("sudo", ["/usr/sbin/mkfs.ext4", CONFIG.baseRootfs], {
execFileSync("sudo", ["/usr/sbin/mkfs.ext4", "-q", CONFIG.baseRootfs], {
stdio: "pipe",
});
execFileSync("sudo", ["mount", CONFIG.baseRootfs, ext4Mount], {
stdio: "pipe",
});
execFileSync("sudo", ["cp", "-a", `${squashMount}/.`, ext4Mount], {
execFileSync("sudo", ["tar", "xzf", alpineTar, "-C", ext4Mount], {
stdio: "pipe",
});
// Bake in DNS config
// DNS
execSync(
`echo "nameserver 8.8.8.8" | sudo tee ${ext4Mount}/etc/resolv.conf > /dev/null`
);
log("Rootfs converted.");
// Inittab for serial console
execSync(`sudo tee ${ext4Mount}/etc/inittab > /dev/null << 'EOF'
::sysinit:/sbin/openrc sysinit
::sysinit:/sbin/openrc boot
::sysinit:/sbin/openrc default
ttyS0::respawn:/sbin/getty -L 115200 ttyS0 vt100
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/openrc shutdown
EOF`);
// Hostname
execSync(
`echo "fireclaw" | sudo tee ${ext4Mount}/etc/hostname > /dev/null`
);
// Allow root login (no password, SSH key auth only)
execSync(
`sudo sed -i 's/root:x:/root::/' ${ext4Mount}/etc/passwd`
);
// Install base packages
log("Installing Alpine packages (openssh, python3, curl, ca-certificates)...");
execFileSync(
"sudo",
["chroot", ext4Mount, "/bin/sh", "-c",
"apk update && apk add --no-cache openssh-server ca-certificates curl jq python3 bash openrc && " +
"rc-update add sshd default && ssh-keygen -A && " +
"echo 'PermitRootLogin prohibit-password' >> /etc/ssh/sshd_config && " +
"mkdir -p /run/openrc && touch /run/openrc/softlevel"],
{ stdio: "pipe", timeout: 120_000 }
);
// Networking init script
execSync(`sudo tee ${ext4Mount}/etc/init.d/networking > /dev/null << 'EOF'
#!/sbin/openrc-run
depend() { need localmount; }
start() { ip link set lo up; return 0; }
EOF`);
execFileSync("sudo", ["chmod", "+x", `${ext4Mount}/etc/init.d/networking`], { stdio: "pipe" });
execFileSync("sudo", ["chroot", ext4Mount, "rc-update", "add", "networking", "boot"], { stdio: "pipe" });
log("Alpine rootfs built.");
} finally {
try {
execFileSync("sudo", ["umount", squashMount], { stdio: "pipe" });
} catch {}
try {
execFileSync("sudo", ["umount", ext4Mount], { stdio: "pipe" });
} catch {}
try {
execFileSync("rmdir", [squashMount], { stdio: "pipe" });
} catch {}
try {
execFileSync("rmdir", [ext4Mount], { stdio: "pipe" });
} catch {}
try {
execFileSync("rm", ["-f", squashfsPath], { stdio: "pipe" });
execFileSync("rm", ["-f", alpineTar], { stdio: "pipe" });
} catch {}
}
}