From 3d2933ec7f1bc26be3cdbfbaf9d6fa21eaa70dec Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 26 Aug 2026 16:24:19 +0800 Subject: [PATCH] ci: package tagged compiler releases --- .github/workflows/release.yml | 129 +++++++++ .github/workflows/tests.yml | 30 +- .github/workflows/windows-build.yml | 30 ++ package.php | 415 +++++++++++++++------------- version.txt | 1 - 5 files changed, 406 insertions(+), 199 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100644 version.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..09311a68 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,129 @@ +name: Package release + +on: + push: + tags: + - "v*" + +permissions: + actions: read + contents: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Publish ${{ github.ref_name }} + runs-on: ubuntu-22.04 + timeout-minutes: 240 + env: + GH_TOKEN: ${{ github.token }} + + steps: + - name: Wait for tested binary packages + shell: bash + run: | + set -euo pipefail + + wait_for_workflow() { + local workflow="$1" + local output_name="$2" + local run_json run_id status conclusion + + for attempt in $(seq 1 960); do + run_json="$(gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow "${workflow}" \ + --commit "${GITHUB_SHA}" \ + --event push \ + --limit 20 \ + --json databaseId,status,conclusion,headBranch,createdAt)" + run_id="$(jq -r --arg tag "${GITHUB_REF_NAME}" ' + [.[] | select(.headBranch == $tag)] + | sort_by(.createdAt) + | last + | .databaseId // empty + ' <<<"${run_json}")" + + if [[ -z "${run_id}" ]]; then + echo "Waiting for ${workflow} to start for tag ${GITHUB_REF_NAME}..." + sleep 15 + continue + fi + + status="$(jq -r --argjson id "${run_id}" ' + .[] | select(.databaseId == $id) | .status + ' <<<"${run_json}")" + conclusion="$(jq -r --argjson id "${run_id}" ' + .[] | select(.databaseId == $id) | .conclusion + ' <<<"${run_json}")" + if [[ "${status}" == "completed" ]]; then + if [[ "${conclusion}" != "success" ]]; then + echo "${workflow} run ${run_id} completed with ${conclusion}" >&2 + exit 1 + fi + echo "${workflow} run ${run_id} succeeded" + echo "${output_name}=${run_id}" >> "${GITHUB_ENV}" + return 0 + fi + + echo "Waiting for ${workflow} run ${run_id}: ${status}" + sleep 15 + done + + echo "Timed out waiting for ${workflow}" >&2 + exit 1 + } + + wait_for_workflow tests.yml TESTS_RUN_ID + wait_for_workflow windows-build.yml WINDOWS_RUN_ID + + - name: Download tested packages + shell: bash + run: | + set -euo pipefail + mkdir -p dist/linux dist/windows + gh run download "${TESTS_RUN_ID}" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern 'release-linux-*' \ + --dir dist/linux + gh run download "${WINDOWS_RUN_ID}" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern 'release-windows-*' \ + --dir dist/windows + + test "$(find dist/linux -type f -name '*.tar.gz' | wc -l)" -eq 1 + test "$(find dist/windows -type f -name '*.zip' | wc -l)" -eq 1 + + - name: Prepare release assets + shell: bash + run: | + set -euo pipefail + mkdir -p release-assets + find dist -type f \( -name '*.tar.gz' -o -name '*.zip' \) \ + -exec cp '{}' release-assets/ \; + test "$(find release-assets -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' \) | wc -l)" -eq 2 + ( + cd release-assets + sha256sum ./*.tar.gz ./*.zip > SHA256SUMS + ) + ls -lh release-assets + + - name: Publish GitHub release + shell: bash + run: | + set -euo pipefail + mapfile -t assets < <(find release-assets -maxdepth 1 -type f | sort) + if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then + gh release upload "${GITHUB_REF_NAME}" "${assets[@]}" \ + --repo "${GITHUB_REPOSITORY}" \ + --clobber + else + gh release create "${GITHUB_REF_NAME}" "${assets[@]}" \ + --repo "${GITHUB_REPOSITORY}" \ + --verify-tag \ + --title "TypePHP ${GITHUB_REF_NAME}" \ + --generate-notes + fi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ef3490e8..f7f7099f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,8 +20,8 @@ jobs: build-phpx: name: Build PHPX (PHP ${{ matrix.php }}) # Skip the whole test pipeline when the push commit message contains `--skip-tests`. - if: ${{ !contains(github.event.head_commit.message || '', '--skip-tests') }} - runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || !contains(github.event.head_commit.message || '', '--skip-tests') }} + runs-on: ubuntu-22.04 timeout-minutes: 30 strategy: fail-fast: false @@ -96,8 +96,8 @@ jobs: phpunit: name: PHPUnit (PHP ${{ matrix.php }}) - if: ${{ !contains(github.event.head_commit.message || '', '--skip-tests') }} - runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || !contains(github.event.head_commit.message || '', '--skip-tests') }} + runs-on: ubuntu-22.04 timeout-minutes: 30 needs: build-phpx strategy: @@ -161,8 +161,8 @@ jobs: phpt: name: PHPT (PHP ${{ matrix.php }}) - if: ${{ !contains(github.event.head_commit.message || '', '--skip-tests') }} - runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || !contains(github.event.head_commit.message || '', '--skip-tests') }} + runs-on: ubuntu-22.04 timeout-minutes: 180 needs: build-phpx strategy: @@ -271,6 +271,24 @@ jobs: php run-tests.php -q -j8 --compiler ./tpc \ -w build/failed-tests.txt -W build/test-results.txt tests/compiler + - name: Package tested Linux compiler + if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5' + shell: bash + run: | + composer install --no-dev --prefer-dist --no-progress --classmap-authoritative + export TYPEPHP_PACKAGE_VERSION="${GITHUB_REF_NAME}" + php package.php + test "$(find . -maxdepth 1 -name 'tpc_v*_linux_*.tar.gz' -type f | wc -l)" -eq 1 + + - name: Upload Linux release package + if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5' + uses: actions/upload-artifact@v4 + with: + name: release-linux-php-${{ matrix.php }} + if-no-files-found: error + retention-days: 1 + path: tpc_v*_linux_*.tar.gz + - name: Upload PHPT failure artifacts if: failure() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index f15ef9ba..c0d2d3cb 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -398,6 +398,36 @@ jobs: throw "Unexpected Windows smoke output: $stdout" } + - name: Package tested Windows compiler + if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5' && matrix.thread_safety == 'zts' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + composer install --no-dev --prefer-dist --no-progress --classmap-authoritative + if ($LASTEXITCODE -ne 0) { + throw "Production Composer install failed with exit code $LASTEXITCODE" + } + $env:TYPEPHP_PACKAGE_VERSION = $env:GITHUB_REF_NAME + php package.php + if ($LASTEXITCODE -ne 0) { + throw "Windows packaging failed with exit code $LASTEXITCODE" + } + + $packages = @(Get-ChildItem 'tpc_v*_windows_*.zip' -File) + if ($packages.Count -ne 1) { + throw "Expected one Windows release package, found $($packages.Count)" + } + + - name: Upload Windows release package + if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5' && matrix.thread_safety == 'zts' + uses: actions/upload-artifact@v4 + with: + name: release-windows-php-${{ matrix.php }}-${{ matrix.thread_safety }} + if-no-files-found: error + retention-days: 1 + path: tpc_v*_windows_*.zip + - name: Upload Windows build outputs if: always() uses: actions/upload-artifact@v4 diff --git a/package.php b/package.php index ff853c03..c916447d 100755 --- a/package.php +++ b/package.php @@ -3,9 +3,9 @@ /** * TypePHP cross-platform release packager. * - * Windows produces a self-contained PHP/PHPX SDK package. Linux and macOS - * retain their system-runtime package layout while sharing the same version, - * staging, archive verification, and cleanup rules. + * Windows produces a self-contained PHP/PHPX SDK package. Linux contains the + * tested ELF compiler and production Composer vendor tree, but no host native + * libraries. All platforms share version, staging, verification, and cleanup. */ if (!chdir(__DIR__)) { @@ -15,7 +15,7 @@ if (!chdir(__DIR__)) { if (in_array('--help', $argv ?? [], true) || in_array('-h', $argv ?? [], true)) { echo "Usage: php package.php\n\n"; echo "Windows: requires PHP_HOME and PHPX_HOME; creates a self-contained SDK package.\n"; - echo "Linux: requires UPX; packages the native binary and release metadata.\n"; + echo "Linux: uses strip; packages the tested ELF and Composer vendor tree.\n"; echo "macOS: uses strip when available; packages the native binary and release metadata.\n"; echo "Supported architectures: 64-bit CPUs, including x86_64 and ARM64.\n"; exit(0); @@ -54,18 +54,10 @@ echo "PHP 目录: {$phpDir}\n"; echo "phpx 目录: {$phpxDir}\n\n"; $compilerExe = 'tpc.exe'; -$versionFile = 'version.txt'; - // ==================== 1. 版本号管理 ==================== echo "[1/7] 处理版本号...\n"; -if (file_exists($versionFile)) { - $versionId = (int)trim(file_get_contents($versionFile)); -} else { - $versionId = 1000; -} - -$versionId++; +$versionId = resolvePackageVersion(); echo "当前版本: {$versionId}\n\n"; @@ -93,7 +85,7 @@ echo "[3/7] 检查必要文件...\n"; $requiredFiles = [ $compilerExe, 'README.md', - 'LICENSE.md', + 'LICENSE', 'composer.json', 'examples/hello.php', ]; @@ -239,49 +231,10 @@ PHPX_HOME must point to the bundled phpx directory. Its required files include: TEXT; mustWriteFile($windowsSetupFile, $windowsSetup); -// ==================== 4. UPX 压缩(可选) ==================== -echo "[4/7] UPX 压缩优化...\n"; - -$useUpx = false; - -// 检查 UPX 是否可用 -exec('where upx 2>nul', $upxOutput, $upxReturn); -if ($upxReturn === 0) { - // 获取 UPX 版本 - exec('upx --version 2>&1', $versionOutput); - $upxVersion = $versionOutput[0] ?? 'unknown'; - echo "检测到 UPX: {$upxVersion}\n"; - - // 只压缩暂存目录中的副本,不修改工作区内的原始 tpc.exe。 - echo "使用 UPX 压缩 {$packagedCompilerExe} ...\n"; - $originalSize = filesize($packagedCompilerExe); - exec('upx --best ' . escapeshellarg($packagedCompilerExe) . ' 2>&1', $upxOutput, $upxResult); - - if ($upxResult === 0) { - echo "✓ UPX 压缩完成\n"; - - // 显示压缩效果 - $compressedSize = filesize($packagedCompilerExe); - $originalSizeMB = round($originalSize / 1024 / 1024, 2); - $compressedSizeMB = round($compressedSize / 1024 / 1024, 2); - $compressionRatio = round((1 - $compressedSize / $originalSize) * 100, 2); - - echo " 原始大小: {$originalSizeMB} MB\n"; - echo " 压缩后: {$compressedSizeMB} MB\n"; - echo " 压缩率: {$compressionRatio}%\n"; - - $useUpx = true; - } else { - echo "✗ UPX 压缩失败,重新复制未压缩的 tpc.exe\n"; - mustCopy($compilerExe, $packagedCompilerExe); - $useUpx = false; - } -} else { - echo "未找到 UPX,跳过压缩步骤\n"; - echo "提示: 安装 UPX 可以显著减小编译器体积\n"; -} - -echo "\n"; +// ==================== 4. 保持标准 PE 格式 ==================== +// Windows 包依靠 ZIP 压缩。不要使用 UPX 改写 tpc.exe,以避免增加杀毒 +// 误报和运行时自解压带来的兼容性变量。 +echo "[4/7] 保持标准 PE 可执行文件,不使用 UPX\n\n"; // ==================== 5. 准备 PHP 目录结构 ==================== echo "[5/7] 准备 PHP 目录结构...\n"; @@ -326,7 +279,7 @@ mustCreateDirectory("{$topLevelDir}/examples"); // 复制项目文件 $projectFiles = [ 'README.md' => '.', - 'LICENSE.md' => '.', + 'LICENSE' => '.', 'composer.json' => '.', 'examples/hello.php' => 'examples', ]; @@ -509,17 +462,6 @@ removeDirectory($topLevelDir); $cleanupStage = false; echo "临时目录已清理\n"; -try { - mustWriteFile($versionFile, (string)$versionId); -} catch (Throwable $error) { - if (is_file($outputFile) && !unlink($outputFile)) { - throw new RuntimeException( - "无法提交版本号,且无法删除未提交的压缩包 {$outputFile}: {$error->getMessage()}", - previous: $error, - ); - } - throw $error; -} $cleanupArchive = false; echo "\n"; @@ -541,7 +483,7 @@ echo " - phpx/src/misc/ (PHPX Embed/CLI runtime adapters)\n"; echo " - PHP 运行时环境 (完整目录结构)\n"; echo " - vendor/ (Composer 依赖包,无需再次安装)\n"; echo " - composer.json (Composer 配置文件)\n"; -echo " - README.md/LICENSE.md (文档)\n"; +echo " - README.md/LICENSE (文档)\n"; echo " - examples/hello.php (PHP 示例代码)\n"; echo " - examples/win32-hello/ (Windows GUI 编程实例)\n"; echo " - examples/tetris-win32/ (俄罗斯方块游戏实例)\n\n"; @@ -552,6 +494,35 @@ echo " 3. set PHPX_HOME=%CD%\\phpx\n"; echo " 4. set PATH=%CD%;%PATH%\n"; echo " 5. 运行: tpc \n\n"; +function resolvePackageVersion(): string +{ + $releaseVersion = getenv('TYPEPHP_PACKAGE_VERSION'); + if (!is_string($releaseVersion) || trim($releaseVersion) === '') { + $releaseVersion = getenv('TYPEPHP_BUILD_ID'); + } + if (!is_string($releaseVersion) || trim($releaseVersion) === '') { + $releaseVersion = getenv('GITHUB_RUN_NUMBER'); + } + if (!is_string($releaseVersion) || trim($releaseVersion) === '') { + $releaseVersion = getenv('GITHUB_RUN_ID'); + } + if (!is_string($releaseVersion) || trim($releaseVersion) === '') { + throw new RuntimeException( + 'Set TYPEPHP_PACKAGE_VERSION for a release or TYPEPHP_BUILD_ID for a preview build', + ); + } + $releaseVersion = trim($releaseVersion); + if (preg_match('/^v(?=\d)/i', $releaseVersion) === 1) { + $releaseVersion = substr($releaseVersion, 1); + } + if (preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]*$/', $releaseVersion) !== 1) { + throw new RuntimeException( + 'Package version contains characters that are unsafe in archive names', + ); + } + return $releaseVersion; +} + function normalizeArchitecture(string $architecture): string { $architecture = strtolower(trim($architecture)); @@ -595,50 +566,54 @@ function packageUnixLike(): void $arch = normalizeArchitecture(php_uname('m')); $binary = 'tpc'; - $versionFile = 'version.txt'; - $phpxSourceDir = getenv('PHPX_HOME'); - if (!is_string($phpxSourceDir) || $phpxSourceDir === '' || !is_dir($phpxSourceDir)) { - $phpxSourceDir = 'vendor/swoole/phpx'; - } - $phpxSourceDir = realpath($phpxSourceDir) ?: rtrim($phpxSourceDir, '/\\'); - $wasiSdkSourceRoot = $phpxSourceDir . '/wasm/wasm32-wasip2'; - $wasiSdkPackageRoot = 'vendor/swoole/phpx/wasm/wasm32-wasip2'; - $requiredFiles = [ - $binary, - 'composer.json', - 'README.md', - 'LICENSE.md', - 'examples/hello.php', - 'completions/tpc.bash', - 'wasm/build-program.sh', - "{$wasiSdkSourceRoot}/.typephp-wasi-sdk-abi", - "{$wasiSdkSourceRoot}/include/php/main/php.h", - "{$wasiSdkSourceRoot}/include/php/main/php_config.h", - "{$wasiSdkSourceRoot}/include/php/Zend/zend_config.h", - "{$wasiSdkSourceRoot}/include/php/ext/date/lib/timelib_config.h", - "{$wasiSdkSourceRoot}/include/phpx/phpx.h", - "{$wasiSdkSourceRoot}/include/phpx/phpx_python.h", - "{$wasiSdkSourceRoot}/include/phpx/typephp_helper.h", - "{$wasiSdkSourceRoot}/include/gmp.h", - "{$wasiSdkSourceRoot}/include/mpfr.h", - "{$wasiSdkSourceRoot}/include/decimal.hh", - ]; - foreach (['libphp.a', 'libphpx.a', 'libgmp.a', 'libgmpxx.a', 'libmpfr.a', 'libmpdec.a', 'libmpdec++.a'] as $library) { - $requiredFiles[] = "{$wasiSdkSourceRoot}/lib/{$library}"; + $requiredFiles = [$binary]; + if ($osType === 'linux') { + $requiredFiles[] = 'vendor/autoload.php'; + $requiredFiles[] = 'vendor/composer/installed.php'; + } else { + $phpxSourceDir = getenv('PHPX_HOME'); + if (!is_string($phpxSourceDir) || $phpxSourceDir === '' || !is_dir($phpxSourceDir)) { + $phpxSourceDir = 'vendor/swoole/phpx'; + } + $phpxSourceDir = realpath($phpxSourceDir) ?: rtrim($phpxSourceDir, '/\\'); + $wasiSdkSourceRoot = $phpxSourceDir . '/wasm/wasm32-wasip2'; + $wasiSdkPackageRoot = 'vendor/swoole/phpx/wasm/wasm32-wasip2'; + array_push( + $requiredFiles, + 'composer.json', + 'README.md', + 'LICENSE', + 'examples/hello.php', + 'completions/tpc.bash', + 'wasm/build-program.sh', + "{$wasiSdkSourceRoot}/.typephp-wasi-sdk-abi", + "{$wasiSdkSourceRoot}/include/php/main/php.h", + "{$wasiSdkSourceRoot}/include/php/main/php_config.h", + "{$wasiSdkSourceRoot}/include/php/Zend/zend_config.h", + "{$wasiSdkSourceRoot}/include/php/ext/date/lib/timelib_config.h", + "{$wasiSdkSourceRoot}/include/phpx/phpx.h", + "{$wasiSdkSourceRoot}/include/phpx/phpx_python.h", + "{$wasiSdkSourceRoot}/include/phpx/typephp_helper.h", + "{$wasiSdkSourceRoot}/include/gmp.h", + "{$wasiSdkSourceRoot}/include/mpfr.h", + "{$wasiSdkSourceRoot}/include/decimal.hh", + ); + foreach (['libphp.a', 'libphpx.a', 'libgmp.a', 'libgmpxx.a', 'libmpfr.a', 'libmpdec.a', 'libmpdec++.a'] as $library) { + $requiredFiles[] = "{$wasiSdkSourceRoot}/lib/{$library}"; + } } foreach ($requiredFiles as $file) { if (!is_file($file)) { throw new RuntimeException("Required package file not found: {$file}"); } } - if (!class_exists('ZipArchive')) { + if ($osType !== 'linux' && !class_exists('ZipArchive')) { throw new RuntimeException('The ZipArchive extension is required'); } - $versionId = is_file($versionFile) ? (int)trim((string)file_get_contents($versionFile)) : 1000; - $versionId++; + $versionId = resolvePackageVersion(); $topLevelDir = "tpc_v{$versionId}_{$osType}_{$arch}"; - $outputFile = $topLevelDir . '.zip'; + $outputFile = $topLevelDir . ($osType === 'linux' ? '.tar.gz' : '.zip'); echo "========================================\n"; echo "TypePHP {$osType} package\n"; @@ -678,106 +653,147 @@ function packageUnixLike(): void throw new RuntimeException("Unable to mark executable: {$stagedBinary}"); } + exec('command -v strip 2>/dev/null', $stripPath, $stripStatus); + if ($stripStatus !== 0) { + throw new RuntimeException('strip is required for Unix-like packaging'); + } + $stripFlag = $osType === 'linux' ? '--strip-unneeded' : '-x'; + exec( + 'strip ' . $stripFlag . ' ' . escapeshellarg($stagedBinary) . ' 2>&1', + $stripOutput, + $stripStatus, + ); + if ($stripStatus !== 0) { + throw new RuntimeException("strip failed:\n" . implode("\n", $stripOutput)); + } + if ($osType === 'linux') { - exec('command -v upx 2>/dev/null', $upxPath, $upxStatus); - if ($upxStatus !== 0) { - throw new RuntimeException( - 'UPX is required for Linux packaging (for example: apt install upx-ucl)', - ); - } - exec('upx --best ' . escapeshellarg($stagedBinary) . ' 2>&1', $upxOutput, $upxStatus); - if ($upxStatus !== 0) { - throw new RuntimeException("UPX failed:\n" . implode("\n", $upxOutput)); - } + // Linux release binaries intentionally use the target system's libphp, + // libphpx, and other native dependencies. Composer sources and headers + // are portable, but test-built host libraries must not enter the archive. + mustCreateDirectory("{$topLevelDir}/vendor"); + copyDirectory( + 'vendor', + "{$topLevelDir}/vendor", + [], + null, + ['a', 'dll', 'dylib', 'exe', 'exp', 'lib', 'o', 'obj', 'pdb', 'so'], + ); } else { - exec('command -v strip 2>/dev/null', $stripPath, $stripStatus); - if ($stripStatus === 0) { - exec('strip -x ' . escapeshellarg($stagedBinary) . ' 2>&1', $stripOutput, $stripStatus); - if ($stripStatus !== 0) { - throw new RuntimeException("strip failed:\n" . implode("\n", $stripOutput)); - } - } else { - echo "Warning: strip was not found; packaging the unstripped binary\n"; - } + mustCopy('composer.json', "{$topLevelDir}/composer.json"); + mustCopy('README.md', "{$topLevelDir}/README.md"); + mustCopy('LICENSE', "{$topLevelDir}/LICENSE"); + mustCreateDirectory("{$topLevelDir}/examples"); + mustCopy('examples/hello.php', "{$topLevelDir}/examples/hello.php"); + mustCreateDirectory("{$topLevelDir}/completions"); + mustCopy('completions/tpc.bash', "{$topLevelDir}/completions/tpc.bash"); + copyDirectory('wasm', "{$topLevelDir}/wasm"); + // Host tools are installed independently; only the target SDK is bundled. + copyDirectory( + "{$phpxSourceDir}/wasm", + "{$topLevelDir}/vendor/swoole/phpx/wasm", + ['bin'], + ); } - mustCopy('composer.json', "{$topLevelDir}/composer.json"); - mustCopy('README.md', "{$topLevelDir}/README.md"); - mustCopy('LICENSE.md', "{$topLevelDir}/LICENSE.md"); - mustCreateDirectory("{$topLevelDir}/examples"); - mustCopy('examples/hello.php', "{$topLevelDir}/examples/hello.php"); - mustCreateDirectory("{$topLevelDir}/completions"); - mustCopy('completions/tpc.bash', "{$topLevelDir}/completions/tpc.bash"); - copyDirectory('wasm', "{$topLevelDir}/wasm"); - // Host build tools such as wit-bindgen are installed independently and - // discovered through PATH; only the target SDK belongs in the package. - copyDirectory("{$phpxSourceDir}/wasm", "{$topLevelDir}/vendor/swoole/phpx/wasm", ['bin']); - - if (is_file($outputFile) && !unlink($outputFile)) { - throw new RuntimeException("Unable to remove existing archive: {$outputFile}"); + $requiredEntries = ["{$topLevelDir}/{$binary}"]; + if ($osType === 'linux') { + $requiredEntries[] = "{$topLevelDir}/vendor/autoload.php"; + $requiredEntries[] = "{$topLevelDir}/vendor/composer/installed.php"; + } else { + array_push( + $requiredEntries, + "{$topLevelDir}/composer.json", + "{$topLevelDir}/README.md", + "{$topLevelDir}/LICENSE", + "{$topLevelDir}/examples/hello.php", + "{$topLevelDir}/completions/tpc.bash", + "{$topLevelDir}/wasm/build-program.sh", + "{$topLevelDir}/{$wasiSdkPackageRoot}/.typephp-wasi-sdk-abi", + "{$topLevelDir}/{$wasiSdkPackageRoot}/include/php/main/php.h", + "{$topLevelDir}/{$wasiSdkPackageRoot}/include/phpx/phpx.h", + ); + foreach (['libphp.a', 'libphpx.a', 'libgmp.a', 'libgmpxx.a', 'libmpfr.a', 'libmpdec.a', 'libmpdec++.a'] as $library) { + $requiredEntries[] = "{$topLevelDir}/{$wasiSdkPackageRoot}/lib/{$library}"; + } } + if ($osType === 'linux') { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + "{$topLevelDir}/vendor", + FilesystemIterator::SKIP_DOTS, + ), + RecursiveIteratorIterator::LEAVES_ONLY, + ); + foreach ($files as $file) { + if ($file->isFile() && isNativeLibraryPath($file->getPathname())) { + throw new RuntimeException( + "Linux package unexpectedly contains a native library: {$file->getPathname()}", + ); + } + } - $zip = new ZipArchive(); - if ($zip->open($outputFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { - throw new RuntimeException("Unable to create archive: {$outputFile}"); - } - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($topLevelDir, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::LEAVES_ONLY, - ); - foreach ($files as $file) { - if (!$file->isFile()) { - continue; + exec('command -v tar 2>/dev/null', $tarPath, $tarStatus); + if ($tarStatus !== 0) { + throw new RuntimeException('tar is required for Linux packaging'); } - $relativePath = str_replace('\\', '/', substr( - $file->getPathname(), - strlen($topLevelDir) + 1, - )); - if (!$zip->addFile($file->getPathname(), "{$topLevelDir}/{$relativePath}")) { - throw new RuntimeException("Unable to add archive entry: {$file->getPathname()}"); + exec( + 'tar -czf ' . escapeshellarg($outputFile) . ' ' . escapeshellarg($topLevelDir) . ' 2>&1', + $tarOutput, + $tarStatus, + ); + if ($tarStatus !== 0) { + throw new RuntimeException("tar failed:\n" . implode("\n", $tarOutput)); } - } - if (!$zip->close()) { - throw new RuntimeException("Unable to finish archive: {$outputFile}"); - } - - $requiredEntries = [ - "{$topLevelDir}/{$binary}", - "{$topLevelDir}/composer.json", - "{$topLevelDir}/README.md", - "{$topLevelDir}/LICENSE.md", - "{$topLevelDir}/examples/hello.php", - "{$topLevelDir}/completions/tpc.bash", - "{$topLevelDir}/wasm/build-program.sh", - "{$topLevelDir}/{$wasiSdkPackageRoot}/.typephp-wasi-sdk-abi", - "{$topLevelDir}/{$wasiSdkPackageRoot}/include/php/main/php.h", - "{$topLevelDir}/{$wasiSdkPackageRoot}/include/phpx/phpx.h", - ]; - foreach (['libphp.a', 'libphpx.a', 'libgmp.a', 'libgmpxx.a', 'libmpfr.a', 'libmpdec.a', 'libmpdec++.a'] as $library) { - $requiredEntries[] = "{$topLevelDir}/{$wasiSdkPackageRoot}/lib/{$library}"; - } - $verificationZip = new ZipArchive(); - if ($verificationZip->open($outputFile) !== true) { - throw new RuntimeException("Unable to verify archive: {$outputFile}"); - } - foreach ($requiredEntries as $entry) { - if ($verificationZip->locateName($entry) === false) { - $verificationZip->close(); - throw new RuntimeException("Archive is missing required entry: {$entry}"); + exec('tar -tzf ' . escapeshellarg($outputFile) . ' 2>&1', $archiveEntries, $tarStatus); + if ($tarStatus !== 0) { + throw new RuntimeException("Unable to verify archive:\n" . implode("\n", $archiveEntries)); } + foreach ($requiredEntries as $entry) { + if (!in_array($entry, $archiveEntries, true)) { + throw new RuntimeException("Archive is missing required entry: {$entry}"); + } + } + } else { + $zip = new ZipArchive(); + if ($zip->open($outputFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + throw new RuntimeException("Unable to create archive: {$outputFile}"); + } + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($topLevelDir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY, + ); + foreach ($files as $file) { + if (!$file->isFile()) { + continue; + } + $relativePath = str_replace('\\', '/', substr( + $file->getPathname(), + strlen($topLevelDir) + 1, + )); + if (!$zip->addFile($file->getPathname(), "{$topLevelDir}/{$relativePath}")) { + throw new RuntimeException("Unable to add archive entry: {$file->getPathname()}"); + } + } + if (!$zip->close()) { + throw new RuntimeException("Unable to finish archive: {$outputFile}"); + } + + $verificationZip = new ZipArchive(); + if ($verificationZip->open($outputFile) !== true) { + throw new RuntimeException("Unable to verify archive: {$outputFile}"); + } + foreach ($requiredEntries as $entry) { + if ($verificationZip->locateName($entry) === false) { + $verificationZip->close(); + throw new RuntimeException("Archive is missing required entry: {$entry}"); + } + } + $verificationZip->close(); } - $verificationZip->close(); removeDirectory($topLevelDir); $cleanupStage = false; - try { - mustWriteFile($versionFile, (string)$versionId); - } catch (Throwable $error) { - if (is_file($outputFile)) { - @unlink($outputFile); - } - throw $error; - } $cleanupArchive = false; $sizeMb = formatMegabytes(filesize($outputFile)); @@ -789,6 +805,14 @@ function formatMegabytes(int $bytes): string return number_format($bytes / 1024 / 1024, 3, '.', ''); } +function isNativeLibraryPath(string $path): bool +{ + return preg_match( + '/\.(?:a|dll|dylib(?:\.\d+)*|exe|exp|lib|o|obj|pdb|so(?:\.\d+)*)$/i', + $path, + ) === 1; +} + /** * 递归复制目录 * @param string $src 源目录 @@ -823,7 +847,14 @@ function copyDirectory(string $src, string $dest, array $excludeDirs = [], ?stri // 如果是文件,检查扩展名是否在排除列表中 if (is_file($srcPath) && !empty($excludeExtensions)) { $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); - if (in_array($ext, $excludeExtensions)) { + $versionedSharedLibrary = ( + in_array('so', $excludeExtensions, true) + && preg_match('/\.so(?:\.\d+)*$/i', $file) === 1 + ) || ( + in_array('dylib', $excludeExtensions, true) + && preg_match('/\.dylib(?:\.\d+)*$/i', $file) === 1 + ); + if ($versionedSharedLibrary || in_array($ext, $excludeExtensions, true)) { continue; } } diff --git a/version.txt b/version.txt deleted file mode 100644 index 76482c7d..00000000 --- a/version.txt +++ /dev/null @@ -1 +0,0 @@ -1112 \ No newline at end of file