Webman 的 PHP 打包构建脚本:编译二进制、归档备份、生成校验包(附完整源码+解析)

一、前言

在使用 Webman 框架开发项目时,官方提供了 build:bin 命令可将项目打包为独立二进制可执行文件,但原生打包存在几个痛点:

  1. 每次打包需要手动敲指令,压缩打包后的文件
  2. 旧构建产物容易覆盖,无自动备份机制
  3. 缺少版本信息、Git 信息、SHA256 哈希、环境文件归档
  4. 跨环境切换 PHP 版本打包繁琐。

为此我基于 PHP 编写了一套全自动构建打包脚本 ,集成「参数解析、旧文件备份、二进制编译、版本信息采集、哈希校验、Tar 打包、中间文件清理」全流程,兼容 Windows + Linux 环境,支持自动识别 PHP 路径、Git 版本、.env 配置,开箱即用。

本文先拆解核心骨干代码 讲解实现思路,最后附上完整可运行源码,适合 Webman 项目线上发布、自动化部署场景。

脚本运行环境:PHP 7.4+、Webman 框架、Git(可选,用于版本采集)

二、脚本整体功能概述

先梳理整套脚本的核心能力,方便理解代码逻辑:

  1. 参数接收:支持传入 PHP 版本/PHP 绝对路径,自动适配 Windows 常用 PHP 目录规则;
  2. 目录管理 :自动创建 build 打包目录,按日期+序号备份历史构建产物;
  3. 版本解析 :优先读取 .envapp_nameVERSION,无配置则自动读取 Git 标签/仓库名;
  4. 二进制构建 :调用 Webman 原生 build:bin 编译独立可执行文件,构建失败自动回退历史可用 PHP;
  5. 信息采集:自动收集 Git 提交哈希、分支、构建人、PHP 版本、文件 SHA256 校验值;
  6. 文件归档 :打包 .env 环境文件、版本说明文件,最终生成 Tar 压缩包;
  7. 自动清理:删除编译中间文件、残留临时文件,保证目录整洁。

三、核心骨干代码拆解 & 示例

将完整脚本抽离出最核心 6 大模块,逐个讲解,每个模块附带精简示例代码。

3.1 基础常量 & 日志输出封装

脚本使用 ANSI 颜色码实现彩色日志,统一封装输出方法,区分 INFO/OK/WARN/ERROR,是整个脚本的基础。

核心代码片段

php 复制代码
<?php
// 控制台颜色常量
const GREEN = "\033[32m";
const RED = "\033[31m";
const YELLOW = "\033[33m";
const CYAN = "\033[36m";
const NC = "\033[0m"; // 恢复默认颜色

class BuildPacker
{
    // 通用单行输出
    private function out(string $msg, string $color = ''): void
    {
        $prefix = $color ? $color . $msg . NC : $msg;
        echo $prefix . PHP_EOL;
    }

    // 信息日志
    private function info(string $msg): void
    {
        $this->out(CYAN . '[INFO]' . NC . ' ' . $msg);
    }

    // 成功日志
    private function ok(string $msg): void
    {
        $this->out(GREEN . '[OK]' . NC . ' ' . $msg);
    }

    // 警告日志
    private function warn(string $msg): void
    {
        $this->out(YELLOW . '[WARNING]' . NC . ' ' . $msg);
    }

    // 错误日志(抛出异常终止程序)
    private function error(string $msg): void
    {
        throw new RuntimeException($msg);
    }
}

说明

  • 颜色常量兼容 Windows 终端、Linux Shell;
  • 统一日志入口,后续所有打印都调用封装方法,便于后期统一修改样式。

3.2 命令行参数解析(PHP 路径适配)

脚本支持两种传参方式:php pkg.php php82(简写版本)、php pkg.php D:\php82\php.exe(绝对路径),自动适配 Windows 常规 PHP 存放目录。

核心代码片段

php 复制代码
private string $phpPath = 'php';

