refactor: extract shared runCommand utility and cleanup codebase
- Create shared command utility in ts/utils/command.ts - Update index.ts, process-manager.ts, system-info.ts to use shared utility - Remove duplicate runCommand implementations from all files - Remove legacy Chrome wrapper methods (startChrome, stopChrome, isChromeRunning) - Consolidate Sway window selectors from 5 to 2 - Remove unused isobuild TypeScript files (mod.ts, deno.json, ts/index.ts) - Make getStatus() async to properly await system info - Add disk and system info sections to UI
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
import { ProcessManager } from './process-manager.ts';
|
import { ProcessManager } from './process-manager.ts';
|
||||||
import { SystemInfo } from './system-info.ts';
|
import { SystemInfo } from './system-info.ts';
|
||||||
import { UIServer } from '../ui/server.ts';
|
import { UIServer } from '../ui/server.ts';
|
||||||
|
import { runCommand } from '../utils/command.ts';
|
||||||
|
|
||||||
export interface DaemonConfig {
|
export interface DaemonConfig {
|
||||||
uiPort: number;
|
uiPort: number;
|
||||||
@@ -60,13 +61,14 @@ export class EcoDaemon {
|
|||||||
return [...this.logs];
|
return [...this.logs];
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(): Record<string, unknown> {
|
async getStatus(): Promise<Record<string, unknown>> {
|
||||||
|
const systemInfo = await this.systemInfo.getInfo();
|
||||||
return {
|
return {
|
||||||
sway: this.swayStatus.state === 'running',
|
sway: this.swayStatus.state === 'running',
|
||||||
swayStatus: this.swayStatus,
|
swayStatus: this.swayStatus,
|
||||||
chromium: this.chromiumStatus.state === 'running',
|
chromium: this.chromiumStatus.state === 'running',
|
||||||
chromiumStatus: this.chromiumStatus,
|
chromiumStatus: this.chromiumStatus,
|
||||||
systemInfo: this.systemInfo.getInfo(),
|
systemInfo,
|
||||||
logs: this.logs.slice(-50),
|
logs: this.logs.slice(-50),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -166,14 +168,14 @@ export class EcoDaemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async ensureSeatd(): Promise<void> {
|
private async ensureSeatd(): Promise<void> {
|
||||||
const status = await this.runCommand('systemctl', ['is-active', 'seatd']);
|
const status = await runCommand('systemctl', ['is-active', 'seatd']);
|
||||||
if (status.success && status.stdout.trim() === 'active') {
|
if (status.success && status.stdout.trim() === 'active') {
|
||||||
this.log('seatd is already running');
|
this.log('seatd is already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log('Starting seatd service...');
|
this.log('Starting seatd service...');
|
||||||
const result = await this.runCommand('systemctl', ['start', 'seatd']);
|
const result = await runCommand('systemctl', ['start', 'seatd']);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
this.log('Warning: Failed to start seatd: ' + result.stderr);
|
this.log('Warning: Failed to start seatd: ' + result.stderr);
|
||||||
}
|
}
|
||||||
@@ -184,9 +186,9 @@ export class EcoDaemon {
|
|||||||
|
|
||||||
// Ensure XDG_RUNTIME_DIR exists
|
// Ensure XDG_RUNTIME_DIR exists
|
||||||
const runtimeDir = `/run/user/${uid}`;
|
const runtimeDir = `/run/user/${uid}`;
|
||||||
await this.runCommand('mkdir', ['-p', runtimeDir]);
|
await runCommand('mkdir', ['-p', runtimeDir]);
|
||||||
await this.runCommand('chown', [`${this.config.user}:${this.config.user}`, runtimeDir]);
|
await runCommand('chown', [`${this.config.user}:${this.config.user}`, runtimeDir]);
|
||||||
await this.runCommand('chmod', ['700', runtimeDir]);
|
await runCommand('chmod', ['700', runtimeDir]);
|
||||||
|
|
||||||
if (mode === 'drm') {
|
if (mode === 'drm') {
|
||||||
this.log('Starting Sway with DRM backend (hardware rendering)');
|
this.log('Starting Sway with DRM backend (hardware rendering)');
|
||||||
@@ -243,31 +245,13 @@ export class EcoDaemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getUserUid(): Promise<number> {
|
private async getUserUid(): Promise<number> {
|
||||||
const result = await this.runCommand('id', ['-u', this.config.user]);
|
const result = await runCommand('id', ['-u', this.config.user]);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error('Failed to get user UID: ' + result.stderr);
|
throw new Error('Failed to get user UID: ' + result.stderr);
|
||||||
}
|
}
|
||||||
return parseInt(result.stdout.trim(), 10);
|
return parseInt(result.stdout.trim(), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runCommand(
|
|
||||||
cmd: string,
|
|
||||||
args: string[]
|
|
||||||
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
|
||||||
const command = new Deno.Command(cmd, {
|
|
||||||
args,
|
|
||||||
stdout: 'piped',
|
|
||||||
stderr: 'piped',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await command.output();
|
|
||||||
return {
|
|
||||||
success: result.success,
|
|
||||||
stdout: new TextDecoder().decode(result.stdout),
|
|
||||||
stderr: new TextDecoder().decode(result.stderr),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runForever(): Promise<void> {
|
private async runForever(): Promise<void> {
|
||||||
// Monitor processes and restart if needed
|
// Monitor processes and restart if needed
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* Manages spawning and monitoring of Sway and Chromium processes
|
* Manages spawning and monitoring of Sway and Chromium processes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { runCommand } from '../utils/command.ts';
|
||||||
|
|
||||||
export interface SwayConfig {
|
export interface SwayConfig {
|
||||||
runtimeDir: string;
|
runtimeDir: string;
|
||||||
backends: string;
|
backends: string;
|
||||||
@@ -18,6 +20,7 @@ export interface BrowserConfig {
|
|||||||
runtimeDir: string;
|
runtimeDir: string;
|
||||||
waylandDisplay: string;
|
waylandDisplay: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
kiosk?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProcessManager {
|
export class ProcessManager {
|
||||||
@@ -70,13 +73,7 @@ focus_follows_mouse yes
|
|||||||
|
|
||||||
# Force all windows fullscreen for kiosk mode
|
# Force all windows fullscreen for kiosk mode
|
||||||
for_window [app_id=".*"] fullscreen enable
|
for_window [app_id=".*"] fullscreen enable
|
||||||
|
|
||||||
# Chromium-specific fullscreen rules
|
|
||||||
for_window [app_id="chromium-browser"] fullscreen enable
|
for_window [app_id="chromium-browser"] fullscreen enable
|
||||||
for_window [app_id="Chromium-browser"] fullscreen enable
|
|
||||||
for_window [app_id="chromium"] fullscreen enable
|
|
||||||
for_window [class="Chromium-browser"] fullscreen enable
|
|
||||||
for_window [class="chromium-browser"] fullscreen enable
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,37 +85,19 @@ for_window [class="chromium-browser"] fullscreen enable
|
|||||||
const configPath = `${configDir}/config`;
|
const configPath = `${configDir}/config`;
|
||||||
|
|
||||||
// Create config directory
|
// Create config directory
|
||||||
await this.runCommand('mkdir', ['-p', configDir]);
|
await runCommand('mkdir', ['-p', configDir]);
|
||||||
await this.runCommand('chown', [`${this.user}:${this.user}`, `/home/${this.user}/.config`]);
|
await runCommand('chown', [`${this.user}:${this.user}`, `/home/${this.user}/.config`]);
|
||||||
await this.runCommand('chown', [`${this.user}:${this.user}`, configDir]);
|
await runCommand('chown', [`${this.user}:${this.user}`, configDir]);
|
||||||
|
|
||||||
// Write config file
|
// Write config file
|
||||||
const configContent = this.generateSwayConfig(config);
|
const configContent = this.generateSwayConfig(config);
|
||||||
await Deno.writeTextFile(configPath, configContent);
|
await Deno.writeTextFile(configPath, configContent);
|
||||||
await this.runCommand('chown', [`${this.user}:${this.user}`, configPath]);
|
await runCommand('chown', [`${this.user}:${this.user}`, configPath]);
|
||||||
|
|
||||||
console.log(`[sway] Config written to ${configPath}`);
|
console.log(`[sway] Config written to ${configPath}`);
|
||||||
return configPath;
|
return configPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runCommand(
|
|
||||||
cmd: string,
|
|
||||||
args: string[]
|
|
||||||
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
|
||||||
const command = new Deno.Command(cmd, {
|
|
||||||
args,
|
|
||||||
stdout: 'piped',
|
|
||||||
stderr: 'piped',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await command.output();
|
|
||||||
return {
|
|
||||||
success: result.success,
|
|
||||||
stdout: new TextDecoder().decode(result.stdout),
|
|
||||||
stderr: new TextDecoder().decode(result.stderr),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async startSway(config: SwayConfig): Promise<void> {
|
async startSway(config: SwayConfig): Promise<void> {
|
||||||
// Write sway config before starting
|
// Write sway config before starting
|
||||||
const configPath = await this.writeSwayConfig(config);
|
const configPath = await this.writeSwayConfig(config);
|
||||||
@@ -287,10 +266,7 @@ for_window [class="chromium-browser"] fullscreen enable
|
|||||||
// Try multiple selectors to ensure we catch the window
|
// Try multiple selectors to ensure we catch the window
|
||||||
const selectors = [
|
const selectors = [
|
||||||
'[app_id="chromium-browser"]',
|
'[app_id="chromium-browser"]',
|
||||||
'[app_id="Chromium-browser"]',
|
|
||||||
'[app_id="chromium"]',
|
'[app_id="chromium"]',
|
||||||
'[class="Chromium-browser"]',
|
|
||||||
'[class="chromium-browser"]',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of selectors) {
|
for (const selector of selectors) {
|
||||||
@@ -301,11 +277,6 @@ for_window [class="chromium-browser"] fullscreen enable
|
|||||||
await this.swaymsg(config, '[app_id="chromium-browser"] focus');
|
await this.swaymsg(config, '[app_id="chromium-browser"] focus');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy method name for backwards compatibility
|
|
||||||
async startChrome(config: BrowserConfig & { kiosk?: boolean }): Promise<void> {
|
|
||||||
return this.startBrowser(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
isSwayRunning(): boolean {
|
isSwayRunning(): boolean {
|
||||||
return this.swayProcess !== null;
|
return this.swayProcess !== null;
|
||||||
}
|
}
|
||||||
@@ -314,11 +285,6 @@ for_window [class="chromium-browser"] fullscreen enable
|
|||||||
return this.browserProcess !== null;
|
return this.browserProcess !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy method name for backwards compatibility
|
|
||||||
isChromeRunning(): boolean {
|
|
||||||
return this.isBrowserRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
async stopSway(): Promise<void> {
|
async stopSway(): Promise<void> {
|
||||||
if (this.swayProcess) {
|
if (this.swayProcess) {
|
||||||
try {
|
try {
|
||||||
@@ -343,11 +309,6 @@ for_window [class="chromium-browser"] fullscreen enable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy method name for backwards compatibility
|
|
||||||
async stopChrome(): Promise<void> {
|
|
||||||
return this.stopBrowser();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async pipeOutput(
|
private async pipeOutput(
|
||||||
process: Deno.ChildProcess,
|
process: Deno.ChildProcess,
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* Gathers CPU, RAM, disk, network, and GPU information
|
* Gathers CPU, RAM, disk, network, and GPU information
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { runCommand } from '../utils/command.ts';
|
||||||
|
|
||||||
export interface CpuInfo {
|
export interface CpuInfo {
|
||||||
model: string;
|
model: string;
|
||||||
cores: number;
|
cores: number;
|
||||||
@@ -66,8 +68,9 @@ export class SystemInfo {
|
|||||||
|
|
||||||
private async getHostname(): Promise<string> {
|
private async getHostname(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const result = await this.runCommand('hostname');
|
const result = await runCommand('hostname', []);
|
||||||
return result.trim();
|
if (!result.success) return 'unknown';
|
||||||
|
return result.stdout.trim();
|
||||||
} catch {
|
} catch {
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
@@ -120,8 +123,9 @@ export class SystemInfo {
|
|||||||
|
|
||||||
private async getDiskInfo(): Promise<DiskInfo[]> {
|
private async getDiskInfo(): Promise<DiskInfo[]> {
|
||||||
try {
|
try {
|
||||||
const output = await this.runCommand('df', ['-B1', '--output=source,target,size,used,avail']);
|
const result = await runCommand('df', ['-B1', '--output=source,target,size,used,avail']);
|
||||||
const lines = output.trim().split('\n').slice(1);
|
if (!result.success) return [];
|
||||||
|
const lines = result.stdout.trim().split('\n').slice(1);
|
||||||
const disks: DiskInfo[] = [];
|
const disks: DiskInfo[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
@@ -150,8 +154,9 @@ export class SystemInfo {
|
|||||||
|
|
||||||
private async getNetworkInfo(): Promise<NetworkInterface[]> {
|
private async getNetworkInfo(): Promise<NetworkInterface[]> {
|
||||||
try {
|
try {
|
||||||
const output = await this.runCommand('ip', ['-j', 'addr']);
|
const cmdResult = await runCommand('ip', ['-j', 'addr']);
|
||||||
const interfaces = JSON.parse(output);
|
if (!cmdResult.success) return [];
|
||||||
|
const interfaces = JSON.parse(cmdResult.stdout);
|
||||||
const result: NetworkInterface[] = [];
|
const result: NetworkInterface[] = [];
|
||||||
|
|
||||||
for (const iface of interfaces) {
|
for (const iface of interfaces) {
|
||||||
@@ -183,8 +188,9 @@ export class SystemInfo {
|
|||||||
|
|
||||||
private async getGpuInfo(): Promise<GpuInfo[]> {
|
private async getGpuInfo(): Promise<GpuInfo[]> {
|
||||||
try {
|
try {
|
||||||
const output = await this.runCommand('lspci', ['-mm']);
|
const result = await runCommand('lspci', ['-mm']);
|
||||||
const lines = output.split('\n');
|
if (!result.success) return [];
|
||||||
|
const lines = result.stdout.split('\n');
|
||||||
const gpus: GpuInfo[] = [];
|
const gpus: GpuInfo[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
@@ -213,19 +219,4 @@ export class SystemInfo {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runCommand(cmd: string, args: string[] = []): Promise<string> {
|
|
||||||
const command = new Deno.Command(cmd, {
|
|
||||||
args,
|
|
||||||
stdout: 'piped',
|
|
||||||
stderr: 'piped',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await command.output();
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error('Command failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TextDecoder().decode(result.stdout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export class UIServer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (path === '/api/status') {
|
if (path === '/api/status') {
|
||||||
const status = this.daemon.getStatus();
|
const status = await this.daemon.getStatus();
|
||||||
return new Response(JSON.stringify(status), { headers });
|
return new Response(JSON.stringify(status), { headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +233,25 @@ export class UIServer {
|
|||||||
<h2>Network</h2>
|
<h2>Network</h2>
|
||||||
<div id="network-list"></div>
|
<div id="network-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Disks</h2>
|
||||||
|
<div id="disk-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>System</h2>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Hostname</div>
|
||||||
|
<div class="stat-value" id="hostname">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Uptime</div>
|
||||||
|
<div class="stat-value" id="uptime">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">GPU</div>
|
||||||
|
<div class="stat-value" id="gpu">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card" style="grid-column: 1 / -1;">
|
<div class="card" style="grid-column: 1 / -1;">
|
||||||
<h2>Logs</h2>
|
<h2>Logs</h2>
|
||||||
<div class="logs" id="logs"></div>
|
<div class="logs" id="logs"></div>
|
||||||
@@ -248,6 +267,15 @@ export class UIServer {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatUptime(seconds) {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const mins = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (days > 0) return days + 'd ' + hours + 'h ' + mins + 'm';
|
||||||
|
if (hours > 0) return hours + 'h ' + mins + 'm';
|
||||||
|
return mins + 'm';
|
||||||
|
}
|
||||||
|
|
||||||
function updateStatus(data) {
|
function updateStatus(data) {
|
||||||
// Services
|
// Services
|
||||||
document.getElementById('sway-status').className =
|
document.getElementById('sway-status').className =
|
||||||
|
|||||||
27
ecoos_daemon/ts/utils/command.ts
Normal file
27
ecoos_daemon/ts/utils/command.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Shared command execution utility
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CommandResult {
|
||||||
|
success: boolean;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCommand(
|
||||||
|
cmd: string,
|
||||||
|
args: string[]
|
||||||
|
): Promise<CommandResult> {
|
||||||
|
const command = new Deno.Command(cmd, {
|
||||||
|
args,
|
||||||
|
stdout: 'piped',
|
||||||
|
stderr: 'piped',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await command.output();
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
stdout: new TextDecoder().decode(result.stdout),
|
||||||
|
stderr: new TextDecoder().decode(result.stderr),
|
||||||
|
};
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@ecobridge/eco-os-isobuild",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"exports": "./mod.ts",
|
|
||||||
"tasks": {
|
|
||||||
"build": "deno run --allow-all mod.ts build",
|
|
||||||
"clean": "deno run --allow-all mod.ts clean",
|
|
||||||
"test-qemu": "deno run --allow-all mod.ts test-qemu"
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"@std/fs": "jsr:@std/fs@^1.0.0",
|
|
||||||
"@std/path": "jsr:@std/path@^1.0.0",
|
|
||||||
"@std/async": "jsr:@std/async@^1.0.0"
|
|
||||||
},
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
/**
|
|
||||||
* EcoOS ISO Builder
|
|
||||||
*
|
|
||||||
* CLI for building custom Ubuntu ISO with EcoOS daemon
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { build } from './ts/index.ts';
|
|
||||||
|
|
||||||
const command = Deno.args[0];
|
|
||||||
|
|
||||||
switch (command) {
|
|
||||||
case 'build':
|
|
||||||
await build();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'clean':
|
|
||||||
console.log('Cleaning build artifacts...');
|
|
||||||
try {
|
|
||||||
await Deno.remove('./output', { recursive: true });
|
|
||||||
await Deno.remove('./build', { recursive: true });
|
|
||||||
console.log('Clean complete.');
|
|
||||||
} catch {
|
|
||||||
console.log('Nothing to clean.');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'test-qemu':
|
|
||||||
console.log('Testing ISO in QEMU...');
|
|
||||||
const isoPath = './output/ecoos.iso';
|
|
||||||
try {
|
|
||||||
await Deno.stat(isoPath);
|
|
||||||
} catch {
|
|
||||||
console.error('ISO not found. Run "deno task build" first.');
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const qemu = new Deno.Command('qemu-system-x86_64', {
|
|
||||||
args: [
|
|
||||||
'-enable-kvm',
|
|
||||||
'-m', '4G',
|
|
||||||
'-cpu', 'host',
|
|
||||||
'-smp', '2',
|
|
||||||
'-cdrom', isoPath,
|
|
||||||
'-boot', 'd',
|
|
||||||
'-vga', 'virtio',
|
|
||||||
'-display', 'gtk',
|
|
||||||
'-device', 'usb-tablet',
|
|
||||||
'-nic', 'user,hostfwd=tcp::3006-:3006',
|
|
||||||
],
|
|
||||||
stdout: 'inherit',
|
|
||||||
stderr: 'inherit',
|
|
||||||
});
|
|
||||||
await qemu.spawn().status;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log(`
|
|
||||||
EcoOS ISO Builder
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
deno task build Build the ISO
|
|
||||||
deno task clean Clean build artifacts
|
|
||||||
deno task test-qemu Test ISO in QEMU
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
- Ubuntu 24.04+ host
|
|
||||||
- live-build, debootstrap, xorriso installed
|
|
||||||
- sudo access for live-build
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user