import { execFile as _execFile, spawn } from "node:child_process"; import { mkdir, readFile, stat, symlink, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; const execFile = promisify(_execFile); /** * @typedef {object} Alpine * @prop {(packages: string[]) => Promise} install * @prop {(runlevel: string, service: string) => Promise} rcUpdate */ /** * Initialize an Alpine 3.18 rootfs at `root` * @param {string} root * @returns {Promise} */ export async function init(root) { await runInContainer( root, ` mkdir -p /rootfs/etc/apk cp -r /etc/apk/keys /rootfs/etc/apk/ echo https://dl-cdn.alpinelinux.org/alpine/v3.18/main >> /rootfs/etc/apk/repositories echo https://dl-cdn.alpinelinux.org/alpine/v3.18/community >> /rootfs/etc/apk/repositories ln -s /var/cache/apk /rootfs/etc/apk/cache apk add --initdb --root /rootfs alpine-base ` ); return { async install(packages) { const worldPath = join(root, "/etc/apk/world"); const oldWorld = (await readFile(worldPath, "utf-8")).split("\n"); const newWorld = oldWorld.concat(packages); await writeFile(worldPath, newWorld.join("\n")); await runInContainer(root, `apk fix --root /rootfs`); }, async rcUpdate(runlevel, service) { const servicePath = `/etc/init.d/${service}`; try { await stat(join(root, servicePath)); } catch (error) { throw new Error(`Can't stat service ${service}: ${error}`); } await symlink( servicePath, join(root, `/etc/runlevels/`, runlevel, service) ); }, }; } const IS_DEV = process.env.NODE_ENV === "development"; /** * Run script inside an Alpine 3.18 container with `root` mounted in /rootfs * @param {string} root * @param {string} script */ async function runInContainer(root, script) { const cacheDir = join(homedir(), ".cache/apkit"); await mkdir(cacheDir, { recursive: true }); const proc = spawn( "podman", [ "run", "-i", "--rm", "-v", `${root}:/rootfs:Z`, "-v", `${cacheDir}:/var/cache/apk:Z`, "docker.io/alpine:3.18", "sh", "-ec", script, ], { stdio: IS_DEV ? "inherit" : "pipe" } ); await new Promise((resolve, reject) => proc.on("exit", (code) => (code === 0 ? resolve(void 0) : reject(code))) ); }