private function parseArguments(): void
{
    global $argv;
    $inputPhp = $argv[1] ?? '';

    // 无传参,使用系统默认 php
    if (empty($inputPhp)) {
        $this->info('未传入 PHP 参数,使用系统默认 PHP');
        return;
    }

    $inputPhp = trim($inputPhp, "'\"");
    // 简写版本:php80 / php81 / php82 等(Windows 固定目录规则)
    if (str_starts_with(strtolower($inputPhp), 'php')) {
        $this->phpPath = "D:\\AppData\\sdk\\{$inputPhp}\\php.exe";
        if (!file_exists($this->phpPath)) {
            $this->error("PHP 目录不存在:{$this->phpPath}");
        }
    } else {
        // 传入完整绝对路径
        $this->phpPath = $inputPhp;
        if (!file_exists($this->phpPath)) {
            $this->error("PHP 路径不存在:{$this->phpPath}");
        }
    }
    $this->info("当前使用 PHP:{$this->phpPath}");
}

使用示例

bash 复制代码
# 1. 使用系统默认 php
php pkg.php

# 2. 简写版本(匹配 D:\AppData\sdk\php82\php.exe)
php pkg.php php82

# 3. 传入完整绝对路径(Windows/Linux 通用)
php pkg.php /usr/local/bin/php

3.3 读取 .env + Git 自动解析应用名、版本号

优先读取项目 .env 中的 app_name(应用名)、VERSION(版本号);无配置则自动读取 Git 标签、仓库名称,实现版本自动生成

核心代码片段

php 复制代码
private string $appName = '';
private string $version = '';
private bool $hasEnvFile = false;

private function resolveAppAndVersion(): void
{
    // 1. 读取 .env 文件
    $this->hasEnvFile = file_exists('.env');
    if ($this->hasEnvFile) {
        $lines = file('.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            $line = trim($line);
            if (empty($line) || $line[0] === '#') continue;
            [$key, $val] = explode('=', $line, 2);
            $key = trim($key);
            $val = trim($val, " \t\"'");
            if (strtolower($key) === 'version') $this->version = $val;
            if (strtolower($key) === 'app_name') $this->appName = $val;
        }
    }

    // 2. 读取 Git 仓库名
    $gitRepoName = '';
    exec('git rev-parse --show-toplevel', $output, $ret);
    if ($ret === 0 && !empty($output)) {
        $gitRepoName = basename($output[0]);
    }

    // 3. 读取 Git Tag 版本
    $gitTag = '';
    exec('git describe --tags --always', $output);
    if (!empty($output[0])) $gitTag = $output[0];

    // 兜底赋值
    $this->appName = $this->appName ?: ($gitRepoName ?: basename(getcwd()));
    $this->version = $this->version ?: ($gitTag ?: '1.0.0');

    $this->info("应用名称:{$this->appName},版本号:{$this->version}");
}

.env 配置示例

env 复制代码
# .env 自定义打包信息
app_name=webman-demo
VERSION=2.1.0

3.4 调用 Webman 编译二进制文件

核心执行逻辑:调用 PHP 执行 Webman 内置 build:bin 命令,编译独立可执行二进制文件,同时做失败重试

核心代码片段

php 复制代码
private string $buildDir = 'build';
private string $webmanBin = 'webman.bin';

private function runBuild(): void
{
    $binFile = "{$this->buildDir}/{$this->webmanBin}";
    // 先清理旧二进制
    if (file_exists($binFile)) unlink($binFile);

    // 执行 Webman 打包命令
    $cmd = "\"{$this->phpPath}\" -d phar.readonly=0 ./webman build:bin";
    passthru($cmd, $exitCode);

    if ($exitCode !== 0) {
        $this->error("Webman 二进制构建失败,请检查 PHP 权限与扩展");
    }

    // 校验产物
    if (!file_exists($binFile)) {
        $this->error("未生成 {$this->webmanBin} 构建产物");
    }
    $this->ok("二进制文件构建完成");
}

关键点说明

  • -d phar.readonly=0:关闭 PHP Phar 只读限制,打包必须参数;
  • passthru 直接输出命令行日志,方便查看 Webman 原生打包报错;
  • 构建前自动删除旧 webman.bin,避免文件占用。

3.5 文件 SHA256 哈希校验 + 版本信息文件生成

为二进制文件生成 SHA256 哈希值 (用于文件完整性校验,防止传输篡改),并整合 PHP 版本、Git 信息、构建时间生成 version_info.txt 说明文件。

