From bd21884925450993d5b75612e70fc2b3acba8c98 Mon Sep 17 00:00:00 2001 From: reaper Date: Sat, 22 Aug 2026 03:29:57 -0500 Subject: [PATCH] Build portable Linux native runtime --- .gitignore | 1 + docs/AURORA_NATIVE_RUNTIME.md | 2 + eslint.config.mjs | 12 +++- package.json | 1 + runtime-build/linux-x64/Dockerfile | 11 ++++ runtime-build/linux-x64/stage-runtime.sh | 79 ++++++++++++++++++++++++ scripts/build-native-linux-x64.cjs | 38 ++++++++++++ scripts/smoke-native-runtime.cjs | 40 ++++++++++++ 8 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 runtime-build/linux-x64/Dockerfile create mode 100644 runtime-build/linux-x64/stage-runtime.sh create mode 100644 scripts/build-native-linux-x64.cjs create mode 100644 scripts/smoke-native-runtime.cjs diff --git a/.gitignore b/.gitignore index 2a17958..9c99d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Dependencies node_modules/ +.eslintcache # Build output out/ diff --git a/docs/AURORA_NATIVE_RUNTIME.md b/docs/AURORA_NATIVE_RUNTIME.md index 8c39d58..77cd8f4 100644 --- a/docs/AURORA_NATIVE_RUNTIME.md +++ b/docs/AURORA_NATIVE_RUNTIME.md @@ -12,6 +12,8 @@ Each signed runtime bundle contains a `runtime.json` manifest plus versioned exe Runtime archives are created from a staging directory with `npm run build:native-runtime -- `. The packager resolves every executable inside the staging root, calculates its SHA-256 checksum, writes the immutable `runtime.json`, excludes the build template, and creates the distributable archive. Dockside independently verifies those checksums before declaring a runtime available. +The first reproducible bundle target is Linux x64. Run `npm run build:native-linux-x64` on a Docker-capable build machine. Docker is used only to create the portable artifact; users of that artifact do not need Docker. The recipe pins PHP 8.5.9, nginx 1.30.4, and MariaDB 11.8.8 with their runtime libraries, runs version smoke checks outside the build container, and then invokes the normal checksum packager. + ## 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. diff --git a/eslint.config.mjs b/eslint.config.mjs index aff5d3f..957c1f4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,7 +6,17 @@ import eslintPluginReactHooks from 'eslint-plugin-react-hooks' import eslintPluginReactRefresh from 'eslint-plugin-react-refresh' export default defineConfig( - { ignores: ['**/node_modules', '**/dist', '**/out'] }, + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/out/**', + 'dist/**', + 'out/**', + 'build/**', + 'website/**' + ] + }, tseslint.configs.recommended, eslintPluginReact.configs.flat.recommended, eslintPluginReact.configs.flat['jsx-runtime'], diff --git a/package.json b/package.json index 944ffbd..0ec8769 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "build:modules": "node scripts/package-modules.cjs", "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", "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/runtime-build/linux-x64/Dockerfile b/runtime-build/linux-x64/Dockerfile new file mode 100644 index 0000000..04eb05d --- /dev/null +++ b/runtime-build/linux-x64/Dockerfile @@ -0,0 +1,11 @@ +FROM php:8.5.9-fpm-alpine3.23 + +RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \ + nginx=1.30.4-r3 mariadb=11.8.8-r0 mariadb-client=11.8.8-r0 bash file pax-utils \ + && mkdir -p /stage/root /stage/bin /stage/lib + +COPY stage-runtime.sh /usr/local/bin/stage-runtime +RUN chmod +x /usr/local/bin/stage-runtime && /usr/local/bin/stage-runtime + +FROM scratch AS export +COPY --from=0 /stage/ / diff --git a/runtime-build/linux-x64/stage-runtime.sh b/runtime-build/linux-x64/stage-runtime.sh new file mode 100644 index 0000000..6ec30d8 --- /dev/null +++ b/runtime-build/linux-x64/stage-runtime.sh @@ -0,0 +1,79 @@ +#!/bin/sh +set -eu + +stage=/stage +root="$stage/root" + +copy_file() { + source_path="$1" + destination="$root$source_path" + mkdir -p "$(dirname "$destination")" + cp -L "$source_path" "$destination" +} + +copy_binary_and_libraries() { + binary="$1" + copy_file "$binary" + scanelf --needed --nobanner "$binary" | awk '{ print $2 }' | tr ',' '\n' | while read -r library; do + [ -n "$library" ] || continue + library_path=$(find /lib /usr/lib /usr/local/lib -name "$library" -print -quit) + [ -n "$library_path" ] || { echo "Missing library $library for $binary" >&2; exit 1; } + copy_file "$library_path" + done +} + +copy_binary_and_libraries /usr/local/bin/php +copy_binary_and_libraries /usr/local/sbin/php-fpm +copy_binary_and_libraries /usr/sbin/nginx +copy_binary_and_libraries /usr/bin/mariadbd +copy_binary_and_libraries /usr/bin/mariadb-install-db +mkdir -p "$root/lib" "$root/usr/lib" +cp -aL /lib/. "$root/lib/" +cp -aL /usr/lib/. "$root/usr/lib/" + +mkdir -p "$root/usr/local/lib/php/extensions" +if [ -d /usr/local/lib/php/extensions ]; then cp -a /usr/local/lib/php/extensions/. "$root/usr/local/lib/php/extensions/"; fi +mkdir -p "$root/usr/local/etc" +cp -a /usr/local/etc/php "$root/usr/local/etc/" +mkdir -p "$root/usr/share" +if [ -d /usr/share/mariadb ]; then cp -a /usr/share/mariadb "$root/usr/share/"; fi + +cat > "$stage/bin/aurora-exec" <<'EOF' +#!/bin/sh +set -eu +runtime_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +target="$1" +shift +export LD_LIBRARY_PATH="$runtime_root/root/lib:$runtime_root/root/usr/lib:$runtime_root/root/usr/local/lib" +export PHPRC="$runtime_root/root/usr/local/etc/php" +export PHP_INI_SCAN_DIR="$runtime_root/root/usr/local/etc/php/conf.d" +exec "$runtime_root/root/lib/ld-musl-x86_64.so.1" --library-path "$LD_LIBRARY_PATH" "$runtime_root/root$target" "$@" +EOF +chmod +x "$stage/bin/aurora-exec" + +for entry in 'php:/usr/local/bin/php' 'php-fpm:/usr/local/sbin/php-fpm' 'nginx:/usr/sbin/nginx' 'mariadbd:/usr/bin/mariadbd' 'mariadb-install-db:/usr/bin/mariadb-install-db'; do + name=${entry%%:*} + target=${entry#*:} + cat > "$stage/bin/$name" <&1 | sed 's#nginx version: nginx/##') +mariadb_version=$(mariadbd --version | sed -n 's/.* Ver \([^ -]*\).*/\1/p') +cat > "$stage/runtime.template.json" <') +const template = JSON.parse(readFileSync(join(root, 'runtime.template.json'), 'utf8')) +const checks = [ + [ + 'PHP', + join(root, 'bin/php'), + ['--version'], + template.components.find((item) => item.id === 'php')?.version + ], + [ + 'nginx', + join(root, 'bin/nginx'), + ['-v'], + template.components.find((item) => item.id === 'nginx')?.version + ], + [ + 'MariaDB', + join(root, 'bin/mariadbd'), + ['--version'], + template.components.find((item) => item.id === 'mariadb')?.version + ] +] +for (const [name, command, args, version] of checks) { + const result = spawnSync(command, args, { encoding: 'utf8' }) + const output = `${result.stdout || ''}${result.stderr || ''}` + if (result.error || result.status !== 0) + throw result.error || new Error(`${name} smoke check failed: ${output}`) + if (!version || !output.includes(version)) + throw new Error(`${name} did not report expected version ${version}: ${output}`) + process.stdout.write(`${name} ${version} OK\n`) +}