import { chmod, chown, copyFile, mkdir, opendir, symlink, writeFile, } from "node:fs/promises"; import path from "node:path"; import { cwd } from "node:process"; import { Fstab } from "./fstab.js"; import { execFile, exists } from "./helpers/better-api.js"; import { PasswdEntry, readPasswd } from "./helpers/passwd.js"; import { logDebug } from "./helpers/logger.js"; import assert from "node:assert"; import { Persist } from "./persist.js"; import { writePasswd } from "./passwd.js"; export class Alpine { dir: string; private constructor({ dir }: { dir: string }) { this.dir = dir; } fstab: Fstab = new Fstab(this); persist: Persist = new Persist(this); packages: string[] = []; async mkdirP(dir: string): Promise { await mkdir(this.path(dir), { recursive: true }); } async writeFile( filePath: string, content: string, permissions?: { uid: number; gid: number } ): Promise { const p = path.join(this.dir, filePath); await this.mkdirP(path.dirname(filePath)); await writeFile(p, content); if (permissions) { await chown(p, permissions.uid, permissions.gid); await chmod(p, 0o600); } } async writeExecutable(filePath: string, content: string): Promise { await this.writeFile(filePath, content); await chmod(this.path(filePath), 0o700); } async assertExists(path: string) { assert(await exists(this.path(path))); } path(p: string): string { return path.join(this.dir, p); } private getRelativeSymlink( target: string, filePath: string ): { target: string; filePath: string } { const realFilePath = path.join(this.dir, filePath); return { target: path.relative( path.dirname(realFilePath), path.join(this.dir, target) ), filePath: realFilePath, }; } async symlink(_target: string, _filePath: string): Promise { const { target, filePath } = this.getRelativeSymlink(_target, _filePath); await symlink(target, filePath); } readPasswd(): Promise { return readPasswd(this.path("/etc/passwd")); } async userAdd( user: string, { system, homeDir, createHome, }: { system: boolean; homeDir?: string; createHome: boolean } = { system: false, homeDir: "", createHome: true, } ): Promise { await execFile("useradd", [ "--user-group", ...(system ? ["--system"] : []), ...(homeDir ? ["--home-dir", homeDir] : []), ...(createHome ? ["--create-home"] : []), "--root", this.dir, user, ]); const passwd = await this.readPasswd(); const entry = passwd.find((e) => e.name === user); if (!entry) { throw new Error("fatal(userAdd): no encontré el usuario " + user); } return entry; } async actuallyInstallPackages(): Promise { await this.installPackages(this.packages); } async installPackages(packages: string[]): Promise { logDebug( "installPackages", await execFile("apk", [ "add", "--clean-protected", "--root", this.dir, ...packages, ]) ); } async addPackages(packages: string[]): Promise { this.packages = this.packages.concat(packages); } static async makeWorld({ dir, packages, }: { dir: string; packages?: string[]; }): Promise { const apkDir = path.join(dir, "/etc/apk"); await mkdir(apkDir, { recursive: true }); // hack { const cacheDir = path.join(cwd(), "cache"); await mkdir("cache", { recursive: true }); await symlink(cacheDir, path.join(apkDir, "cache")); } { const apkKeysDir = path.join(apkDir, "keys"); const keysSrcDir = "alpine/keys"; await mkdir(apkKeysDir, { recursive: true }); for await (const { name } of await opendir(keysSrcDir)) await copyFile( path.join(keysSrcDir, name), path.join(apkKeysDir, name) ); } await writeFile( path.join(apkDir, "repositories"), [ "https://dl-cdn.alpinelinux.org/alpine/v3.17/main", "https://dl-cdn.alpinelinux.org/alpine/v3.17/community", ].join("\n") ); logDebug( "makeWorld", await execFile("apk", [ "add", "--initdb", "--clean-protected", "--root", dir, ...["alpine-baselayout", "busybox", "libc-utils", "alpine-keys"], ...(packages || []), ]) ); const alpine = new Alpine({ dir }); await alpine.fstab.write(); await alpine.persist.write(); await writePasswd(alpine); return alpine; } }