核心代码片段

php 复制代码
private string $hashValue = '';

// 计算 SHA256 哈希
private function calculateSHA256(): void
{
    $file = "{$this->buildDir}/{$this->appName}";
    $this->hashValue = hash_file('sha256', $file);
    $this->ok("文件 SHA256:{$this->hashValue}");
}

// 生成版本信息文件
private function collectVersionInfoAndGenerateFile(): void
{
    // 获取 PHP 版本
    exec("\"{$this->phpPath}\" -r \"echo PHP_VERSION;\"", $phpOut);
    $phpVer = $phpOut[0] ?? 'unknown';

    // 获取 Git 信息
    exec('git rev-parse --short HEAD', $gitHashOut);
    $gitHash = $gitHashOut[0] ?? 'unknown';
    $buildTime = date('Y-m-d H:i:s');

    // 拼接内容
    $content = <<<TEXT
App Name       : {$this->appName}
Version        : {$this->version}
Git Hash       : $gitHash
Build Time     : $buildTime
PHP Version    : $phpVer
PHP Path       : {$this->phpPath}
File SHA256    : {$this->hashValue}
TEXT;

    $filePath = "{$this->buildDir}/version_info.txt";
    file_put_contents($filePath, $content);
    $this->ok("版本信息文件已生成:{$filePath}");
}

3.6 Tar 归档打包(双方案兼容)

提供两套打包方案:优先使用 PHP 内置 PharData 扩展(跨平台),无扩展则调用系统 tar 命令,最终将二进制、版本文件、环境文件打包为 .tar 压缩包。

核心代码片段

php 复制代码
private function createTarArchive(): void
{
    $tarFile = "{$this->buildDir}/{$this->appName}.tar";
    if (file_exists($tarFile)) unlink($tarFile);

    // 方案1:使用 PHP PharData 打包(推荐,纯PHP实现)
    if (class_exists('PharData')) {
        $tar = new PharData($tarFile);
        $tar->addFile("{$this->buildDir}/{$this->appName}", $this->appName);
        $tar->addFile("{$this->buildDir}/version_info.txt", 'version_info.txt');
        $this->ok("Tar 打包完成(PharData):{$tarFile}");
        return;
    }

    // 方案2:调用系统 tar 命令(Linux/Git Bash)
    chdir($this->buildDir);
    $cmd = "tar -cf \"{$this->appName}.tar\" {$this->appName} version_info.txt";
    exec($cmd, $_, $ret);
    chdir(dirname(__FILE__));

    if ($ret === 0) {
        $this->ok("Tar 打包完成(系统tar):{$tarFile}");
    } else {
        $this->error("Tar 打包失败,请安装 tar 工具");
    }
}

四、完整可运行源码

将以上模块整合为完整脚本,直接放置在 Webman 项目根目录 使用:

php 复制代码
#!/usr/bin/env php
<?php

/**
 * Webman 构建打包脚本 (PHP 版)
 * 用法: php pkg.php [php路径|php版本]
 */

declare(strict_types=1);

// 控制台颜色常量
const GREEN = "\033[32m";
const RED = "\033[31m";
const YELLOW = "\033[33m";
const CYAN = "\033[36m";
const NC = "\033[0m";

class BuildPacker
{
    private string $scriptDir;
    private string $buildDir = 'build';
    private string $webmanBin = 'webman.bin';
    private string $phpPath = 'php';
    private string $altPhpPath = '';
    private string $appName = '';
    private string $version = '';
    private string $backupDir = '';
    private string $hashValue = '';
    private bool $hasEnvFile = false;

    // 输出封装
    private function out(string $msg, string $color = ''): void
    {
        $prefix = $color !== '' ? $color . $msg . NC : $msg;
        echo $prefix . "\n";
    }

    private function br(): void
    {
        echo "\n";
    }

    private function info(string $msg): void
    {
        $this->out(CYAN . '[INFO]' . NC . ' ' . $msg);
    }

    private function ok(string $msg): void
    {
        $this->out(GREEN . '[OK]' . NC . ' ' . $msg);
    }

