apkit/index.js

85 lines
2.4 KiB
JavaScript

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<void>} install
* @prop {(runlevel: string, service: string) => Promise<void>} rcUpdate
*/
/**
* Initialize an Alpine 3.18 rootfs at `root`
* @param {string} root
* @returns {Promise<Alpine>}
*/
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)))
);
}