一、前言
在使用 Webman 框架开发项目时,官方提供了 build:bin 命令可将项目打包为独立二进制可执行文件,但原生打包存在几个痛点:
- 每次打包需要手动敲指令,压缩打包后的文件
- 旧构建产物容易覆盖,无自动备份机制
- 缺少版本信息、Git 信息、SHA256 哈希、环境文件归档
- 跨环境切换 PHP 版本打包繁琐。
为此我基于 PHP 编写了一套全自动构建打包脚本 ,集成「参数解析、旧文件备份、二进制编译、版本信息采集、哈希校验、Tar 打包、中间文件清理」全流程,兼容 Windows + Linux 环境,支持自动识别 PHP 路径、Git 版本、.env 配置,开箱即用。
本文先拆解核心骨干代码 讲解实现思路,最后附上完整可运行源码,适合 Webman 项目线上发布、自动化部署场景。
脚本运行环境:PHP 7.4+、Webman 框架、Git(可选,用于版本采集)
二、脚本整体功能概述
先梳理整套脚本的核心能力,方便理解代码逻辑:
- 参数接收:支持传入 PHP 版本/PHP 绝对路径,自动适配 Windows 常用 PHP 目录规则;
- 目录管理 :自动创建
build打包目录,按日期+序号备份历史构建产物; - 版本解析 :优先读取
.env中app_name、VERSION,无配置则自动读取 Git 标签/仓库名; - 二进制构建 :调用 Webman 原生
build:bin编译独立可执行文件,构建失败自动回退历史可用 PHP; - 信息采集:自动收集 Git 提交哈希、分支、构建人、PHP 版本、文件 SHA256 校验值;
- 文件归档 :打包
.env环境文件、版本说明文件,最终生成 Tar 压缩包; - 自动清理:删除编译中间文件、残留临时文件,保证目录整洁。
三、核心骨干代码拆解 & 示例
将完整脚本抽离出最核心 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();
五、使用方法
-
将
pkg.php放到 Webman 项目根目录 (和webman脚本同级); -
打开终端,进入项目根目录执行:
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 -
执行完成后,所有产物存放在项目
build目录下:应用名:Webman 独立二进制可执行文件;应用名.tar:最终发布压缩包;version_info.txt:版本、Git、哈希、PHP 信息;.env-版本号:对应版本的环境配置文件;- 历史备份文件:按日期存放在
build/年月日_序号目录。