// os-builder.mjs — FedAC OS image assembly for the /os endpoint // // Downloads a pre-baked base image from CDN, injects a piece bundle into // the FEDAC-PIECE ext4 partition via debugfs, and streams the result. // // After building, uploads the finished ISO to DO Spaces CDN so repeat // downloads are served at CDN speed (~100+ MB/s) instead of droplet speed. // // Requires: e2fsprogs (debugfs) installed on the server. import { promises as fs } from "fs"; import fsSync from "fs"; import path from "path"; import { execSync } from "child_process"; import { createHash, randomUUID } from "crypto"; import { createBundle, createJSPieceBundle } from "./bundler.mjs"; import { S3Client, PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, DeleteObjectsCommand } from "@aws-sdk/client-s3"; import { MongoClient } from "mongodb"; // ─── MongoDB (OS build history) ───────────────────────────────────── let mongoClient; let mongoDB; async function connectMongo() { if (mongoDB) return mongoDB; const uri = process.env.MONGODB_CONNECTION_STRING; const dbName = process.env.MONGODB_NAME; if (!uri || !dbName) return null; try { mongoClient = await MongoClient.connect(uri); mongoDB = mongoClient.db(dbName); console.log("✅ [os] Connected to MongoDB for OS build history"); return mongoDB; } catch (err) { console.error("❌ [os] MongoDB connect failed:", err.message); return null; } } connectMongo(); async function logOSBuild(record) { try { const db = await connectMongo(); if (!db) return; await db.collection("oven-os-builds").insertOne({ ...record, when: new Date() }); } catch (err) { console.error("[os] Failed to log build:", err.message); } } // ─── Configuration ────────────────────────────────────────────────── const TEMP_DIR = process.env.OS_TEMP_DIR || "/tmp"; const CONFIGURED_CACHE_DIR = (process.env.OS_CACHE_DIR || "").trim(); const DEFAULT_CACHE_DIR = "/opt/oven/cache"; const FALLBACK_CACHE_DIR = path.join(TEMP_DIR, "oven-cache"); const WORKDIR_CACHE_DIR = path.join(process.cwd(), ".cache", "oven-os"); const BASE_IMAGE_URLS = { fedora: process.env.FEDAC_BASE_IMAGE_URL || "https://releases.aesthetic.computer/os/fedora-base-latest.img", alpine: process.env.ALPINE_BASE_IMAGE_URL || "https://releases.aesthetic.computer/os/alpine-base-latest.img", }; const MANIFEST_URLS = { fedora: process.env.FEDAC_MANIFEST_URL || "https://releases.aesthetic.computer/os/fedora-base-manifest.json", alpine: process.env.ALPINE_MANIFEST_URL || "https://releases.aesthetic.computer/os/alpine-base-manifest.json", }; // Legacy aliases const BASE_IMAGE_URL = BASE_IMAGE_URLS.fedora; const MANIFEST_URL = MANIFEST_URLS.fedora; let resolvedCacheDir = null; let resolvingCacheDirPromise = null; function getCacheDirCandidates() { return [...new Set([ CONFIGURED_CACHE_DIR || DEFAULT_CACHE_DIR, DEFAULT_CACHE_DIR, FALLBACK_CACHE_DIR, WORKDIR_CACHE_DIR, ])]; } async function resolveCacheDir() { if (resolvedCacheDir) return resolvedCacheDir; if (resolvingCacheDirPromise) return resolvingCacheDirPromise; resolvingCacheDirPromise = (async () => { const candidates = getCacheDirCandidates(); const errors = []; const preferred = CONFIGURED_CACHE_DIR || DEFAULT_CACHE_DIR; for (const dir of candidates) { const probePath = path.join(dir, `.cache-probe-${process.pid}-${Date.now()}`); try { await fs.mkdir(dir, { recursive: true }); await fs.writeFile(probePath, "ok"); await fs.unlink(probePath); resolvedCacheDir = dir; if (dir !== preferred) { console.warn(`[os] Cache fallback active: ${preferred} is not writable. Using ${dir}`); } return dir; } catch (err) { errors.push(`${dir} (${err.code || err.message})`); } } throw new Error( `No writable OS cache directory. Tried: ${errors.join(", ")}`, ); })(); try { return await resolvingCacheDirPromise; } finally { resolvingCacheDirPromise = null; } } function buildLocalCacheFilenamePattern(flavor) { if (flavor) { const safeFlavor = String(flavor).replace(/[^a-z0-9_-]/gi, ""); return new RegExp(`^${safeFlavor}-base\\.img(?:\\.sha256|\\.downloading)?$`, "i"); } return /^[a-z0-9_-]+-base\.img(?:\.sha256|\.downloading)?$/i; } export async function clearOSBuildLocalCache(flavor) { const cacheDirs = [...new Set([ resolvedCacheDir, ...getCacheDirCandidates(), ].filter(Boolean))]; const filenamePattern = buildLocalCacheFilenamePattern(flavor); let deleted = 0; const errors = []; for (const cacheDir of cacheDirs) { let entries = []; try { entries = await fs.readdir(cacheDir); } catch (err) { if (err.code === "ENOENT") continue; errors.push(`${cacheDir}: ${err.code || err.message}`); continue; } for (const entry of entries) { if (!filenamePattern.test(entry)) continue; try { await fs.unlink(path.join(cacheDir, entry)); deleted++; } catch (err) { errors.push(`${cacheDir}/${entry}: ${err.code || err.message}`); } } } if (errors.length) { console.warn("[os] Local cache clear warnings:", errors.join("; ")); } return { deleted, dirs: cacheDirs, errors }; } // ─── CDN Cache (DO Spaces) ────────────────────────────────────────── // After building an ISO, upload it to Spaces so repeat downloads bypass // the droplet entirely and go through the CDN edge network. const SPACES_REGION = process.env.OS_SPACES_REGION || "us-east-1"; const SPACES_ENDPOINT = process.env.OS_SPACES_ENDPOINT || process.env.ART_SPACES_ENDPOINT || "https://sfo3.digitaloceanspaces.com"; const SPACES_BUCKET = process.env.OS_SPACES_BUCKET || "releases-aesthetic-computer"; const SPACES_CDN_BASE = ( process.env.OS_SPACES_CDN_BASE || "https://releases.aesthetic.computer" ).replace(/\/+$/, ""); const SPACES_PREFIX = "os/builds"; function getSpacesClient() { const accessKeyId = process.env.OS_SPACES_KEY || process.env.ART_SPACES_KEY; const secretAccessKey = process.env.OS_SPACES_SECRET || process.env.ART_SPACES_SECRET; if (!accessKeyId || !secretAccessKey) return null; return new S3Client({ region: SPACES_REGION, endpoint: SPACES_ENDPOINT, credentials: { accessKeyId, secretAccessKey }, }); } function buildCDNKey(target, density, bundleHash, baseVersion, flavor = "alpine") { // e.g. os/builds/notepat-d8-ab12cd34-v2025-02-26-alpine.img const safe = String(target).replace(/[^a-zA-Z0-9_-]/g, "_"); return `${SPACES_PREFIX}/${safe}-d${density}-${bundleHash}-v${baseVersion}-${flavor}.img`; } function buildCDNUrl(key) { return `${SPACES_CDN_BASE}/${key}`; } // Check if a cached ISO already exists on CDN. async function checkCDNCache(key) { const client = getSpacesClient(); if (!client) return false; try { await client.send(new HeadObjectCommand({ Bucket: SPACES_BUCKET, Key: key })); return true; } catch { return false; } } // Upload a built ISO to CDN for fast repeat downloads. async function uploadToCDN(imagePath, key, target, flavor) { const client = getSpacesClient(); if (!client) return null; await client.send( new PutObjectCommand({ Bucket: SPACES_BUCKET, Key: key, Body: fsSync.createReadStream(imagePath), ContentType: "application/octet-stream", ContentDisposition: `attachment; filename="${target}-${flavor}-os.iso"`, ACL: "public-read", CacheControl: "public, max-age=86400", // 24h — rebuild invalidates via new hash }), ); return buildCDNUrl(key); } // Purge all cached OS build images from CDN, optionally filtered by flavor. export async function purgeOSBuildCache(flavor) { const client = getSpacesClient(); if (!client) return { deleted: 0, error: "S3 not configured" }; let deleted = 0; let continuationToken; try { do { const listCmd = new ListObjectsV2Command({ Bucket: SPACES_BUCKET, Prefix: `${SPACES_PREFIX}/`, ContinuationToken: continuationToken, }); const listing = await client.send(listCmd); continuationToken = listing.IsTruncated ? listing.NextContinuationToken : undefined; if (!listing.Contents?.length) break; // Filter by flavor suffix if specified (e.g. "-alpine.img" or "-fedora.img"). const toDelete = flavor ? listing.Contents.filter((o) => o.Key.endsWith(`-${flavor}.img`)) : listing.Contents; if (!toDelete.length) continue; await client.send( new DeleteObjectsCommand({ Bucket: SPACES_BUCKET, Delete: { Objects: toDelete.map((o) => ({ Key: o.Key })) }, }), ); deleted += toDelete.length; } while (continuationToken); } catch (err) { console.error("[os] CDN purge error:", err.message); return { deleted, error: err.message }; } console.log(`[os] Purged ${deleted} cached OS build(s) from CDN${flavor ? ` (${flavor})` : ""}`); return { deleted }; } function hashContent(content) { return createHash("sha256").update(content).digest("hex").slice(0, 8); } // Concurrency limit — each build copies ~3GB, so cap parallel builds. const MAX_CONCURRENT_BUILDS = 2; let activeBuildCount = 0; const recentBuilds = []; const MAX_RECENT = 20; function formatSeconds(ms) { return `${(ms / 1000).toFixed(1)}s`; } function runCommand(command) { execSync(command, { stdio: "pipe" }); } function copyBaseImageFast(basePath, tempImagePath) { // Reflink/sparse copy is usually much faster for large images on CoW filesystems. try { runCommand(`cp --reflink=auto --sparse=always "${basePath}" "${tempImagePath}"`); return "reflink"; } catch { return "regular"; } } function detectPiecePartitionFromImage(imagePath) { // Parse GPT table directly from the image so builds keep working even if a // stale/incorrect manifest offset was published. const cmd = `parted -s -m "${imagePath}" unit B print`; const output = execSync(cmd, { encoding: "utf8" }); const lines = output.split("\n").map((line) => line.trim()).filter(Boolean); let fallbackPart3 = null; for (const line of lines) { // Format: ":B:B:B:::" if (!/^[0-9]+:/.test(line)) continue; const parts = line.split(":"); if (parts.length < 4) continue; const num = parts[0]; const start = parseInt((parts[1] || "").replace(/B/g, ""), 10); const size = parseInt((parts[3] || "").replace(/B/g, ""), 10); if (!Number.isFinite(start) || !Number.isFinite(size) || size <= 0) continue; const name = (parts[5] || "").replace(/;$/, "").trim(); const isPieceByName = name.toUpperCase() === "PIECE"; const isPart3 = num === "3"; if (isPieceByName) { return { piecePartitionOffset: start, piecePartitionSize: size }; } if (isPart3) { fallbackPart3 = { piecePartitionOffset: start, piecePartitionSize: size }; } } return fallbackPart3; } // ─── Manifest ─────────────────────────────────────────────────────── const cachedManifests = {}; async function fetchManifest(onProgress, flavor = "alpine") { if (cachedManifests[flavor]) return cachedManifests[flavor]; const manifestUrl = MANIFEST_URLS[flavor] || MANIFEST_URLS.fedora; onProgress?.({ stage: "manifest", message: `Fetching ${flavor} base image manifest...`, step: 1, totalSteps: 9 }); const res = await fetch(manifestUrl); if (!res.ok) throw new Error(`Manifest fetch failed (${flavor}): ${res.status}`); cachedManifests[flavor] = await res.json(); const m = cachedManifests[flavor]; const distroLabel = m.alpine ? `Alpine ${m.alpine}` : m.fedora ? `Fedora ${m.fedora}` : flavor; onProgress?.({ stage: "manifest", message: `Base image v${m.version}, ${distroLabel}`, step: 1, totalSteps: 9, }); return m; } // Invalidate manifest cache (call after base image update). export function invalidateManifest(flavor) { if (flavor) { delete cachedManifests[flavor]; } else { for (const key of Object.keys(cachedManifests)) delete cachedManifests[key]; } } // ─── Base Image Cache ─────────────────────────────────────────────── async function ensureBaseImage(onProgress, flavor = "alpine") { const cacheDir = await resolveCacheDir(); await fs.mkdir(cacheDir, { recursive: true }); const manifest = await fetchManifest(onProgress, flavor); const basePath = path.join(cacheDir, `${flavor}-base.img`); const hashPath = `${basePath}.sha256`; // Check if cached image matches manifest size AND sha256 hash. try { const stat = await fs.stat(basePath); const cachedHash = (await fs.readFile(hashPath, "utf-8")).trim(); if (stat.size === manifest.totalSize && cachedHash === manifest.sha256) { onProgress?.({ stage: "base", message: `${flavor} base image cached and ready`, step: 1, totalSteps: 9 }); return { basePath, manifest }; } onProgress?.({ stage: "base", message: `${flavor} base image outdated (hash mismatch), re-downloading...`, step: 1, totalSteps: 9 }); } catch { // File or hash sidecar doesn't exist — need to download. } // Download base image. const baseImageUrl = BASE_IMAGE_URLS[flavor] || BASE_IMAGE_URLS.fedora; onProgress?.({ stage: "base", message: `Downloading ${flavor} base image from CDN...`, step: 1, totalSteps: 9 }); const res = await fetch(baseImageUrl); if (!res.ok) throw new Error(`Base image download failed (${flavor}): ${res.status}`); const tmpPath = `${basePath}.downloading`; const writer = fsSync.createWriteStream(tmpPath); const reader = res.body.getReader(); let downloaded = 0; const total = manifest.totalSize || parseInt(res.headers.get("content-length"), 10) || 0; while (true) { const { done, value } = await reader.read(); if (done) break; writer.write(value); downloaded += value.length; if (total > 0 && downloaded % (50 * 1024 * 1024) < value.length) { const pct = Math.round((downloaded / total) * 100); onProgress?.({ stage: "base", message: `Downloading base image... ${pct}%` }); } } writer.end(); await new Promise((resolve, reject) => { writer.on("finish", resolve); writer.on("error", reject); }); await fs.rename(tmpPath, basePath); // Write sha256 sidecar so future cache checks validate the hash, not just size. if (manifest.sha256) await fs.writeFile(hashPath, manifest.sha256 + "\n"); onProgress?.({ stage: "base", message: "Base image downloaded and cached" }); return { basePath, manifest }; } // ─── Piece Injection ──────────────────────────────────────────────── function extractPartition(imagePath, manifest, tempPartPath) { const { piecePartitionOffset, piecePartitionSize } = manifest; try { runCommand( `dd if="${imagePath}" of="${tempPartPath}" bs=4M iflag=skip_bytes,count_bytes skip=${piecePartitionOffset} count=${piecePartitionSize} status=none`, ); } catch { runCommand( `dd if="${imagePath}" of="${tempPartPath}" bs=512 skip=${Math.floor(piecePartitionOffset / 512)} count=${Math.floor(piecePartitionSize / 512)} 2>/dev/null`, ); } } function injectFilesIntoPartition(tempPartPath, files) { for (const [filename, content] of Object.entries(files)) { const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_"); const tmpPath = `${tempPartPath}.${safeName}.${randomUUID().slice(0, 8)}`; fsSync.writeFileSync(tmpPath, content); try { try { runCommand(`debugfs -w -R "rm ${filename}" "${tempPartPath}" 2>/dev/null`); } catch { // File may not exist yet. } runCommand(`debugfs -w -R "write ${tmpPath} ${filename}" "${tempPartPath}"`); } finally { try { fsSync.unlinkSync(tmpPath); } catch { // ignore } } } } function writePartitionBack(imagePath, manifest, tempPartPath) { const { piecePartitionOffset, piecePartitionSize } = manifest; try { runCommand( `dd if="${tempPartPath}" of="${imagePath}" bs=4M oflag=seek_bytes seek=${piecePartitionOffset} count=${piecePartitionSize} iflag=count_bytes conv=notrunc,fsync status=none`, ); } catch { runCommand( `dd if="${tempPartPath}" of="${imagePath}" bs=512 seek=${Math.floor(piecePartitionOffset / 512)} count=${Math.floor(piecePartitionSize / 512)} conv=notrunc 2>/dev/null`, ); } } function buildFedOSShellHTML(target) { const escapedTarget = String(target || "piece").replace(//g, ">"); return ` FedOS - ${escapedTarget}
fedos - ${escapedTarget}
VOL --
BAT --
NET --
`; } // ─── Main Build Flow ──────────────────────────────────────────────── export async function streamOSImage(res, target, isJSPiece, density, onProgress, flavor = "alpine", { nocache = false } = {}) { if (activeBuildCount >= MAX_CONCURRENT_BUILDS) { throw new Error( `Server busy: ${activeBuildCount}/${MAX_CONCURRENT_BUILDS} OS builds in progress. Try again shortly.`, ); } activeBuildCount++; const buildId = randomUUID().slice(0, 8); const startTime = Date.now(); const timing = {}; const tempImagePath = path.join(TEMP_DIR, `fedac-os-${buildId}.img`); const tempPartPath = path.join(TEMP_DIR, `fedac-part-${buildId}.img`); try { // 1. Ensure base image is cached + fetch manifest for version tag. const baseStart = Date.now(); const { basePath, manifest } = await ensureBaseImage(onProgress, flavor); timing.base = Date.now() - baseStart; let resolvedManifest = { ...manifest }; try { const detected = detectPiecePartitionFromImage(basePath); if (detected?.piecePartitionOffset > 0 && detected?.piecePartitionSize > 0) { const mismatch = detected.piecePartitionOffset !== manifest.piecePartitionOffset || detected.piecePartitionSize !== manifest.piecePartitionSize; if (mismatch) { console.warn( `[os] Manifest partition mismatch (${flavor}):` + ` manifest=${manifest.piecePartitionOffset}/${manifest.piecePartitionSize}` + ` detected=${detected.piecePartitionOffset}/${detected.piecePartitionSize}.` + ` Using detected values.`, ); } resolvedManifest = { ...manifest, ...detected }; } } catch (err) { console.warn(`[os] Failed to detect partition table from image: ${err.message}`); } if (!resolvedManifest.piecePartitionOffset || resolvedManifest.piecePartitionOffset <= 0) { throw new Error( `Base image missing valid piecePartitionOffset (${flavor}) — rebuild base image with --base-image flag`, ); } // 2. Build piece bundle. const bundleStart = Date.now(); onProgress?.({ stage: "bundle", message: `Bundling ${target}...`, step: 2, totalSteps: 9 }); const bundleResult = isJSPiece ? await createJSPieceBundle(target, (p) => onProgress?.({ stage: "bundle", message: p.message, step: 2, totalSteps: 9 }), true, density) : await createBundle(target, (p) => onProgress?.({ stage: "bundle", message: p.message, step: 2, totalSteps: 9 }), true, density); const pieceHtml = bundleResult.html; timing.bundle = Date.now() - bundleStart; onProgress?.({ stage: "bundle", message: `Bundle ready: ${bundleResult.sizeKB}KB (${formatSeconds(timing.bundle)})`, step: 2, totalSteps: 9, }); // 2b. Compute bundle hash + CDN key for caching. const bundleHash = hashContent(pieceHtml); const baseVersion = manifest.version || "unknown"; const cdnKey = buildCDNKey(target, density, bundleHash, baseVersion, flavor); // 3. Check CDN cache — skip the entire build if an identical ISO exists. const cacheStart = Date.now(); onProgress?.({ stage: "cache-check", message: nocache ? "Skipping cache (nocache)..." : "Checking CDN cache...", step: 3, totalSteps: 9 }); const cdnHit = nocache ? false : await checkCDNCache(cdnKey); timing.cacheCheck = Date.now() - cacheStart; if (cdnHit) { const cdnUrl = buildCDNUrl(cdnKey); const elapsed = Date.now() - startTime; timing.total = elapsed; onProgress?.({ stage: "done", message: `CDN cache hit — redirecting (${formatSeconds(elapsed)})`, cdnUrl, cached: true, timings: timing, step: 9, totalSteps: 9, }); recentBuilds.unshift({ buildId, target, isJSPiece, density, flavor, elapsed, timings: timing, cached: true, cdnUrl, time: new Date().toISOString(), }); if (recentBuilds.length > MAX_RECENT) recentBuilds.pop(); logOSBuild({ buildId, target, isJSPiece, density, flavor, elapsed, timings: timing, cached: true, cdnUrl, baseVersion: manifest.version, baseSha256: manifest.sha256, success: true }); // Redirect to CDN for download (fast edge delivery). if (res) { res.redirect(302, cdnUrl); } return { buildId, elapsed, timings: timing, cdnUrl, cached: true, filename: `${target}-${flavor}-os.iso` }; } // 4. Copy base image to temp. const copyStart = Date.now(); onProgress?.({ stage: "copy", message: "Copying base image to workspace...", step: 4, totalSteps: 9 }); const copyMode = copyBaseImageFast(basePath, tempImagePath); if (copyMode === "regular") { await fs.copyFile(basePath, tempImagePath); } timing.copy = Date.now() - copyStart; onProgress?.({ stage: "copy", message: `Base image copied (${formatSeconds(timing.copy)}, ${copyMode})`, step: 4, totalSteps: 9, }); // 5. Extract FEDAC-PIECE partition. const extractStart = Date.now(); onProgress?.({ stage: "extract", message: "Extracting piece partition from image...", step: 5, totalSteps: 9 }); extractPartition(tempImagePath, resolvedManifest, tempPartPath); timing.extract = Date.now() - extractStart; onProgress?.({ stage: "extract", message: `Partition extracted (${formatSeconds(timing.extract)})`, step: 5, totalSteps: 9, }); // 6. Inject piece-app.html and shell piece.html via debugfs. const injectStart = Date.now(); onProgress?.({ stage: "inject", message: "Writing piece-app.html into partition via debugfs...", step: 6, totalSteps: 9 }); injectFilesIntoPartition(tempPartPath, { "piece-app.html": pieceHtml, "piece.html": buildFedOSShellHTML(target), }); timing.inject = Date.now() - injectStart; onProgress?.({ stage: "inject", message: `Piece files injected (${formatSeconds(timing.inject)})`, step: 6, totalSteps: 9, }); // 7. Write modified partition back into the image. const writeStart = Date.now(); onProgress?.({ stage: "write-back", message: "Writing modified partition back to image...", step: 7, totalSteps: 9 }); writePartitionBack(tempImagePath, resolvedManifest, tempPartPath); timing.writeBack = Date.now() - writeStart; onProgress?.({ stage: "write-back", message: `Partition written back (${formatSeconds(timing.writeBack)})`, step: 7, totalSteps: 9, }); // 8. Upload to CDN (concurrent with streaming to client). onProgress?.({ stage: "upload", message: "Uploading ISO to CDN for caching...", step: 8, totalSteps: 9 }); const uploadPromise = uploadToCDN(tempImagePath, cdnKey, target, flavor) .then((url) => { if (url) { console.log(`[os] CDN upload complete: ${url}`); onProgress?.({ stage: "upload", message: "ISO cached to CDN", step: 8, totalSteps: 9 }); } return url; }) .catch((err) => { console.error(`[os] CDN upload failed (non-fatal): ${err.message}`); onProgress?.({ stage: "upload", message: `CDN upload skipped: ${err.message}`, step: 8, totalSteps: 9 }); return null; }); // 9. Stream to client (if res provided). let cdnUrl = null; if (res) { const streamStart = Date.now(); const fileStat = await fs.stat(tempImagePath); const filename = `${target}-${flavor}-os.iso`; res.set({ "Content-Type": "application/octet-stream", "Content-Disposition": `attachment; filename="${filename}"`, "Content-Length": fileStat.size, "Cache-Control": "no-cache", }); const sizeMB = Math.round(fileStat.size / 1024 / 1024); onProgress?.({ stage: "stream", message: `Streaming ${sizeMB}MB ISO to client...`, step: 9, totalSteps: 9 }); const readStream = fsSync.createReadStream(tempImagePath); await new Promise((resolve, reject) => { readStream.pipe(res); readStream.on("end", resolve); readStream.on("error", reject); res.on("close", () => { readStream.destroy(); resolve(); }); }); timing.stream = Date.now() - streamStart; } // Wait for CDN upload before cleaning up temp file. cdnUrl = await uploadPromise; const elapsed = Date.now() - startTime; timing.total = elapsed; onProgress?.({ stage: "done", message: `OS ISO ready in ${formatSeconds(elapsed)} (copy ${formatSeconds(timing.copy)}, extract ${formatSeconds(timing.extract)}, inject ${formatSeconds(timing.inject)}, write-back ${formatSeconds(timing.writeBack)})${cdnUrl ? " — cached to CDN" : ""}`, cdnUrl, timings: timing, step: 9, totalSteps: 9, }); recentBuilds.unshift({ buildId, target, isJSPiece, density, flavor, elapsed, timings: timing, cdnUrl, time: new Date().toISOString(), }); if (recentBuilds.length > MAX_RECENT) recentBuilds.pop(); logOSBuild({ buildId, target, isJSPiece, density, flavor, elapsed, timings: timing, cached: false, cdnUrl, bundleHash, baseVersion, baseSha256: manifest.sha256, success: true }); return { buildId, elapsed, timings: timing, cdnUrl, flavor, filename: `${target}-${flavor}-os.iso`, }; } catch (err) { const elapsed = Date.now() - startTime; logOSBuild({ buildId, target, isJSPiece, density, flavor, elapsed, timings: timing, cached: false, success: false, error: err.message }); throw err; } finally { activeBuildCount--; try { await fs.unlink(tempImagePath); } catch { // ignore } try { await fs.unlink(tempPartPath); } catch { // ignore } } } // ─── Status ───────────────────────────────────────────────────────── export function getOSBuildStatus() { return { activeBuildCount, maxConcurrent: MAX_CONCURRENT_BUILDS, recentBuilds: recentBuilds.slice(0, 10), cacheDir: resolvedCacheDir || (CONFIGURED_CACHE_DIR || DEFAULT_CACHE_DIR), baseImageUrl: BASE_IMAGE_URL, manifestUrl: MANIFEST_URL, }; } export { ensureBaseImage };