From cefc3f4f5e6e9da38ce2b787c35182eb5c6de11f Mon Sep 17 00:00:00 2001 From: Tianfeng Han Date: Fri, 31 Jul 2026 16:47:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9B=E6=89=93=E5=8C=85=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=EF=BC=8C3=E4=B8=AA=E5=B9=B3=E5=8F=B0=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E4=B8=80=E4=B8=AA=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++ package-on-windows.php => package.php | 244 ++++++++++++++++++++++++-- package.sh | 221 ----------------------- version.txt | 2 +- 4 files changed, 242 insertions(+), 237 deletions(-) rename package-on-windows.php => package.php (72%) delete mode 100755 package.sh diff --git a/README.md b/README.md index afdbe2bd..9e65fd40 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,15 @@ vim /etc/ld.so.conf.d/swoole.conf /home/swoole/workspace/projects/phpx/lib /opt/php-8.4/lib/ ``` + +## Release packaging + +Use the same PHP entry point on Windows, Linux, and macOS: + +```shell +php package.php +``` + +Windows packaging requires `PHP_HOME` and `PHPX_HOME`; Linux packaging requires +UPX; macOS uses `strip` when available. TypePHP rejects 32-bit targets and +supports common 64-bit CPU architectures, including x86-64 and ARM64. diff --git a/package-on-windows.php b/package.php similarity index 72% rename from package-on-windows.php rename to package.php index 63ba0f70..383d0f30 100644 --- a/package-on-windows.php +++ b/package.php @@ -1,15 +1,29 @@ #!/usr/bin/env php 'x86_64', - 'ARM64' => 'arm64', - default => null, -}; -if ($arch === null || PHP_INT_SIZE !== 8) { +if (PHP_INT_SIZE !== 8) { $detectedArchitecture = $processorArchitecture ?: 'unknown'; - echo "错误: TypePHP 不支持 32 位或未知 Windows 架构 - {$detectedArchitecture}\n"; - echo "仅支持 Windows x86_64 和 ARM64\n"; + echo "错误: TypePHP 不支持 32 位 Windows 架构 - {$detectedArchitecture}\n"; exit(1); } +$arch = normalizeArchitecture((string)$processorArchitecture); $osType = 'windows'; $outputFile = "tpc_v{$versionId}_{$osType}_{$arch}.zip"; @@ -186,7 +195,13 @@ if (is_dir($topLevelDir)) { mustCreateDirectory($topLevelDir); $cleanupStage = true; -register_shutdown_function(static function () use (&$cleanupStage, $topLevelDir): void { +$cleanupArchive = true; +register_shutdown_function(static function () use ( + &$cleanupStage, + &$cleanupArchive, + $topLevelDir, + $outputFile, +): void { if ($cleanupStage && is_dir($topLevelDir)) { try { removeDirectory($topLevelDir); @@ -194,6 +209,9 @@ register_shutdown_function(static function () use (&$cleanupStage, $topLevelDir) fwrite(STDERR, "警告: 无法清理临时目录 {$topLevelDir}: {$error->getMessage()}\n"); } } + if ($cleanupArchive && is_file($outputFile) && !unlink($outputFile)) { + fwrite(STDERR, "警告: 无法清理未提交的压缩包 {$outputFile}\n"); + } }); $packagedCompilerExe = "{$topLevelDir}/{$compilerExe}"; @@ -501,6 +519,7 @@ try { } throw $error; } +$cleanupArchive = false; echo "\n"; @@ -532,6 +551,201 @@ echo " 3. set PHPX_HOME=%CD%\\phpx\n"; echo " 4. set PATH=%CD%;%PATH%\n"; echo " 5. 运行: tpc \n\n"; +function normalizeArchitecture(string $architecture): string +{ + $architecture = strtolower(trim($architecture)); + $normalized = match ($architecture) { + 'x86_64', 'x86-64', 'amd64', 'x64' => 'x86_64', + 'aarch64', 'arm64', 'arm64b', 'arm64e' => 'arm64', + 'powerpc64', 'ppc64' => 'ppc64', + 'powerpc64le', 'ppc64le' => 'ppc64le', + 'riscv64' => 'riscv64', + 's390x' => 's390x', + 'loongarch64' => 'loongarch64', + 'mips64' => 'mips64', + 'mips64el' => 'mips64el', + 'sparc64' => 'sparc64', + default => null, + }; + if ($normalized !== null) { + return $normalized; + } + + // Preserve future 64-bit architecture names while keeping archive names safe. + if (str_contains($architecture, '64') + && preg_match('/^[a-z0-9][a-z0-9._-]*$/', $architecture) === 1) { + return str_replace('-', '_', $architecture); + } + + throw new RuntimeException("Unsupported or non-64-bit architecture: {$architecture}"); +} + +function packageUnixLike(): void +{ + $osType = match (PHP_OS_FAMILY) { + 'Linux' => 'linux', + 'Darwin' => 'macos', + default => throw new RuntimeException('Unsupported operating system: ' . PHP_OS_FAMILY), + }; + if (PHP_INT_SIZE !== 8) { + throw new RuntimeException('TypePHP only supports 64-bit systems'); + } + + $arch = normalizeArchitecture(php_uname('m')); + + $binary = 'tpc'; + $versionFile = 'version.txt'; + $requiredFiles = [ + $binary, + 'composer.json', + 'README.md', + 'LICENSE.md', + 'examples/hello.php', + ]; + foreach ($requiredFiles as $file) { + if (!is_file($file)) { + throw new RuntimeException("Required package file not found: {$file}"); + } + } + if (!class_exists('ZipArchive')) { + throw new RuntimeException('The ZipArchive extension is required'); + } + + $versionId = is_file($versionFile) ? (int)trim((string)file_get_contents($versionFile)) : 1000; + $versionId++; + $topLevelDir = "tpc_v{$versionId}_{$osType}_{$arch}"; + $outputFile = $topLevelDir . '.zip'; + + echo "========================================\n"; + echo "TypePHP {$osType} package\n"; + echo "========================================\n"; + echo "Version: {$versionId}\n"; + echo "Architecture: {$arch}\n"; + echo "Output: {$outputFile}\n\n"; + + if (is_dir($topLevelDir)) { + removeDirectory($topLevelDir); + } + mustCreateDirectory($topLevelDir); + + $cleanupStage = true; + $cleanupArchive = true; + register_shutdown_function(static function () use ( + &$cleanupStage, + &$cleanupArchive, + $topLevelDir, + $outputFile, + ): void { + if ($cleanupStage && is_dir($topLevelDir)) { + try { + removeDirectory($topLevelDir); + } catch (Throwable $error) { + fwrite(STDERR, "Warning: unable to clean {$topLevelDir}: {$error->getMessage()}\n"); + } + } + if ($cleanupArchive && is_file($outputFile) && !unlink($outputFile)) { + fwrite(STDERR, "Warning: unable to clean uncommitted archive {$outputFile}\n"); + } + }); + + $stagedBinary = "{$topLevelDir}/{$binary}"; + mustCopy($binary, $stagedBinary); + if (!chmod($stagedBinary, 0755)) { + throw new RuntimeException("Unable to mark executable: {$stagedBinary}"); + } + + 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)); + } + } 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.md', "{$topLevelDir}/LICENSE.md"); + mustCreateDirectory("{$topLevelDir}/examples"); + mustCopy('examples/hello.php', "{$topLevelDir}/examples/hello.php"); + + if (is_file($outputFile) && !unlink($outputFile)) { + throw new RuntimeException("Unable to remove existing archive: {$outputFile}"); + } + + $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}"); + } + + $requiredEntries = [ + "{$topLevelDir}/{$binary}", + "{$topLevelDir}/composer.json", + "{$topLevelDir}/README.md", + "{$topLevelDir}/LICENSE.md", + "{$topLevelDir}/examples/hello.php", + ]; + $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(); + + removeDirectory($topLevelDir); + $cleanupStage = false; + try { + mustWriteFile($versionFile, (string)$versionId); + } catch (Throwable $error) { + if (is_file($outputFile)) { + @unlink($outputFile); + } + throw $error; + } + $cleanupArchive = false; + + $sizeMb = round(filesize($outputFile) / 1024 / 1024, 2); + echo "Package successful: {$outputFile} ({$sizeMb} MB)\n"; +} + /** * 递归复制目录 * @param string $src 源目录 diff --git a/package.sh b/package.sh deleted file mode 100755 index f9d39657..00000000 --- a/package.sh +++ /dev/null @@ -1,221 +0,0 @@ -#!/bin/bash - -# 打包脚本:创建包含指定文件的 zip 压缩包 -# Linux 系统下使用 UPX 压缩二进制文件,macOS 系统使用 strip 删除调试符号 -# 支持版本管理,每次打包版本号自动递增 -# 输出文件名格式:tpc_v{版本}_{操作系统}_{架构}.zip - -BINARY_FILE="tpc" -VERSION_FILE="version.txt" -BACKUP_FILE="${BINARY_FILE}.backup" - -# 读取或初始化版本号 -if [ -f "$VERSION_FILE" ]; then - VERSION_ID=$(cat "$VERSION_FILE") -else - VERSION_ID=1000 -fi - -# 版本号递增 -VERSION_ID=$((VERSION_ID + 1)) -echo "$VERSION_ID" > "$VERSION_FILE" - -echo "当前版本: $VERSION_ID" - -# 生成带版本号的二进制文件名 -VERSIONED_BINARY="${BINARY_FILE}_v${VERSION_ID}" - -# 检查必要文件是否存在 -REQUIRED_FILES=( - "$BINARY_FILE" - "composer.json" - "README.md" - "LICENSE.md" - "examples/hello.php" -) - -echo "检查文件..." -for file in "${REQUIRED_FILES[@]}"; do - if [ ! -e "$file" ]; then - echo "错误: 文件不存在 - $file" - exit 1 - fi -done - -echo "所有文件检查通过" - -# 复制二进制文件为带版本号的名称 -echo "创建版本化二进制文件: $VERSIONED_BINARY" -cp "$BINARY_FILE" "$VERSIONED_BINARY" - -# 检测操作系统 -detect_os() { - case "$(uname -s)" in - Darwin*) - echo "macos" - ;; - Linux*) - echo "linux" - ;; - *) - echo "unknown" - ;; - esac -} - -# 检测硬件架构 -detect_arch() { - case "$(uname -m)" in - x86_64|amd64) - echo "x86_64" - ;; - aarch64|arm64) - echo "arm64" - ;; - armv7l|armv7) - echo "armv7" - ;; - i386|i686) - echo "i386" - ;; - *) - echo "$(uname -m)" - ;; - esac -} - -OS_TYPE=$(detect_os) -ARCH_TYPE=$(detect_arch) -echo "检测到操作系统: $OS_TYPE" -echo "检测到硬件架构: $ARCH_TYPE" - -# 生成带版本号、操作系统和架构的输出文件名 -OUTPUT_FILE="tpc_v${VERSION_ID}_${OS_TYPE}_${ARCH_TYPE}.zip" - -echo "输出文件: $OUTPUT_FILE" - -# 检查 upx 是否安装(仅 Linux 需要) -if [ "$OS_TYPE" = "linux" ]; then - if ! command -v upx &> /dev/null; then - echo "错误: 未找到 upx 命令,请先安装 upx" - echo "Ubuntu/Debian: sudo apt-get install upx-ucl" - echo "CentOS/RHEL: sudo yum install upx" - # 清理临时文件 - rm -f "$VERSIONED_BINARY" - exit 1 - fi - - echo "检测到 upx: $(upx --version | head -n 1)" -fi - -# 根据操作系统决定使用不同的优化方式 -if [ "$OS_TYPE" = "linux" ]; then - # 备份原始二进制文件 - echo "备份原始二进制文件..." - cp "$BINARY_FILE" "$BACKUP_FILE" - - # 使用 UPX 压缩版本化二进制文件 - echo "使用 UPX 压缩 $VERSIONED_BINARY ..." - upx --best "$VERSIONED_BINARY" - - if [ $? -ne 0 ]; then - echo "✗ UPX 压缩失败!" - # 恢复原始文件并清理临时文件 - mv "$BACKUP_FILE" "$BINARY_FILE" - rm -f "$VERSIONED_BINARY" - exit 1 - fi - - echo "✓ UPX 压缩完成" - echo " 原始大小: $(du -h "$BACKUP_FILE" | cut -f1)" - echo " 压缩后: $(du -h "$VERSIONED_BINARY" | cut -f1)" - - # 根据操作系统使用不同的 stat 命令 - if [ "$OS_TYPE" = "linux" ]; then - ORIGINAL_SIZE=$(stat -c%s "$BACKUP_FILE") - COMPRESSED_SIZE=$(stat -c%s "$VERSIONED_BINARY") - else - ORIGINAL_SIZE=$(stat -f%z "$BACKUP_FILE") - COMPRESSED_SIZE=$(stat -f%z "$VERSIONED_BINARY") - fi - - COMPRESSION_RATIO=$(echo "scale=2; (1 - $COMPRESSED_SIZE / $ORIGINAL_SIZE) * 100" | bc) - echo " 压缩率: ${COMPRESSION_RATIO}%" -else - echo "macOS 系统,使用 strip 删除调试符号..." - - # 备份原始二进制文件 - echo "备份原始二进制文件..." - cp "$BINARY_FILE" "$BACKUP_FILE" - - # 检查 strip 是否可用 - if ! command -v strip &> /dev/null; then - echo "警告: 未找到 strip 命令,跳过符号剥离" - else - # 使用 strip 删除调试符号 - echo "使用 strip 处理 $VERSIONED_BINARY ..." - strip -x "$VERSIONED_BINARY" - - if [ $? -ne 0 ]; then - echo "✗ strip 处理失败!" - # 恢复原始文件并清理临时文件 - mv "$BACKUP_FILE" "$BINARY_FILE" - rm -f "$VERSIONED_BINARY" - exit 1 - fi - - echo "✓ strip 处理完成" - echo " 原始大小: $(du -h "$BACKUP_FILE" | awk '{print $1}')" - echo " 处理后: $(du -h "$VERSIONED_BINARY" | awk '{print $1}')" - - # 计算大小变化 - ORIGINAL_SIZE=$(stat -f%z "$BACKUP_FILE") - STRIPPED_SIZE=$(stat -f%z "$VERSIONED_BINARY") - - if [ $ORIGINAL_SIZE -gt 0 ]; then - REDUCTION_RATIO=$(echo "scale=2; (1 - $STRIPPED_SIZE / $ORIGINAL_SIZE) * 100" | bc) - echo " 减小率: ${REDUCTION_RATIO}%" - fi - fi -fi - -# 删除旧的压缩包(如果存在) -if [ -f "$OUTPUT_FILE" ]; then - rm "$OUTPUT_FILE" - echo "已删除旧的压缩包" -fi - -# 创建 zip 压缩包(使用带版本号的二进制文件) -echo "创建压缩包: $OUTPUT_FILE" -zip "$OUTPUT_FILE" \ - "$VERSIONED_BINARY" \ - composer.json \ - README.md \ - LICENSE.md \ - examples/hello.php - -PACK_RESULT=$? - -# 恢复原始二进制文件并清理临时文件 -echo "恢复原始二进制文件..." -mv "$BACKUP_FILE" "$BINARY_FILE" -rm -f "$VERSIONED_BINARY" - -if [ $PACK_RESULT -eq 0 ]; then - echo "✓ 打包成功!" - echo "版本号: $VERSION_ID" - - # 根据操作系统使用不同的 du 命令 - if [ "$OS_TYPE" = "linux" ]; then - PACKAGE_SIZE=$(du -h "$OUTPUT_FILE" | cut -f1) - else - PACKAGE_SIZE=$(du -h "$OUTPUT_FILE" | awk '{print $1}') - fi - - echo "压缩包大小: $PACKAGE_SIZE" - echo "包含文件:" - unzip -l "$OUTPUT_FILE" | grep -E "\.php$|compiler|\.json$|\.md$" -else - echo "✗ 打包失败!" - exit 1 -fi diff --git a/version.txt b/version.txt index 87e831c3..e50f8eba 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1094 \ No newline at end of file +1096 \ No newline at end of file