    private function warn(string $msg): void
    {
        $this->out(YELLOW . '[WARNING]' . NC . ' ' . $msg);
    }

    private function error(string $msg): void
    {
        throw new \RuntimeException($msg);
    }

    private function step(string $num, string $msg): void
    {
        $this->out(CYAN . "[{$num}]" . NC . ' ' . $msg);
    }

    private function raw(string $msg): void
    {
        echo $msg;
    }

    // 初始化
    public function __construct()
    {
        $this->scriptDir = dirname(realpath(__FILE__));
        if (!chdir($this->scriptDir)) {
            $this->error("无法切换到脚本所在目录: {$this->scriptDir}");
        }
    }

    // 主入口
    public function run(): void
    {
        try {
            $this->parseArguments();
            $this->checkBuildDir();
            $this->showEnvInfo();
            $this->resolveAppAndVersion();
            $this->backupOldFiles();
            $this->cleanOldBin();
            $this->runBuild();
            $this->checkBuildResult();
            $this->renameToAppName();
            $this->cleanRootResidual();
            $this->calculateSHA256();
            $this->collectVersionInfoAndGenerateFile();
            $this->copyVersionedEnv();
            $this->checkPackFiles();
            $this->createTarArchive();
            $this->cleanIntermediates();
            $this->printSummary();
        } catch (\RuntimeException $e) {
            $this->out(RED . '[ERROR]' . NC . ' ' . $e->getMessage());
            exit(1);
        }
    }

    // 解析命令行参数
    private function parseArguments(): void
    {
        global $argv;
        $inputPhp = $argv[1] ?? '';
        if ($inputPhp === '') {
            $this->info('未传入 PHP 参数,使用系统默认 PHP(php 命令)');
            return;
        }

        $inputPhp = trim($inputPhp, "'\"");
        if (strcasecmp(substr($inputPhp, 0, 3), 'php') === 0) {
            $this->phpPath = "D:\\AppData\\sdk\\$inputPhp\\php.exe";
            if (!file_exists($this->phpPath)) {
                $this->error("传入的 PHP 版本目录不存在!路径:{$this->phpPath}");
            }
            $this->info("已使用传入的 PHP 版本路径:{$this->phpPath}");
        } else {
            $this->phpPath = $inputPhp;
            if (!file_exists($this->phpPath)) {
                $this->error("传入的 PHP 全路径不存在!路径:{$this->phpPath}");
            }
            $this->info("已使用传入的 PHP 全路径:{$this->phpPath}");
        }
    }

    // 检查/创建 build 目录
    private function checkBuildDir(): void
    {
        if (!is_dir($this->buildDir)) {
            $this->warn('未找到 build 目录,正在自动创建...');
            if (!mkdir($this->buildDir) && !is_dir($this->buildDir)) {
                $this->error('build 目录创建失败,请检查权限!');
            }
            $this->ok('build 目录创建成功!');
        }
    }

    private function showEnvInfo(): void
    {
        $this->br();
        $this->info('.env 文件中可设置以下变量(非必需):');
        $this->raw("    VERSION    - 应用版本号(如 2.0.0)\n");
        $this->raw("    app_name   - 应用名称(如 my-app)\n");
        $this->raw("  若未设置,版本号将回退到 Git 标签或自动生成,应用名称回退到仓库名或文件夹名。\n");
    }

