Files
eco_os/isobuild/ts/index.ts

255 lines
7.6 KiB
TypeScript
Raw Normal View History

2026-01-08 12:28:54 +00:00
/**
* ISO Build Orchestration
*
* Builds the EcoOS custom Ubuntu ISO
*/
import * as path from '@std/path';
const ROOT = path.dirname(path.dirname(path.fromFileUrl(import.meta.url)));
const DAEMON_DIR = path.join(path.dirname(ROOT), 'ecoos_daemon');
const BUILD_DIR = path.join(ROOT, 'build');
const OUTPUT_DIR = path.join(ROOT, 'output');
const CONFIG_DIR = path.join(ROOT, 'config');
export async function build(): Promise<void> {
console.log('=== EcoOS ISO Builder ===\n');
// Step 1: Check prerequisites
console.log('[1/7] Checking prerequisites...');
await checkPrerequisites();
// Step 2: Bundle the daemon
console.log('[2/7] Bundling ecoos_daemon...');
await bundleDaemon();
// Step 3: Prepare build directory
console.log('[3/7] Preparing build directory...');
await prepareBuildDir();
// Step 4: Configure live-build
console.log('[4/7] Configuring live-build...');
await configureLiveBuild();
// Step 5: Copy package lists
console.log('[5/7] Copying package lists...');
await copyPackageLists();
// Step 6: Copy daemon and configs to chroot includes
console.log('[6/7] Preparing chroot includes...');
await prepareChrootIncludes();
// Step 7: Build ISO
console.log('[7/7] Building ISO (this may take a while)...');
await buildIso();
console.log('\n=== Build Complete ===');
console.log(`ISO: ${OUTPUT_DIR}/ecoos.iso`);
}
async function checkPrerequisites(): Promise<void> {
const commands = ['lb', 'debootstrap', 'xorriso'];
for (const cmd of commands) {
try {
const result = await run('which', [cmd]);
if (!result.success) {
throw new Error(`${cmd} not found`);
}
} catch {
console.error(`Missing prerequisite: ${cmd}`);
console.error('Install with: sudo apt install live-build debootstrap xorriso');
Deno.exit(1);
}
}
console.log(' All prerequisites found.');
}
async function bundleDaemon(): Promise<void> {
// Compile the daemon to a single executable
const bundleDir = path.join(DAEMON_DIR, 'bundle');
await Deno.mkdir(bundleDir, { recursive: true });
const result = await run('deno', [
'compile',
'--allow-all',
'--output', path.join(bundleDir, 'eco-daemon'),
path.join(DAEMON_DIR, 'mod.ts'),
], { cwd: DAEMON_DIR });
if (!result.success) {
console.error('Failed to bundle daemon');
console.error(result.stderr);
Deno.exit(1);
}
console.log(' Daemon bundled successfully.');
}
async function prepareBuildDir(): Promise<void> {
// Clean and create build directory
try {
await Deno.remove(BUILD_DIR, { recursive: true });
} catch {
// Directory may not exist
}
await Deno.mkdir(BUILD_DIR, { recursive: true });
console.log(' Build directory prepared.');
}
async function configureLiveBuild(): Promise<void> {
// Initialize live-build config
const result = await run('lb', ['config'], { cwd: BUILD_DIR });
if (!result.success) {
console.error('Failed to initialize live-build');
console.error(result.stderr);
Deno.exit(1);
}
// Copy our auto/config
const autoDir = path.join(BUILD_DIR, 'auto');
await Deno.mkdir(autoDir, { recursive: true });
const configSrc = path.join(CONFIG_DIR, 'live-build', 'auto', 'config');
const configDst = path.join(autoDir, 'config');
await Deno.copyFile(configSrc, configDst);
await Deno.chmod(configDst, 0o755);
// Re-run lb config with our settings
const result2 = await run('lb', ['config'], { cwd: BUILD_DIR });
if (!result2.success) {
console.error('Failed to configure live-build');
Deno.exit(1);
}
console.log(' live-build configured.');
}
async function copyPackageLists(): Promise<void> {
const srcDir = path.join(CONFIG_DIR, 'live-build', 'package-lists');
const dstDir = path.join(BUILD_DIR, 'config', 'package-lists');
await Deno.mkdir(dstDir, { recursive: true });
for await (const entry of Deno.readDir(srcDir)) {
if (entry.isFile && entry.name.endsWith('.list.chroot')) {
const src = path.join(srcDir, entry.name);
const dst = path.join(dstDir, entry.name);
await Deno.copyFile(src, dst);
}
}
console.log(' Package lists copied.');
}
async function prepareChrootIncludes(): Promise<void> {
const includesDir = path.join(BUILD_DIR, 'config', 'includes.chroot');
// Create directory structure
await Deno.mkdir(path.join(includesDir, 'opt', 'eco', 'bin'), { recursive: true });
await Deno.mkdir(path.join(includesDir, 'opt', 'eco', 'daemon'), { recursive: true });
await Deno.mkdir(path.join(includesDir, 'etc', 'systemd', 'system'), { recursive: true });
// Copy bundled daemon
const daemonSrc = path.join(DAEMON_DIR, 'bundle', 'eco-daemon');
const daemonDst = path.join(includesDir, 'opt', 'eco', 'bin', 'eco-daemon');
try {
await Deno.copyFile(daemonSrc, daemonDst);
await Deno.chmod(daemonDst, 0o755);
} catch (e) {
console.error('Failed to copy daemon bundle:', e);
// Fall back to copying source files
console.log(' Copying daemon source files instead...');
await copyDir(path.join(DAEMON_DIR, 'ts'), path.join(includesDir, 'opt', 'eco', 'daemon'));
await Deno.copyFile(
path.join(DAEMON_DIR, 'mod.ts'),
path.join(includesDir, 'opt', 'eco', 'daemon', 'mod.ts')
);
}
// Copy systemd service
const serviceSrc = path.join(CONFIG_DIR, 'systemd', 'eco-daemon.service');
const serviceDst = path.join(includesDir, 'etc', 'systemd', 'system', 'eco-daemon.service');
await Deno.copyFile(serviceSrc, serviceDst);
// Copy autoinstall config
const autoinstallDir = path.join(BUILD_DIR, 'config', 'includes.binary');
await Deno.mkdir(autoinstallDir, { recursive: true });
const userDataSrc = path.join(CONFIG_DIR, 'autoinstall', 'user-data');
const userDataDst = path.join(autoinstallDir, 'autoinstall', 'user-data');
await Deno.mkdir(path.dirname(userDataDst), { recursive: true });
await Deno.copyFile(userDataSrc, userDataDst);
// Create empty meta-data file (required for cloud-init)
await Deno.writeTextFile(path.join(path.dirname(userDataDst), 'meta-data'), '');
console.log(' Chroot includes prepared.');
}
async function buildIso(): Promise<void> {
const result = await run('sudo', ['lb', 'build'], {
cwd: BUILD_DIR,
stdout: 'inherit',
stderr: 'inherit',
});
if (!result.success) {
console.error('ISO build failed');
Deno.exit(1);
}
// Move ISO to output directory
await Deno.mkdir(OUTPUT_DIR, { recursive: true });
for await (const entry of Deno.readDir(BUILD_DIR)) {
if (entry.isFile && entry.name.endsWith('.iso')) {
const src = path.join(BUILD_DIR, entry.name);
const dst = path.join(OUTPUT_DIR, 'ecoos.iso');
await Deno.rename(src, dst);
break;
}
}
}
async function run(
cmd: string,
args: string[],
options: { cwd?: string; stdout?: 'piped' | 'inherit'; stderr?: 'piped' | 'inherit' } = {}
): Promise<{ success: boolean; stdout: string; stderr: string }> {
const command = new Deno.Command(cmd, {
args,
cwd: options.cwd,
stdout: options.stdout ?? 'piped',
stderr: options.stderr ?? 'piped',
});
const result = await command.output();
return {
success: result.success,
stdout: new TextDecoder().decode(result.stdout),
stderr: new TextDecoder().decode(result.stderr),
};
}
async function copyDir(src: string, dst: string): Promise<void> {
await Deno.mkdir(dst, { recursive: true });
for await (const entry of Deno.readDir(src)) {
const srcPath = path.join(src, entry.name);
const dstPath = path.join(dst, entry.name);
if (entry.isDirectory) {
await copyDir(srcPath, dstPath);
} else {
await Deno.copyFile(srcPath, dstPath);
}
}
}