diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 14254ad..62128e9 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -6,7 +6,7 @@ on: release_tag: description: Existing GitHub release tag to receive the Windows installer required: true - default: v2.0.0-alpha.28 + default: v2.0.0-alpha.29 push: tags: - 'v*' @@ -37,6 +37,13 @@ jobs: npm run typecheck npm run test:run + - name: Build and verify Aurora Native for Windows + shell: pwsh + run: | + ./runtime-build/windows-x64/stage-runtime.ps1 + node scripts/smoke-native-runtime.cjs dist/native-runtime-stage/win32-x64 + node scripts/package-native-runtime.cjs dist/native-runtime-stage/win32-x64 dist/native-runtime/aurora-native-0.1.0-win32-x64.tar.gz + - name: Read application version shell: pwsh run: | @@ -68,6 +75,7 @@ jobs: dist/latest.yml dist/SHA256SUMS-windows.txt dist/module-catalog/*.pac + dist/native-runtime/aurora-native-0.1.0-win32-x64.tar.gz - name: Publish Windows assets if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' diff --git a/package-lock.json b/package-lock.json index 7f109f5..f70c5d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aurora-dockside", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aurora-dockside", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "hasInstallScript": true, "dependencies": { "@electron-toolkit/preload": "^3.0.2", diff --git a/package.json b/package.json index e90f965..8bcb737 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aurora-dockside", - "version": "2.0.0-alpha.28", + "version": "2.0.0-alpha.29", "description": "A modern, modular Docker development platform for building, running, and managing web applications locally.", "desktopName": "aurora-dockside.desktop", "main": "./out/main/index.js", @@ -23,6 +23,7 @@ "build:connector": "node scripts/package-connector.cjs", "build:native-runtime": "node scripts/package-native-runtime.cjs", "build:native-linux-x64": "node scripts/build-native-linux-x64.cjs", + "build:native-windows-x64": "powershell -ExecutionPolicy Bypass -File runtime-build/windows-x64/stage-runtime.ps1", "check:php-releases": "node scripts/check-php-releases.mjs", "module:new": "node scripts/create-module.cjs", "postinstall": "electron-builder install-app-deps", diff --git a/packages/aurora-module-wordpress/main/index.cjs b/packages/aurora-module-wordpress/main/index.cjs index bf521e0..e421951 100644 --- a/packages/aurora-module-wordpress/main/index.cjs +++ b/packages/aurora-module-wordpress/main/index.cjs @@ -38,11 +38,12 @@ exports.projectCreate = async function projectCreate(context) { const s = context.settings const title = String(s.title || '').trim() || context.projectName const native = context.environment.runtimeEngine === 'native' - const base = native ? [`--path=${context.directory}`] : wpArgs(context, false) + const nativePrefix = native ? context.native.wpPrefixArgs || [] : [] + const base = native ? [...nativePrefix, `--path=${context.directory}`] : wpArgs(context, false) const networkBase = native ? base : wpArgs(context, true) const command = native ? context.native.wp : 'docker' const siteUrl = native ? context.urls.http : context.urls.https - const wp = (label, args) => context.run(label, command, [...(native ? [] : networkBase), ...args]) + const wp = (label, args) => context.run(label, command, [...networkBase, ...args]) if (native) { await context.native.start() const port = Number(context.native.databasePort) diff --git a/runtime-build/windows-x64/stage-runtime.ps1 b/runtime-build/windows-x64/stage-runtime.ps1 new file mode 100644 index 0000000..a6c953e --- /dev/null +++ b/runtime-build/windows-x64/stage-runtime.ps1 @@ -0,0 +1,61 @@ +param( + [string]$Stage = "dist/native-runtime-stage/win32-x64", + [string]$PhpVersion = "8.5.9", + [string]$NginxVersion = "1.30.4", + [string]$MariaDbVersion = "11.8.8", + [string]$WpCliVersion = "2.12.0", + [string]$AdminerVersion = "6.0.1", + [string]$RuntimeVersion = "0.1.0" +) + +$ErrorActionPreference = "Stop" +$stagePath = [IO.Path]::GetFullPath($Stage) +$downloads = Join-Path ([IO.Path]::GetTempPath()) "aurora-native-downloads" +Remove-Item $stagePath -Recurse -Force -ErrorAction SilentlyContinue +New-Item $stagePath -ItemType Directory -Force | Out-Null +New-Item $downloads -ItemType Directory -Force | Out-Null + +function Fetch([string]$Url, [string]$Destination) { + Write-Host "Downloading $Url" + Invoke-WebRequest -Uri $Url -OutFile $Destination +} + +$phpZip = Join-Path $downloads "php.zip" +$nginxZip = Join-Path $downloads "nginx.zip" +$mariaZip = Join-Path $downloads "mariadb.zip" +Fetch "https://windows.php.net/downloads/releases/archives/php-$PhpVersion-nts-Win32-vs17-x64.zip" $phpZip +Fetch "https://nginx.org/download/nginx-$NginxVersion.zip" $nginxZip +Fetch "https://archive.mariadb.org/mariadb-$MariaDbVersion/winx64-packages/mariadb-$MariaDbVersion-winx64.zip" $mariaZip + +$phpRoot = Join-Path $stagePath "bin/php" +$nginxParent = Join-Path $stagePath "bin/nginx-unpack" +$mariaParent = Join-Path $stagePath "bin/mariadb-unpack" +Expand-Archive $phpZip $phpRoot -Force +Expand-Archive $nginxZip $nginxParent -Force +Expand-Archive $mariaZip $mariaParent -Force +Move-Item (Join-Path $nginxParent "nginx-$NginxVersion") (Join-Path $stagePath "bin/nginx") +Move-Item (Join-Path $mariaParent "mariadb-$MariaDbVersion-winx64") (Join-Path $stagePath "bin/mariadb") +Remove-Item $nginxParent, $mariaParent -Recurse -Force + +$tools = Join-Path $stagePath "tools" +New-Item $tools -ItemType Directory -Force | Out-Null +Fetch "https://github.com/wp-cli/wp-cli/releases/download/v$WpCliVersion/wp-cli-$WpCliVersion.phar" (Join-Path $tools "wp-cli.phar") +Fetch "https://github.com/vrana/adminer/releases/download/v$AdminerVersion/adminer-$AdminerVersion.php" (Join-Path $tools "adminer.php") + +$template = @{ + schema = 1 + runtimeVersion = $RuntimeVersion + platform = "win32" + arch = "x64" + components = @( + @{ id = "php"; version = $PhpVersion; executable = "bin/php/php-cgi.exe" } + @{ id = "nginx"; version = $NginxVersion; executable = "bin/nginx/nginx.exe" } + @{ id = "mariadb"; version = $MariaDbVersion; executable = "bin/mariadb/bin/mariadbd.exe" } + @{ id = "mariadb-client"; version = $MariaDbVersion; executable = "bin/mariadb/bin/mariadb.exe" } + @{ id = "mariadb-dump"; version = $MariaDbVersion; executable = "bin/mariadb/bin/mariadb-dump.exe" } + @{ id = "adminer"; version = $AdminerVersion; executable = "tools/adminer.php" } + @{ id = "wp-cli"; version = $WpCliVersion; executable = "tools/wp-cli.phar" } + ) +} +$template | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $stagePath "runtime.template.json") +Write-Host $stagePath diff --git a/scripts/smoke-native-runtime.cjs b/scripts/smoke-native-runtime.cjs index 2bb0142..60c032c 100644 --- a/scripts/smoke-native-runtime.cjs +++ b/scripts/smoke-native-runtime.cjs @@ -10,40 +10,37 @@ const root = resolve(process.argv[2] || '') if (!root || !existsSync(join(root, 'runtime.template.json'))) throw new Error('Usage: node scripts/smoke-native-runtime.cjs ') const template = JSON.parse(readFileSync(join(root, 'runtime.template.json'), 'utf8')) +const windows = template.platform === 'win32' +const php = windows ? join(root, 'bin/php/php.exe') : join(root, 'bin/php') const checks = [ - [ - 'PHP', - join(root, 'bin/php'), - ['--version'], - template.components.find((item) => item.id === 'php')?.version - ], + ['PHP', php, ['--version'], template.components.find((item) => item.id === 'php')?.version], [ 'nginx', - join(root, 'bin/nginx'), + windows ? join(root, 'bin/nginx/nginx.exe') : join(root, 'bin/nginx'), ['-v'], template.components.find((item) => item.id === 'nginx')?.version ], [ 'MariaDB', - join(root, 'bin/mariadbd'), + windows ? join(root, 'bin/mariadb/bin/mariadbd.exe') : join(root, 'bin/mariadbd'), ['--version'], template.components.find((item) => item.id === 'mariadb')?.version ], [ 'WP-CLI', - join(root, 'bin/wp'), - ['--version'], + windows ? php : join(root, 'bin/wp'), + windows ? [join(root, 'tools/wp-cli.phar'), '--version'] : ['--version'], template.components.find((item) => item.id === 'wp-cli')?.version ], [ 'MariaDB client', - join(root, 'bin/mariadb'), + windows ? join(root, 'bin/mariadb/bin/mariadb.exe') : join(root, 'bin/mariadb'), ['--version'], template.components.find((item) => item.id === 'mariadb')?.version ], [ 'MariaDB dump', - join(root, 'bin/mariadb-dump'), + windows ? join(root, 'bin/mariadb/bin/mariadb-dump.exe') : join(root, 'bin/mariadb-dump'), ['--version'], template.components.find((item) => item.id === 'mariadb')?.version ] @@ -59,7 +56,7 @@ for (const [name, command, args, version] of checks) { } const extensions = spawnSync( - join(root, 'bin/php'), + php, ['-r', "exit(extension_loaded('mysqli') && extension_loaded('pdo_mysql') ? 0 : 1);"], { encoding: 'utf8' } ) @@ -68,8 +65,11 @@ if (extensions.error || extensions.status !== 0) process.stdout.write('PHP mysqli and pdo_mysql extensions OK\n') const adminer = spawnSync( - join(root, 'bin/php'), - ['-l', join(root, 'root/usr/share/aurora/adminer.php')], + php, + [ + '-l', + windows ? join(root, 'tools/adminer.php') : join(root, 'root/usr/share/aurora/adminer.php') + ], { encoding: 'utf8' } ) if (adminer.error || adminer.status !== 0) @@ -80,14 +80,18 @@ const databaseDirectory = mkdtempSync(join(tmpdir(), 'aurora-native-mariadb-')) const temporaryDirectory = mkdtempSync(join(tmpdir(), 'aurora-native-mariadb-tmp-')) try { const result = spawnSync( - join(root, 'bin/mariadb-install-db'), - [ - '--no-defaults', - `--datadir=${databaseDirectory}`, - `--tmpdir=${temporaryDirectory}`, - '--auth-root-authentication-method=normal', - '--skip-test-db' - ], + windows + ? join(root, 'bin/mariadb/bin/mariadb-install-db.exe') + : join(root, 'bin/mariadb-install-db'), + windows + ? [`--datadir=${databaseDirectory}`, '--password='] + : [ + '--no-defaults', + `--datadir=${databaseDirectory}`, + `--tmpdir=${temporaryDirectory}`, + '--auth-root-authentication-method=normal', + '--skip-test-db' + ], { encoding: 'utf8' } ) if (result.error || result.status !== 0) diff --git a/src/main/index.ts b/src/main/index.ts index 15372e1..57ce1bb 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,6 +14,7 @@ import { registerRemoteIpc } from './ipc/remote' import { registerRuntimeIpc } from './ipc/runtime' import { killAllRunningCommands, powerOffAllProjects } from './commandRunner' import { startAutomaticUpdates } from './updater' +import { ensureBundledNativeRuntime } from './nativeRuntime' function createWindow(): void { // Create the browser window. @@ -51,7 +52,7 @@ function createWindow(): void { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. -app.whenReady().then(() => { +app.whenReady().then(async () => { // Set app user model id for windows electronApp.setAppUserModelId('com.aurora-dockside.app') @@ -73,6 +74,7 @@ app.whenReady().then(() => { registerRemoteIpc() registerRuntimeIpc() + await ensureBundledNativeRuntime() createWindow() startAutomaticUpdates() diff --git a/src/main/ipc/database.ts b/src/main/ipc/database.ts index e11b13d..f6dc5a7 100644 --- a/src/main/ipc/database.ts +++ b/src/main/ipc/database.ts @@ -61,7 +61,16 @@ async function runDatabasePipe( docroot: config.docroot, ports: config.nativePorts }) - command = join(runtimeRoot(), 'bin', mode === 'dump' ? 'mariadb-dump' : 'mariadb') + command = + process.platform === 'win32' + ? join( + runtimeRoot(), + 'bin', + 'mariadb', + 'bin', + mode === 'dump' ? 'mariadb-dump.exe' : 'mariadb.exe' + ) + : join(runtimeRoot(), 'bin', mode === 'dump' ? 'mariadb-dump' : 'mariadb') args = [ '--host=127.0.0.1', `--port=${config.nativePorts.database}`, diff --git a/src/main/ipc/projects.ts b/src/main/ipc/projects.ts index 0e10b88..7b5fb2e 100644 --- a/src/main/ipc/projects.ts +++ b/src/main/ipc/projects.ts @@ -118,6 +118,7 @@ export function registerProjectsIpc(): void { const php = nativeServiceSpecs(native) .find((spec) => spec.id.endsWith(':php')) ?.command.replace(/php-fpm$/, 'php') + .replace(/php-cgi\.exe$/i, 'php.exe') if (!php) throw new Error('Native PHP executable not found.') return runCommandStreamed(id, php, ['-i'], e.sender, { cwd: native.root }) } diff --git a/src/main/moduleRuntime.ts b/src/main/moduleRuntime.ts index b821883..e9ffb9f 100644 --- a/src/main/moduleRuntime.ts +++ b/src/main/moduleRuntime.ts @@ -109,8 +109,18 @@ export async function runModuleLifecycleHook( urls: urls ? Object.freeze(urls) : undefined, native: nativeDefinition ? Object.freeze({ - php: join(runtimeRoot(), 'bin', 'php'), - wp: join(runtimeRoot(), 'bin', 'wp'), + php: + process.platform === 'win32' + ? join(runtimeRoot(), 'bin', 'php', 'php.exe') + : join(runtimeRoot(), 'bin', 'php'), + wp: + process.platform === 'win32' + ? join(runtimeRoot(), 'bin', 'php', 'php.exe') + : join(runtimeRoot(), 'bin', 'wp'), + wpPrefixArgs: + process.platform === 'win32' + ? ['-d', 'memory_limit=512M', join(runtimeRoot(), 'tools', 'wp-cli.phar')] + : [], databaseHost: '127.0.0.1', databasePort: nativeDefinition.ports.database, start: () => startNativeProject(nativeDefinition) diff --git a/src/main/native/nativeProject.ts b/src/main/native/nativeProject.ts index d4453e0..c78121b 100644 --- a/src/main/native/nativeProject.ts +++ b/src/main/native/nativeProject.ts @@ -21,6 +21,14 @@ function installedRoot(project: NativeProjectDefinition): string { return project.installedRuntimeRoot ?? runtimeRoot() } +function runtimeExecutable(project: NativeProjectDefinition, name: string): string { + if (process.platform !== 'win32') return join(installedRoot(project), 'bin', name) + if (name === 'php' || name === 'php-cgi') + return join(installedRoot(project), 'bin', 'php', `${name}.exe`) + if (name === 'nginx') return join(installedRoot(project), 'bin', 'nginx', 'nginx.exe') + return join(installedRoot(project), 'bin', 'mariadb', 'bin', `${name}.exe`) +} + function nativeDirectory(root: string): string { return join(root, '.aurora', 'native') } @@ -109,7 +117,10 @@ http { } export function renderNativeAdminerBootstrap(project: NativeProjectDefinition): string { - const adminer = join(installedRoot(project), 'root', 'usr', 'share', 'aurora', 'adminer.php') + const adminer = + process.platform === 'win32' + ? join(installedRoot(project), 'tools', 'adminer.php') + : join(installedRoot(project), 'root', 'usr', 'share', 'aurora', 'adminer.php') return ` @@ -137,8 +148,12 @@ export function renderMariaDbConfig( installedRuntimeRoot = runtimeRoot() ): string { const directory = nativeDirectory(project.root) + const basedir = + process.platform === 'win32' + ? join(installedRuntimeRoot, 'bin', 'mariadb') + : join(installedRuntimeRoot, 'root', 'usr') return `[mariadbd] -basedir=${join(installedRuntimeRoot, 'root', 'usr')} +basedir=${basedir} datadir=${join(directory, 'data', 'mariadb')} tmpdir=${join(directory, 'tmp')} bind-address=127.0.0.1 @@ -172,20 +187,30 @@ export async function provisionNativeProject(project: NativeProjectDefinition): writeFile( join(directory, 'config', 'mariadb.cnf'), renderMariaDbConfig(project, installedRoot(project)) - ) + ), + ...(process.platform === 'win32' + ? [ + writeFile( + join(directory, 'config', 'php.ini'), + `extension_dir="${join(installedRoot(project), 'bin', 'php', 'ext')}"\nextension=mysqli\nextension=pdo_mysql\nextension=mbstring\nextension=curl\nextension=openssl\nextension=zip\ndisplay_errors=On\nlog_errors=On\nerror_log="${join(directory, 'logs', 'php.log')}"\n` + ) + ] + : []) ]) try { await access(join(directory, 'data', 'mariadb', 'mysql')) } catch { await execFileAsync( - join(installedRoot(project), 'bin', 'mariadb-install-db'), - [ - '--no-defaults', - `--datadir=${join(directory, 'data', 'mariadb')}`, - `--tmpdir=${join(directory, 'tmp')}`, - '--auth-root-authentication-method=normal', - '--skip-test-db' - ], + runtimeExecutable(project, 'mariadb-install-db'), + process.platform === 'win32' + ? [`--datadir=${join(directory, 'data', 'mariadb')}`, '--password='] + : [ + '--no-defaults', + `--datadir=${join(directory, 'data', 'mariadb')}`, + `--tmpdir=${join(directory, 'tmp')}`, + '--auth-root-authentication-method=normal', + '--skip-test-db' + ], { cwd: project.root, maxBuffer: 16 * 1024 * 1024 } ) } @@ -202,19 +227,32 @@ export function nativeServiceSpecs(project: NativeProjectDefinition): NativeServ return [ { ...common('database'), - command: join(installedRoot(project), 'bin', 'mariadbd'), - args: [`--defaults-file=${join(directory, 'config', 'mariadb.cnf')}`], + command: runtimeExecutable(project, 'mariadbd'), + args: [ + `--defaults-file=${join(directory, 'config', 'mariadb.cnf')}`, + ...(process.platform === 'win32' ? ['--console'] : []) + ], ready: { port: project.ports.database, timeoutMs: 30000 } }, { ...common('php'), - command: join(installedRoot(project), 'bin', 'php-fpm'), - args: ['--nodaemonize', '--fpm-config', join(directory, 'config', 'php-fpm.conf')], + command: runtimeExecutable(project, process.platform === 'win32' ? 'php-cgi' : 'php-fpm'), + args: + process.platform === 'win32' + ? ['-b', `127.0.0.1:${project.ports.php}`] + : ['--nodaemonize', '--fpm-config', join(directory, 'config', 'php-fpm.conf')], + env: + process.platform === 'win32' + ? { + PHPRC: join(directory, 'config', 'php.ini'), + PHP_FCGI_MAX_REQUESTS: '0' + } + : undefined, ready: { port: project.ports.php } }, { ...common('web'), - command: join(installedRoot(project), 'bin', 'nginx'), + command: runtimeExecutable(project, 'nginx'), args: ['-c', join(directory, 'config', 'nginx.conf'), '-p', `${directory}/`], ready: { port: project.ports.http } } diff --git a/src/main/nativeRuntime.ts b/src/main/nativeRuntime.ts index f7f0d77..b331607 100644 --- a/src/main/nativeRuntime.ts +++ b/src/main/nativeRuntime.ts @@ -136,7 +136,13 @@ export async function installNativeRuntimeArchive(source: string): Promise { + if ((await nativeStatus()).available) return + try { + await installBundledNativeRuntime() + } catch (error) { + // Development builds and platforms without a packaged target keep the manual installer available. + console.warn('Aurora Native bundled runtime was not installed:', error) + } +} + async function nativeStatus(): Promise { if (!supportedPlatforms.has(process.platform) || !supportedArchitectures.has(process.arch)) return {