    // 解析应用名、版本号
    private function resolveAppAndVersion(): void
    {
        $this->br();
        $this->info('正在解析应用名称和版本号...');

        $this->hasEnvFile = file_exists('.env');
        if ($this->hasEnvFile) {
            foreach (file('.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
                $line = trim($line);
                if ($line === '' || $line[0] === '#') continue;
                $parts = explode('=', $line, 2);
                if (count($parts) !== 2) continue;
                [$key, $val] = $parts;
                $val = trim($val, " \t\n\r\0\x0B\"'");
                if (strcasecmp(trim($key), 'VERSION') === 0) $this->version = $val;
                elseif (strcasecmp(trim($key), 'app_name') === 0) $this->appName = $val;
            }
        }

        $gitRepoName = '';
        $output = [];
        exec('git rev-parse --show-toplevel', $output, $ret);
        if ($ret === 0 && !empty($output)) $gitRepoName = basename($output[0]);

        $gitTag = '';
        $gitCount = '0';
        $output = [];
        exec('git describe --tags --exact-match', $output);
        if (!empty($output[0])) {
            $gitTag = $output[0];
        } else {
            $output = [];
            exec('git describe --tags --always', $output);
            if (!empty($output[0])) $gitTag = $output[0];
        }

        $output = [];
        exec('git rev-list --count HEAD', $output);
        if (!empty($output[0])) $gitCount = $output[0];

        $this->appName = $this->appName ?: ($gitRepoName ?: basename(getcwd()));
        if (empty($this->version)) {
            if ($gitTag) {
                $this->version = preg_replace('/-(\d+)-g([0-9a-f]+)$/', '+c$1-$2', $gitTag);
            } else {
                $this->version = "1.0.0-c{$gitCount}";
            }
        }

        $this->info("  应用名称: " . GREEN . "{$this->appName}" . NC);
        $this->info("  版本号  : " . GREEN . "{$this->version}" . NC);
    }

    // 备份旧构建产物
    private function backupOldFiles(): void
    {
        $this->br();
        $this->info('准备备份旧构建产物...');

        $dateDir = date('Ymd');
        $this->backupDir = "{$this->buildDir}/{$dateDir}";
        $seq = 1;
        while (is_dir($this->backupDir)) {
            $this->backupDir = "{$this->buildDir}/{$dateDir}_" . ++$seq;
        }

        if (!mkdir($this->backupDir) && !is_dir($this->backupDir)) {
            $this->warn('无法创建备份目录,将继续执行,但旧文件无法安全归档');
            $this->backupDir = '';
            return;
        }

        $moved = false;
        $patterns = [
            "{$this->buildDir}/{$this->appName}",
            "{$this->buildDir}/{$this->appName}.tar",
            "{$this->buildDir}/version_info.txt",
        ];
        foreach ($patterns as $src) {
            if (file_exists($src)) {
                rename($src, "{$this->backupDir}/" . basename($src));
                $moved = true;
            }
        }
        foreach (glob("{$this->buildDir}/.env-*") as $env) {
            rename($env, "{$this->backupDir}/" . basename($env));
            $moved = true;
        }
        foreach (['*.phar', '*.zip'] as $pat) {
            foreach (glob("{$this->buildDir}/$pat") as $f) {
                rename($f, "{$this->backupDir}/" . basename($f));
                $moved = true;
            }
        }

        $this->out($moved ? GREEN . '[OK]' . NC . " 旧构建产物已备份至: {$this->backupDir}"
            : CYAN . '[INFO]' . NC . ' 没有需要备份的旧文件');
    }

    // 清理旧二进制文件
    private function cleanOldBin(): void
    {
        $this->br();
        $this->step('1/10', "清理旧的 {$this->webmanBin} ...");
        $target = "{$this->buildDir}/{$this->webmanBin}";
        if (file_exists($target)) {
            if (unlink($target)) {
                $this->ok("已删除旧的 {$this->webmanBin}");
            } else {
                $this->warn("无法删除旧的 {$this->webmanBin} (可能被占用)");
            }
        } else {
            $this->info("未找到旧的 {$this->webmanBin} ,无需清理");
        }
    }

    // 执行 Webman 构建命令
    private function runBuild(): void
    {
        $retry = 0;
        do {
            $this->br();
            $this->step('2/10', "执行构建命令: \"{$this->phpPath}\" -d phar.readonly=0 ./webman build:bin");
            passthru("\"{$this->phpPath}\" -d phar.readonly=0 ./webman build:bin", $exitCode);
            if ($exitCode === 0) return;

            $this->br();
            $this->warn("PHP 构建命令执行失败: 请检查 PHP 路径是否正确,并确保有足够的权限执行构建命令");
            $path = $this->getLastPhpPath($this->buildDir);
            if ($retry === 0 && !empty($path) && file_exists($path)) {
                $this->altPhpPath = $path;
                $this->phpPath = $path;
                $this->warn("尝试回退到上次成功打包时使用的 PHP:{$this->altPhpPath}");
                continue;
            }
            $this->error("构建失败,当前 PHP 路径:{$this->phpPath}");
        } while ($retry < 2);
    }

    private function getLastPhpPath(string $buildPath): string
    {
        if (!is_dir($buildPath)) {
            throw new InvalidArgumentException("Build path does not exist or is not a directory: {$buildPath}");
        }

        $directoryIterator = new RecursiveDirectoryIterator(
            $buildPath,
            FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS
        );

        $iterator = new RecursiveIteratorIterator(
            $directoryIterator,
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isFile() && $file->getFilename() === 'version_info.txt') {
                $filePath = $file->getRealPath();
                $phpPath = $this->validatePhpPathInVersionFile($filePath);
                if ($phpPath !== '' && $phpPath !== $this->phpPath) {
                    $this->altPhpPath = $phpPath;
                    return $phpPath;
                }
            }
        }
        return "";
    }

