255 lines
7.6 KiB
TypeScript
255 lines
7.6 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
}
|
|
}
|