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/年月日_序号 目录。
相关推荐
酉鬼女又兒18 小时前
零基础入门计算机网络:网络层核心任务、三大关键问题、两种服务类型与 TCP/IP 网际层协议体系全解析
服务器·网络·网络协议·tcp/ip·计算机网络·php·求职招聘
神仙别闹19 小时前
基于 PHP + MySQL学生信息管理系统
android·mysql·php
天启HTTP21 小时前
开启全局代理后网络变慢,问题出在哪
开发语言·前端·网络·tcp/ip·php
荒-漠1 天前
phpstorm2026版本汉化
php·phpstorm
狗凯之家源码网1 天前
PHP 原版公众号无限回调系统修复版效果实测
开源·php
神仙别闹1 天前
基于 PHP + MySQL 图书库存管理系统
android·mysql·php
2601_961845151 天前
2026四级作文预测题|英语四级写作押题+提纲PDF
java·c语言·数据库·c++·python·pdf·php
CRMEB系统商城2 天前
CRMEB多商户系统(Java)v2.3公测版发布
java·开发语言·人工智能·小程序·开源·php
修炼室2 天前
外网环境原生直连校内服务器:基于内网穿透 + SSH 密钥认证的完整实践指南
服务器·ssh·php