    private function validatePhpPathInVersionFile(string $filePath): string
    {
        if (!is_readable($filePath)) return "";
        $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if ($lines === false) return "";

        foreach ($lines as $line) {
            if (preg_match('/^PHP\s+Path\s+:\s*(.+)$/i', $line, $matches)) {
                $phpPath = trim($matches[1]);
                if (!empty($phpPath) && file_exists($phpPath) && is_executable($phpPath)) {
                    if ($this->isValidPhpExecutable($phpPath)) {
                        return $phpPath;
                    }
                }
            }
        }
        return "";
    }

    private function isValidPhpExecutable(string $phpPath): bool
    {
        $output = [];
        $returnVar = 0;
        $command = escapeshellarg($phpPath) . ' -v ';
        exec($command, $output, $returnVar);
        return $returnVar === 0 && !empty($output) && str_contains(implode("\n", $output), 'PHP');
    }

    // 检查构建产物
    private function checkBuildResult(): void
    {
        $this->br();
        $this->step('3/10', '检查构建产物...');
        $bin = "{$this->buildDir}/{$this->webmanBin}";
        if (!file_exists($bin)) {
            $this->error("构建失败!未生成 {$this->webmanBin} 文件");
        }
        $this->ok("构建成功,已生成 {$this->webmanBin}");
    }

    // 重命名二进制文件
    private function renameToAppName(): void
    {
        $this->br();
        $this->step('4/10', "重命名文件: {$this->webmanBin} -> \"{$this->appName}\"");
        if (!rename("{$this->buildDir}/{$this->webmanBin}", "{$this->buildDir}/{$this->appName}")) {
            $this->error('重命名失败(目标文件可能被占用)');
        }
        $this->ok('重命名成功!');
    }

    // 清理根目录残留文件
    private function cleanRootResidual(): void
    {
        $this->br();
        $this->step('5/10', "清理根目录下可能残留的 \"{$this->appName}\" ...");
        if (file_exists($this->appName)) {
            if (unlink($this->appName)) {
                $this->ok("已删除根目录下的 \"{$this->appName}\"");
            } else {
                $this->warn("无法删除根目录的 \"{$this->appName}\"(可能被占用)");
            }
        } else {
            $this->info("根目录下无残留 \"{$this->appName}\"");
        }
    }

    // 计算 SHA256 哈希
    private function calculateSHA256(): void
    {
        $this->br();
        $this->step('6/10', "计算 \"{$this->appName}\" 的 SHA256 哈希值...");
        $file = "{$this->buildDir}/{$this->appName}";
        if (file_exists($file)) {
            $hash = hash_file('sha256', $file);
            if ($hash !== false) {
                $this->hashValue = $hash;
                $this->ok("哈希值: {$this->hashValue}");
                return;
            }
        }
        $this->warn('哈希值计算失败(文件可能不存在或无权限)');
    }

