diff --git a/README.md b/README.md index b0178b6..1f6c059 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A tool for building and packaging PHP and shared extensions with static-php-cli. 1. Clone the repository: ``` - git clone https://github.com/static-php/spc-packages.git + git clone https://github.com/static-php/packages.git cd spc-packages ``` diff --git a/bin/createrepo_static b/bin/createrepo_static index 510a397..871eb18 100755 --- a/bin/createrepo_static +++ b/bin/createrepo_static @@ -56,14 +56,36 @@ def parse_rpm_info(filename): stream = f"8.{php_patch[-1]}" return name, version, release, arch, stream + # Match static-php + match_static = re.match(r'(static-php)-(?P\d+)-(?P\d+)\.(?P[^.]+)\.rpm', basename) + if match_static: + name = match_static.group(1) + version = match_static.group("version") + release = match_static.group("release") + arch = match_static.group("arch") + stream = "common" + return name, version, release, arch, stream + return None def build_module_structure(rpm_map, platform): documents = [] timestamp = int(datetime.now(timezone.utc).strftime('%Y%m%d')) + # Collect common artifacts (static-php) + common_artifacts = [] + if "common" in rpm_map: + for pkg in rpm_map["common"]: + info = parse_rpm_info(pkg) + if info: + name, version, release, arch, _ = info + common_artifacts.append(f"{name}-0:{version}-{release}.{arch}") + for stream, pkg_list in sorted(rpm_map.items()): - artifacts = [] + if stream == "common": + continue + + artifacts = common_artifacts.copy() for pkg in sorted(pkg_list): info = parse_rpm_info(pkg) @@ -128,10 +150,12 @@ for rpm in rpm_files: # Build modules.yaml modules_yaml = build_module_structure(rpm_map, platform) -# Determine highest stream for default +# Determine highest stream for default (exclude "common") if rpm_map: - default_stream = sorted(rpm_map.keys())[-1] # Use highest PHP version - modules_yaml.append(build_defaults_document(default_stream)) + streams = [s for s in rpm_map.keys() if s != "common"] + if streams: + default_stream = sorted(streams)[-1] + modules_yaml.append(build_defaults_document(default_stream)) output_path = os.path.join(os.getcwd(), "modules.yaml") with open(output_path, "w") as f: diff --git a/bin/spp b/bin/spp index 3623415..b71f299 100755 --- a/bin/spp +++ b/bin/spp @@ -1,15 +1,13 @@ #!/usr/bin/env php -zts + // php-zts8.3 -> -zts8.3 + // php-zts83 -> -zts83 + return str_replace('php', '', $prefix); +} + +function getVarLibdir(): string +{ + // Get /var/lib/{prefix} path + $prefix = \staticphp\step\CreatePackages::getPrefix(); + return '/var/lib/' . $prefix; +} + +function getSharedir(): string +{ + // Get /usr/share/{prefix} path + $prefix = \staticphp\step\CreatePackages::getPrefix(); + return '/usr/share/' . $prefix; } diff --git a/composer.json b/composer.json index 5912514..6c547af 100644 --- a/composer.json +++ b/composer.json @@ -23,9 +23,10 @@ "prefer-stable": true, "require": { "php": ">=8.4", + "ext-yaml": "*", + "ext-zlib": "*", "crazywhalecc/static-php-cli": "dev-henderkes-patch-1", "laravel/helpers": "^1.7", - "ext-zlib": "*", "twig/twig": "^3.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index d01e798..4f96ca8 100644 --- a/composer.lock +++ b/composer.lock @@ -81,12 +81,12 @@ "source": { "type": "git", "url": "https://github.com/crazywhalecc/static-php-cli.git", - "reference": "0eda08d9bc7eb04c5f8e83a3e16a14b1a1825458" + "reference": "53f7cdefe0e791d621d86cc2ce6938d228ec3b19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/crazywhalecc/static-php-cli/zipball/0eda08d9bc7eb04c5f8e83a3e16a14b1a1825458", - "reference": "0eda08d9bc7eb04c5f8e83a3e16a14b1a1825458", + "url": "https://api.github.com/repos/crazywhalecc/static-php-cli/zipball/53f7cdefe0e791d621d86cc2ce6938d228ec3b19", + "reference": "53f7cdefe0e791d621d86cc2ce6938d228ec3b19", "shasum": "" }, "require": { @@ -141,7 +141,7 @@ "type": "other" } ], - "time": "2025-12-18T13:52:02+00:00" + "time": "2025-12-18T19:12:01+00:00" }, { "name": "doctrine/inflector", @@ -2228,12 +2228,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "e5034c4df32edeafb119b2c1e2b58876d0286ea8" + "reference": "f960b7bc5c3bae7e348f7b65736072f74493bc3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/e5034c4df32edeafb119b2c1e2b58876d0286ea8", - "reference": "e5034c4df32edeafb119b2c1e2b58876d0286ea8", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/f960b7bc5c3bae7e348f7b65736072f74493bc3a", + "reference": "f960b7bc5c3bae7e348f7b65736072f74493bc3a", "shasum": "" }, "conflict": { @@ -2286,7 +2286,7 @@ "automad/automad": "<2.0.0.0-alpha5", "automattic/jetpack": "<9.8", "awesome-support/awesome-support": "<=6.0.7", - "aws/aws-sdk-php": "<3.288.1", + "aws/aws-sdk-php": "<3.368", "azuracast/azuracast": "<=0.23.1", "b13/seo_basics": "<0.8.2", "backdrop/backdrop": "<=1.32", @@ -3225,7 +3225,7 @@ "type": "tidelift" } ], - "time": "2025-12-17T21:06:23+00:00" + "time": "2025-12-18T19:06:20+00:00" } ], "aliases": [], diff --git a/config/static-php.repo b/config/static-php.repo index 43be095..cb2bc5e 100644 --- a/config/static-php.repo +++ b/config/static-php.repo @@ -4,3 +4,12 @@ baseurl=https://rpm.henderkes.com/\$basearch/el\$releasever enabled=1 gpgcheck=1 gpgkey=https://key.henderkes.com/static-php.gpg +excludepkgs=*-debuginfo + +[static-php-debuginfo] +name=Static PHP repository - Debuginfo +baseurl=https://rpm.henderkes.com/$basearch/el$releasever +enabled=0 +gpgcheck=1 +gpgkey=https://key.henderkes.com/static-php.asc +includepkgs=*-debuginfo diff --git a/config/templates/craft.yml.twig b/config/templates/craft.yml.twig index 0f8d2c2..4ce9f7d 100644 --- a/config/templates/craft.yml.twig +++ b/config/templates/craft.yml.twig @@ -47,11 +47,7 @@ download-options: no-alt: false shallow-clone: true {% if php_version == '8.5' %} - ignore-cache-sources: 'php-src,swoole,apcu,pdo_sqlsrv' - custom-url: - - 'pdo_sqlsrv:https://github.com/stancl/msphpsql/archive/refs/heads/push-posxmnwpksqv.tar.gz' - - 'swoole:https://github.com/swoole/swoole-src/archive/eb726b72b6caf4a86bf0af2d29fca40b56d352e9.tar.gz' - - 'amqp:https://github.com/php-amqp/php-amqp/archive/refs/pull/595/merge.tar.gz' + ignore-cache-sources: '' {% else -%} ignore-cache-sources: 'php-src' {% endif -%} @@ -87,7 +83,7 @@ extra-env: {% else -%} SPC_TARGET: '{{ target }}' {% endif -%} - EXTENSION_DIR: "{{ is_rhel ? '/usr/lib64/php-zts/modules' : '/usr/lib/php-zts/modules' }}" + EXTENSION_DIR: "{{ moduledir }}" SPC_CMD_VAR_PHP_EMBED_TYPE: 'shared' SPC_MICRO_PATCHES: SPC_DEFAULT_C_FLAGS: "{{ cflags }}" diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index 588c86e..ef10311 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -44,7 +44,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) protected function createDirectories(): void { - $paths = [BUILD_ROOT_PATH, BUILD_BIN_PATH, BUILD_LIB_PATH, BUILD_MODULES_PATH, DIST_PATH, DIST_RPM_PATH, DIST_DEB_PATH]; + $paths = [BUILD_ROOT_PATH, BUILD_BIN_PATH, BUILD_LIB_PATH, BUILD_MODULES_PATH, DIST_PATH, DIST_RPM_PATH, DIST_DEB_PATH, DIST_APK_PATH]; foreach ($paths as $path) { if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) { throw new \RuntimeException("Failed to create directory: " . $path); diff --git a/src/extension.php b/src/extension.php index f448603..8395cef 100644 --- a/src/extension.php +++ b/src/extension.php @@ -146,18 +146,24 @@ public function getFpmConfig(): array $depends = array_merge($depends, $ordered); + $versionedConflicts = CreatePackages::getVersionedConflicts('-' . $this->name); return [ 'config-files' => [ getConfdir() . '/conf.d/' . $this->prefix . $this->name . '.ini', ], 'depends' => $depends, + 'provides' => [ + 'php-zts-' . $this->name, + ], + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, 'files' => [ ...($this->getIniPath() ? [$this->getIniPath() => getConfdir() . '/conf.d/' . $this->prefix . $this->name . '.ini'] : [] ), ...($this->isSharedExtension() ? - [BUILD_MODULES_PATH . '/' . $this->name . '.so' => getLibdir() . '/' . CreatePackages::getPrefix() . '/modules/' . $this->name . '.so'] + [BUILD_MODULES_PATH . '/' . $this->name . '.so' => getModuledir() . '/' . $this->name . '.so'] : [] ), ] @@ -179,9 +185,24 @@ protected function getIniPath(): ?string } $tempIniPath = TEMP_DIR . '/' . $this->prefix . $this->name . '.ini'; $iniContent = file_get_contents($iniPath); + + // Get the dynamic prefix for path replacements + $prefix = CreatePackages::getPrefix(); + + // Replace extension directives and versioned paths $iniContent = str_replace( - [';extension=' . $this->name, ';zend_extension=' . $this->name], - ['extension=' . $this->name, 'zend_extension=' . $this->name], + [ + ';extension=' . $this->name, + ';zend_extension=' . $this->name, + '/usr/share/php-zts/', + '/usr/local/share/php-zts/', + ], + [ + 'extension=' . $this->name, + 'zend_extension=' . $this->name, + '/usr/share/' . $prefix . '/', + '/usr/local/share/' . $prefix . '/', + ], $iniContent ); file_put_contents($tempIniPath, $iniContent); @@ -209,7 +230,7 @@ public function getDebuginfoFpmConfig(): array if (!file_exists($src)) { return []; } - $targetSo = getLibdir() . '/' . CreatePackages::getPrefix() . '/modules/' . $this->name . '.so'; + $targetSo = getModuledir() . '/' . $this->name . '.so'; $target = '/usr/lib/debug' . $targetSo . '.debug'; return [ 'depends' => [CreatePackages::getPrefix() . '-' . $this->name], diff --git a/src/package/cgi.php b/src/package/cgi.php index 05cc07e..f91d8b6 100644 --- a/src/package/cgi.php +++ b/src/package/cgi.php @@ -14,12 +14,18 @@ public function getName(): string public function getFpmConfig(): array { + $versionedConflicts = CreatePackages::getVersionedConflicts('-cgi'); return [ 'depends' => [ CreatePackages::getPrefix() . '-cli', ], + 'provides' => [ + 'php-zts-cgi', + ], + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, 'files' => [ - BUILD_BIN_PATH . '/php-cgi' => '/usr/bin/php-cgi-zts', + BUILD_BIN_PATH . '/php-cgi' => '/usr/bin/php-cgi' . getBinarySuffix(), ] ]; } @@ -38,7 +44,7 @@ public function getDebuginfoFpmConfig(): array return [ 'depends' => [CreatePackages::getPrefix() . '-cgi'], 'files' => [ - $src => '/usr/lib/debug/usr/bin/php-cgi-zts.debug', + $src => '/usr/lib/debug/usr/bin/php-cgi' . getBinarySuffix() . '.debug', ], ]; } diff --git a/src/package/cli.php b/src/package/cli.php index 5e11c7c..8018439 100644 --- a/src/package/cli.php +++ b/src/package/cli.php @@ -17,19 +17,32 @@ public function getFpmConfig(): array { $config = CraftConfig::getInstance(); $staticExtensions = $config->getStaticExtensions(); + $prefix = CreatePackages::getPrefix(); $contents = file_get_contents(INI_PATH . '/php.ini'); - $contents = str_replace('$libdir', getLibdir() . '/' . CreatePackages::getPrefix(), $contents); + $contents = str_replace( + [ + '$libdir', + '/etc/php-zts', + ], + [ + getPhpLibdir(), + getConfdir(), + ], + $contents + ); file_put_contents(TEMP_DIR . '/php.ini', $contents); - $provides = ['php-zts']; - $replaces = []; + $provides = ['php-zts', $prefix]; + $versionedConflicts = CreatePackages::getVersionedConflicts('-cli'); + $replaces = $versionedConflicts; + $conflicts = $versionedConflicts; $configFiles = [ getConfdir(), getConfdir() . '/php.ini' ]; $files = [ TEMP_DIR . '/php.ini' => getConfdir() . '/php.ini', - BUILD_BIN_PATH . '/php' => '/usr/bin/php-zts', + BUILD_BIN_PATH . '/php' => '/usr/bin/php' . getBinarySuffix(), ]; foreach ($staticExtensions as $ext) { @@ -39,7 +52,23 @@ public function getFpmConfig(): array // Add .ini files for statically compiled extensions $iniFile = INI_PATH . "/extension/{$ext}.ini"; if (file_exists($iniFile)) { - $files[$iniFile] = getConfdir() . "/conf.d/{$ext}.ini"; + // Process the .ini file to replace hardcoded paths + $iniContents = file_get_contents($iniFile); + $iniContents = str_replace( + [ + '/usr/share/php-zts/', + '/usr/local/share/php-zts/', + ], + [ + getSharedir() . '/', + '/usr/local/share/' . $prefix . '/', + ], + $iniContents + ); + $tempIniPath = TEMP_DIR . "/{$ext}.ini"; + file_put_contents($tempIniPath, $iniContents); + + $files[$tempIniPath] = getConfdir() . "/conf.d/{$ext}.ini"; $configFiles[] = getConfdir() . "/conf.d/{$ext}.ini"; } } @@ -47,39 +76,41 @@ public function getFpmConfig(): array if (!file_exists(BUILD_ROOT_PATH . '/license/LICENSE')) { copy(BASE_PATH . '/LICENSE', BUILD_ROOT_PATH . '/license/LICENSE'); } - $files[BUILD_ROOT_PATH . '/license'] = '/usr/share/licenses/php-zts/'; + $files[BUILD_ROOT_PATH . '/license'] = '/usr/share/licenses/' . CreatePackages::getPrefix() . '/'; return [ 'config-files' => $configFiles, 'empty_directories' => [ - '/usr/share/php-zts/preload', - '/var/lib/php-zts/session', - '/var/lib/php-zts/wsdlcache', - '/var/lib/php-zts/opcache', + getSharedir() . '/preload', + getVarLibdir() . '/session', + getVarLibdir() . '/wsdlcache', + getVarLibdir() . '/opcache', ], 'directories' => [ - '/usr/share/php-zts/preload', - '/var/lib/php-zts/session', - '/var/lib/php-zts/wsdlcache', - '/var/lib/php-zts/opcache', + getSharedir() . '/preload', + getVarLibdir() . '/session', + getVarLibdir() . '/wsdlcache', + getVarLibdir() . '/opcache', ], 'provides' => $provides, 'replaces' => $replaces, + 'conflicts' => $conflicts, 'files' => $files ]; } public function getFpmExtraArgs(): array { - $afterInstallScript = <<<'BASH' -#!/bin/bash + $binarySuffix = getBinarySuffix(); + $afterInstallScript = << [CreatePackages::getPrefix() . '-cli'], 'files' => [ diff --git a/src/package/devel.php b/src/package/devel.php index 18d7c83..9ab1ca8 100644 --- a/src/package/devel.php +++ b/src/package/devel.php @@ -38,7 +38,7 @@ public function getFpmConfig(): array $phpConfigContent ); $phpVersion = str_replace('.', '', SPP_PHP_VERSION); - $libName = 'lib' . CreatePackages::getPrefix() . "-$phpVersion.so"; + $libName = 'libphp-zts-' . $phpVersion . '.so'; $phpConfigContent = str_replace('libphp.so', $libName, $phpConfigContent); file_put_contents($modifiedPhpConfigPath, $phpConfigContent); @@ -55,7 +55,7 @@ public function getFpmConfig(): array ], [ 'prefix="/usr"', - 'datarootdir="/php-zts"', + 'datarootdir="/' . \staticphp\step\CreatePackages::getPrefix() . '"', ], $phpizeContent ); @@ -65,8 +65,8 @@ public function getFpmConfig(): array '"`eval echo ${prefix}/include`/php"' ], [ - str_replace('/usr/', '', getLibdir()) . '/' . CreatePackages::getPrefix() . '`', - '"`eval echo ${prefix}/include`/' . CreatePackages::getPrefix() . '"' + str_replace('/usr/', '', getPhpLibdir()) . '`', + '"`eval echo ${prefix}/include`/' . \staticphp\step\CreatePackages::getPrefix() . '"' ], $phpizeContent ); @@ -74,20 +74,24 @@ public function getFpmConfig(): array file_put_contents($modifiedPhpizePath, $phpizeContent); chmod($modifiedPhpizePath, 0755); + $versionedConflicts = CreatePackages::getVersionedConflicts('-devel'); return [ 'files' => [ - $modifiedPhpConfigPath => '/usr/bin/php-config-zts', - $modifiedPhpizePath => '/usr/bin/phpize-zts', - BUILD_INCLUDE_PATH . '/php/' => '/usr/include/php-zts', - BUILD_LIB_PATH . '/php/build' => getLibdir() . '/' . CreatePackages::getPrefix(), + $modifiedPhpConfigPath => '/usr/bin/php-config' . getBinarySuffix(), + $modifiedPhpizePath => '/usr/bin/phpize' . getBinarySuffix(), + BUILD_INCLUDE_PATH . '/php/' => '/usr/include/' . \staticphp\step\CreatePackages::getPrefix(), + BUILD_LIB_PATH . '/php/build' => getPhpLibdir(), ], 'depends' => [ CreatePackages::getPrefix() . '-cli', ], 'provides' => [ - 'php-config-zts', - 'phpize-zts', - ] + 'php-zts-devel', + 'php-config' . getBinarySuffix(), + 'phpize' . getBinarySuffix(), + ], + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, ]; } diff --git a/src/package/embed.php b/src/package/embed.php index dd1ab78..5f7c658 100644 --- a/src/package/embed.php +++ b/src/package/embed.php @@ -15,15 +15,21 @@ public function getName(): string public function getFpmConfig(): array { $phpVersion = str_replace('.', '', SPP_PHP_VERSION); - $name = 'lib' . CreatePackages::getPrefix() . "-$phpVersion.so"; + $name = 'libphp-zts-' . $phpVersion . '.so'; + $versionedConflicts = CreatePackages::getVersionedConflicts('-embed'); return [ 'depends' => [ CreatePackages::getPrefix() . '-cli', ], 'provides' => [ $name, + 'php-zts-embed', + CreatePackages::getPrefix() . '-embed', + 'php-zts-embedded', CreatePackages::getPrefix() . '-embedded' ], + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, 'files' => [ BUILD_LIB_PATH . '/' . $name => getLibdir() . '/' . $name, ] @@ -38,7 +44,7 @@ public function getFpmExtraArgs(): array public function getDebuginfoFpmConfig(): array { $phpVersionDigits = str_replace('.', '', SPP_PHP_VERSION); - $libName = 'lib' . CreatePackages::getPrefix() . "-{$phpVersionDigits}.so"; + $libName = 'libphp-zts-' . $phpVersionDigits . '.so'; $src = BUILD_ROOT_PATH . '/debug/' . $libName . '.debug'; if (!file_exists($src)) { return []; diff --git a/src/package/fpm.php b/src/package/fpm.php index 6092f31..95ad371 100644 --- a/src/package/fpm.php +++ b/src/package/fpm.php @@ -11,29 +11,61 @@ public function getName(): string { return CreatePackages::getPrefix() . '-fpm'; } - + public function getFpmConfig(): array { $contents = file_get_contents(INI_PATH . '/php-fpm.conf'); $contents = str_replace('$confdir', getConfdir(), $contents); file_put_contents(TEMP_DIR . '/php-fpm.conf', $contents); + + // Process the systemd service file to replace hardcoded paths + $serviceContents = file_get_contents(INI_PATH . '/php-fpm.service'); + $binarySuffix = getBinarySuffix(); + $serviceContents = str_replace( + [ + '/usr/sbin/php-fpm-zts', + 'RuntimeDirectory=php-fpm-zts', + ], + [ + '/usr/sbin/php-fpm' . $binarySuffix, + 'RuntimeDirectory=php-fpm' . $binarySuffix, + ], + $serviceContents + ); + file_put_contents(TEMP_DIR . '/php-fpm.service', $serviceContents); + + // Process www.conf to replace hardcoded paths + $wwwContents = file_get_contents(INI_PATH . '/www.conf'); + $wwwContents = str_replace( + '/var/lib/php-zts/', + getVarLibdir() . '/', + $wwwContents + ); + file_put_contents(TEMP_DIR . '/www.conf', $wwwContents); + + $versionedConflicts = CreatePackages::getVersionedConflicts('-fpm'); return [ 'depends' => [ CreatePackages::getPrefix() . '-cli', ], + 'provides' => [ + 'php-zts-fpm', + ], + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, 'files' => [ TEMP_DIR . '/php-fpm.conf' => getConfdir() . '/php-fpm.conf', - INI_PATH . '/www.conf' => getConfdir() . '/fpm.d/www.conf', - INI_PATH . '/php-fpm.service' => '/usr/lib/systemd/system/php-fpm-zts.service', - BUILD_BIN_PATH . '/php-fpm' => '/usr/sbin/php-fpm-zts', + TEMP_DIR . '/www.conf' => getConfdir() . '/fpm.d/www.conf', + TEMP_DIR . '/php-fpm.service' => '/usr/lib/systemd/system/php-fpm' . getBinarySuffix() . '.service', + BUILD_BIN_PATH . '/php-fpm' => '/usr/sbin/php-fpm' . getBinarySuffix(), ], 'empty_directories' => [ getConfdir() . '/fpm.d/', - '/var/log/php-zts/php-fpm', + '/var/log/' . CreatePackages::getPrefix() . '/php-fpm', ], 'directories' => [ getConfdir() . '/fpm.d/', - '/var/log/php-zts/php-fpm', + '/var/log/' . CreatePackages::getPrefix() . '/php-fpm', ], ]; } @@ -49,7 +81,7 @@ public function getDebuginfoFpmConfig(): array if (!file_exists($src)) { return []; } - $target = '/usr/lib/debug/usr/sbin/php-fpm-zts.debug'; + $target = '/usr/lib/debug/usr/sbin/php-fpm' . getBinarySuffix() . '.debug'; return [ 'depends' => [CreatePackages::getPrefix() . '-fpm'], 'files' => [ diff --git a/src/package/frankenphp.php b/src/package/frankenphp.php index a1f9b0e..a58d28f 100644 --- a/src/package/frankenphp.php +++ b/src/package/frankenphp.php @@ -10,6 +10,14 @@ class frankenphp implements package { public function getName(): string { + // RPM packages use frankenphp (unversioned, for module system) + // DEB/APK packages use versioned frankenphp8.3 or frankenphp83 + $prefix = CreatePackages::getPrefix(); + + // Extract version from prefix (e.g., "php-zts8.3" -> "8.3", "php-zts" -> "") + if (preg_match('/php-zts(\d+\.?\d*)/', $prefix, $matches)) { + return 'frankenphp' . $matches[1]; + } return 'frankenphp'; } @@ -33,6 +41,24 @@ public function getLicense(): string return 'MIT'; } + /** + * Get list of versioned frankenphp packages to conflict/replace with + * For RPM packages, returns empty array (RPM uses module system instead) + */ + private function getVersionedConflicts(): array + { + // Get the conflicts list from CreatePackages using the franken prefix + $phpConflicts = CreatePackages::getVersionedConflicts(''); + + // Transform php-zts8.3 conflicts to frankenphp8.3 conflicts + $conflicts = []; + foreach ($phpConflicts as $conflict) { + $conflicts[] = str_replace('php-zts', 'frankenphp', $conflict); + } + + return $conflicts; + } + /** * Create FrankenPHP packages (both RPM and DEB) */ @@ -50,6 +76,9 @@ public function createPackages(array $packageTypes, array $binaryDependencies, ? if (in_array('deb', $packageTypes, true)) { $this->createDebPackage($architecture, $binaryDependencies, $iterationOverride); } + if (in_array('apk', $packageTypes, true)) { + $this->createApkPackage($architecture, $binaryDependencies, $iterationOverride); + } } /** @@ -57,24 +86,26 @@ public function createPackages(array $packageTypes, array $binaryDependencies, ? */ public function createRpmPackage(string $architecture, array $binaryDependencies, ?string $iterationOverride = null): void { + CreatePackages::setCurrentPackageType('rpm'); echo "Creating RPM package for FrankenPHP...\n"; $packageFolder = DIST_PATH . '/frankenphp/package'; $phpVersion = str_replace('.', '', SPP_PHP_VERSION); - $phpEmbedName = 'lib' . CreatePackages::getPrefix() . '-' . $phpVersion . '.so'; + $phpEmbedName = 'libphp-zts-' . $phpVersion . '.so'; $ldLibraryPath = 'LD_LIBRARY_PATH=' . BUILD_LIB_PATH; [, $output] = shell()->execWithResult($ldLibraryPath . ' ' . BUILD_BIN_PATH . '/frankenphp --version'); $output = implode("\n", $output); preg_match('/FrankenPHP v(\d+\.\d+\.\d+)/', $output, $matches); - $latestTag = $matches[1]; - $version = $latestTag . '_' . $phpVersion; + $version = $matches[1]; - $name = "frankenphp"; + $name = $this->getName(); $computed = (string)$this->getNextIteration($name, $version, $architecture); $iteration = $iterationOverride ?? $computed; + $versionedConflicts = $this->getVersionedConflicts(); + $fpmArgs = [ 'fpm', '-s', 'dir', @@ -85,8 +116,16 @@ public function createRpmPackage(string $architecture, array $binaryDependencies '-v', $version, '--license', $this->getLicense(), '--config-files', '/etc/frankenphp/Caddyfile', + '--provides', 'frankenphp', ]; + foreach ($versionedConflicts as $conflict) { + $fpmArgs[] = '--conflicts'; + $fpmArgs[] = $conflict; + $fpmArgs[] = '--replaces'; + $fpmArgs[] = $conflict; + } + foreach ($binaryDependencies as $lib => $dependencyVersion) { $fpmArgs[] = '--depends'; $fpmArgs[] = "$lib({$dependencyVersion})(64bit)"; @@ -156,11 +195,12 @@ public function createRpmPackage(string $architecture, array $binaryDependencies */ public function createDebPackage(string $architecture, array $binaryDependencies, ?string $iterationOverride = null): void { + CreatePackages::setCurrentPackageType('deb'); echo "Creating DEB package for FrankenPHP...\n"; $packageFolder = DIST_PATH . '/frankenphp/package'; $phpVersion = str_replace('.', '', SPP_PHP_VERSION); - $phpEmbedName = 'lib' . CreatePackages::getPrefix() . '-' . $phpVersion . '.so'; + $phpEmbedName = 'libphp-zts-' . $phpVersion . '.so'; $ldLibraryPath = 'LD_LIBRARY_PATH=' . BUILD_LIB_PATH; [, $output] = shell()->execWithResult($ldLibraryPath . ' ' . BUILD_BIN_PATH . '/frankenphp --version'); @@ -168,12 +208,14 @@ public function createDebPackage(string $architecture, array $binaryDependencies preg_match('/FrankenPHP v(\d+\.\d+\.\d+)/', $output, $matches); $version = $matches[1]; - $name = "frankenphp"; + $name = $this->getName(); $computed = (string)$this->getNextIteration($name, $version, $architecture); $iteration = $iterationOverride ?? $computed; $debIteration = $iteration; + $versionedConflicts = $this->getVersionedConflicts(); + $fpmArgs = [ 'fpm', '-s', 'dir', @@ -184,8 +226,16 @@ public function createDebPackage(string $architecture, array $binaryDependencies '-v', $version, '--license', $this->getLicense(), '--config-files', '/etc/frankenphp/Caddyfile', + '--provides', 'frankenphp', ]; + foreach ($versionedConflicts as $conflict) { + $fpmArgs[] = '--conflicts'; + $fpmArgs[] = $conflict; + $fpmArgs[] = '--replaces'; + $fpmArgs[] = $conflict; + } + $systemLibraryMap = [ 'ld-linux-x86-64.so.2' => 'libc6', 'ld-linux-aarch64.so.1' => 'libc6', @@ -218,6 +268,14 @@ public function createDebPackage(string $architecture, array $binaryDependencies throw new \RuntimeException(sprintf('Directory "%s" was not created', "{$packageFolder}/empty/")); } + // Determine the FrankenPHP suffix (just version, not -zts prefix) + // Extract version from package name: frankenphp8.5 or frankenphp85 + $prefix = CreatePackages::getPrefix(); + $frankenphpSuffix = ''; + if (preg_match('/php-zts(\d+\.?\d*)/', $prefix, $matches)) { + $frankenphpSuffix = $matches[1]; + } + $fpmArgs = [...$fpmArgs, ...[ '--depends', $phpEmbedName, '--after-install', "{$packageFolder}/debian/postinst.sh", @@ -226,8 +284,8 @@ public function createDebPackage(string $architecture, array $binaryDependencies '--iteration', $debIteration, '--rpm-user', 'frankenphp', '--rpm-group', 'frankenphp', - BUILD_BIN_PATH . '/frankenphp=/usr/bin/frankenphp', - "{$packageFolder}/debian/frankenphp.service=/usr/lib/systemd/system/frankenphp.service", + BUILD_BIN_PATH . '/frankenphp=/usr/bin/frankenphp' . $frankenphpSuffix, + "{$packageFolder}/debian/frankenphp.service=/usr/lib/systemd/system/frankenphp{$frankenphpSuffix}.service", "{$packageFolder}/Caddyfile=/etc/frankenphp/Caddyfile", "{$packageFolder}/content/=/usr/share/frankenphp", "{$packageFolder}/empty/=/var/lib/frankenphp" @@ -269,6 +327,221 @@ public function createDebPackage(string $architecture, array $binaryDependencies } } + /** + * Create APK package for FrankenPHP + */ + public function createApkPackage(string $architecture, array $binaryDependencies, ?string $iterationOverride = null): void + { + CreatePackages::setCurrentPackageType('apk'); + echo "Creating APK package for FrankenPHP using nfpm...\n"; + + $packageFolder = DIST_PATH . '/frankenphp/package'; + $phpVersion = str_replace('.', '', SPP_PHP_VERSION); + $phpEmbedName = 'libphp-zts-' . $phpVersion . '.so'; + + $ldLibraryPath = 'LD_LIBRARY_PATH=' . BUILD_LIB_PATH; + [, $output] = shell()->execWithResult($ldLibraryPath . ' ' . BUILD_BIN_PATH . '/frankenphp --version'); + $output = implode("\n", $output); + preg_match('/FrankenPHP v(\d+\.\d+\.\d+)/', $output, $matches); + $version = $matches[1]; + + $name = $this->getName(); + + $computed = (string)$this->getNextIteration($name, $version, $architecture); + $iteration = $iterationOverride ?? $computed; + + $versionedConflicts = $this->getVersionedConflicts(); + + // Build nfpm config + $nfpmConfig = [ + 'name' => $name, + 'arch' => $architecture, + 'platform' => 'linux', + 'version' => $version, + 'release' => $iteration, + 'section' => 'default', + 'priority' => 'optional', + 'maintainer' => 'Marc Henderkes ', + 'description' => "FrankenPHP - Modern PHP application server", + 'vendor' => 'Marc Henderkes', + 'homepage' => 'https://apks.henderkes.com', + 'license' => $this->getLicense(), + 'apk' => [ + 'signature' => [ + 'key_name' => CreatePackages::getPrefix(), + ], + ], + ]; + + // Build dependencies + $depends = [$phpEmbedName]; + + // Alpine library dependencies + $alpineLibMap = [ + 'ld-linux-x86-64.so.2' => 'musl', + 'ld-linux-aarch64.so.1' => 'musl', + 'libc.so.6' => 'musl', + 'libm.so.6' => 'musl', + 'libpthread.so.0' => 'musl', + 'libutil.so.1' => 'musl', + 'libdl.so.2' => 'musl', + 'librt.so.1' => 'musl', + 'libresolv.so.2' => 'musl', + 'libgcc_s.so.1' => 'libgcc', + 'libstdc++.so.6' => 'libstdc++', + ]; + + foreach ($binaryDependencies as $lib => $ver) { + if (isset($alpineLibMap[$lib])) { + $packageName = $alpineLibMap[$lib]; + } else { + $packageName = preg_replace('/\.so(\.\d+)*$/', '', $lib); + } + $numericVersion = preg_replace('/[^0-9.]/', '', $ver); + $depends[] = "{$packageName}>={$numericVersion}"; + } + + $nfpmConfig['depends'] = $depends; + $nfpmConfig['provides'] = ['frankenphp']; + $nfpmConfig['replaces'] = $versionedConflicts; + $nfpmConfig['conflicts'] = $versionedConflicts; + + // Determine the FrankenPHP suffix + $prefix = CreatePackages::getPrefix(); + $frankenphpSuffix = ''; + if (preg_match('/php-zts(\d+\.?\d*)/', $prefix, $matches)) { + $frankenphpSuffix = $matches[1]; + } + + $alpineFolder = BASE_PATH . '/src/package/frankenphp'; + + // Build contents + $contents = [ + [ + 'src' => BUILD_BIN_PATH . '/frankenphp', + 'dst' => '/usr/bin/frankenphp' . $frankenphpSuffix, + ], + [ + 'src' => "{$alpineFolder}/alpine/frankenphp.openrc", + 'dst' => "/etc/init.d/frankenphp{$frankenphpSuffix}", + ], + [ + 'src' => "{$packageFolder}/Caddyfile", + 'dst' => '/etc/frankenphp/Caddyfile', + 'type' => 'config', + ], + [ + 'src' => "{$packageFolder}/content/", + 'dst' => '/usr/share/frankenphp/', + ], + [ + 'dst' => '/var/lib/frankenphp', + 'type' => 'dir', + ], + [ + 'dst' => '/etc/frankenphp/Caddyfile.d', + 'type' => 'dir', + ], + ]; + + $nfpmConfig['contents'] = $contents; + + // Add scripts + $nfpmConfig['scripts'] = [ + 'postinstall' => "{$alpineFolder}/alpine/post-install.sh", + 'preremove' => "{$alpineFolder}/alpine/pre-deinstall.sh", + 'postremove' => "{$alpineFolder}/alpine/post-deinstall.sh", + ]; + + // Write nfpm config + $nfpmConfigFile = TEMP_DIR . "/nfpm-{$name}.yaml"; + if (!yaml_emit_file($nfpmConfigFile, $nfpmConfig, YAML_UTF8_ENCODING)) { + throw new \RuntimeException("Failed to write YAML file: {$nfpmConfigFile}"); + } + + echo "nfpm config written to: {$nfpmConfigFile}\n"; + + // Run nfpm + $outputFile = DIST_APK_PATH . "/{$name}-{$version}-r{$iteration}.{$architecture}.apk"; + $nfpmProcess = new Process([ + 'nfpm', 'package', + '--config', $nfpmConfigFile, + '--packager', 'apk', + '--target', $outputFile + ]); + $nfpmProcess->setTimeout(null); + $nfpmProcess->run(function ($type, $buffer) { + echo $buffer; + }); + + if (!$nfpmProcess->isSuccessful()) { + echo "nfpm config file contents:\n"; + echo file_get_contents($nfpmConfigFile); + throw new \RuntimeException("nfpm package creation failed: " . $nfpmProcess->getErrorOutput()); + } + + @unlink($nfpmConfigFile); + + echo "APK package created: {$outputFile}\n"; + + // Create FrankenPHP debuginfo package if debug file exists + $frankenDbg = BUILD_ROOT_PATH . '/debug/frankenphp.debug'; + if (file_exists($frankenDbg)) { + $this->createApkDebuginfo($name, $version, $iteration, $architecture, $frankenDbg, $frankenphpSuffix); + } + } + + private function createApkDebuginfo(string $name, string $version, string $iteration, string $architecture, string $frankenDbg, string $frankenphpSuffix): void + { + $dbgName = $name . '-debuginfo'; + + $nfpmConfig = [ + 'name' => $dbgName, + 'arch' => $architecture, + 'platform' => 'linux', + 'version' => $version, + 'release' => $iteration, + 'section' => 'default', + 'priority' => 'optional', + 'maintainer' => 'Marc Henderkes ', + 'description' => "Debug symbols for FrankenPHP", + 'vendor' => 'Marc Henderkes', + 'homepage' => 'https://apks.henderkes.com', + 'license' => $this->getLicense(), + 'depends' => [sprintf('%s=%s-r%s', $name, $version, $iteration)], + 'contents' => [ + [ + 'src' => $frankenDbg, + 'dst' => '/usr/lib/debug/usr/bin/frankenphp' . $frankenphpSuffix . '.debug', + ], + ], + ]; + + $nfpmConfigFile = TEMP_DIR . "/nfpm-{$dbgName}.yaml"; + if (!yaml_emit_file($nfpmConfigFile, $nfpmConfig, YAML_UTF8_ENCODING)) { + throw new \RuntimeException("Failed to write YAML file: {$nfpmConfigFile}"); + } + + $outputFile = DIST_APK_PATH . "/{$dbgName}-{$version}-r{$iteration}.{$architecture}.apk"; + $dbgProcess = new Process([ + 'nfpm', 'package', + '--config', $nfpmConfigFile, + '--packager', 'apk', + '--target', $outputFile + ]); + $dbgProcess->setTimeout(null); + $dbgProcess->run(function ($type, $buffer) { + echo $buffer; + }); + + if (!$dbgProcess->isSuccessful()) { + throw new \RuntimeException("nfpm debuginfo package creation failed: " . $dbgProcess->getErrorOutput()); + } + + @unlink($nfpmConfigFile); + echo "APK debuginfo package created: {$outputFile}\n"; + } + /** * Prepare FrankenPHP repository by cloning or updating */ diff --git a/src/package/frankenphp/alpine/frankenphp.openrc b/src/package/frankenphp/alpine/frankenphp.openrc new file mode 100644 index 0000000..23e5e36 --- /dev/null +++ b/src/package/frankenphp/alpine/frankenphp.openrc @@ -0,0 +1,28 @@ +#!/sbin/openrc-run + +name="FrankenPHP" +description="Modern PHP app server" + +command="/usr/bin/frankenphp" +command_args="run --environ --config /etc/frankenphp/Caddyfile" +command_user="frankenphp:frankenphp" +command_background="yes" +pidfile="/run/frankenphp/frankenphp.pid" +start_stop_daemon_args="--chdir /var/lib/frankenphp" + +depend() { + need net + after firewall +} + +start_pre() { + checkpath --directory --owner frankenphp:frankenphp --mode 0755 /run/frankenphp + + $command validate --config /etc/frankenphp/Caddyfile +} + +reload() { + ebegin "Reloading $name configuration" + $command reload --config /etc/frankenphp/Caddyfile --force + eend $? +} diff --git a/src/package/frankenphp/alpine/post-deinstall.sh b/src/package/frankenphp/alpine/post-deinstall.sh new file mode 100755 index 0000000..fd102fc --- /dev/null +++ b/src/package/frankenphp/alpine/post-deinstall.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +if getent passwd frankenphp >/dev/null; then + deluser frankenphp +fi + +if getent group frankenphp >/dev/null; then + delgroup frankenphp +fi + +rmdir /var/lib/frankenphp 2>/dev/null || true + +exit 0 diff --git a/src/package/frankenphp/alpine/post-install.sh b/src/package/frankenphp/alpine/post-install.sh new file mode 100755 index 0000000..5afeea7 --- /dev/null +++ b/src/package/frankenphp/alpine/post-install.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +if ! getent group frankenphp >/dev/null; then + addgroup -S frankenphp +fi + +if ! getent passwd frankenphp >/dev/null; then + adduser -S -h /var/lib/frankenphp -s /sbin/nologin -G frankenphp -g "FrankenPHP web server" frankenphp +fi + +chown -R frankenphp:frankenphp /var/lib/frankenphp +chmod 755 /var/lib/frankenphp + +# allow binding to privileged ports +if command -v setcap >/dev/null 2>&1; then + setcap cap_net_bind_service=+ep /usr/bin/frankenphp || true +fi + +# trust FrankenPHP certificates +if [ -x /usr/bin/frankenphp ]; then + HOME=/var/lib/frankenphp /usr/bin/frankenphp trust || true +fi + +if command -v rc-update >/dev/null 2>&1; then + rc-update add frankenphp default + rc-service frankenphp start +fi + +exit 0 diff --git a/src/package/frankenphp/alpine/pre-deinstall.sh b/src/package/frankenphp/alpine/pre-deinstall.sh new file mode 100755 index 0000000..59713ea --- /dev/null +++ b/src/package/frankenphp/alpine/pre-deinstall.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if command -v rc-service >/dev/null 2>&1; then + rc-service frankenphp stop || true +fi + +if command -v rc-update >/dev/null 2>&1; then + rc-update del frankenphp default || true +fi + +exit 0 diff --git a/src/package/pie.php b/src/package/pie.php index 48874ea..496c80f 100644 --- a/src/package/pie.php +++ b/src/package/pie.php @@ -12,7 +12,7 @@ class pie implements package { public function getName(): string { - return 'pie-zts'; + return 'pie-' . CreatePackages::getPrefix(); } /** @@ -24,8 +24,8 @@ public function getVersion(): string // Ensure artifacts exist and get the staged phar path [$pharSource] = $this->prepareArtifacts(); - $proc = new Process(['php', $pharSource, '-V']); - $proc->setTimeout(2); + $proc = new Process(['php', $pharSource, '-V'], env: self::getCleanEnvironment()); + $proc->setTimeout(30); $proc->run(); if (!$proc->isSuccessful()) { // Include both stdout and stderr for parsing attempt/fallback @@ -50,14 +50,26 @@ public function getFpmConfig(): array $prefix = CreatePackages::getPrefix(); + // Get versioned conflicts for pie packages (pie-php-zts8.0, pie-php-zts8.1, etc.) + $phpConflicts = CreatePackages::getVersionedConflicts(''); + $versionedConflicts = []; + foreach ($phpConflicts as $conflict) { + $versionedConflicts[] = 'pie-' . $conflict; + } + return [ 'depends' => [ $prefix . '-cli', $prefix . '-devel', ], + 'provides' => [ + 'pie-zts', + ], + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, 'files' => [ - $pharSource => '/usr/share/php-zts/pie.phar', - $wrapperSource => '/usr/bin/pie-zts', + $pharSource => getSharedir() . '/pie.phar', + $wrapperSource => '/usr/bin/pie' . getBinarySuffix(), ], ]; } @@ -77,6 +89,23 @@ public function getLicense(): string return 'BSD-3-Clause'; } + /** + * Get environment without Xdebug variables that would cause connection attempts + */ + private static function getCleanEnvironment(): array + { + $env = $_SERVER; + + // Explicitly disable Xdebug-related environment variables + // Must be set to empty/0, not unset, as they inherit from parent + $env['XDEBUG_SESSION'] = ''; + $env['XDEBUG_CONFIG'] = ''; + $env['XDEBUG_MODE'] = 'off'; + $env['PHP_IDE_CONFIG'] = ''; + + return $env; + } + private function prepareArtifacts(): array { $pharPath = DOWNLOAD_PATH . '/pie.phar'; @@ -84,7 +113,28 @@ private function prepareArtifacts(): array $this->downloadLatestPiePhar($pharPath); } - $wrapperPath = INI_PATH . '/pie-zts'; + // Process the pie wrapper script to replace hardcoded paths + $wrapperSource = INI_PATH . '/pie-zts'; + $wrapperPath = TEMP_DIR . '/pie' . getBinarySuffix(); + $wrapperContents = file_get_contents($wrapperSource); + $binarySuffix = getBinarySuffix(); + + $wrapperContents = str_replace( + [ + '/usr/bin/php-zts', + '/usr/share/php-zts/', + '/usr/bin/php-config-zts', + ], + [ + '/usr/bin/php' . $binarySuffix, + getSharedir() . '/', + '/usr/bin/php-config' . $binarySuffix, + ], + $wrapperContents + ); + file_put_contents($wrapperPath, $wrapperContents); + chmod($wrapperPath, 0755); + return [$pharPath, $wrapperPath]; } diff --git a/src/package/spx.php b/src/package/spx.php index 1064691..cf48843 100644 --- a/src/package/spx.php +++ b/src/package/spx.php @@ -9,6 +9,7 @@ class spx extends extension { public function getFpmConfig(): array { + $versionedConflicts = CreatePackages::getVersionedConflicts('-spx'); return [ 'config-files' => [ getConfdir() . '/conf.d/20-spx.ini', @@ -16,10 +17,15 @@ public function getFpmConfig(): array 'depends' => [ CreatePackages::getPrefix() . '-cli' ], + 'provides' => [ + 'php-zts-spx', + ], + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, 'files' => [ - BUILD_MODULES_PATH . '/spx.so' => getLibdir() . '/' . CreatePackages::getPrefix() . '/modules/spx.so', + BUILD_MODULES_PATH . '/spx.so' => getModuledir() . '/spx.so', $this->getIniPath() => getConfdir() . '/conf.d/20-spx.ini', - BUILD_ROOT_PATH . '/share/misc/php-spx/assets/web-ui' => '/usr/share/php-zts/misc/php-spx/assets/web-ui', + BUILD_ROOT_PATH . '/share/misc/php-spx/assets/web-ui' => '/usr/share/' . \staticphp\step\CreatePackages::getPrefix() . '/misc/php-spx/assets/web-ui', ] ]; } diff --git a/src/step/CreatePackages.php b/src/step/CreatePackages.php index 537875d..b3fb41b 100644 --- a/src/step/CreatePackages.php +++ b/src/step/CreatePackages.php @@ -16,7 +16,12 @@ class CreatePackages private static $binaryDependencies = []; private static $packageTypes = []; private static ?string $iterationOverride = null; + private static ?string $currentPackageType = null; + public static function setCurrentPackageType(?string $type): void + { + self::$currentPackageType = $type; + } public static function run($packageNames = null, string $packageTypes = 'rpm,deb', string $phpVersion = '8.4', ?string $iteration = null): true { @@ -88,16 +93,6 @@ private static function createGenericPackage(string $name): void $pkg = new $packageClass(); if (method_exists($pkg, 'getVersion')) { $pkgVersion = $pkg->getVersion(); - if ($pkgVersion !== $phpVersion) { - // Extract major and minor version numbers from PHP version - if (preg_match('/^(\d+)\.(\d+)/', $phpVersion, $matches)) { - $majorMinor = $matches[1] . $matches[2]; // Combine major and minor without dot - $pkgVersion .= '_' . $majorMinor; - } - else { - throw new \RuntimeException("Warning: Could not extract major.minor from PHP version: {$phpVersion}"); - } - } } $package = $pkg ?? new $packageClass(); @@ -270,18 +265,6 @@ private static function getExtensionVersion(string $extension, string $phpVersio echo "Detected version for extension {$extension}: {$extensionVersion}\n"; - // If extension version is different from PHP version, add postfix based on PHP major.minor version - if ($extensionVersion !== $phpVersion) { - // Extract major and minor version numbers from PHP version - if (preg_match('/^(\d+)\.(\d+)/', $phpVersion, $matches)) { - $majorMinor = $matches[1] . $matches[2]; // Combine major and minor without dot - $extensionVersion .= '_' . $majorMinor; - } - else { - throw new \RuntimeException("Warning: Could not extract major.minor from PHP version: {$phpVersion}"); - } - } - return $extensionVersion; } @@ -294,10 +277,15 @@ private static function createPackageWithFpm(\staticphp\package $package, string if (in_array('deb', self::$packageTypes, true)) { self::createDebPackage($package, $phpVersion, $architecture, $iteration, $isDebuginfo); } + + if (in_array('apk', self::$packageTypes, true)) { + self::createApkPackage($package, $phpVersion, $architecture, $iteration, $isDebuginfo); + } } private static function createRpmPackage(\staticphp\package $package, string $phpVersion, string $architecture, string $iteration, bool $isDebuginfo = false): void { + self::$currentPackageType = 'rpm'; $name = $isDebuginfo ? $package->getName() . '-debuginfo' : $package->getName(); $config = $isDebuginfo ? $package->getDebuginfoFpmConfig() : $package->getFpmConfig(); $extraArgs = $isDebuginfo ? [] : $package->getFpmExtraArgs(); @@ -363,6 +351,13 @@ private static function createRpmPackage(\staticphp\package $package, string $ph } } + if (isset($config['conflicts']) && is_array($config['conflicts'])) { + foreach ($config['conflicts'] as $conflict) { + $fpmArgs[] = '--conflicts'; + $fpmArgs[] = $conflict; + } + } + foreach (self::$binaryDependencies as $lib => $version) { $fpmArgs[] = '--depends'; $fpmArgs[] = "{$lib}({$version})(64bit)"; @@ -436,14 +431,13 @@ private static function createDebPackage( bool $isDebuginfo = false, ): void { + self::$currentPackageType = 'deb'; $name = $isDebuginfo ? $package->getName() . '-debuginfo' : $package->getName(); $config = $isDebuginfo ? $package->getDebuginfoFpmConfig() : $package->getFpmConfig(); $extraArgs = $isDebuginfo ? [] : $package->getFpmExtraArgs(); echo "Creating DEB package for {$name}...\n"; - $phpVersion = preg_replace('/_\d+$/', '', $phpVersion); - //$osRelease = parse_ini_file('/etc/os-release'); //$distroCodename = $osRelease['VERSION_CODENAME'] ?? null; //$debIteration = $distroCodename !== '' ? "{$iteration}~{$distroCodename}" : $iteration; @@ -506,6 +500,13 @@ private static function createDebPackage( } } + if (isset($config['conflicts']) && is_array($config['conflicts'])) { + foreach ($config['conflicts'] as $conflict) { + $fpmArgs[] = '--conflicts'; + $fpmArgs[] = $conflict; + } + } + $systemLibraryMap = [ 'ld-linux-x86-64.so.2' => 'libc6', 'ld-linux-aarch64.so.1' => 'libc6', @@ -558,6 +559,14 @@ private static function createDebPackage( if (isset($config['files']) && is_array($config['files'])) { foreach ($config['files'] as $source => $dest) { if (file_exists($source)) { + // Check if this is a binary that needs its debug link fixed + // Only fix binaries in BUILD_BIN_PATH that are being renamed + if (str_starts_with($source, BUILD_BIN_PATH . '/') && + is_executable($source) && + basename($source) !== basename($dest)) { + // Fix the debug link and use the temporary binary instead + $source = self::fixBinaryDebugLink($source, $dest); + } $fpmArgs[] = $source . '=' . $dest; } else { @@ -591,6 +600,179 @@ private static function createDebPackage( echo "DEB package created: " . DIST_DEB_PATH . "/{$name}_{$phpVersion}-{$debIteration}_{$architecture}.deb\n"; } + private static function createApkPackage(\staticphp\package $package, string $phpVersion, string $architecture, string $iteration, bool $isDebuginfo = false): void + { + self::$currentPackageType = 'apk'; + $name = $isDebuginfo ? $package->getName() . '-debuginfo' : $package->getName(); + $config = $isDebuginfo ? $package->getDebuginfoFpmConfig() : $package->getFpmConfig(); + $extraArgs = $isDebuginfo ? [] : $package->getFpmExtraArgs(); + + echo "Creating APK package for {$name} using nfpm...\n"; + + // APK uses r{iteration} format for revision number + $apkIteration = $iteration; + $fullVersion = "{$phpVersion}-r{$apkIteration}"; + + // Use nfpm instead of fpm for APK packages + self::createApkWithNfpm($package, $name, $phpVersion, $architecture, $apkIteration, $config, $isDebuginfo); + } + private static function createApkWithNfpm(\staticphp\package $package, string $name, string $phpVersion, string $architecture, string $iteration, array $config, bool $isDebuginfo): void + { + $fullVersion = "{$phpVersion}-r{$iteration}"; + + // Create nfpm YAML config + $nfpmConfig = [ + 'name' => $name, + 'arch' => $architecture, + 'platform' => 'linux', + 'version' => $phpVersion, + 'release' => $iteration, + 'section' => 'default', + 'priority' => 'optional', + 'maintainer' => 'Marc Henderkes ', + 'description' => "Static PHP Package for {$name}", + 'vendor' => 'Marc Henderkes', + 'homepage' => 'https://apks.henderkes.com', + 'license' => $package->getLicense(), + 'apk' => [ + 'signature' => [ + 'key_name' => self::getPrefix(), + ], + ], + ]; + + // Build dependencies + $depends = []; + + // Ensure non-CLI packages depend on the same PHP major.minor + if ($name !== self::getPrefix() . '-cli') { + [$fullPhpVersion] = self::getPhpVersionAndArchitecture(); + if (preg_match('/^(\d+)\.(\d+)/', $fullPhpVersion, $m)) { + $maj = (int)$m[1]; + $min = (int)$m[2]; + $nextMin = $min + 1; + $lowerBound = sprintf('%d.%d', $maj, $min); + $upperBound = sprintf('%d.%d', $maj, $nextMin); + $depends[] = self::getPrefix() . "-cli>={$lowerBound}"; + $depends[] = self::getPrefix() . "-cli<{$upperBound}"; + } + } + + // Debuginfo packages depend on their base package + if (str_ends_with($name, '-debuginfo')) { + $base = preg_replace('/-debuginfo$/', '', $name); + $depends[] = sprintf('%s=%s', $base, $fullVersion); + } + + // Alpine library dependencies + $alpineLibMap = [ + 'ld-linux-x86-64' => 'musl', + 'ld-linux-aarch64' => 'musl', + 'libc' => 'musl', + 'libm' => 'musl', + 'libpthread' => 'musl', + 'libutil' => 'musl', + 'libdl' => 'musl', + 'librt' => 'musl', + 'libresolv' => 'musl', + 'libgcc_s' => 'libgcc', + ]; + + foreach (self::$binaryDependencies as $lib => $version) { + $packageName = preg_replace('/\.so(\.\d+)*$/', '', $lib); + if (isset($alpineLibMap[$packageName])) { + $packageName = $alpineLibMap[$packageName]; + } + $numericVersion = preg_replace('/[^0-9.]/', '', $version); + $depends[] = "{$packageName}>={$numericVersion}"; + } + + if (isset($config['depends']) && is_array($config['depends'])) { + $depends = array_merge($depends, $config['depends']); + } + + if (!empty($depends)) { + $nfpmConfig['depends'] = $depends; + } + + // Add provides, replaces, conflicts + if (isset($config['provides']) && is_array($config['provides'])) { + $nfpmConfig['provides'] = $config['provides']; + } + if (isset($config['replaces']) && is_array($config['replaces'])) { + $nfpmConfig['replaces'] = $config['replaces']; + } + if (isset($config['conflicts']) && is_array($config['conflicts'])) { + $nfpmConfig['conflicts'] = $config['conflicts']; + } + + // Build contents (files) + $contents = []; + if (isset($config['files']) && is_array($config['files'])) { + foreach ($config['files'] as $source => $dest) { + if (file_exists($source)) { + // Fix debug link for renamed binaries + if (str_starts_with($source, BUILD_BIN_PATH . '/') && + is_executable($source) && + basename($source) !== basename($dest)) { + $source = self::fixBinaryDebugLink($source, $dest); + } + $contentItem = ['src' => $source, 'dst' => $dest]; + // Mark config files + if (isset($config['config-files']) && in_array($dest, $config['config-files'])) { + $contentItem['type'] = 'config'; + } + $contents[] = $contentItem; + } else { + echo "Warning: Source file not found: {$source}\n"; + } + } + } + + // Handle empty directories + if (isset($config['empty_directories']) && is_array($config['empty_directories'])) { + foreach ($config['empty_directories'] as $dir) { + $contents[] = ['dst' => $dir, 'type' => 'dir']; + } + } + + if (!empty($contents)) { + $nfpmConfig['contents'] = $contents; + } + + // Write nfpm config to YAML file + $nfpmConfigFile = TEMP_DIR . "/nfpm-{$name}.yaml"; + if (!yaml_emit_file($nfpmConfigFile, $nfpmConfig, YAML_UTF8_ENCODING)) { + throw new \RuntimeException("Failed to write YAML file: {$nfpmConfigFile}"); + } + + echo "nfpm config written to: {$nfpmConfigFile}\n"; + + // Run nfpm to create the package + $outputFile = DIST_APK_PATH . "/{$name}-{$phpVersion}-r{$iteration}.{$architecture}.apk"; + $nfpmProcess = new Process([ + 'nfpm', 'package', + '--config', $nfpmConfigFile, + '--packager', 'apk', + '--target', $outputFile + ]); + $nfpmProcess->setTimeout(null); + $nfpmProcess->run(function ($type, $buffer) { + echo $buffer; + }); + + if (!$nfpmProcess->isSuccessful()) { + echo "nfpm config file contents:\n"; + echo file_get_contents($nfpmConfigFile); + throw new \RuntimeException("nfpm package creation failed: " . $nfpmProcess->getErrorOutput()); + } + + // Clean up config file + @unlink($nfpmConfigFile); + + echo "APK package created: {$outputFile}\n"; + } + private static function getPhpVersionAndArchitecture(): array { if (!empty(self::$versionArch)) { @@ -635,15 +817,26 @@ private static function getPhpVersionAndArchitecture(): array private static function getBinaryDependencies(string $binaryPath): array { - $process = new Process(['ldd', '-v', $binaryPath]); - $process->run(); + // Detect if this is a musl binary + $fileProcess = new Process(['file', $binaryPath]); + $fileProcess->run(); + $fileOutput = $fileProcess->getOutput(); + $isMusl = str_contains($fileOutput, 'musl') || str_contains($fileOutput, 'statically linked'); + + // For musl binaries, we need to use the musl dynamic linker instead of ldd + if ($isMusl) { + $output = self::getMuslBinaryDependencies($binaryPath); + } else { + $process = new Process(['ldd', '-v', $binaryPath]); + $process->run(); + + if (!$process->isSuccessful()) { + throw new \RuntimeException("ldd failed: " . $process->getErrorOutput()); + } - if (!$process->isSuccessful()) { - throw new \RuntimeException("ldd failed: " . $process->getErrorOutput()); + $output = $process->getOutput(); } - $output = $process->getOutput(); - $output = preg_replace('/.*?' . preg_quote($binaryPath, '/') . ':\s*\n/s', '', $output, 1); $output = preg_replace('/\n\s*\/.*?:.*/s', '', $output, 1); @@ -675,6 +868,144 @@ private static function getBinaryDependencies(string $binaryPath): array return $dependencies; } + /** + * Get dependencies for musl-linked binaries using the musl dynamic linker + */ + private static function getMuslBinaryDependencies(string $binaryPath): string + { + // Detect architecture from the binary + $archProcess = new Process(['uname', '-m']); + $archProcess->run(); + $arch = trim($archProcess->getOutput()); + + // Map architecture to musl loader name + $archMap = [ + 'x86_64' => 'x86_64', + 'aarch64' => 'aarch64', + 'arm64' => 'aarch64', + 'armv7l' => 'armv7', + 'armhf' => 'armhf', + ]; + + $muslArch = $archMap[$arch] ?? 'x86_64'; + + // Try to find the musl dynamic linker in common locations + $basePaths = ['/lib', '/usr/lib', '/usr/lib64']; + $muslLoaders = []; + + foreach ($basePaths as $basePath) { + $muslLoaders[] = "{$basePath}/ld-musl-{$muslArch}.so.1"; + // Also try without .1 suffix (some systems) + $muslLoaders[] = "{$basePath}/ld-musl-{$muslArch}.so"; + } + + $muslLoader = null; + foreach ($muslLoaders as $loader) { + if (file_exists($loader)) { + $muslLoader = $loader; + break; + } + } + + if ($muslLoader === null) { + throw new \RuntimeException("Could not find musl dynamic linker for architecture {$arch} (tried: " . implode(', ', $muslLoaders) . ")"); + } + + echo "Using musl dynamic linker: {$muslLoader}\n"; + + // Use the musl loader to list dependencies + $process = new Process([$muslLoader, '--list', $binaryPath]); + $process->run(); + + if (!$process->isSuccessful()) { + // If the binary is statically linked, --list might fail + // Check if it's actually static + $readelfProcess = new Process(['readelf', '-d', $binaryPath]); + $readelfProcess->run(); + if (!str_contains($readelfProcess->getOutput(), 'NEEDED')) { + echo "Binary {$binaryPath} appears to be statically linked (no dynamic dependencies)\n"; + return ''; + } + throw new \RuntimeException("Musl ldd failed: " . $process->getErrorOutput()); + } + + return $process->getOutput(); + } + + /** + * Fix GNU debuglink in a binary to match its new filename + * This is needed when binaries are renamed during packaging (e.g., php -> php-zts8.3) + */ + private static function fixBinaryDebugLink(string $sourceBinary, string $targetBinaryName): string + { + // Extract just the filename from the target path + $targetFilename = basename($targetBinaryName); + $newDebugFileName = $targetFilename . '.debug'; + + // Create a temporary copy of the binary to modify + $tempBinary = TEMP_DIR . '/' . $targetFilename; + + // Copy the source binary to temp location + if (!copy($sourceBinary, $tempBinary)) { + echo "Warning: Failed to copy {$sourceBinary} to {$tempBinary}, debug link won't be fixed\n"; + return $sourceBinary; + } + + // Ensure the temporary binary is executable + chmod($tempBinary, 0755); + + // Find the original debug file + // Map binary names to their debug files + $binaryName = basename($sourceBinary); + $debugMap = [ + 'php' => BUILD_ROOT_PATH . '/debug/php-zts.debug', + 'php-cgi' => BUILD_ROOT_PATH . '/debug/php-cgi-zts.debug', + 'php-fpm' => BUILD_ROOT_PATH . '/debug/php-fpm-zts.debug', + 'frankenphp' => BUILD_ROOT_PATH . '/debug/frankenphp.debug', + ]; + + $originalDebugFile = $debugMap[$binaryName] ?? null; + + // If no debug file exists, we can't fix the debug link + if ($originalDebugFile === null || !file_exists($originalDebugFile)) { + echo "No debug file found for {$binaryName}, skipping debug link fix\n"; + return $tempBinary; + } + + // Create a temporary copy of the debug file with the new name + // objcopy needs the actual file to exist to compute the checksum + $tempDebugFile = TEMP_DIR . '/' . $newDebugFileName; + if (!copy($originalDebugFile, $tempDebugFile)) { + echo "Warning: Failed to copy debug file, debug link won't be fixed\n"; + return $tempBinary; + } + + // Remove existing debug link + $removeProcess = new Process(['objcopy', '--remove-section=.gnu_debuglink', $tempBinary]); + $removeProcess->run(); + if (!$removeProcess->isSuccessful()) { + echo "Warning: Failed to remove debug link from {$tempBinary}: " . $removeProcess->getErrorOutput() . "\n"; + @unlink($tempDebugFile); + return $sourceBinary; + } + + // Add new debug link pointing to the renamed debug file + $addProcess = new Process(['objcopy', '--add-gnu-debuglink=' . $tempDebugFile, $tempBinary]); + $addProcess->run(); + if (!$addProcess->isSuccessful()) { + echo "Warning: Failed to add debug link to {$tempBinary}: " . $addProcess->getErrorOutput() . "\n"; + @unlink($tempDebugFile); + return $sourceBinary; + } + + echo "Fixed debug link in {$targetFilename}: {$newDebugFileName}\n"; + + // Clean up the temporary debug file (we don't need it anymore, just needed it for objcopy) + @unlink($tempDebugFile); + + return $tempBinary; + } + private static function getNextIteration(string $name, string $phpVersion, string $architecture): int { $maxIteration = 0; @@ -699,11 +1030,80 @@ private static function getNextIteration(string $name, string $phpVersion, strin } } + $apkPattern = DIST_APK_PATH . "/{$name}-{$phpVersion}-r*.{$architecture}.apk"; + $apkFiles = glob($apkPattern); + + foreach ($apkFiles as $file) { + if (preg_match("/{$name}-{$phpVersion}-r(\d+)\.{$architecture}\.apk$/", $file, $matches)) { + $iteration = (int)$matches[1]; + $maxIteration = max($maxIteration, $iteration); + } + } + return $maxIteration + 1; } public static function getPrefix(): string { + $phpVersion = SPP_PHP_VERSION; + + // RPM packages always use php-zts (for module system) + if (self::$currentPackageType === 'rpm') { + return 'php-zts'; + } + + // APK packages use php-zts83 (no dot) + if (self::$currentPackageType === 'apk') { + if (preg_match('/^(\d+)\.(\d+)/', $phpVersion, $matches)) { + return 'php-zts' . $matches[1] . $matches[2]; + } + return 'php-zts'; + } + + // DEB packages use php-zts8.3 (with dot) + if (preg_match('/^(\d+)\.(\d+)/', $phpVersion, $matches)) { + return 'php-zts' . $matches[1] . '.' . $matches[2]; + } return 'php-zts'; } + + /** + * Get list of versioned package names to conflict/replace with + * For example, for php-zts8.5-cli, returns [php-zts8.0-cli, php-zts8.1-cli, ..., php-zts8.9-cli] excluding 8.5 + * For RPM packages, returns empty array (RPM uses module system instead) + */ + public static function getVersionedConflicts(string $suffix): array + { + // RPM packages use module system, no versioned conflicts needed + if (self::$currentPackageType === 'rpm') { + return []; + } + + $conflicts = []; + $phpVersion = SPP_PHP_VERSION; + + if (!preg_match('/^(\d+)\.(\d+)/', $phpVersion, $matches)) { + return []; + } + + $currentMajor = (int)$matches[1]; + $currentMinor = (int)$matches[2]; + + // Generate conflicts for versions 8.0 through 8.9 + for ($minor = 0; $minor <= 9; $minor++) { + // Skip the current version + if ($currentMajor === 8 && $minor === $currentMinor) { + continue; + } + + // APK uses php-zts83 format (no dot), DEB uses php-zts8.3 (with dot) + if (self::$currentPackageType === 'apk') { + $conflicts[] = "php-zts{$currentMajor}{$minor}{$suffix}"; + } else { + $conflicts[] = "php-zts{$currentMajor}.{$minor}{$suffix}"; + } + } + + return $conflicts; + } } diff --git a/src/step/RunSPC.php b/src/step/RunSPC.php index a72ac51..f2d452b 100644 --- a/src/step/RunSPC.php +++ b/src/step/RunSPC.php @@ -134,7 +134,7 @@ public static function run(bool $debug = false, string $phpVersion = '8.4', ?arr $args[] = '--debug'; } - $process = new Process($args, BASE_PATH . '/vendor/crazywhalecc/static-php-cli'); + $process = new Process($args, BASE_PATH . '/vendor/crazywhalecc/static-php-cli', env: ['CI' => true]); $process->setTimeout(null); if (Process::isTtySupported()) { $process->setTty(true); // Interactive mode diff --git a/src/util/TwigRenderer.php b/src/util/TwigRenderer.php index 2d6c700..c12e0ac 100644 --- a/src/util/TwigRenderer.php +++ b/src/util/TwigRenderer.php @@ -52,13 +52,18 @@ public static function renderCraftTemplate(string $phpVersion = '8.4', ?string $ } // Prepare template variables + $prefix = CreatePackages::getPrefix(); + $libdir = in_array($majorOsVersion, ['7', '8', '9', '10']) ? '/usr/lib64' : '/usr/lib'; + $templateVars = [ 'php_version' => $phpVersion, 'php_version_nodot' => str_replace('.', '', $phpVersion), 'target' => SPP_TARGET, 'arch' => $arch, 'os' => $majorOsVersion, - 'prefix' => CreatePackages::getPrefix(), + 'prefix' => $prefix, + 'confdir' => '/etc/' . $prefix, + 'moduledir' => $libdir . '/' . $prefix . '/modules', // Optional filter: when provided, craft.yml will include only selected packages // across extensions/shared-extensions/sapi, while always including cli SAPI. 'filter_packages' => $packages,