diff --git a/.github/workflows/build-apk-forgejo.yml b/.github/workflows/build-apk-forgejo.yml index afd4354..0f86e1f 100644 --- a/.github/workflows/build-apk-forgejo.yml +++ b/.github/workflows/build-apk-forgejo.yml @@ -99,14 +99,19 @@ jobs: - name: Install APT dependencies run: | sudo apt-get update - sudo apt-get install -y curl ruby build-essential tar zstd python3 python3-requests + sudo apt-get install -y curl build-essential tar zstd python3 python3-requests sudo apt-get upgrade -y - sudo gem install fpm - name: Install nfpm run: | - curl -sfL https://goreleaser.com/static/run | bash -s -- --version - curl -sfL https://goreleaser.com/static/run | bash -s -- install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest + if [[ "${{ matrix.arch }}" == "aarch64" ]]; then + NFPM_ARCH="arm64" + else + NFPM_ARCH="amd64" + fi + curl -L -o nfpm.deb https://github.com/goreleaser/nfpm/releases/download/v2.44.0/nfpm_2.44.0_${NFPM_ARCH}.deb + sudo dpkg -i nfpm.deb + rm nfpm.deb - name: Install composer run: | @@ -150,9 +155,9 @@ jobs: PACKAGES_FLAG="--packages=${{ env.PACKAGES }}" fi if [[ -n "${ITERATION}" ]]; then - php bin/spp all --target=native-native-musl --phpv=${{ matrix.php-version }} --prefix="-zts" --type=apk --iteration="${ITERATION}" $PACKAGES_FLAG -dynamic + php bin/spp all --target="native-native-musl -dynamic" --phpv=${{ matrix.php-version }} --prefix="-zts" --type=apk --iteration="${ITERATION}" $PACKAGES_FLAG else - php bin/spp all --target=native-native-musl --phpv=${{ matrix.php-version }} --prefix="-zts" --type=apk $PACKAGES_FLAG -dynamic + php bin/spp all --target="native-native-musl -dynamic" --phpv=${{ matrix.php-version }} --prefix="-zts" --type=apk $PACKAGES_FLAG fi - name: Upload to Forgejo diff --git a/.github/workflows/build-deb-forgejo.yml b/.github/workflows/build-deb-forgejo.yml index 106d97e..28e6616 100644 --- a/.github/workflows/build-deb-forgejo.yml +++ b/.github/workflows/build-deb-forgejo.yml @@ -3,6 +3,18 @@ name: Build and upload Debian packages to Forgejo on: workflow_dispatch: inputs: + iteration: + description: "Optional: override package iteration (integer). Leave empty for auto" + required: false + default: "" + php_versions: + description: "Optional: PHP versions (comma-separated, e.g., 8.2,8.5). Leave empty for all" + required: false + default: "" + architectures: + description: "Optional: Architectures (comma-separated, e.g., amd64,arm64). Leave empty for all" + required: false + default: "" debug_tmate: description: "Open tmate session on failure" type: boolean @@ -15,9 +27,48 @@ permissions: contents: read jobs: + setup-matrix: + runs-on: ubuntu-24.04 + permissions: + contents: read + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Set up matrix + id: set-matrix + run: | + # Default values + default_php='["8.2","8.3","8.4","8.5"]' + default_arch='["amd64","arm64"]' + + # Parse inputs or use defaults + if [[ -n "${INPUTS_PHP_VERSIONS}" ]]; then + php_versions=$(echo "${INPUTS_PHP_VERSIONS}" | jq -R 'split(",") | map(gsub("^\\s+|\\s+$";""))') + else + php_versions=$default_php + fi + + if [[ -n "${INPUTS_ARCHITECTURES}" ]]; then + arch_versions=$(echo "${INPUTS_ARCHITECTURES}" | jq -R 'split(",") | map(gsub("^\\s+|\\s+$";""))') + else + arch_versions=$default_arch + fi + + # Create matrix JSON (compact, single line) + matrix=$(jq -nc \ + --argjson php "$php_versions" \ + --argjson arch "$arch_versions" \ + '{"php-version":$php,"arch":$arch}') + + echo "matrix=$matrix" >> $GITHUB_OUTPUT + env: + INPUTS_PHP_VERSIONS: ${{ inputs.php_versions }} + INPUTS_ARCHITECTURES: ${{ inputs.architectures }} + build: - name: Build Debian packages for PHP ${{ matrix.php_version }} on ${{ matrix.arch }} - runs-on: ${{ matrix.arch == 'aarch64' && 'ubuntu-24.04-arm64' || 'ubuntu-24.04' }} + needs: setup-matrix + name: Build for ${{ matrix.arch }} PHP ${{ matrix.php-version }} + runs-on: ubuntu-24.04${{ matrix.arch == 'arm64' && '-arm' || '' }} container: image: debian:11 permissions: @@ -26,13 +77,12 @@ jobs: run: shell: bash strategy: - matrix: - php_version: ['8.2', '8.3', '8.4', '8.5'] - arch: ['x86_64', 'aarch64'] + fail-fast: false + matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BASH_ENV: /tmp/gha-bashenv - PHP_VERSION: ${{ matrix.php_version }} + ITERATION: ${{ inputs.iteration || '' }} steps: - name: Checkout code @@ -41,10 +91,18 @@ jobs: ref: versioned persist-credentials: false + - name: Set architecture variables + run: | + if [[ "${{ matrix.arch }}" == "arm64" ]]; then + echo "RPM_ARCH=aarch64" >> $GITHUB_ENV + else + echo "RPM_ARCH=x86_64" >> $GITHUB_ENV + fi + - name: Set PHP version short run: | # Convert "8.5" to "85" - PHP_VERSION_SHORT=$(echo "$PHP_VERSION" | tr -d '.') + PHP_VERSION_SHORT=$(echo "${{ matrix.php-version }}" | tr -d '.') echo "PHP_VERSION_SHORT=$PHP_VERSION_SHORT" >> $GITHUB_ENV - name: Bootstrap container @@ -56,13 +114,13 @@ jobs: - name: Install cmake run: | - curl -o cmake.tar.gz -fsSL https://github.com/Kitware/CMake/releases/download/v3.31.4/cmake-3.31.4-linux-${{ matrix.arch }}.tar.gz && \ + curl -o cmake.tar.gz -fsSL https://github.com/Kitware/CMake/releases/download/v3.31.4/cmake-3.31.4-linux-$(uname -m).tar.gz && \ sudo tar -xzf cmake.tar.gz -C /usr/local --strip-components=1 && \ rm cmake.tar.gz - name: Install composer run: | - sudo curl -L https://files.henderkes.com/${{ matrix.arch }}-linux/php -o /usr/local/bin/php + sudo curl -L https://files.henderkes.com/${RPM_ARCH}-linux/php -o /usr/local/bin/php sudo chmod +x /usr/local/bin/php sudo curl -sS https://raw.githubusercontent.com/composer/getcomposer.org/f3108f64b4e1c1ce6eb462b159956461592b3e3e/web/installer | php -- --quiet sudo mv composer.phar /usr/local/bin/composer @@ -96,21 +154,40 @@ jobs: rm downloads.tar.gz - name: Build PHP packages - run: bin/spp all --target=${{ matrix.arch }}-native-gnu --phpv=${{ matrix.php_version }} --prefix="-zts" --type=deb + run: | + if [[ -n "${ITERATION}" ]]; then + bin/spp all --target=native-native-gnu --phpv=${{ matrix.php-version }} --prefix="-zts" --type=deb --iteration="${ITERATION}" + else + bin/spp all --target=native-native-gnu --phpv=${{ matrix.php-version }} --prefix="-zts" --type=deb + fi - name: Upload to Forgejo working-directory: dist/deb run: | - ../../bin/forgejo-helper upload deb "${{ env.PHP_VERSION_SHORT }}" "${{ secrets.FORGEJO_PASSWORD }}" "*.deb" + ../../bin/forgejo-helper upload debian "${{ env.PHP_VERSION_SHORT }}" "${{ secrets.FORGEJO_PASSWORD }}" "*.deb" - name: Upload logs on failure if: ${{ failure() }} uses: actions/upload-artifact@v4 with: - name: build-logs-deb-${{ matrix.arch }}-php${{ matrix.php_version }} + name: build-logs-${{ matrix.arch }}-php${{ matrix.php-version }} path: vendor/crazywhalecc/static-php-cli/log + - name: Install tmate + if: ${{ failure() && inputs.debug_tmate == true }} + run: | + case "${RPM_ARCH}" in + x86_64) arch="amd64" ;; + aarch64) arch="arm64v8" ;; + esac + dir="tmate-2.4.0-static-linux-$arch" + curl -L "https://github.com/tmate-io/tmate/releases/download/2.4.0/$dir.tar.xz" | tar -xJ -O "$dir/tmate" > /usr/bin/tmate + chmod +x /usr/bin/tmate + - name: Setup tmate session if: ${{ failure() && inputs.debug_tmate == true }} uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3 - timeout-minutes: 10 + with: + install-dependencies: false + sudo: false + timeout-minutes: 30 diff --git a/.github/workflows/build-gcc-deb-packages.yml b/.github/workflows/build-gcc-deb-packages.yml index 57e4eb6..b819628 100644 --- a/.github/workflows/build-gcc-deb-packages.yml +++ b/.github/workflows/build-gcc-deb-packages.yml @@ -94,13 +94,7 @@ jobs: rm downloads.tar.gz - name: Build PHP - run: php bin/spp build --target=native-native-gnu --phpv=${{ matrix.php-version }} - - - name: Remove spc build dir to clear up space for gh runners - run: sudo rm -rf vendor/crazywhalecc/static-php-cli/source vendor/crazywhalecc/static-php-cli/buildroot - - - name: Build deb packages - run: php bin/spp package --target=native-native-gnu --type=deb --phpv=${{ matrix.php-version }} + run: php bin/spp all --target=native-native-gnu --phpv=${{ matrix.php-version }} --prefix="-zts" --type=deb - name: Stage deb artifacts run: | @@ -226,4 +220,3 @@ jobs: # if: ${{ failure() && github.event_name == 'workflow_dispatch' }} # uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3 # timeout-minutes: 10 -# token: diff --git a/.github/workflows/spc-download.yml b/.github/workflows/spc-download.yml index 77a594e..9b571ea 100644 --- a/.github/workflows/spc-download.yml +++ b/.github/workflows/spc-download.yml @@ -18,11 +18,20 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - name: Set up PHP - uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 - with: - php-version: '8.4' - tools: composer:v2 + - name: Set architecture variables + run: | + if [[ "${{ matrix.arch }}" == "arm64" ]]; then + echo "RPM_ARCH=aarch64" >> $GITHUB_ENV + else + echo "RPM_ARCH=x86_64" >> $GITHUB_ENV + fi + + - name: Install composer + run: | + sudo curl -L https://files.henderkes.com/${RPM_ARCH}-linux/php -o /usr/local/bin/php + sudo chmod +x /usr/local/bin/php + sudo curl -sS https://raw.githubusercontent.com/composer/getcomposer.org/f3108f64b4e1c1ce6eb462b159956461592b3e3e/web/installer | php -- --quiet + sudo mv composer.phar /usr/local/bin/composer - name: Checkout code uses: actions/checkout@v4 @@ -58,3 +67,9 @@ jobs: run: gh workflow run build-gcc-deb-packages.yml env: GH_TOKEN: ${{ secrets.GH_PAT }} # use our own user as the triggering user + + - name: Trigger build-deb-forgejo workflow on versioned branch + if: success() + run: gh workflow run build-deb-forgejo.yml --ref versioned + env: + GH_TOKEN: ${{ secrets.GH_PAT }} # use our own user as the triggering user 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..dde01c7 100755 --- a/bin/createrepo_static +++ b/bin/createrepo_static @@ -20,7 +20,8 @@ def parse_rpm_info(filename): basename = os.path.basename(filename) # Match standard php-zts RPMs - match_php = re.match(r'(?Pphp-zts-[^-]+)-(?P\d+\.\d+\.\d+)-(?P[^.]+)\.(?P[^.]+)\.rpm', basename) + # Format: php-zts-cli-8.5.1-1.el10.x86_64.rpm (with optional .el10/.el9/etc) + match_php = re.match(r'(?Pphp-zts-[^-]+)-(?P\d+\.\d+\.\d+)-(?P[^.]+)(?:\.[^.]+)?\.(?Px86_64|aarch64|noarch)\.rpm', basename) if match_php: name = match_php.group("name") version = match_php.group("version") @@ -30,7 +31,8 @@ def parse_rpm_info(filename): return name, version, release, arch, stream # Match extension packages - match_ext = re.match(r'(?Pphp-zts-[^-]+)-(?P\d+\.\d+\.\d+_\d+)-(?P[^.]+)\.(?P[^.]+)\.rpm', basename) + # Format: php-zts-apcu-5.1.28_85-1.el10.x86_64.rpm (with optional .el10/.el9/etc) + match_ext = re.match(r'(?Pphp-zts-[^-]+)-(?P\d+\.\d+\.\d+(?:~[a-z0-9]+)?_\d+)-(?P[^.]+)(?:\.[^.]+)?\.(?Px86_64|aarch64|noarch)\.rpm', basename) if match_ext: name = match_ext.group("name") version = match_ext.group("version") @@ -46,7 +48,8 @@ def parse_rpm_info(filename): return name, version, release, arch, stream # Match frankenphp - match_franken = re.match(r'(frankenphp)-(?P\d+\.\d+\.\d+_\d+)-(?P\d+)\.(?P[^.]+)\.rpm', basename) + # Format: frankenphp-1.11.0_85-1.el10.x86_64.rpm (with optional .el10/.el9/etc) + match_franken = re.match(r'(frankenphp)-(?P\d+\.\d+\.\d+_\d+)-(?P\d+)(?:\.[^.]+)?\.(?Px86_64|aarch64|noarch)\.rpm', basename) if match_franken: name = match_franken.group(1) version = match_franken.group("version") @@ -56,14 +59,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 +153,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/forgejo-helper b/bin/forgejo-helper new file mode 100755 index 0000000..c3fd059 --- /dev/null +++ b/bin/forgejo-helper @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +import sys +import requests +import re +from pathlib import Path +from fnmatch import fnmatch + +def get_remote_packages(owner, password, pkg_type): + url = f"https://git.henderkes.com/api/v1/packages/{owner}" + params = {"type": pkg_type, "limit": 100} + response = requests.get(url, params=params, auth=(owner, password)) + response.raise_for_status() + return response.json() + +def list_packages(owner, password, pkg_type, pattern=None): + packages = get_remote_packages(owner, password, pkg_type) + for pkg in packages: + full_name = f"{pkg['name']}-{pkg['version']}" + if pattern and not fnmatch(full_name, pattern): + continue + print(full_name) + +def delete_packages(owner, password, pkg_type, pattern): + packages = get_remote_packages(owner, password, pkg_type) + for pkg in packages: + full_name = f"{pkg['name']}-{pkg['version']}" + if fnmatch(full_name, pattern): + print(f"Deleting: {pkg['name']} {pkg['version']}") + if pkg_type == 'debian': + for arch in ['amd64', 'arm64']: + url = f"https://git.henderkes.com/api/packages/{owner}/debian/pool/php-zts/main/{pkg['name']}/{pkg['version']}/{arch}" + resp = requests.delete(url, auth=(owner, password)) + print(f"Delete {arch} response: {resp.status_code}") + if resp.status_code not in [200, 204, 404]: + print(f"Error: {resp.text}") + else: + url = f"https://git.henderkes.com/api/v1/packages/{owner}/{pkg_type}/{pkg['name']}/{pkg['version']}" + resp = requests.delete(url, auth=(owner, password)) + print(f"Delete response: {resp.status_code}") + if resp.status_code not in [200, 204]: + print(f"Error: {resp.text}") + +def upload_packages(owner, password, pkg_type, pattern, repo="php-zts", branch="main"): + for pkg_file in Path(".").glob(pattern): + if not pkg_file.is_file(): + continue + + if pkg_type == 'rpm': + match = re.match(r'(.+?)-(\d+[^-]+)-(\d+)\.[^.]+\.(el\d+)\.', pkg_file.name) + if not match: + continue + name, ver, rel, el_version = match.groups() + version = f"{ver}-{rel}" + elif pkg_type == 'debian': + match = re.match(r'(.+?)_(\d+[^_]+)_(amd64|arm64)\.deb', pkg_file.name) + if not match: + continue + name, version, arch = match.groups() + else: + match = re.match(r'(.+?)-(\d+\..+)\.(x86_64|aarch64)\.(apk|pkg\.tar\.zst)', pkg_file.name) + if not match: + continue + name, version, arch, ext = match.groups() + + print(f"Uploading: {pkg_file.name}") + if pkg_type == 'rpm': + url = f"https://git.henderkes.com/api/packages/{owner}/rpm/{el_version}/upload?sign=true" + elif pkg_type == 'debian': + url = f"https://git.henderkes.com/api/packages/{owner}/debian/pool/php-zts/main/upload" + else: + url = f"https://git.henderkes.com/api/packages/{owner}/{pkg_type}/{branch}/{repo}" + with open(pkg_file, 'rb') as f: + resp = requests.put(url, files={'file': f}, auth=(owner, password)) + if resp.status_code == 201: + print(f"Upload successful: {resp.status_code}") + elif resp.status_code == 409: + print(f"Skip: {pkg_file.name} (already exists)") + continue + else: + print(f"Upload response: {resp.status_code}") + print(f"Error: {resp.text}") + continue + + link_name = name.replace('_', '-') if pkg_type == 'debian' else name + print(f"Linking: {link_name} to {repo}") + url = f"https://git.henderkes.com/api/v1/packages/{owner}/{pkg_type}/{link_name}/-/link/{repo}" + resp = requests.post(url, auth=(owner, password)) + print(f"Link response: {resp.status_code}") + if resp.status_code != 201: + print(f"Error: {resp.text}") + +if __name__ == "__main__": + if len(sys.argv) < 5: + print("Usage: script.py [pattern]") + print("Examples:") + print(" script.py upload alpine 85 pass '*.apk'") + print(" script.py upload rpm 85 pass '*.rpm'") + print(" script.py upload debian 85 pass '*.deb'") + print(" script.py list alpine 85 pass") + print(" script.py list alpine 85 pass '*8.3.29*'") + print(" script.py delete alpine 85 pass '*cli*'") + sys.exit(1) + + action = sys.argv[1] + pkg_type = sys.argv[2] + owner = sys.argv[3] + password = sys.argv[4] + + if pkg_type not in ['alpine', 'arch', 'debian', 'rpm']: + print(f"Error: Invalid package type. Must be alpine, arch, debian, or rpm") + sys.exit(1) + + if action == "upload": + pattern = sys.argv[5] if len(sys.argv) > 5 else "*.apk" + upload_packages(owner, password, pkg_type, pattern) + elif action == "list": + pattern = sys.argv[5] if len(sys.argv) > 5 else None + list_packages(owner, password, pkg_type, pattern) + elif action == "delete": + if len(sys.argv) < 6: + print("Error: delete requires a pattern") + sys.exit(1) + delete_packages(owner, password, pkg_type, sys.argv[5]) + else: + print(f"Unknown action: {action}") + sys.exit(1) diff --git a/bin/spp b/bin/spp index 3623415..500e55a 100755 --- a/bin/spp +++ b/bin/spp @@ -1,15 +1,13 @@ #!/usr/bin/env 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 4f96ca8..26f3dba 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3a028b36b942ff3da62d5e6cc6d84937", + "content-hash": "8887f9391893506dae15b45829a13386", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -81,12 +81,12 @@ "source": { "type": "git", "url": "https://github.com/crazywhalecc/static-php-cli.git", - "reference": "53f7cdefe0e791d621d86cc2ce6938d228ec3b19" + "reference": "6b5200002e6d744a450038e982c3c88d386ef3e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/crazywhalecc/static-php-cli/zipball/53f7cdefe0e791d621d86cc2ce6938d228ec3b19", - "reference": "53f7cdefe0e791d621d86cc2ce6938d228ec3b19", + "url": "https://api.github.com/repos/crazywhalecc/static-php-cli/zipball/6b5200002e6d744a450038e982c3c88d386ef3e6", + "reference": "6b5200002e6d744a450038e982c3c88d386ef3e6", "shasum": "" }, "require": { @@ -141,7 +141,7 @@ "type": "other" } ], - "time": "2025-12-18T19:12:01+00:00" + "time": "2025-12-20T22:29:25+00:00" }, { "name": "doctrine/inflector", @@ -2228,12 +2228,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "f960b7bc5c3bae7e348f7b65736072f74493bc3a" + "reference": "a08c38341cd11dc1b1cb4fa87f65913c95908d73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/f960b7bc5c3bae7e348f7b65736072f74493bc3a", - "reference": "f960b7bc5c3bae7e348f7b65736072f74493bc3a", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/a08c38341cd11dc1b1cb4fa87f65913c95908d73", + "reference": "a08c38341cd11dc1b1cb4fa87f65913c95908d73", "shasum": "" }, "conflict": { @@ -3225,7 +3225,7 @@ "type": "tidelift" } ], - "time": "2025-12-18T19:06:20+00:00" + "time": "2025-12-19T16:40:43+00:00" } ], "aliases": [], @@ -3238,6 +3238,7 @@ "prefer-lowest": false, "platform": { "php": ">=8.4", + "ext-yaml": "*", "ext-zlib": "*" }, "platform-dev": {}, 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 3b70767..f158b0e 100644 --- a/config/templates/craft.yml.twig +++ b/config/templates/craft.yml.twig @@ -63,13 +63,13 @@ craft-options: {% set arch_flags = arch == 'x86_64' ? ' -mtls-dialect=gnu2 -m64 -fcf-protection' : '' %} {% if arch == 'x86_64' and os == '10' %} {% set arch_flags = arch_flags ~ ' -march=x86-64-v3' %} {% endif %} {% if arch == 'x86_64' and os == '9' %} {% set arch_flags = arch_flags ~ ' -march=x86-64-v2' %} {% endif %} -{% set using_gcc = target == ('native-native-gnu' or target == 'native-native') and php_version < '8.5' %} +{% set using_gcc = (target == 'native-native-gnu' or target == 'native-native') and php_version < '8.5' %} {% set specs_cflags = '-specs=/usr/lib/rpm/redhat/redhat-hardened-cc1' %} {% set specs_ldflags = '-specs=/usr/lib/rpm/redhat/redhat-hardened-ld' ~ (os == '10' ? ' -Wl,-z,pack-relative-relocs' : '') %} {% set cflags = '-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fasynchronous-unwind-tables -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fno-strict-aliasing' ~ arch_flags %} -{% set is_rhel = os in ['7', '8', '9', '10'] %} +{% set is_rhel = os in ['7', '8', '9', '10'] and target == 'native-native-gnu' %} {% if using_gcc and is_rhel %} {% if os == '7' or os == '8' %} {% set cflags = cflags ~ ' -Wp,-D_FORTIFY_SOURCE=2' ~ ' ' ~ specs_cflags %} @@ -87,14 +87,14 @@ 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 }}" SPC_DEFAULT_CXX_FLAGS: "{{ cflags }}" SPC_DEFAULT_LD_FLAGS: "-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -Wl,-z,noexecstack -Wl,--build-id=sha1 {{ using_gcc and is_rhel ? specs_ldflags : '' }}" SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS: "{{ cflags }} -g -fPIE{{ using_gcc ? '' : ' -flto'}}" - SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS: "-pie -release zts-{{ php_version_nodot }}{{ using_gcc ? '' : ' -flto'}}" + SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS: "-pie -release {{ release_prefix }}-{{ php_version_nodot }}{{ using_gcc ? '' : ' -flto'}}" SPC_CMD_PREFIX_PHP_CONFIGURE: "./configure --prefix= --with-valgrind=no --disable-shared --enable-static --disable-all --disable-cgi --disable-phpdbg --disable-debug --with-pic --disable-dependency-tracking --enable-rtld-now --enable-re2c-cgoto --disable-rpath" SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES: "--with github.com/dunglas/frankenphp/caddy --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/baldinof/caddy-supervisor" PHP_BUILD_PROVIDER: "Static PHP #StandWithUkraine" diff --git a/config/templates/pie-wrapper.twig b/config/templates/pie-wrapper.twig new file mode 100644 index 0000000..93075cc --- /dev/null +++ b/config/templates/pie-wrapper.twig @@ -0,0 +1,18 @@ +#!/bin/sh +# pie{{ binary_suffix }} wrapper +# Runs php/pie with the PHP binary. Adds --with-php-config only when not provided by the user. + +have_cfg=0 +for arg in "$@"; do + case "$arg" in + --with-php-config|--with-php-config=*) + have_cfg=1 + ;; + esac +done + +if [ "$have_cfg" -eq 1 ]; then + exec /usr/bin/php{{ binary_suffix }} {{ sharedir }}/pie.phar "$@" +else + exec /usr/bin/php{{ binary_suffix }} {{ sharedir }}/pie.phar --with-php-config=/usr/bin/php-config{{ binary_suffix }} "$@" +fi diff --git a/src/Command/AllCommand.php b/src/Command/AllCommand.php index e6e9ca3..e9ebea0 100644 --- a/src/Command/AllCommand.php +++ b/src/Command/AllCommand.php @@ -19,7 +19,6 @@ protected function configure(): void { parent::configure(); $this - ->addOption('type', null, InputOption::VALUE_REQUIRED, 'Specify which package types to build (rpm,deb)', 'rpm') ->addOption('packages', null, InputOption::VALUE_REQUIRED, 'Specify which packages to build (comma-separated)') ->addOption('iteration', null, InputOption::VALUE_REQUIRED, 'Specify iteration number to use for packages (overrides auto-detected)'); } @@ -27,16 +26,24 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $debug = $input->getOption('debug'); - $packageNames = $input->getOption('packages'); - $packageTypes = $input->getOption('type'); - $phpVersion = $input->getOption('phpv'); + $packagesOpt = $input->getOption('packages'); $iteration = $input->getOption('iteration'); + $phpVersion = SPP_PHP_VERSION; // Get from constant + + // Process packages option + $packages = null; + if (is_string($packagesOpt) && $packagesOpt !== '') { + $packages = array_values(array_filter(array_map('trim', explode(',', $packagesOpt)))); + } // Run build step $output->writeln("Building PHP with extensions using static-php-cli..."); $output->writeln("Using PHP version: {$phpVersion}"); + if ($packages) { + $output->writeln("Building packages: " . implode(', ', $packages)); + } - $buildResult = RunSPC::run($debug, $phpVersion); + $buildResult = RunSPC::run($debug, $phpVersion, $packages); if (!$buildResult) { $output->writeln("Build failed."); @@ -45,15 +52,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Run package step - if ($packageNames) { - // Split by comma to support multiple packages - $packageNames = explode(',', $packageNames); - $output->writeln("Creating packages for: " . implode(', ', $packageNames) . "..."); + if ($packages) { + $output->writeln("Creating packages for: " . implode(', ', $packages) . "..."); } else { $output->writeln("Creating packages for all extensions..."); } - $packageResult = CreatePackages::run($packageNames, $packageTypes, $phpVersion, $iteration); + // All parameters now come from constants set by BaseCommand::initialize() + $packageResult = CreatePackages::run($packages, $iteration); if (!$packageResult) { $output->writeln("Package creation failed."); diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index 588c86e..51751cd 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -14,7 +14,9 @@ protected function configure(): void $this ->addOption('debug', null, InputOption::VALUE_NONE, 'Print debug messages') ->addOption('phpv', null, InputOption::VALUE_REQUIRED, 'Specify PHP version to build', '8.4') - ->addOption('target', null, InputOption::VALUE_REQUIRED, 'Specify the target triple for Zig (e.g., x86_64-linux-gnu, aarch64-linux-gnu)', 'native-native'); + ->addOption('target', null, InputOption::VALUE_REQUIRED, 'Specify the target triple for Zig (e.g., x86_64-linux-gnu, aarch64-linux-gnu)', 'native-native') + ->addOption('prefix', null, InputOption::VALUE_REQUIRED, 'Specify the package prefix (e.g., -zts, -zts8.5, -zts85)', '-zts') + ->addOption('type', null, InputOption::VALUE_REQUIRED, 'Specify package type: rpm (uses /usr/lib64), deb (uses /usr/lib), or apk (uses /usr/lib). Required.', null); } protected function initialize(InputInterface $input, OutputInterface $output) @@ -22,6 +24,19 @@ protected function initialize(InputInterface $input, OutputInterface $output) // Define build paths with PHP version $phpVersion = $input->getOption('phpv') ?? '8.4'; $target = $input->getOption('target') ?? 'native-native'; + $prefix = $input->getOption('prefix') ?? '-zts'; + $type = $input->getOption('type'); + + // Validate that --type is provided + if ($type === null) { + throw new \InvalidArgumentException('The --type option is required. Specify: rpm, deb, or apk'); + } + + // Validate type value + $validTypes = ['rpm', 'deb', 'apk']; + if (!in_array($type, $validTypes, true)) { + throw new \InvalidArgumentException('Invalid --type value. Must be one of: ' . implode(', ', $validTypes)); + } // Check if constants are already defined if (defined('SPP_PHP_VERSION')) { @@ -32,6 +47,8 @@ protected function initialize(InputInterface $input, OutputInterface $output) // Define constants define('SPP_PHP_VERSION', $phpVersion); define('SPP_TARGET', $target); + define('SPP_PREFIX', $prefix); + define('SPP_TYPE', $type); define('BUILD_ROOT_PATH', BASE_PATH . '/build/' . $phpVersion); define('BUILD_BIN_PATH', BUILD_ROOT_PATH . '/bin'); define('BUILD_LIB_PATH', BUILD_ROOT_PATH . '/lib'); @@ -44,7 +61,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/Command/PackageCommand.php b/src/Command/PackageCommand.php index 8d3cf17..2b7d8a6 100644 --- a/src/Command/PackageCommand.php +++ b/src/Command/PackageCommand.php @@ -18,7 +18,6 @@ protected function configure(): void { parent::configure(); $this - ->addOption('type', null, InputOption::VALUE_REQUIRED, 'Specify which package types to build (rpm,deb)', 'rpm') ->addOption('packages', null, InputOption::VALUE_REQUIRED, 'Specify which packages to build (comma-separated)') ->addOption('iteration', null, InputOption::VALUE_REQUIRED, 'Specify iteration number to use for packages (overrides auto-detected)'); } @@ -26,8 +25,6 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $packageNames = $input->getOption('packages'); - $packageTypes = $input->getOption('type'); - $phpVersion = $input->getOption('phpv'); $iteration = $input->getOption('iteration'); if ($packageNames) { @@ -37,7 +34,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("Creating packages for all extensions..."); } - $result = CreatePackages::run($packageNames, $packageTypes, $phpVersion, $iteration); + // All parameters now come from constants set by BaseCommand::initialize() + $result = CreatePackages::run($packageNames, $iteration); if ($result) { $output->writeln("Package creation completed successfully."); diff --git a/src/extension.php b/src/extension.php index f448603..ae4d6c8 100644 --- a/src/extension.php +++ b/src/extension.php @@ -146,18 +146,22 @@ 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' => [], + '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 +183,31 @@ 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 ALL hardcoded php paths with prefix-based 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, + ], + [ + 'extension=' . $this->name, + 'zend_extension=' . $this->name, + ], + $iniContent + ); + $iniContent = preg_replace( + [ + '#/usr/share/php[^/]*/#', + '#/usr/local/share/php[^/]*/#', + ], + [ + '/usr/share/' . $prefix . '/', + '/usr/local/share/' . $prefix . '/', + ], $iniContent ); file_put_contents($tempIniPath, $iniContent); @@ -209,7 +235,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/ini/pie-zts b/src/ini/pie-zts deleted file mode 100755 index 98dab58..0000000 --- a/src/ini/pie-zts +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# pie-zts wrapper -# Runs php/pie with the ZTS PHP binary. Adds --with-php-config only when not provided by the user. - -have_cfg=0 -for arg in "$@"; do - case "$arg" in - --with-php-config|--with-php-config=*) - have_cfg=1 - ;; - esac -done - -if [ "$have_cfg" -eq 1 ]; then - exec /usr/bin/php-zts /usr/share/php-zts/pie.phar "$@" -else - exec /usr/bin/php-zts /usr/share/php-zts/pie.phar --with-php-config=/usr/bin/php-config-zts "$@" -fi diff --git a/src/package/cgi.php b/src/package/cgi.php index 05cc07e..7543162 100644 --- a/src/package/cgi.php +++ b/src/package/cgi.php @@ -14,12 +14,16 @@ public function getName(): string public function getFpmConfig(): array { + $versionedConflicts = CreatePackages::getVersionedConflicts('-cgi'); return [ 'depends' => [ CreatePackages::getPrefix() . '-cli', ], + 'provides' => [], + '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(), ] ]; } @@ -31,14 +35,15 @@ public function getFpmExtraArgs(): array public function getDebuginfoFpmConfig(): array { - $src = BUILD_ROOT_PATH . '/debug/php-cgi-zts.debug'; + $binarySuffix = getBinarySuffix(); + $src = BUILD_ROOT_PATH . '/debug/php-cgi.debug'; if (!file_exists($src)) { return []; } return [ 'depends' => [CreatePackages::getPrefix() . '-cgi'], 'files' => [ - $src => '/usr/lib/debug/usr/bin/php-cgi-zts.debug', + $src => '/usr/lib/debug/usr/bin/php-cgi' . $binarySuffix . '.debug', ], ]; } diff --git a/src/package/cli.php b/src/package/cli.php index 5e11c7c..0b2a19e 100644 --- a/src/package/cli.php +++ b/src/package/cli.php @@ -17,19 +17,24 @@ 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', getPhpLibdir(), $contents); + // Replace ALL hardcoded /etc/php* paths with prefix-based conf dir + $contents = preg_replace('#/etc/php[^/]*#', getConfdir(), $contents); file_put_contents(TEMP_DIR . '/php.ini', $contents); - $provides = ['php-zts']; - $replaces = []; + $provides = [$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 +44,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 ALL hardcoded php paths with prefix-based paths + $iniContents = file_get_contents($iniFile); + $iniContents = preg_replace( + [ + '#/usr/share/php[^/]*/#', + '#/usr/local/share/php[^/]*/#', + ], + [ + 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 +68,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..b702913 100644 --- a/src/package/devel.php +++ b/src/package/devel.php @@ -18,6 +18,12 @@ public function getFpmConfig(): array $phpConfigContent = file_get_contents($phpConfigPath); + // Replace buildroot paths with BUILD_ROOT_PATH + $builtDir = BASE_PATH . '/vendor/crazywhalecc/static-php-cli/buildroot'; + $phpConfigContent = str_replace($builtDir, BUILD_ROOT_PATH, $phpConfigContent); + $phpConfigContent = str_replace('/app/buildroot', BUILD_ROOT_PATH, $phpConfigContent); + + $binarySuffix = getBinarySuffix(); $phpConfigContent = preg_replace( [ '/^prefix=.*$/m', @@ -25,22 +31,32 @@ public function getFpmConfig(): array '/^libs=.*$/m', '/^program_prefix=.*$/m', '/^program_suffix=.*$/m', - '#/php(?!-zts)#' ], [ 'prefix="/usr"', 'ldflags="-lpthread"', 'libs=""', 'program_prefix=""', - 'program_suffix="-zts"', - '/php-zts' + 'program_suffix="' . $binarySuffix . '"', ], $phpConfigContent ); + + // Replace all /php paths with versioned paths + $phpConfigContent = preg_replace('#/php(?!' . preg_quote($binarySuffix, '#') . ')#', '/' . CreatePackages::getPrefix(), $phpConfigContent); $phpVersion = str_replace('.', '', SPP_PHP_VERSION); - $libName = 'lib' . CreatePackages::getPrefix() . "-$phpVersion.so"; + // Use release prefix format for libphp (leading dash removed, dots kept) + $releasePrefix = ltrim($binarySuffix, '-'); + $libName = $releasePrefix !== '' + ? 'libphp-' . $releasePrefix . '-' . $phpVersion . '.so' + : 'libphp-' . $phpVersion . '.so'; $phpConfigContent = str_replace('libphp.so', $libName, $phpConfigContent); + // For APK, sed is in /bin/sed instead of /usr/bin/sed + if (defined('SPP_TYPE') && SPP_TYPE === 'apk') { + $phpConfigContent = str_replace('/usr/bin/sed', '/bin/sed', $phpConfigContent); + } + file_put_contents($modifiedPhpConfigPath, $phpConfigContent); chmod($modifiedPhpConfigPath, 0755); @@ -48,6 +64,11 @@ public function getFpmConfig(): array $modifiedPhpizePath = TEMP_DIR . '/phpize'; $phpizeContent = file_get_contents($phpizePath); + + // Replace buildroot paths with BUILD_ROOT_PATH + $phpizeContent = str_replace($builtDir, BUILD_ROOT_PATH, $phpizeContent); + $phpizeContent = str_replace('/app/buildroot', BUILD_ROOT_PATH, $phpizeContent); + $phpizeContent = preg_replace( [ '/^prefix=.*$/m', @@ -55,7 +76,7 @@ public function getFpmConfig(): array ], [ 'prefix="/usr"', - 'datarootdir="/php-zts"', + 'datarootdir="/' . \staticphp\step\CreatePackages::getPrefix() . '"', ], $phpizeContent ); @@ -65,29 +86,42 @@ 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 ); + // For APK, sed is in /bin/sed instead of /usr/bin/sed + if (defined('SPP_TYPE') && SPP_TYPE === 'apk') { + $phpizeContent = str_replace('/usr/bin/sed', '/bin/sed', $phpizeContent); + } + file_put_contents($modifiedPhpizePath, $phpizeContent); chmod($modifiedPhpizePath, 0755); + $versionedConflicts = CreatePackages::getVersionedConflicts('-devel'); + + // APK needs sed dependency since php-config uses sed + $depends = [CreatePackages::getPrefix() . '-cli']; + if (defined('SPP_TYPE') && SPP_TYPE === 'apk') { + $depends[] = 'sed'; + } + 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(), - ], - 'depends' => [ - CreatePackages::getPrefix() . '-cli', + $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() . '/build', ], + 'depends' => $depends, 'provides' => [ - 'php-config-zts', - 'phpize-zts', - ] + 'php-config' . getBinarySuffix(), + 'phpize' . getBinarySuffix(), + ], + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, ]; } diff --git a/src/package/embed.php b/src/package/embed.php index dd1ab78..e944ece 100644 --- a/src/package/embed.php +++ b/src/package/embed.php @@ -15,17 +15,30 @@ public function getName(): string public function getFpmConfig(): array { $phpVersion = str_replace('.', '', SPP_PHP_VERSION); - $name = 'lib' . CreatePackages::getPrefix() . "-$phpVersion.so"; + $prefix = getBinarySuffix(); // e.g., "-zts", "-nts", "-zts8.5", or "" + // SPC produces libphp-{prefix}-{version}.so with only leading dash removed from prefix + // e.g., "-zts" -> "libphp-zts-85.so", "-zts8.5" -> "libphp-zts8.5-85.so", "" -> "libphp-85.so" + $releasePrefix = ltrim($prefix, '-'); + $libphp = $releasePrefix !== '' + ? 'libphp-' . $releasePrefix . '-' . $phpVersion . '.so' + : 'libphp-' . $phpVersion . '.so'; + $versionedConflicts = CreatePackages::getVersionedConflicts('-embed'); + $provides = [ + $libphp, + CreatePackages::getPrefix() . '-embedded' + ]; + if ($this->getName() !== CreatePackages::getPrefix() . '-embed') { + $provides[] = CreatePackages::getPrefix() . '-embed'; + } return [ 'depends' => [ CreatePackages::getPrefix() . '-cli', ], - 'provides' => [ - $name, - CreatePackages::getPrefix() . '-embedded' - ], + 'provides' => $provides, + 'replaces' => $versionedConflicts, + 'conflicts' => $versionedConflicts, 'files' => [ - BUILD_LIB_PATH . '/' . $name => getLibdir() . '/' . $name, + BUILD_LIB_PATH . '/' . $libphp => getLibdir() . '/' . $libphp, ] ]; } @@ -38,8 +51,15 @@ public function getFpmExtraArgs(): array public function getDebuginfoFpmConfig(): array { $phpVersionDigits = str_replace('.', '', SPP_PHP_VERSION); - $libName = 'lib' . CreatePackages::getPrefix() . "-{$phpVersionDigits}.so"; - $src = BUILD_ROOT_PATH . '/debug/' . $libName . '.debug'; + $prefix = getBinarySuffix(); + // SPC produces libphp-{prefix}-{version}.so with only leading dash removed from prefix + $releasePrefix = ltrim($prefix, '-'); + $libName = $releasePrefix !== '' + ? 'libphp-' . $releasePrefix . '-' . $phpVersionDigits . '.so' + : 'libphp-' . $phpVersionDigits . '.so'; + + // Debug file is just libphp.so.debug (without version/prefix) + $src = BUILD_ROOT_PATH . '/debug/libphp.so.debug'; if (!file_exists($src)) { return []; } diff --git a/src/package/fpm.php b/src/package/fpm.php index 6092f31..4453e3d 100644 --- a/src/package/fpm.php +++ b/src/package/fpm.php @@ -11,29 +11,72 @@ public function getName(): string { return CreatePackages::getPrefix() . '-fpm'; } - + public function getFpmConfig(): array { + $prefix = CreatePackages::getPrefix(); $contents = file_get_contents(INI_PATH . '/php-fpm.conf'); $contents = str_replace('$confdir', getConfdir(), $contents); + // Replace ALL hardcoded /var/log/php* paths with prefix-based paths + $contents = preg_replace('#/var/log/php[^/]*/#', '/var/log/' . $prefix . '/', $contents); + // Replace ALL hardcoded /run/php-fpm* paths with prefix-based paths + $contents = preg_replace('#/run/php-fpm[^/]*/#', '/run/php-fpm' . getBinarySuffix() . '/', $contents); file_put_contents(TEMP_DIR . '/php-fpm.conf', $contents); + + // Process the systemd service file to replace ALL hardcoded paths + $serviceContents = file_get_contents(INI_PATH . '/php-fpm.service'); + $binarySuffix = getBinarySuffix(); + $serviceContents = preg_replace( + [ + '#/usr/sbin/php-fpm[^ ]*#', + '#RuntimeDirectory=php-fpm[^ ]*#', + ], + [ + '/usr/sbin/php-fpm' . $binarySuffix, + 'RuntimeDirectory=php-fpm' . $binarySuffix, + ], + $serviceContents + ); + file_put_contents(TEMP_DIR . '/php-fpm.service', $serviceContents); + + // Process www.conf to replace ALL hardcoded paths + $wwwContents = file_get_contents(INI_PATH . '/www.conf'); + $wwwContents = preg_replace( + [ + '#/var/lib/php[^/]*/#', + '#/var/log/php[^/]*/#', + '#/run/php-fpm[^/]*/#', + ], + [ + getVarLibdir() . '/', + '/var/log/' . $prefix . '/', + '/run/php-fpm' . $binarySuffix . '/', + ], + $wwwContents + ); + file_put_contents(TEMP_DIR . '/www.conf', $wwwContents); + + $versionedConflicts = CreatePackages::getVersionedConflicts('-fpm'); return [ 'depends' => [ CreatePackages::getPrefix() . '-cli', ], + 'provides' => [], + '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', ], ]; } @@ -45,11 +88,12 @@ public function getFpmExtraArgs(): array public function getDebuginfoFpmConfig(): array { - $src = BUILD_ROOT_PATH . '/debug/php-fpm-zts.debug'; + $binarySuffix = getBinarySuffix(); + $src = BUILD_ROOT_PATH . '/debug/php-fpm.debug'; if (!file_exists($src)) { return []; } - $target = '/usr/lib/debug/usr/sbin/php-fpm-zts.debug'; + $target = '/usr/lib/debug/usr/sbin/php-fpm' . $binarySuffix . '.debug'; return [ 'depends' => [CreatePackages::getPrefix() . '-fpm'], 'files' => [ diff --git a/src/package/frankenphp.php b/src/package/frankenphp.php index a1f9b0e..2406220 100644 --- a/src/package/frankenphp.php +++ b/src/package/frankenphp.php @@ -10,6 +10,17 @@ class frankenphp implements package { public function getName(): string { + // Extract version suffix from prefix for frankenphp naming + // e.g., "php-zts8.3" -> "frankenphp8.3", "php-nts85" -> "frankenphp85", "php-zts" -> "frankenphp" + $prefix = CreatePackages::getPrefix(); + + // Remove "php" and any non-digit prefix to get just the version part + // php-zts8.5 -> -zts8.5 -> 8.5 + // php-nts85 -> -nts85 -> 85 + $suffix = str_replace('php', '', $prefix); + if (preg_match('/(\d+\.?\d*)/', $suffix, $matches)) { + return 'frankenphp' . $matches[1]; + } return 'frankenphp'; } @@ -33,10 +44,19 @@ public function getLicense(): string return 'MIT'; } + /** + * Get list of versioned frankenphp packages to conflict/replace with + * Returns empty array - versioned FrankenPHP packages can coexist + */ + private function getVersionedConflicts(): array + { + return []; + } + /** * Create FrankenPHP packages (both RPM and DEB) */ - public function createPackages(array $packageTypes, array $binaryDependencies, ?string $iterationOverride = null): void + public function createPackages(string $packageType, array $binaryDependencies, ?string $iterationOverride = null, bool $debuginfo = false): void { echo "Creating FrankenPHP package\n"; @@ -44,49 +64,83 @@ public function createPackages(array $packageTypes, array $binaryDependencies, ? $this->prepareFrankenPhpRepository(); - if (in_array('rpm', $packageTypes, true)) { - $this->createRpmPackage($architecture, $binaryDependencies, $iterationOverride); + if ($packageType === 'rpm') { + $this->createRpmPackage($architecture, $binaryDependencies, $iterationOverride, $debuginfo); } - if (in_array('deb', $packageTypes, true)) { - $this->createDebPackage($architecture, $binaryDependencies, $iterationOverride); + if ($packageType === 'deb') { + $this->createDebPackage($architecture, $binaryDependencies, $iterationOverride, $debuginfo); + } + if ($packageType === 'apk') { + $this->createApkPackage($architecture, $binaryDependencies, $iterationOverride, $debuginfo); } } /** * Create RPM package for FrankenPHP */ - public function createRpmPackage(string $architecture, array $binaryDependencies, ?string $iterationOverride = null): void + public function createRpmPackage(string $architecture, array $binaryDependencies, ?string $iterationOverride = null, bool $debuginfo = false): void { echo "Creating RPM package for FrankenPHP...\n"; $packageFolder = DIST_PATH . '/frankenphp/package'; $phpVersion = str_replace('.', '', SPP_PHP_VERSION); - $phpEmbedName = 'lib' . CreatePackages::getPrefix() . '-' . $phpVersion . '.so'; + $binarySuffix = getBinarySuffix(); + // SPC produces libphp-{prefix}-{version}.so with only leading dash removed from prefix + $releasePrefix = ltrim($binarySuffix, '-'); + $phpEmbedName = $releasePrefix !== '' + ? 'libphp-' . $releasePrefix . '-' . $phpVersion . '.so' + : 'libphp-' . $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; + if (!preg_match('/FrankenPHP v(\d+\.\d+\.\d+)/', $output, $matches)) { + throw new \RuntimeException("Unable to detect FrankenPHP version from output: " . $output); + } + $version = $matches[1]; + + // Append PHP version suffix to FrankenPHP version + $phpMajorMinor = SPP_PHP_VERSION; + if (preg_match('/^(\d+)\.(\d+)/', $phpMajorMinor, $phpMatches)) { + $phpVersionSuffix = $phpMatches[1] . $phpMatches[2]; // e.g., "85" from "8.5" + } else { + $phpVersionSuffix = str_replace('.', '', $phpMajorMinor); + } + $rpmVersion = $version . '_' . $phpVersionSuffix; - $name = "frankenphp"; + $name = $this->getName(); - $computed = (string)$this->getNextIteration($name, $version, $architecture); + // Calculate iteration for RPM (with possible override) + $computed = (string)$this->getNextIteration($name, $rpmVersion, $architecture, 'rpm'); $iteration = $iterationOverride ?? $computed; + $versionedConflicts = $this->getVersionedConflicts(); + + // Generate full package filename with distribution version + $distVersion = $this->getDistVersion(); + $distSuffix = $distVersion !== '' ? ".{$distVersion}" : ''; + $packageFile = DIST_RPM_PATH . "/{$name}-{$rpmVersion}-{$iteration}{$distSuffix}.{$architecture}.rpm"; + $fpmArgs = [ 'fpm', '-s', 'dir', '-t', 'rpm', '--rpm-compression', 'xz', - '-p', DIST_RPM_PATH, + '-p', $packageFile, // Full path with distVersion in filename '-n', $name, - '-v', $version, + '-v', $rpmVersion, '--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)"; @@ -121,23 +175,24 @@ public function createRpmPackage(string $architecture, array $binaryDependencies echo $buffer; }); - echo "RPM package created: " . DIST_RPM_PATH . "/{$name}-{$version}-{$iteration}.{$architecture}.rpm\n"; + echo "RPM package created: {$packageFile}\n"; // Create FrankenPHP debuginfo package if debug file exists $frankenDbg = BUILD_ROOT_PATH . '/debug/frankenphp.debug'; if (file_exists($frankenDbg)) { + $dbgPackageFile = DIST_RPM_PATH . "/{$name}-debuginfo-{$rpmVersion}-{$iteration}{$distSuffix}.{$architecture}.rpm"; $dbgArgs = [ 'fpm', '-s', 'dir', '-t', 'rpm', '--rpm-compression', 'xz', - '-p', DIST_RPM_PATH, + '-p', $dbgPackageFile, '-n', $name . '-debuginfo', - '-v', $version, + '-v', $rpmVersion, '--iteration', $iteration, '--architecture', $architecture, '--license', $this->getLicense(), - '--depends', sprintf('%s = %s-%s', $name, $version, $iteration), + '--depends', sprintf('%s = %s-%s', $name, $rpmVersion, $iteration), $frankenDbg . '=/usr/lib/debug/usr/bin/frankenphp.debug', ]; $dbgProcess = new Process($dbgArgs); @@ -148,44 +203,85 @@ public function createRpmPackage(string $architecture, array $binaryDependencies if (!$dbgProcess->isSuccessful()) { throw new \RuntimeException("RPM debuginfo package creation failed: " . $dbgProcess->getErrorOutput()); } + + echo "RPM debuginfo package created: {$dbgPackageFile}\n"; } } /** * Create DEB package for FrankenPHP */ - public function createDebPackage(string $architecture, array $binaryDependencies, ?string $iterationOverride = null): void + public function createDebPackage(string $architecture, array $binaryDependencies, ?string $iterationOverride = null, bool $debuginfo = false): void { echo "Creating DEB package for FrankenPHP...\n"; $packageFolder = DIST_PATH . '/frankenphp/package'; $phpVersion = str_replace('.', '', SPP_PHP_VERSION); - $phpEmbedName = 'lib' . CreatePackages::getPrefix() . '-' . $phpVersion . '.so'; + $binarySuffix = getBinarySuffix(); + // SPC produces libphp-{prefix}-{version}.so with only leading dash removed from prefix + $releasePrefix = ltrim($binarySuffix, '-'); + $phpEmbedName = $releasePrefix !== '' + ? 'libphp-' . $releasePrefix . '-' . $phpVersion . '.so' + : 'libphp-' . $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); + if (!preg_match('/FrankenPHP v(\d+\.\d+\.\d+)/', $output, $matches)) { + throw new \RuntimeException("Unable to detect FrankenPHP version from output: " . $output); + } $version = $matches[1]; - $name = "frankenphp"; + $name = $this->getName(); + + // Convert system architecture to Debian architecture naming + $debArch = match($architecture) { + 'x86_64' => 'amd64', + 'aarch64' => 'arm64', + default => $architecture, + }; + + // For DEB packages, append PHP version to package version for proper sorting + // e.g., 1.11.0+php85 is higher than 1.11.0+php83 + $phpMajorMinor = SPP_PHP_VERSION; + if (preg_match('/^(\d+)\.(\d+)/', $phpMajorMinor, $phpMatches)) { + $phpVersionSuffix = $phpMatches[1] . $phpMatches[2]; // e.g., "85" from "8.5" + } else { + $phpVersionSuffix = str_replace('.', '', $phpMajorMinor); + } + $debVersion = $version . '+php' . $phpVersionSuffix; - $computed = (string)$this->getNextIteration($name, $version, $architecture); + // Calculate iteration for DEB (with possible override) + $computed = (string)$this->getNextIteration($name, $debVersion, $debArch, 'deb'); $iteration = $iterationOverride ?? $computed; $debIteration = $iteration; + $versionedConflicts = $this->getVersionedConflicts(); + + // Debian filename format: {name}_{version}-{revision}_{arch}.deb + $packageFile = DIST_DEB_PATH . "/{$name}_{$debVersion}-{$debIteration}_{$debArch}.deb"; + $fpmArgs = [ 'fpm', '-s', 'dir', '-t', 'deb', '--deb-compression', 'xz', - '-p', DIST_DEB_PATH, + '-p', $packageFile, '-n', $name, - '-v', $version, + '-v', $debVersion, + '--architecture', $debArch, '--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 +314,15 @@ 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 prefix) + // Extract version from package name: frankenphp8.5 or frankenphp85 + $prefix = CreatePackages::getPrefix(); + $frankenphpSuffix = ''; + // Extract version numbers from prefix (e.g., "php-zts8.5" -> "8.5", "php-nts85" -> "85") + if (preg_match('/(\d+\.?\d*)/', $prefix, $matches)) { + $frankenphpSuffix = $matches[1]; + } + $fpmArgs = [...$fpmArgs, ...[ '--depends', $phpEmbedName, '--after-install', "{$packageFolder}/debian/postinst.sh", @@ -226,8 +331,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" @@ -239,36 +344,276 @@ public function createDebPackage(string $architecture, array $binaryDependencies echo $buffer; }); - echo "DEB package created: " . DIST_DEB_PATH . "/{$name}-{$version}-{$debIteration}.{$architecture}.deb\n"; + echo "DEB package created: {$packageFile}\n"; + + // Create FrankenPHP debuginfo package if debug file exists (only if --debuginfo flag set for DEB) + if ($debuginfo) { + $frankenDbg = BUILD_ROOT_PATH . '/debug/frankenphp.debug'; + if (file_exists($frankenDbg)) { + $dbgDebName = "{$name}-debuginfo"; + $dbgPackageFile = DIST_DEB_PATH . "/{$dbgDebName}_{$debVersion}-{$debIteration}_{$debArch}.deb"; + $dbgArgs = [ + 'fpm', + '-s', 'dir', + '-t', 'deb', + '--deb-compression', 'xz', + '-p', $dbgPackageFile, + '-n', $dbgDebName, + '-v', $debVersion, + '--iteration', $debIteration, + '--architecture', $debArch, + '--license', $this->getLicense(), + '--depends', sprintf('%s (= %s-%s)', $name, $debVersion, $debIteration), + $frankenDbg . '=/usr/lib/debug/usr/bin/frankenphp.debug', + ]; + $dbgProcess = new Process($dbgArgs); + $dbgProcess->setTimeout(null); + $dbgProcess->run(function ($type, $buffer) { + echo $buffer; + }); + if (!$dbgProcess->isSuccessful()) { + throw new \RuntimeException("DEB debuginfo package creation failed: " . $dbgProcess->getErrorOutput()); + } + + echo "DEB debuginfo package created: {$dbgPackageFile}\n"; + } + } + } - // Create FrankenPHP debuginfo package if debug file exists - $frankenDbg = BUILD_ROOT_PATH . '/debug/frankenphp.debug'; - if (file_exists($frankenDbg)) { - $dbgArgs = [ - 'fpm', - '-s', 'dir', - '-t', 'deb', - '--deb-compression', 'xz', - '-p', DIST_DEB_PATH, - '-n', $name . '-debuginfo', - '-v', $version, - '--iteration', $debIteration, - '--architecture', $architecture, - '--license', $this->getLicense(), - '--depends', sprintf('%s (= %s-%s)', $name, $version, $debIteration), - $frankenDbg . '=/usr/lib/debug/usr/bin/frankenphp.debug', - ]; - $dbgProcess = new Process($dbgArgs); - $dbgProcess->setTimeout(null); - $dbgProcess->run(function ($type, $buffer) { - echo $buffer; - }); - if (!$dbgProcess->isSuccessful()) { - throw new \RuntimeException("DEB debuginfo package creation failed: " . $dbgProcess->getErrorOutput()); + /** + * Create APK package for FrankenPHP + */ + public function createApkPackage(string $architecture, array $binaryDependencies, ?string $iterationOverride = null, bool $debuginfo = false): void + { + echo "Creating APK package for FrankenPHP using nfpm...\n"; + + $packageFolder = DIST_PATH . '/frankenphp/package'; + $phpVersion = str_replace('.', '', SPP_PHP_VERSION); + $binarySuffix = getBinarySuffix(); + // SPC produces libphp-{prefix}-{version}.so with only leading dash removed from prefix + $releasePrefix = ltrim($binarySuffix, '-'); + $phpEmbedName = $releasePrefix !== '' + ? 'libphp-' . $releasePrefix . '-' . $phpVersion . '.so' + : 'libphp-' . $phpVersion . '.so'; + + $ldLibraryPath = 'LD_LIBRARY_PATH=' . BUILD_LIB_PATH; + [, $output] = shell()->execWithResult($ldLibraryPath . ' ' . BUILD_BIN_PATH . '/frankenphp --version'); + $output = implode("\n", $output); + if (!preg_match('/FrankenPHP v(\d+\.\d+\.\d+)/', $output, $matches)) { + throw new \RuntimeException("Unable to detect FrankenPHP version from output: " . $output); + } + $version = $matches[1]; + + $name = $this->getName(); + + // For APK packages, append PHP version to package version for proper sorting + // e.g., 1.11.0_85 is higher than 1.11.0_83 + $phpMajorMinor = SPP_PHP_VERSION; + if (preg_match('/^(\d+)\.(\d+)/', $phpMajorMinor, $phpMatches)) { + $phpVersionSuffix = $phpMatches[1] . $phpMatches[2]; // e.g., "85" from "8.5" + } else { + $phpVersionSuffix = str_replace('.', '', $phpMajorMinor); + } + $apkVersion = $version . '_' . $phpVersionSuffix; + + // Calculate iteration for APK (with possible override) + $computed = (string)$this->getNextIteration($name, $apkVersion, $architecture, 'apk'); + $iteration = $iterationOverride ?? $computed; + + $versionedConflicts = $this->getVersionedConflicts(); + + // Build nfpm config + $nfpmConfig = [ + 'name' => $name, + 'arch' => $architecture, + 'platform' => 'linux', + 'version' => $apkVersion, + '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(), + ]; + + // Build dependencies + // For APK, depend on the embed package, not the .so file + $embedPackageName = CreatePackages::getPrefix() . '-embed'; + $depends = [$embedPackageName]; + + // 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'] = [$this->getName() !== 'frankenphp' ? 'frankenphp' : '']; + $nfpmConfig['replaces'] = $versionedConflicts; + $nfpmConfig['conflicts'] = $versionedConflicts; + + // Determine the FrankenPHP suffix (just version numbers) + $prefix = CreatePackages::getPrefix(); + $frankenphpSuffix = ''; + if (preg_match('/(\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 with full filename including PHP version suffix + $phpSuffix = $this->getPhpVersionSuffix(); + $outputFile = DIST_APK_PATH . "/{$name}-{$version}-r{$iteration}.{$phpSuffix}.{$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 (only if --debuginfo flag set for APK) + if ($debuginfo) { + $frankenDbg = BUILD_ROOT_PATH . '/debug/frankenphp.debug'; + if (file_exists($frankenDbg)) { + $this->createApkDebuginfo($name, $apkVersion, $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}"); + } + + $phpSuffix = $this->getPhpVersionSuffix(); + $outputFile = DIST_APK_PATH . "/{$dbgName}-{$version}-r{$iteration}.{$phpSuffix}.{$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 */ @@ -357,30 +702,113 @@ private function getPhpVersionAndArchitecture(): array /** * Get next iteration number for package */ - private function getNextIteration(string $name, string $version, string $architecture): int + private function getNextIteration(string $name, string $version, string $architecture, string $packageType): int { $maxIteration = 0; - $rpmPattern = DIST_RPM_PATH . "/{$name}-{$version}-*.{$architecture}.rpm"; - $rpmFiles = glob($rpmPattern); - - foreach ($rpmFiles as $file) { - if (preg_match("/{$name}-{$version}-(\d+)\.{$architecture}\.rpm$/", $file, $matches)) { - $iteration = (int)$matches[1]; - $maxIteration = max($maxIteration, $iteration); + if ($packageType === 'rpm') { + // RPM: {name}-{version}-{iteration}.{distVersion}.{arch}.rpm + // Also match old formats: + // - {name}-{version}-{iteration}.{phpSuffix}.{distVersion}.{arch}.rpm (with phpSuffix) + // - {name}-{version}-{iteration}.{arch}.rpm (no distVersion) + $rpmPattern = DIST_RPM_PATH . "/{$name}-{$version}-*.rpm"; + $rpmFiles = glob($rpmPattern); + + foreach ($rpmFiles as $file) { + // Match all formats: iteration followed by 0-2 parts, then arch.rpm + if (preg_match("/{$name}-" . preg_quote($version, '/') . "-(\d+)(?:\.[^.]+){0,2}\.{$architecture}\.rpm$/", $file, $matches)) { + $iteration = (int)$matches[1]; + $maxIteration = max($maxIteration, $iteration); + } } } - $debPattern = DIST_DEB_PATH . "/{$name}_{$version}-*_{$architecture}.deb"; - $debFiles = glob($debPattern); + if ($packageType === 'deb') { + // DEB: {name}-{phpSuffix}_{version}-{iteration}_{arch}.deb + // Also match old formats for backwards compatibility + $debPattern = DIST_DEB_PATH . "/{$name}*.deb"; + $debFiles = glob($debPattern); + + foreach ($debFiles as $file) { + // Match new format: {name}-{phpSuffix}_{version}-{iteration}_{arch}.deb + // The name might have the phpSuffix included or not + if (preg_match("/" . preg_quote($name, '/') . "(?:-[^_]+)?_" . preg_quote($version, '/') . "-(\d+)_{$architecture}\.deb$/", $file, $matches)) { + $iteration = (int)$matches[1]; + $maxIteration = max($maxIteration, $iteration); + } + } + } - foreach ($debFiles as $file) { - if (preg_match("/{$name}_{$version}-(\d+)_{$architecture}\.deb$/", $file, $matches)) { - $iteration = (int)$matches[1]; - $maxIteration = max($maxIteration, $iteration); + if ($packageType === 'apk') { + // APK: {name}-{version}-r{iteration}.{phpSuffix}.{arch}.apk + // Also match old format: {name}-{version}-r{iteration}.{arch}.apk (no phpSuffix) + $apkPattern = DIST_APK_PATH . "/{$name}-{$version}-r*.apk"; + $apkFiles = glob($apkPattern); + + foreach ($apkFiles as $file) { + // Match both formats: r{iteration} followed by 0-1 parts, then arch.apk + if (preg_match("/{$name}-" . preg_quote($version, '/') . "-r(\d+)(?:\.[^.]+)?\.{$architecture}\.apk$/", $file, $matches)) { + $iteration = (int)$matches[1]; + $maxIteration = max($maxIteration, $iteration); + } } } return $maxIteration + 1; } + + /** + * Get PHP version suffix for package filenames (e.g., "static-83" for PHP 8.3) + */ + private function getPhpVersionSuffix(): string + { + [$phpVersion,] = $this->getPhpVersionAndArchitecture(); + + // Extract major.minor version (e.g., "8.3.29" -> "8.3") + if (preg_match('/^(\d+)\.(\d+)/', $phpVersion, $matches)) { + $majorMinorNoDot = $matches[1] . $matches[2]; // e.g., "83" + } else { + $majorMinorNoDot = str_replace('.', '', $phpVersion); + } + + // Construct suffix: static-{version} (e.g., "static-83") + return 'static-' . $majorMinorNoDot; + } + + /** + * Get distribution version for RPM filenames (e.g., "el9", "el8", "fc39") + */ + private function getDistVersion(): string + { + if (!file_exists('/etc/os-release')) { + return ''; + } + + $osRelease = parse_ini_file('/etc/os-release'); + if (!$osRelease || !isset($osRelease['ID'], $osRelease['VERSION_ID'])) { + return ''; + } + + $id = $osRelease['ID']; + $versionId = $osRelease['VERSION_ID']; + + // Extract major version number + if (preg_match('/^(\d+)/', $versionId, $matches)) { + $majorVersion = $matches[1]; + } else { + return ''; + } + + // Map distribution ID to prefix + $distMap = [ + 'rhel' => 'el', + 'centos' => 'el', + 'rocky' => 'el', + 'almalinux' => 'el', + 'fedora' => 'fc', + ]; + + $prefix = $distMap[$id] ?? ''; + return $prefix !== '' ? $prefix . $majorVersion : ''; + } } 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..d78af85 --- /dev/null +++ b/src/package/frankenphp/alpine/post-install.sh @@ -0,0 +1,33 @@ +#!/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 run >/dev/null 2>&1 & + FRANKENPHP_PID=$! + sleep 2 + HOME=/var/lib/frankenphp /usr/bin/frankenphp trust || true + kill -TERM $FRANKENPHP_PID 2>/dev/null || 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/meta.php b/src/package/meta.php new file mode 100644 index 0000000..53f8a5b --- /dev/null +++ b/src/package/meta.php @@ -0,0 +1,47 @@ + [ + CreatePackages::getPrefix() . '-cli', + ], + 'provides' => [], + 'replaces' => [], + 'conflicts' => [], + 'files' => [], + ]; + } + + public function getFpmExtraArgs(): array + { + return []; + } + + public function getDebuginfoFpmConfig(): array + { + return []; + } + + public function getLicense(): string + { + return 'PHP-3.01'; + } +} diff --git a/src/package/pie.php b/src/package/pie.php index 48874ea..8bccbc7 100644 --- a/src/package/pie.php +++ b/src/package/pie.php @@ -12,7 +12,8 @@ class pie implements package { public function getName(): string { - return 'pie-zts'; + // Return pie with the suffix (e.g., "pie-zts", "pie-zts8.5", "pie-zts85") + return 'pie' . getBinarySuffix(); } /** @@ -24,7 +25,7 @@ public function getVersion(): string // Ensure artifacts exist and get the staged phar path [$pharSource] = $this->prepareArtifacts(); - $proc = new Process(['php', $pharSource, '-V']); + $proc = new Process(['php', $pharSource, '-V'], env: self::getCleanEnvironment()); $proc->setTimeout(2); $proc->run(); if (!$proc->isSuccessful()) { @@ -50,14 +51,26 @@ public function getFpmConfig(): array $prefix = CreatePackages::getPrefix(); + // Get versioned conflicts for pie packages (pie-zts8.0, pie-zts8.1, etc.) + // Replace the 'php' prefix from conflicts with 'pie' + $phpConflicts = CreatePackages::getVersionedConflicts(''); + $versionedConflicts = []; + foreach ($phpConflicts as $conflict) { + // Replace 'php' with 'pie' (e.g., php-zts8.5 -> pie-zts8.5, php-nts85 -> pie-nts85) + $versionedConflicts[] = str_replace('php', 'pie', $conflict); + } + return [ 'depends' => [ $prefix . '-cli', $prefix . '-devel', ], + 'provides' => [], + '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 +90,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 +114,18 @@ private function prepareArtifacts(): array $this->downloadLatestPiePhar($pharPath); } - $wrapperPath = INI_PATH . '/pie-zts'; + // Render the pie wrapper script using Twig template + $binarySuffix = getBinarySuffix(); + $wrapperPath = TEMP_DIR . '/pie' . $binarySuffix; + + $wrapperContents = \staticphp\util\TwigRenderer::render('pie-wrapper.twig', [ + 'binary_suffix' => $binarySuffix, + 'sharedir' => getSharedir(), + ]); + + 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..2a283cf 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,13 @@ public function getFpmConfig(): array 'depends' => [ CreatePackages::getPrefix() . '-cli' ], + 'provides' => [], + '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..2030643 100644 --- a/src/step/CreatePackages.php +++ b/src/step/CreatePackages.php @@ -14,11 +14,12 @@ class CreatePackages private static $sharedExtensions = []; private static $sapis = []; private static $binaryDependencies = []; - private static $packageTypes = []; + private static string $packageType = 'rpm'; private static ?string $iterationOverride = null; + private static string $prefix = '-zts'; + private static bool $debuginfo = false; - - public static function run($packageNames = null, string $packageTypes = 'rpm,deb', string $phpVersion = '8.4', ?string $iteration = null): true + public static function run($packageNames = null, ?string $iteration = null, ?bool $debuginfo = null): true { self::loadConfig(); @@ -28,8 +29,15 @@ public static function run($packageNames = null, string $packageTypes = 'rpm,deb $phpBinary = BUILD_BIN_PATH . '/php'; self::$binaryDependencies = self::getBinaryDependencies($phpBinary); - self::$packageTypes = explode(',', strtolower($packageTypes)); - self::$iterationOverride = $iteration !== null && $iteration !== '' ? (string)$iteration : null; + // Use values from constants set by BaseCommand + self::$prefix = defined('SPP_PREFIX') ? SPP_PREFIX : '-zts'; + self::$packageType = defined('SPP_TYPE') ? SPP_TYPE : 'rpm'; + self::$iterationOverride = $iteration !== null && $iteration !== '' ? $iteration : null; + + // Set debuginfo flag from parameter + if ($debuginfo !== null) { + self::$debuginfo = $debuginfo; + } if ($packageNames !== null) { if (is_string($packageNames)) { @@ -63,6 +71,10 @@ public static function run($packageNames = null, string $packageTypes = 'rpm,deb self::createSapiPackages(); self::createSapiPackage('devel'); self::createGenericPackage('pie'); + // Create metapackage for APK to allow "apk add php-zts85" + if (self::$packageType === 'apk') { + self::createGenericPackage('meta'); + } self::createExtensionPackages(); } @@ -88,28 +100,18 @@ 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(); - $computed = (string)self::getNextIteration($package->getName(), $pkgVersion, $architecture); - $iteration = self::$iterationOverride ?? $computed; - - self::createPackageWithFpm($package, $pkgVersion, $architecture, $iteration); + self::createPackageWithFpm($package, $pkgVersion, $architecture); + // Create debuginfo packages: always for RPM, only if --debuginfo flag set for others $dbgConfig = $package->getDebuginfoFpmConfig(); if (is_array($dbgConfig) && !empty($dbgConfig['files'])) { - self::createPackageWithFpm($package, $pkgVersion, $architecture, $iteration, true); + if (self::$packageType === 'rpm' || self::$debuginfo) { + self::createPackageWithFpm($package, $pkgVersion, $architecture, true); + } } } @@ -150,7 +152,7 @@ private static function createSapiPackage(string $sapi): void // FrankenPHP has a special package creation flow if ($sapi === 'frankenphp') { $package = new $packageClass(); - $package->createPackages(self::$packageTypes, self::$binaryDependencies, self::$iterationOverride); + $package->createPackages(self::$packageType, self::$binaryDependencies, self::$iterationOverride, self::$debuginfo); return; } @@ -158,14 +160,14 @@ private static function createSapiPackage(string $sapi): void $package = new $packageClass(); - $computed = (string)self::getNextIteration($package->getName(), $phpVersion, $architecture); - $iteration = self::$iterationOverride ?? $computed; - - self::createPackageWithFpm($package, $phpVersion, $architecture, $iteration); + self::createPackageWithFpm($package, $phpVersion, $architecture); + // Create debuginfo packages: always for RPM, only if --debuginfo flag set for others $dbgConfig = $package->getDebuginfoFpmConfig(); if (is_array($dbgConfig) && !empty($dbgConfig['files'])) { - self::createPackageWithFpm($package, $phpVersion, $architecture, $iteration, true); + if (self::$packageType === 'rpm' || self::$debuginfo) { + self::createPackageWithFpm($package, $phpVersion, $architecture, true); + } } } @@ -186,9 +188,6 @@ private static function createExtensionPackage(string $extension): void [$phpVersion, $architecture] = self::getPhpVersionAndArchitecture(); $extensionVersion = self::getExtensionVersion($extension, $phpVersion); - $computed = (string)self::getNextIteration(self::getPrefix() . "-{$extension}", $extensionVersion, $architecture); - $iteration = self::$iterationOverride ?? $computed; - $package = new extension($extension); $packageClass = "\\staticphp\\package\\{$extension}"; if (class_exists($packageClass)) { @@ -200,11 +199,14 @@ private static function createExtensionPackage(string $extension): void return; } - self::createPackageWithFpm($package, $extensionVersion, $architecture, $iteration); + self::createPackageWithFpm($package, $extensionVersion, $architecture); + // Create debuginfo packages: always for RPM, only if --debuginfo flag set for others $dbgConfig = $package->getDebuginfoFpmConfig(); if (is_array($dbgConfig) && !empty($dbgConfig['files'])) { - self::createPackageWithFpm($package, $extensionVersion, $architecture, $iteration, true); + if (self::$packageType === 'rpm' || self::$debuginfo) { + self::createPackageWithFpm($package, $extensionVersion, $architecture, true); + } } } @@ -270,33 +272,25 @@ 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; } - private static function createPackageWithFpm(\staticphp\package $package, string $phpVersion, string $architecture, string $iteration, bool $isDebuginfo = false): void + private static function createPackageWithFpm(\staticphp\package $package, string $phpVersion, string $architecture, bool $isDebuginfo = false): void { - if (in_array('rpm', self::$packageTypes, true)) { - self::createRpmPackage($package, $phpVersion, $architecture, $iteration, $isDebuginfo); + if (self::$packageType === 'rpm') { + self::createRpmPackage($package, $phpVersion, $architecture, $isDebuginfo); + } + + if (self::$packageType === 'deb') { + self::createDebPackage($package, $phpVersion, $architecture, $isDebuginfo); } - if (in_array('deb', self::$packageTypes, true)) { - self::createDebPackage($package, $phpVersion, $architecture, $iteration, $isDebuginfo); + if (self::$packageType === 'apk') { + self::createApkPackage($package, $phpVersion, $architecture, $isDebuginfo); } } - private static function createRpmPackage(\staticphp\package $package, string $phpVersion, string $architecture, string $iteration, bool $isDebuginfo = false): void + private static function createRpmPackage(\staticphp\package $package, string $phpVersion, string $architecture, bool $isDebuginfo = false): void { $name = $isDebuginfo ? $package->getName() . '-debuginfo' : $package->getName(); $config = $isDebuginfo ? $package->getDebuginfoFpmConfig() : $package->getFpmConfig(); @@ -304,21 +298,45 @@ private static function createRpmPackage(\staticphp\package $package, string $ph echo "Creating RPM package for {$name}...\n"; + // For RPM packages, append PHP version to package version for extensions + // This ensures proper version ordering when the same extension version is built for different PHP versions + [$fullPhpVersion] = self::getPhpVersionAndArchitecture(); + $rpmVersion = $phpVersion; + + // If package version differs from PHP version, it's an extension - append PHP version + if ($phpVersion !== $fullPhpVersion) { + if (preg_match('/^(\d+)\.(\d+)/', $fullPhpVersion, $phpMatches)) { + $phpVersionSuffix = $phpMatches[1] . $phpMatches[2]; // e.g., "85" from "8.5" + } else { + $phpVersionSuffix = str_replace('.', '', $fullPhpVersion); + } + $rpmVersion = $phpVersion . '_' . $phpVersionSuffix; + } + + // Calculate iteration for RPM (with possible override) + $computed = (string)self::getNextIteration($name, $rpmVersion, $architecture, 'rpm'); + $iteration = self::$iterationOverride ?? $computed; + + // Generate full package filename with distribution version + $distVersion = self::getDistVersion(); + $distSuffix = $distVersion !== '' ? ".{$distVersion}" : ''; + $packageFile = DIST_RPM_PATH . "/{$name}-{$rpmVersion}-{$iteration}{$distSuffix}.{$architecture}.rpm"; + $fpmArgs = [...[ 'fpm', '-s', 'dir', '-t', 'rpm', '--rpm-compression', 'xz', - '-p', DIST_RPM_PATH, + '-p', $packageFile, // Full path with phpSuffix and distVersion in filename '--name', $name, - '--version', $phpVersion, + '--version', $rpmVersion, '--iteration', $iteration, '--architecture', $architecture, '--description', "Static PHP Package for {$name}", '--license', $package->getLicense(), - '--maintainer', 'Marc Henderkes ', - '--vendor', 'Marc Henderkes ', - '--url', 'rpms.henderkes.com', + '--maintainer', 'Marc Henderkes ', + '--vendor', 'Marc Henderkes ', + '--url', 'pkgs.henderkes.com', ], ...$extraArgs]; // Ensure non-CLI packages depend on the same PHP major.minor as php-zts-cli (ignore iteration/patch) @@ -341,17 +359,17 @@ private static function createRpmPackage(\staticphp\package $package, string $ph if (str_ends_with($name, '-debuginfo')) { $base = preg_replace('/-debuginfo$/', '', $name); $fpmArgs[] = '--depends'; - $fpmArgs[] = sprintf('%s = %s-%s', $base, $phpVersion, $iteration); + $fpmArgs[] = sprintf('%s = %s-%s', $base, $rpmVersion, $iteration); } if (isset($config['provides']) && is_array($config['provides'])) { foreach ($config['provides'] as $provide) { $fpmArgs[] = '--provides'; - $fpmArgs[] = "$provide = $phpVersion-$iteration"; + $fpmArgs[] = "$provide = $rpmVersion-$iteration"; if (str_ends_with($provide, '.so')) { $provide = str_replace('.so', '.so()(64bit)', $provide); $fpmArgs[] = '--provides'; - $fpmArgs[] = "$provide = $phpVersion-$iteration"; + $fpmArgs[] = "$provide = $rpmVersion-$iteration"; } } } @@ -359,7 +377,14 @@ private static function createRpmPackage(\staticphp\package $package, string $ph if (isset($config['replaces']) && is_array($config['replaces'])) { foreach ($config['replaces'] as $replace) { $fpmArgs[] = '--replaces'; - $fpmArgs[] = "$replace < {$phpVersion}-{$iteration}"; + $fpmArgs[] = "$replace < {$rpmVersion}-{$iteration}"; + } + } + + if (isset($config['conflicts']) && is_array($config['conflicts'])) { + foreach ($config['conflicts'] as $conflict) { + $fpmArgs[] = '--conflicts'; + $fpmArgs[] = $conflict; } } @@ -426,13 +451,14 @@ private static function createRpmPackage(\staticphp\package $package, string $ph if (!$rpmProcess->isSuccessful()) { throw new \RuntimeException("RPM package creation failed: " . $rpmProcess->getErrorOutput()); } + + echo "RPM package created: {$packageFile}\n"; } private static function createDebPackage( \staticphp\package $package, string $phpVersion, string $architecture, - string $iteration, bool $isDebuginfo = false, ): void { @@ -442,29 +468,57 @@ private static function createDebPackage( echo "Creating DEB package for {$name}...\n"; - $phpVersion = preg_replace('/_\d+$/', '', $phpVersion); + // Convert system architecture to Debian architecture naming + $debArch = match($architecture) { + 'x86_64' => 'amd64', + 'aarch64' => 'arm64', + default => $architecture, + }; + + // For DEB packages, append PHP version to package version for extensions + // This ensures proper version ordering when the same extension version is built for different PHP versions + // e.g., redis 6.0.2+php85 is higher than redis 6.0.2+php83 + [$fullPhpVersion] = self::getPhpVersionAndArchitecture(); + $debVersion = $phpVersion; + + // If package version differs from PHP version, it's an extension - append PHP version + if ($phpVersion !== $fullPhpVersion) { + if (preg_match('/^(\d+)\.(\d+)/', $fullPhpVersion, $phpMatches)) { + $phpVersionSuffix = $phpMatches[1] . $phpMatches[2]; // e.g., "85" from "8.5" + } else { + $phpVersionSuffix = str_replace('.', '', $fullPhpVersion); + } + $debVersion = $phpVersion . '+php' . $phpVersionSuffix; + } + + // Calculate iteration for DEB (with possible override) + $computed = (string)self::getNextIteration($name, $debVersion, $debArch, 'deb'); + $iteration = self::$iterationOverride ?? $computed; //$osRelease = parse_ini_file('/etc/os-release'); //$distroCodename = $osRelease['VERSION_CODENAME'] ?? null; //$debIteration = $distroCodename !== '' ? "{$iteration}~{$distroCodename}" : $iteration; $debIteration = $iteration; - $fullVersion = "{$phpVersion}-{$debIteration}"; + $fullVersion = "{$debVersion}-{$debIteration}"; + + // Debian filename format: {name}_{version}-{revision}_{arch}.deb + $packageFile = DIST_DEB_PATH . "/{$name}_{$debVersion}-{$debIteration}_{$debArch}.deb"; $fpmArgs = [...[ 'fpm', '-s', 'dir', '-t', 'deb', '--deb-compression', 'xz', - '-p', DIST_DEB_PATH, + '-p', $packageFile, '--name', $name, - '--version', $phpVersion, - '--architecture', $architecture, + '--version', $debVersion, + '--architecture', $debArch, '--iteration', $debIteration, // Debian revision (includes distro) '--description', "Static PHP Package for {$name}", '--license', $package->getLicense(), - '--maintainer', 'Marc Henderkes ', - '--vendor', 'Marc Henderkes ', - '--url', 'debs.henderkes.com', + '--maintainer', 'Marc Henderkes ', + '--vendor', 'Marc Henderkes ', + '--url', 'pkgs.henderkes.com', ], ...$extraArgs]; // Ensure non-CLI packages depend on the same PHP major.minor as php-zts-cli (ignore iteration/patch) @@ -506,6 +560,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 +619,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 { @@ -587,8 +656,210 @@ private static function createDebPackage( $debProcess->run(function ($type, $buffer) { echo $buffer; }); + if (!$debProcess->isSuccessful()) { + throw new \RuntimeException("DEB package creation failed: " . $debProcess->getErrorOutput()); + } - echo "DEB package created: " . DIST_DEB_PATH . "/{$name}_{$phpVersion}-{$debIteration}_{$architecture}.deb\n"; + echo "DEB package created: {$packageFile}\n"; + } + + private static function createApkPackage(\staticphp\package $package, string $phpVersion, string $architecture, bool $isDebuginfo = false): void + { + $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"; + + // For APK packages, append PHP version to package version for extensions + // This ensures proper version ordering when the same extension version is built for different PHP versions + [$fullPhpVersion] = self::getPhpVersionAndArchitecture(); + $apkVersion = $phpVersion; + + // If package version differs from PHP version, it's an extension - append PHP version + if ($phpVersion !== $fullPhpVersion) { + if (preg_match('/^(\d+)\.(\d+)/', $fullPhpVersion, $phpMatches)) { + $phpVersionSuffix = $phpMatches[1] . $phpMatches[2]; // e.g., "85" from "8.5" + } else { + $phpVersionSuffix = str_replace('.', '', $fullPhpVersion); + } + $apkVersion = $phpVersion . '_' . $phpVersionSuffix; + } + + // Calculate iteration for APK (with possible override) + $computed = (string)self::getNextIteration($name, $apkVersion, $architecture, 'apk'); + $iteration = self::$iterationOverride ?? $computed; + + // APK uses r{iteration} format for revision number + $apkIteration = $iteration; + $fullVersion = "{$apkVersion}-r{$apkIteration}"; + + // Use nfpm instead of fpm for APK packages + self::createApkWithNfpm($package, $name, $apkVersion, $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'])) { + // For APK cli packages: filter out the base prefix from provides since we have a separate meta package + // This prevents conflicts between php-zts-cli (which provides php-zts) and the php-zts meta package + $provides = $config['provides']; + if ($name === self::getPrefix() . '-cli') { + $provides = array_values(array_filter($provides, fn($p) => $p !== self::getPrefix())); + $provides = array_values($provides); + } + $nfpmConfig['provides'] = $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 with full filename including PHP version suffix + $phpSuffix = self::getPhpVersionSuffix(); + $outputFile = DIST_APK_PATH . "/{$name}-{$phpVersion}-r{$iteration}.{$phpSuffix}.{$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 @@ -635,15 +906,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,27 +957,192 @@ private static function getBinaryDependencies(string $binaryPath): array return $dependencies; } - private static function getNextIteration(string $name, string $phpVersion, string $architecture): int + /** + * Get dependencies for musl-linked binaries using the musl dynamic linker + */ + private static function getMuslBinaryDependencies(string $binaryPath): string { - $maxIteration = 0; + // 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; - $rpmPattern = DIST_RPM_PATH . "/{$name}-{$phpVersion}-*.{$architecture}.rpm"; - $rpmFiles = glob($rpmPattern); + // 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 using the prefix + $binaryName = basename($sourceBinary); + $binarySuffix = getBinarySuffix(); + $debugMap = [ + 'php' => BUILD_ROOT_PATH . '/debug/php.debug', + 'php-cgi' => BUILD_ROOT_PATH . '/debug/php-cgi.debug', + 'php-fpm' => BUILD_ROOT_PATH . '/debug/php-fpm.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"; - foreach ($rpmFiles as $file) { - if (preg_match("/{$name}-{$phpVersion}-(\d+)\.{$architecture}\.rpm$/", $file, $matches)) { - $iteration = (int)$matches[1]; - $maxIteration = max($maxIteration, $iteration); + // 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, string $packageType): int + { + $maxIteration = 0; + + if ($packageType === 'rpm') { + // RPM: {name}-{version}-{iteration}.{distVersion}.{arch}.rpm + // Also match old formats: + // - {name}-{version}-{iteration}.{phpSuffix}.{distVersion}.{arch}.rpm (with phpSuffix) + // - {name}-{version}-{iteration}.{arch}.rpm (no distVersion) + $rpmPattern = DIST_RPM_PATH . "/{$name}-{$phpVersion}-*.rpm"; + $rpmFiles = glob($rpmPattern); + + foreach ($rpmFiles as $file) { + // Match all formats: iteration followed by 0-2 parts, then arch.rpm + if (preg_match("/{$name}-" . preg_quote($phpVersion, '/') . "-(\d+)(?:\.[^.]+){0,2}\.{$architecture}\.rpm$/", $file, $matches)) { + $iteration = (int)$matches[1]; + $maxIteration = max($maxIteration, $iteration); + } } } - $debPattern = DIST_DEB_PATH . "/{$name}_{$phpVersion}-*_{$architecture}.deb"; - $debFiles = glob($debPattern); + if ($packageType === 'deb') { + // DEB: {name}_{version}-{iteration}_{arch}.deb + $debPattern = DIST_DEB_PATH . "/{$name}_{$phpVersion}-*.deb"; + $debFiles = glob($debPattern); - foreach ($debFiles as $file) { - if (preg_match("/{$name}_{$phpVersion}-(\d+)_{$architecture}\.deb$/", $file, $matches)) { - $iteration = (int)$matches[1]; - $maxIteration = max($maxIteration, $iteration); + foreach ($debFiles as $file) { + // Match: {name}_{version}-{iteration}_{arch}.deb + if (preg_match("/" . preg_quote($name, '/') . "_" . preg_quote($phpVersion, '/') . "-(\d+)_{$architecture}\.deb$/", $file, $matches)) { + $iteration = (int)$matches[1]; + $maxIteration = max($maxIteration, $iteration); + } + } + } + + if ($packageType === 'apk') { + // APK: {name}-{version}-r{iteration}.{phpSuffix}.{arch}.apk + // Also match old format: {name}-{version}-r{iteration}.{arch}.apk (no phpSuffix) + $apkPattern = DIST_APK_PATH . "/{$name}-{$phpVersion}-r*.apk"; + $apkFiles = glob($apkPattern); + + foreach ($apkFiles as $file) { + // Match both formats: r{iteration} followed by 0-1 parts, then arch.apk + if (preg_match("/{$name}-" . preg_quote($phpVersion, '/') . "-r(\d+)(?:\.[^.]+)?\.{$architecture}\.apk$/", $file, $matches)) { + $iteration = (int)$matches[1]; + $maxIteration = max($maxIteration, $iteration); + } } } @@ -704,6 +1151,74 @@ private static function getNextIteration(string $name, string $phpVersion, strin public static function getPrefix(): string { - return 'php-zts'; + // Return the prefix set by the user, prepended with "php" + // For example: "-zts" becomes "php-zts", "-zts8.5" becomes "php-zts8.5" + return 'php' . self::$prefix; + } + + /** + * Get PHP version suffix for package filenames (e.g., "static-83" for PHP 8.3) + */ + private static function getPhpVersionSuffix(): string + { + [$phpVersion,] = self::getPhpVersionAndArchitecture(); + + // Extract major.minor version (e.g., "8.3.29" -> "8.3") + if (preg_match('/^(\d+)\.(\d+)/', $phpVersion, $matches)) { + $majorMinorNoDot = $matches[1] . $matches[2]; // e.g., "83" + } else { + $majorMinorNoDot = str_replace('.', '', $phpVersion); + } + + // Construct suffix: static-{version} (e.g., "static-83") + return 'static-' . $majorMinorNoDot; + } + + /** + * Get distribution version for RPM filenames (e.g., "el9", "el8", "fc39") + */ + private static function getDistVersion(): string + { + if (!file_exists('/etc/os-release')) { + return ''; + } + + $osRelease = parse_ini_file('/etc/os-release'); + if (!$osRelease || !isset($osRelease['ID'], $osRelease['VERSION_ID'])) { + return ''; + } + + $id = $osRelease['ID']; + $versionId = $osRelease['VERSION_ID']; + + // Extract major version number + if (preg_match('/^(\d+)/', $versionId, $matches)) { + $majorVersion = $matches[1]; + } else { + return ''; + } + + // Map distribution ID to prefix + $distMap = [ + 'rhel' => 'el', + 'centos' => 'el', + 'rocky' => 'el', + 'almalinux' => 'el', + 'fedora' => 'fc', + ]; + + $prefix = $distMap[$id] ?? ''; + return $prefix !== '' ? $prefix . $majorVersion : ''; + } + + /** + * 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 (using unversioned prefix like -zts), returns empty array (RPM uses module system instead) + */ + public static function getVersionedConflicts(string $suffix): array + { + // Versioned packages can coexist - no conflicts + return []; } } diff --git a/src/step/RunSPC.php b/src/step/RunSPC.php index a72ac51..3c9ef5f 100644 --- a/src/step/RunSPC.php +++ b/src/step/RunSPC.php @@ -2,114 +2,12 @@ namespace staticphp\step; -use ArrayIterator; use Exception; -use FilesystemIterator; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; -use SPC\store\FileSystem; -use SplFileInfo; use Symfony\Component\Process\Process; use staticphp\util\TwigRenderer; class RunSPC { - private static function replaceInFiles(string $dir, string $builtDir, string $movedDir): void { - if (is_dir($dir)) { - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS) - ); - } - else { - $files = new ArrayIterator([new SplFileInfo($dir)]); - } - - foreach ($files as $file) { - if (!$file->isFile()) { - continue; - } - - $path = $file->getPathname(); - $contents = file_get_contents($path); - - if ($contents === false) { - continue; - } - if (!str_contains($contents, $builtDir)) { - continue; - } - - $newContents = str_replace($builtDir, $movedDir, $contents); - - if ($newContents !== $contents) { - file_put_contents($path, $newContents); - } - } - } - - private static function fixGnuDebugLinks(): void - { - $debugDir = BUILD_ROOT_PATH . '/debug'; - $binDir = BUILD_BIN_PATH; - - if (!is_dir($debugDir)) { - echo "No debug directory found at {$debugDir}, skipping GNU debuglink normalization.\n"; - return; - } - - $ensureRename = function (string $from, string $to) { - if ($from === $to) { - return; - } - if (file_exists($from)) { - if (!file_exists($to)) { - if (!@rename($from, $to)) { - echo "Failed to rename {$from} -> {$to}\n"; - } else { - echo "Renamed {$from} -> {$to}\n"; - } - } else { - @unlink($from); - } - } - }; - - $sapiMap = [ - $binDir . '/php' => $debugDir . '/php-zts.debug', - $binDir . '/php-fpm' => $debugDir . '/php-fpm-zts.debug', - $binDir . '/php-cgi' => $debugDir . '/php-cgi-zts.debug', - $binDir . '/frankenphp' => $debugDir . '/frankenphp.debug', - ]; - - $ensureRename($debugDir . '/php.debug', $debugDir . '/php-zts.debug'); - $ensureRename($debugDir . '/php-fpm.debug', $debugDir . '/php-fpm-zts.debug'); - $ensureRename($debugDir . '/php-cgi.debug', $debugDir . '/php-cgi-zts.debug'); - - foreach ($sapiMap as $binary => $dbgFile) { - if (!file_exists($binary)) { - continue; - } - self::runProcess(['objcopy', '--remove-section=.gnu_debuglink', $binary], "Removed existing gnu-debuglink from {$binary}"); - if (file_exists($dbgFile)) { - self::runProcess(['objcopy', '--add-gnu-debuglink=' . $dbgFile, $binary], "Added gnu-debuglink to {$binary} -> {$dbgFile}"); - } - } - } - - private static function runProcess(array $cmd, string $okMessage): void - { - $p = new Process($cmd); - $p->setTimeout(null); - $p->run(); - if ($p->isSuccessful()) { - echo $okMessage . "\n"; - } else { - // Log but do not fail the build - $bin = is_array($cmd) ? implode(' ', $cmd) : (string)$cmd; - echo "Warning: command failed: {$bin}\n" . $p->getErrorOutput() . "\n"; - } - } - public static function run(bool $debug = false, string $phpVersion = '8.4', ?array $packages = null): bool { $craftYmlDest = BASE_PATH . '/vendor/crazywhalecc/static-php-cli/craft.yml'; @@ -134,7 +32,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 @@ -148,26 +46,9 @@ public static function run(bool $debug = false, string $phpVersion = '8.4', ?arr echo "Static PHP CLI build completed successfully.\n"; - // Free up space for github runners - if (getenv('CI') || getenv('GITHUB_ACTION')) { - FileSystem::removeDir(BASE_PATH . '/vendor/crazywhalecc/static-php-cli/source'); - FileSystem::removeDir(BASE_PATH . '/vendor/crazywhalecc/static-php-cli/downloads'); - } - // Copy the built files to our build directory self::copyBuiltFiles($phpVersion); - // Fix the prefix - $builtDir = BASE_PATH . '/vendor/crazywhalecc/static-php-cli/buildroot'; - $movedDir = BUILD_ROOT_PATH; - self::replaceInFiles(BUILD_BIN_PATH . '/php-config', $builtDir, $movedDir); - self::replaceInFiles(BUILD_BIN_PATH . '/php-config', '/app/buildroot', $movedDir); - self::replaceInFiles(BUILD_LIB_PATH . '/pkgconfig', $builtDir, $movedDir); - self::replaceInFiles(BUILD_LIB_PATH . '/pkgconfig', '/app/buildroot', $movedDir); - - // After files are copied and paths fixed, normalize GNU debug links - self::fixGnuDebugLinks(); - return true; } catch (Exception $e) { echo "Error running static-php-cli with: " . $e->getMessage() . "\n"; diff --git a/src/util/TwigRenderer.php b/src/util/TwigRenderer.php index 2d6c700..be573a3 100644 --- a/src/util/TwigRenderer.php +++ b/src/util/TwigRenderer.php @@ -8,6 +8,26 @@ class TwigRenderer { + /** + * Renders any Twig template with the given variables + * + * @param string $templateName Template file name (e.g., 'pie-wrapper.twig') + * @param array $variables Variables to pass to the template + * @return string The rendered template content + * @throws \RuntimeException If there's an error rendering the template + */ + public static function render(string $templateName, array $variables = []): string + { + $loader = new FilesystemLoader(BASE_PATH . '/config/templates'); + $twig = new Environment($loader); + + try { + return $twig->render($templateName, $variables); + } catch (\Exception $e) { + throw new \RuntimeException("Error rendering template {$templateName}: " . $e->getMessage()); + } + } + /** * Renders a Twig template with the given variables * @@ -52,13 +72,27 @@ public static function renderCraftTemplate(string $phpVersion = '8.4', ?string $ } // Prepare template variables + // Use SPP_PREFIX and SPP_TYPE constants set by BaseCommand + $prefix = 'php' . (defined('SPP_PREFIX') ? SPP_PREFIX : '-zts'); + $packageType = defined('SPP_TYPE') ? SPP_TYPE : 'rpm'; + $libdir = $packageType === 'rpm' ? '/usr/lib64' : '/usr/lib'; + + // Get the binary suffix (e.g., "-zts", "-nts", "-zts8.5") + $binarySuffix = defined('SPP_PREFIX') ? SPP_PREFIX : '-zts'; + // For the -release flag: remove only the leading dash (keep dots) + // e.g., "-zts" -> "zts", "-zts8.5" -> "zts8.5" + $releasePrefix = ltrim($binarySuffix, '-'); + $templateVars = [ 'php_version' => $phpVersion, 'php_version_nodot' => str_replace('.', '', $phpVersion), 'target' => SPP_TARGET, 'arch' => $arch, 'os' => $majorOsVersion, - 'prefix' => CreatePackages::getPrefix(), + 'prefix' => $prefix, + 'release_prefix' => $releasePrefix, + '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,