    // 生成版本信息文件
    private function collectVersionInfoAndGenerateFile(): void
    {
        $this->br();
        $this->step('7/10', '收集完整版本信息...');

        exec("\"{$this->phpPath}\" -r \"echo PHP_VERSION;\"", $out, $ret);
        $phpVer = ($ret === 0 && !empty($out[0])) ? $out[0] : 'unknown';

        $gitHash = 'unknown';
        $gitBranch = 'unknown';
        $gitBuilder = 'unknown';
        $gitCount = '0';
        $out = [];
        exec('git rev-parse --short HEAD', $out);
        if (!empty($out[0])) $gitHash = $out[0];
        $out = [];
        exec('git rev-parse --abbrev-ref HEAD', $out);
        if (!empty($out[0])) $gitBranch = $out[0];

        $out = [];
        exec('git config user.name', $out);
        if (!empty($out[0])) $gitBuilder = $out[0];

        $out = [];
        exec('git rev-list --count HEAD', $out);
        if (!empty($out[0])) $gitCount = $out[0];
        $buildTime = date('Y-m-d H:i:s');

        $this->raw("    Git   Hash     : $gitHash\n");
        $this->raw("    Git   Branch   : $gitBranch\n");
        $this->raw("    Git   Revision : $gitCount\n");
        $this->raw("    Build Author   : $gitBuilder\n");
        $this->raw("    Build Time     : $buildTime\n");
        $this->raw("    PHP   Version  : $phpVer\n");

        $this->br();
        $this->step('8/10', '生成版本信息文件...');
        $content = "App Name       : {$this->appName}\n" .
            "Version        : {$this->version}\n" .
            "------------------------\n" .
            "Git   Hash     : $gitHash\n" .
            "Git   Branch   : $gitBranch\n" .
            "Git   Revision : $gitCount\n" .
            "------------------------\n" .
            "Build Author   : $gitBuilder\n" .
            "Build Time     : $buildTime\n" .
            "------------------------\n" .
            "PHP   Version  : $phpVer\n" .
            "PHP   Path     : {$this->phpPath}\n" .
            "------------------------\n" .
            "File  SHA256   : {$this->hashValue}\n";
        if (file_put_contents("{$this->buildDir}/version_info.txt", $content) === false) {
            $this->error('版本信息文件生成失败!');
        }
        $this->ok("版本信息已写入 {$this->buildDir}/version_info.txt");
    }

    // 备份 .env 文件
    private function copyVersionedEnv(): void
    {
        $this->br();
        $this->step('9/10', '处理 .env 版本化文件...');
        if ($this->hasEnvFile) {
            $dest = "{$this->buildDir}/.env-{$this->version}";
            if (copy('.env', $dest)) {
                $this->ok("已生成 $dest");
            } else {
                $this->warn('生成 .env-版本号 文件失败');
            }
        } else {
            $this->warn('未找到 .env 文件,跳过生成');
        }
    }

    // 检查待打包文件完整性
    private function checkPackFiles(): void
    {
        $this->br();
        $this->info('检查打包所需文件...');
        $missing = [];
        $appFile = "{$this->buildDir}/{$this->appName}";
        $infoFile = "{$this->buildDir}/version_info.txt";
        $envFile = "{$this->buildDir}/.env-{$this->version}";

        if (!file_exists($appFile)) $missing[] = $this->appName;
        if (!file_exists($infoFile)) $missing[] = 'version_info.txt';
        if ($this->hasEnvFile && !file_exists($envFile)) $missing[] = ".env-{$this->version}";

        if (!empty($missing)) {
            $this->error('以下文件缺失,无法打包:' . implode(' ', $missing));
        }
        $this->ok('所有文件完整,准备打包');
    }

    // 生成 Tar 压缩包
    private function createTarArchive(): void
    {
        $this->br();
        $this->step('10/10', "打包为 {$this->appName}.tar ...");
        $tarFile = "{$this->buildDir}/{$this->appName}.tar";
        if (file_exists($tarFile)) unlink($tarFile);

        if (class_exists('PharData')) {
            try {
                $tar = new PharData($tarFile);
                $tar->addFile("{$this->buildDir}/{$this->appName}", $this->appName);
                $tar->addFile("{$this->buildDir}/version_info.txt", 'version_info.txt');
                $envPath = "{$this->buildDir}/.env-{$this->version}";
                if ($this->hasEnvFile && file_exists($envPath)) {
                    $tar->addFile($envPath, ".env-{$this->version}");
                }
                $this->ok("打包成功(PharData): $tarFile");
                return;
            } catch (\Exception $e) {
                $this->error("PharData 打包失败: " . $e->getMessage());
            }
        }

        exec('tar --version ', $_, $ret);
        if ($ret !== 0) {
            $this->error('未找到 tar 命令,且 PharData 不可用,请安装 Git for Windows 或启用 phar 扩展!');
        }
        chdir($this->buildDir);
        $cmd = "tar -cf \"{$this->appName}.tar\" \"{$this->appName}\" \"version_info.txt\"";
        if ($this->hasEnvFile && file_exists(".env-{$this->version}")) {
            $cmd .= " \".env-{$this->version}\"";
        }
        exec($cmd . ' ', $_, $ret);
        chdir($this->scriptDir);

        if ($ret === 0) {
            $this->ok("打包成功: $tarFile");
        } else {
            $this->error('打包失败,请检查文件是否完整!');
        }
    }

