diff --git a/docs/AURORA_NATIVE_RUNTIME.md b/docs/AURORA_NATIVE_RUNTIME.md new file mode 100644 index 0000000..58bb83a --- /dev/null +++ b/docs/AURORA_NATIVE_RUNTIME.md @@ -0,0 +1,26 @@ +# Aurora Native Runtime + +Aurora Native is a per-platform PHP development engine that does not require Docker. The existing container engine remains supported while native runtime bundles are developed and verified. + +## Runtime targets + +- Linux: x64 and arm64 +- macOS: Intel and Apple silicon +- Windows: x64, with arm64 evaluated after the first stable runtime + +Each signed runtime bundle contains a `runtime.json` manifest plus versioned executables for PHP, web servers, databases, Node.js, and application tooling. Bundles install below Aurora's user-data directory and never modify system PHP or database installations. + +## Isolation model + +Every project receives reserved loopback ports, generated service configuration, isolated database data, logs, PID files, and environment variables below `.aurora/native`. A shared Aurora router owns friendly HTTPS project hostnames. Project files remain directly accessible on the host. + +## Delivery sequence + +1. Runtime manifest, platform detection, checksum verification, and engine abstraction. +2. Native process supervisor and loopback port allocator. +3. Linux x64 bundle with PHP 8.4, nginx, and MariaDB 11.8. +4. WordPress provisioning, lifecycle, logs, database import/export, and Adminer. +5. macOS arm64/x64 and Windows x64 bundles. +6. Additional PHP/database versions, Apache, Drupal, Node.js, and developer services. + +Native project creation must stay disabled until the platform bundle passes executable, service-health, database, routing, and cleanup checks. Existing projects default to the container engine for backward compatibility. diff --git a/src/main/index.ts b/src/main/index.ts index ea1e1a0..15372e1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -11,6 +11,7 @@ import { registerCreateIpc } from './ipc/create' import { registerWindowIpc } from './ipc/window' import { registerSecretsIpc } from './ipc/secrets' import { registerRemoteIpc } from './ipc/remote' +import { registerRuntimeIpc } from './ipc/runtime' import { killAllRunningCommands, powerOffAllProjects } from './commandRunner' import { startAutomaticUpdates } from './updater' @@ -70,6 +71,7 @@ app.whenReady().then(() => { registerWindowIpc() registerSecretsIpc() registerRemoteIpc() + registerRuntimeIpc() createWindow() startAutomaticUpdates() diff --git a/src/main/ipc/runtime.ts b/src/main/ipc/runtime.ts new file mode 100644 index 0000000..41ff521 --- /dev/null +++ b/src/main/ipc/runtime.ts @@ -0,0 +1,6 @@ +import { ipcMain } from 'electron' +import { getRuntimeStatus } from '../nativeRuntime' + +export function registerRuntimeIpc(): void { + ipcMain.handle('runtime:status', () => getRuntimeStatus()) +} diff --git a/src/main/nativeRuntime.test.ts b/src/main/nativeRuntime.test.ts new file mode 100644 index 0000000..7ecfb13 --- /dev/null +++ b/src/main/nativeRuntime.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest' +import { validateNativeRuntimeManifest } from './nativeRuntime' + +const valid = { schema: 1, runtimeVersion: '0.1.0', platform: 'linux', arch: 'x64', components: [{ id: 'php', version: '8.4.12', executable: 'php/8.4/bin/php', sha256: 'a'.repeat(64) }] } + +describe('Aurora Native runtime manifest', () => { + it('accepts a signed-component-shaped platform manifest', () => expect(validateNativeRuntimeManifest(valid)).toMatchObject(valid)) + it('rejects executable paths that escape the runtime', () => expect(() => validateNativeRuntimeManifest({ ...valid, components: [{ ...valid.components[0], executable: '../php' }] })).toThrow(/Unsafe executable path/)) + it('rejects malformed component checksums', () => expect(() => validateNativeRuntimeManifest({ ...valid, components: [{ ...valid.components[0], sha256: 'bad' }] })).toThrow(/checksum/)) +}) diff --git a/src/main/nativeRuntime.ts b/src/main/nativeRuntime.ts new file mode 100644 index 0000000..da8d59d --- /dev/null +++ b/src/main/nativeRuntime.ts @@ -0,0 +1,68 @@ +import { app } from 'electron' +import { execFile } from 'child_process' +import { access, readFile } from 'fs/promises' +import { isAbsolute, join, resolve, sep } from 'path' +import { promisify } from 'util' +import type { AuroraNativeRuntimeManifest, AuroraRuntimeStatus } from '../shared/types' + +const execFileAsync = promisify(execFile) +const supportedPlatforms = new Set(['linux', 'darwin', 'win32']) +const supportedArchitectures = new Set(['x64', 'arm64']) +const componentIds = new Set(['php', 'nginx', 'apache', 'mariadb', 'mysql', 'postgres', 'node', 'composer', 'wp-cli', 'drush']) + +export function validateNativeRuntimeManifest(value: unknown): AuroraNativeRuntimeManifest { + if (!value || typeof value !== 'object') throw new Error('Runtime manifest must be an object.') + const manifest = value as Record + if (manifest.schema !== 1) throw new Error('Unsupported native runtime manifest schema.') + if (typeof manifest.runtimeVersion !== 'string' || !/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(manifest.runtimeVersion)) throw new Error('Invalid native runtime version.') + if (typeof manifest.platform !== 'string' || !supportedPlatforms.has(manifest.platform)) throw new Error('Unsupported native runtime platform.') + if (typeof manifest.arch !== 'string' || !supportedArchitectures.has(manifest.arch)) throw new Error('Unsupported native runtime architecture.') + if (!Array.isArray(manifest.components) || manifest.components.length === 0) throw new Error('Native runtime manifest has no components.') + const components = manifest.components.map((item) => { + if (!item || typeof item !== 'object') throw new Error('Invalid native runtime component.') + const component = item as Record + if (typeof component.id !== 'string' || !componentIds.has(component.id)) throw new Error('Unknown native runtime component.') + if (typeof component.version !== 'string' || !component.version.trim()) throw new Error(`Missing version for ${component.id}.`) + if (typeof component.executable !== 'string' || !component.executable || isAbsolute(component.executable) || component.executable.split(/[\\/]/).includes('..')) throw new Error(`Unsafe executable path for ${component.id}.`) + if (typeof component.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(component.sha256)) throw new Error(`Invalid checksum for ${component.id}.`) + return component as unknown as AuroraNativeRuntimeManifest['components'][number] + }) + return { schema: 1, runtimeVersion: manifest.runtimeVersion, platform: manifest.platform as AuroraNativeRuntimeManifest['platform'], arch: manifest.arch as AuroraNativeRuntimeManifest['arch'], components } +} + +function runtimeRoot(): string { + return join(app.getPath('userData'), 'runtimes', `${process.platform}-${process.arch}`) +} + +async function nativeStatus(): Promise { + if (!supportedPlatforms.has(process.platform) || !supportedArchitectures.has(process.arch)) return { available: false, platform: process.platform, arch: process.arch, components: [], reason: 'This platform does not have an Aurora Native runtime target yet.' } + const root = runtimeRoot() + try { + const manifest = validateNativeRuntimeManifest(JSON.parse(await readFile(join(root, 'runtime.json'), 'utf8'))) + if (manifest.platform !== process.platform || manifest.arch !== process.arch) throw new Error('The installed runtime targets a different platform.') + const canonicalRoot = resolve(root) + for (const component of manifest.components) { + const executable = resolve(root, component.executable) + if (executable !== canonicalRoot && !executable.startsWith(`${canonicalRoot}${sep}`)) throw new Error(`Unsafe executable path for ${component.id}.`) + await access(executable) + } + return { available: true, platform: process.platform, arch: process.arch, runtimeVersion: manifest.runtimeVersion, components: manifest.components } + } catch (error) { + return { available: false, platform: process.platform, arch: process.arch, components: [], reason: error instanceof Error && !error.message.includes('ENOENT') ? error.message : 'Aurora Native runtime is not installed yet.' } + } +} + +async function containerStatus(): Promise { + for (const provider of ['docker', 'podman'] as const) { + try { + const { stdout } = await execFileAsync(provider, ['--version'], { timeout: 5000 }) + return { available: true, provider, version: stdout.trim() } + } catch { /* try next provider */ } + } + return { available: false, provider: null, reason: 'Docker or Podman was not detected.' } +} + +export async function getRuntimeStatus(): Promise { + const [container, native] = await Promise.all([containerStatus(), nativeStatus()]) + return { selectedEngine: native.available ? 'native' : 'container', container, native } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index e41ff64..ec7d1a5 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,6 +11,7 @@ import type { AuroraSiteCredentials, AuroraRemoteSiteProfile, AuroraRemoteSiteStatus, + AuroraRuntimeStatus, AuroraStackOptions, EnvironmentUpdate, LogDataEvent, @@ -155,6 +156,9 @@ const api = { pull: (operationId: string, name: string): Promise => ipcRenderer.invoke('remote:pull', operationId, name) }, + runtime: { + status: (): Promise => ipcRenderer.invoke('runtime:status') + }, zoom: { in: (): Promise => ipcRenderer.invoke('window:zoomIn'), out: (): Promise => ipcRenderer.invoke('window:zoomOut'), diff --git a/src/shared/types.ts b/src/shared/types.ts index 3ea9c07..ad43cfd 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,4 +1,26 @@ export type ProjectStatus = 'running' | 'stopped' | 'paused' | 'starting' | 'stopping' | string +export type AuroraRuntimeEngine = 'container' | 'native' + +export interface AuroraNativeRuntimeComponent { + id: 'php' | 'nginx' | 'apache' | 'mariadb' | 'mysql' | 'postgres' | 'node' | 'composer' | 'wp-cli' | 'drush' + version: string + executable: string + sha256: string +} + +export interface AuroraNativeRuntimeManifest { + schema: 1 + runtimeVersion: string + platform: 'linux' | 'darwin' | 'win32' + arch: 'x64' | 'arm64' + components: AuroraNativeRuntimeComponent[] +} + +export interface AuroraRuntimeStatus { + selectedEngine: AuroraRuntimeEngine + container: { available: boolean; provider: 'docker' | 'podman' | null; version?: string; reason?: string } + native: { available: boolean; platform: string; arch: string; runtimeVersion?: string; components: AuroraNativeRuntimeComponent[]; reason?: string } +} export interface AuroraProjectSummary { name: string