    // 清理中间文件
    private function cleanIntermediates(): void
    {
        $this->br();
        $this->info('清理构建过程中产生的中间缓存...');
        foreach (['webman.phar', "{$this->buildDir}/webman.phar"] as $f) {
            if (file_exists($f) && unlink($f)) {
                $this->ok("已删除 $f");
            }
        }
        foreach (glob("{$this->buildDir}/*.zip") as $zip) {
            if (unlink($zip)) {
                $this->ok("已删除 $zip");
            }
        }
        $this->ok('中间缓存清理完成');
    }

    // 输出汇总信息
    private function printSummary(): void
    {
        exec("\"{$this->phpPath}\" -r \"echo PHP_VERSION;\"", $out, $ret);
        $phpVer = ($ret === 0 && !empty($out[0])) ? $out[0] : 'unknown';

        $this->br();
        $this->raw("==============================================\n");
        $this->raw(GREEN . "所有操作已全部执行完毕!" . NC . "\n");
        $this->raw("==============================================\n");
        $this->raw("应用名称    : {$this->appName}\n");
        $this->raw("版本号      : {$this->version}\n");
        $this->raw("打包文件    : {$this->buildDir}/{$this->appName}.tar\n");
        $this->raw("备份目录    : {$this->backupDir}\n");
        $this->raw("PHP 路径    : {$this->phpPath}\n");
        $this->raw("PHP 版本    : {$phpVer}\n");
        $this->raw("==============================================\n");
    }
}

// 启动脚本
(new BuildPacker())->run();

五、使用方法

  1. pkg.php 放到 Webman 项目根目录 (和 webman 脚本同级);

  2. 打开终端,进入项目根目录执行:

    bash 复制代码
    # 1. 使用系统默认 PHP
    php pkg.php
    
    # 2. Windows 简写(匹配 D:\AppData\sdk\php82\php.exe)
    php pkg.php php82
    
    # 3. 指定 PHP 绝对路径(Windows/Linux 通用)
    php pkg.php D:\php82\php.exe
  3. 执行完成后,所有产物存放在项目 build 目录下:

    • 应用名:Webman 独立二进制可执行文件;
    • 应用名.tar:最终发布压缩包;
    • version_info.txt:版本、Git、哈希、PHP 信息;
    • .env-版本号:对应版本的环境配置文件;
    • 历史备份文件:按日期存放在 build/年月日_序号 目录。
相关推荐
两个人的幸福10 天前
Windows 桌面应用自研 PHP 队列(下):完整代码与六大工程化优化
php
BingoGo12 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack12 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
用户30745969820713 天前
PHP 扩展——从入门到理解
php
鹏仔先生14 天前
拷贝漫画APP下载页PHP程序,后台带免费AI写作
php
云水一下14 天前
从零开始学 PHP 系列(一):PHP 的前世今生与开发环境搭建
开发语言·php
xingpanvip14 天前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
酉鬼女又兒14 天前
零基础入门计算机网络运输层:端到端通信核心作用、端口号分类规则、复用分用工作机制及UDP与TCP协议全方位对比详解
网络·网络协议·tcp/ip·计算机网络·考研·udp·php
dog25014 天前
不要再继续优化 TCP
网络协议·tcp/ip·php
Channing Lewis14 天前
PHP 解析 Excel 的那些坑:一次“行号错位”引发的数据丢失
开发语言·php·excel