801 lines
28 KiB
PHP
801 lines
28 KiB
PHP
<?php
|
||
// enhanced_package_generator.php
|
||
#!/usr/bin/env php
|
||
|
||
/**
|
||
* 部署包生成器
|
||
* 生成部署包, 只包含新增、修改的文件
|
||
*/
|
||
|
||
class DeployPackageGenerator
|
||
{
|
||
private string $sourceDir; // 目录A(新版本)
|
||
private string $targetDir; // 目录B(旧版本)
|
||
private string $outputPath; // 输出ZIP文件路径
|
||
private array $config; // 配置选项
|
||
private array $log = []; // 日志记录
|
||
private array $comparisonResult = []; // 比较结果
|
||
|
||
public function __construct(string $sourceDir, string $targetDir, string $outputPath, array $config = [])
|
||
{
|
||
$this->sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR);
|
||
$this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR);
|
||
$this->outputPath = $outputPath;
|
||
|
||
$this->config = array_merge([
|
||
'exclude_patterns' => [
|
||
'/\.git/', // Git目录
|
||
'/\.svn/', // SVN目录
|
||
'/node_modules/', // Node模块
|
||
'/vendor/', // Composer依赖
|
||
'/cache/', // 缓存目录
|
||
'/logs/', // 日志目录
|
||
'/temp/', // 临时目录
|
||
'/tmp/', // 临时目录
|
||
'/backup/', // 备份目录
|
||
'/runtime/', // 运行时目录
|
||
'/upload/', // 上传目录
|
||
'/\.DS_Store$/', // Mac系统文件
|
||
'/Thumbs\.db$/', // Windows缩略图
|
||
'/\.log$/', // 日志文件
|
||
'/\.tmp$/', // 临时文件
|
||
'/\.env$/', // 环境变量
|
||
'/composer\.lock$/', // Composer依赖锁文件
|
||
],
|
||
'include_patterns' => [], // 包含模式(优先于排除)
|
||
'compare_method' => 'content', // content, size, mtime, hash
|
||
'hash_algorithm' => 'md5', // md5, sha1, crc32
|
||
'min_file_size' => 0, // 最小文件大小(字节)
|
||
'max_file_size' => 10485760, // 最大文件大小(10MB)
|
||
'dry_run' => false, // 试运行
|
||
'verbose' => false, // 详细输出
|
||
'backup_old' => true, // 备份旧版本
|
||
'compression_level' => 9, // ZIP压缩级别 0-9
|
||
], $config);
|
||
|
||
$this->validateDirectories();
|
||
}
|
||
|
||
/**
|
||
* 验证目录有效性
|
||
*/
|
||
private function validateDirectories(): void
|
||
{
|
||
if (!is_dir($this->sourceDir)) {
|
||
throw new InvalidArgumentException("源目录不存在: {$this->sourceDir}");
|
||
}
|
||
|
||
if (!is_readable($this->sourceDir)) {
|
||
throw new InvalidArgumentException("源目录不可读: {$this->sourceDir}");
|
||
}
|
||
|
||
if (!is_dir($this->targetDir)) {
|
||
$this->logMessage('WARNING', "目标目录不存在,将创建完整部署包: {$this->targetDir}");
|
||
} elseif (!is_readable($this->targetDir)) {
|
||
throw new InvalidArgumentException("目标目录不可读: {$this->targetDir}");
|
||
}
|
||
|
||
$outputDir = dirname($this->outputPath);
|
||
if (!is_dir($outputDir)) {
|
||
throw new InvalidArgumentException("输出目录不存在: {$outputDir}");
|
||
}
|
||
|
||
if (!is_writable($outputDir)) {
|
||
throw new InvalidArgumentException("输出目录不可写: {$outputDir}");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成部署包
|
||
*/
|
||
public function generate(): array
|
||
{
|
||
$this->logMessage('INFO', '开始生成部署包');
|
||
$this->logMessage('INFO', "源目录: {$this->sourceDir}");
|
||
$this->logMessage('INFO', "目标目录: {$this->targetDir}");
|
||
$this->logMessage('INFO', "输出文件: {$this->outputPath}");
|
||
|
||
try {
|
||
// 步骤1:分析目录结构
|
||
$this->logMessage('INFO', '分析目录结构...');
|
||
$sourceFiles = $this->analyzeDirectory($this->sourceDir);
|
||
$targetFiles = is_dir($this->targetDir) ? $this->analyzeDirectory($this->targetDir) : [];
|
||
|
||
$this->logMessage('INFO', "源目录文件数: " . count($sourceFiles));
|
||
$this->logMessage('INFO', "目标目录文件数: " . count($targetFiles));
|
||
|
||
// 步骤2:比较文件差异
|
||
$this->logMessage('INFO', '比较文件差异...');
|
||
$diffFiles = $this->compareFiles($sourceFiles, $targetFiles);
|
||
|
||
// 步骤3:生成部署包
|
||
$this->logMessage('INFO', '生成部署ZIP包...');
|
||
$result = $this->createDeployPackage($diffFiles);
|
||
|
||
$this->logMessage('SUCCESS', '部署包生成完成');
|
||
return array_merge($result, [
|
||
'comparison' => $this->comparisonResult
|
||
]);
|
||
|
||
} catch (Exception $e) {
|
||
$this->logMessage('ERROR', "生成失败: " . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 分析目录结构
|
||
*/
|
||
private function analyzeDirectory(string $directory): array
|
||
{
|
||
$files = [];
|
||
$basePath = rtrim($directory, DIRECTORY_SEPARATOR);
|
||
|
||
$iterator = new RecursiveIteratorIterator(
|
||
new RecursiveDirectoryIterator(
|
||
$directory,
|
||
FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS
|
||
),
|
||
RecursiveIteratorIterator::SELF_FIRST
|
||
);
|
||
|
||
foreach ($iterator as $file) {
|
||
if ($file->isDir()) {
|
||
continue;
|
||
}
|
||
|
||
$filePath = $file->getPathname();
|
||
$relativePath = $this->getRelativePath($filePath, $basePath);
|
||
|
||
// 检查是否应该排除
|
||
if ($this->shouldExclude($relativePath)) {
|
||
continue;
|
||
}
|
||
|
||
// 检查文件大小限制
|
||
$fileSize = $file->getSize();
|
||
if ($fileSize < $this->config['min_file_size'] || $fileSize > $this->config['max_file_size']) {
|
||
$this->logMessage('DEBUG', "跳过文件(大小限制): {$relativePath} ({$fileSize} bytes)");
|
||
continue;
|
||
}
|
||
|
||
$files[$relativePath] = [
|
||
'full_path' => $filePath,
|
||
'size' => $fileSize,
|
||
'modified' => $file->getMTime(),
|
||
'permissions' => $file->getPerms(),
|
||
'hash' => $this->calculateFileHash($filePath)
|
||
];
|
||
}
|
||
|
||
return $files;
|
||
}
|
||
|
||
/**
|
||
* 比较文件差异
|
||
*/
|
||
private function compareFiles(array $sourceFiles, array $targetFiles): array
|
||
{
|
||
$diffFiles = [
|
||
'added' => [], // 新增文件
|
||
'modified' => [], // 修改文件
|
||
'deleted' => [], // 删除文件(记录以便清理)
|
||
'unchanged' => [] // 未改变文件
|
||
];
|
||
|
||
$this->comparisonResult = [
|
||
'total_source_files' => count($sourceFiles),
|
||
'total_target_files' => count($targetFiles),
|
||
'files_analyzed' => 0
|
||
];
|
||
|
||
// 检查新增和修改的文件
|
||
foreach ($sourceFiles as $relativePath => $sourceFileInfo) {
|
||
$this->comparisonResult['files_analyzed']++;
|
||
|
||
if (!isset($targetFiles[$relativePath])) {
|
||
// 新增文件
|
||
$diffFiles['added'][$relativePath] = $sourceFileInfo;
|
||
$this->logMessage('INFO', "新增文件: {$relativePath}");
|
||
} else {
|
||
// 文件存在,检查是否需要更新
|
||
$targetFileInfo = $targetFiles[$relativePath];
|
||
|
||
if ($this->needsUpdate($sourceFileInfo, $targetFileInfo)) {
|
||
$diffFiles['modified'][$relativePath] = $sourceFileInfo;
|
||
$this->logMessage('INFO', "修改文件: {$relativePath}");
|
||
} else {
|
||
$diffFiles['unchanged'][$relativePath] = $sourceFileInfo;
|
||
$this->logMessage('DEBUG', "未改变: {$relativePath}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查需要删除的文件
|
||
foreach ($targetFiles as $relativePath => $targetFileInfo) {
|
||
if (!isset($sourceFiles[$relativePath])) {
|
||
$diffFiles['deleted'][$relativePath] = $targetFileInfo;
|
||
$this->logMessage('INFO', "待删除文件: {$relativePath}");
|
||
}
|
||
}
|
||
|
||
$this->comparisonResult = array_merge($this->comparisonResult, [
|
||
'added_files' => count($diffFiles['added']),
|
||
'modified_files' => count($diffFiles['modified']),
|
||
'deleted_files' => count($diffFiles['deleted']),
|
||
'unchanged_files' => count($diffFiles['unchanged'])
|
||
]);
|
||
|
||
$this->logMessage('INFO', "比较完成: 新增{$this->comparisonResult['added_files']}个, 修改{$this->comparisonResult['modified_files']}个, 删除{$this->comparisonResult['deleted_files']}个");
|
||
|
||
return $diffFiles;
|
||
}
|
||
|
||
/**
|
||
* 检查文件是否需要更新
|
||
*/
|
||
private function needsUpdate(array $sourceFile, array $targetFile): bool
|
||
{
|
||
$method = $this->config['compare_method'];
|
||
|
||
switch ($method) {
|
||
case 'content':
|
||
// 内容比较(最准确)
|
||
return $sourceFile['hash'] !== $targetFile['hash'];
|
||
|
||
case 'size':
|
||
// 大小比较
|
||
return $sourceFile['size'] !== $targetFile['size'];
|
||
|
||
case 'mtime':
|
||
// 修改时间比较
|
||
return $sourceFile['modified'] > $targetFile['modified'];
|
||
|
||
case 'hash':
|
||
default:
|
||
// 哈希比较(默认)
|
||
return $sourceFile['hash'] !== $targetFile['hash'];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建部署包
|
||
*/
|
||
protected function createDeployPackage(array $diffFiles): array
|
||
{
|
||
if ($this->config['dry_run']) {
|
||
$this->logMessage('DRY_RUN', "试运行模式,将创建包含 " .
|
||
(count($diffFiles['added']) + count($diffFiles['modified'])) . " 个文件的ZIP包");
|
||
return $this->simulatePackageCreation($diffFiles);
|
||
}
|
||
|
||
$zip = new ZipArchive();
|
||
$openResult = $zip->open($this->outputPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
||
|
||
if ($openResult !== true) {
|
||
throw new RuntimeException("无法创建ZIP文件,错误代码: {$openResult}");
|
||
}
|
||
|
||
$addedCount = 0;
|
||
$modifiedCount = 0;
|
||
|
||
// 添加新增和修改的文件
|
||
$filesToAdd = array_merge($diffFiles['added'], $diffFiles['modified']);
|
||
|
||
|
||
$fileIndex = 0;
|
||
foreach ($filesToAdd as $relativePath => $fileInfo) {
|
||
try {
|
||
// 确保目录结构被正确保留
|
||
// 首先尝试直接添加文件
|
||
if ($zip->addFile($fileInfo['full_path'], $relativePath)) {
|
||
$zip->setCompressionName($relativePath, ZipArchive::CM_DEFLATE);
|
||
$zip->setCompressionIndex($fileIndex, ZipArchive::CM_DEFLATE, $this->config['compression_level']);
|
||
$fileIndex++;
|
||
|
||
if (isset($diffFiles['added'][$relativePath])) {
|
||
$addedCount++;
|
||
} else {
|
||
$modifiedCount++;
|
||
}
|
||
|
||
$this->logMessage('DEBUG', "添加到ZIP: {$relativePath}");
|
||
} else {
|
||
// 如果直接添加失败,尝试先创建目录结构
|
||
$dir = dirname($relativePath);
|
||
if ($dir !== '.' && $dir !== '') {
|
||
// 确保所有父目录都被创建
|
||
$parts = explode(DIRECTORY_SEPARATOR, $dir);
|
||
$currentPath = '';
|
||
foreach ($parts as $part) {
|
||
$currentPath .= $part . DIRECTORY_SEPARATOR;
|
||
$currentPath = rtrim($currentPath, DIRECTORY_SEPARATOR);
|
||
if ($currentPath !== '' && !$zip->addEmptyDir($currentPath)) {
|
||
// 添加空目录失败可能是因为目录已存在,继续尝试
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 再次尝试添加文件
|
||
if ($zip->addFile($fileInfo['full_path'], $relativePath)) {
|
||
$zip->setCompressionName($relativePath, ZipArchive::CM_DEFLATE);
|
||
$zip->setCompressionIndex($fileIndex, ZipArchive::CM_DEFLATE, $this->config['compression_level']);
|
||
$fileIndex++;
|
||
|
||
if (isset($diffFiles['added'][$relativePath])) {
|
||
$addedCount++;
|
||
} else {
|
||
$modifiedCount++;
|
||
}
|
||
|
||
$this->logMessage('DEBUG', "添加到ZIP (创建目录后): {$relativePath}");
|
||
} else {
|
||
$this->logMessage('WARNING', "添加文件失败: {$relativePath}");
|
||
}
|
||
}
|
||
} catch (Exception $e) {
|
||
$this->logMessage('ERROR', "处理文件失败 {$relativePath}: " . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
// 创建删除列表文件
|
||
if (!empty($diffFiles['deleted'])) {
|
||
$deleteListContent = "# 需要删除的文件列表\n# 生成时间: " . date('Y-m-d H:i:s') . "\n\n";
|
||
foreach (array_keys($diffFiles['deleted']) as $fileToDelete) {
|
||
$deleteListContent .= $fileToDelete . "\n";
|
||
}
|
||
|
||
$zip->addFromString('_DELETE_LIST.txt', $deleteListContent);
|
||
$this->logMessage('INFO', "创建删除列表,包含 " . count($diffFiles['deleted']) . " 个文件");
|
||
}
|
||
|
||
// 创建部署清单
|
||
$manifest = $this->createManifest($diffFiles);
|
||
$zip->addFromString('_DEPLOY_MANIFEST.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
|
||
// 关闭ZIP文件
|
||
if (!$zip->close()) {
|
||
throw new RuntimeException("ZIP文件保存失败");
|
||
}
|
||
|
||
// 验证ZIP文件
|
||
$zipSize = filesize($this->outputPath);
|
||
$fileCount = $zip->numFiles;
|
||
|
||
$result = [
|
||
'output_path' => $this->outputPath,
|
||
'file_size' => $zipSize,
|
||
'file_size_human' => $this->formatBytes($zipSize),
|
||
'files_added' => $addedCount,
|
||
'files_modified' => $modifiedCount,
|
||
'files_deleted' => count($diffFiles['deleted']),
|
||
'total_files' => $addedCount + $modifiedCount,
|
||
'compression_level' => $this->config['compression_level'],
|
||
'created_at' => date('Y-m-d H:i:s')
|
||
];
|
||
|
||
$this->logMessage('SUCCESS', "部署包创建成功: {$result['file_size_human']}, 包含 {$result['total_files']} 个文件");
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 创建部署清单
|
||
*/
|
||
private function createManifest(array $diffFiles): array
|
||
{
|
||
return [
|
||
'metadata' => [
|
||
'generator' => 'DeployPackageGenerator',
|
||
'version' => '1.0',
|
||
'created_at' => date('c'),
|
||
'source_dir' => $this->sourceDir,
|
||
'target_dir' => $this->targetDir,
|
||
'compare_method' => $this->config['compare_method']
|
||
],
|
||
'statistics' => $this->comparisonResult,
|
||
'files' => [
|
||
'added' => array_keys($diffFiles['added']),
|
||
'modified' => array_keys($diffFiles['modified']),
|
||
'deleted' => array_keys($diffFiles['deleted'])
|
||
],
|
||
'file_details' => [
|
||
'added' => $diffFiles['added'],
|
||
'modified' => $diffFiles['modified']
|
||
]
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 试运行模式
|
||
*/
|
||
private function simulatePackageCreation(array $diffFiles): array
|
||
{
|
||
$totalSize = 0;
|
||
|
||
foreach (array_merge($diffFiles['added'], $diffFiles['modified']) as $fileInfo) {
|
||
$totalSize += $fileInfo['size'];
|
||
}
|
||
|
||
return [
|
||
'output_path' => $this->outputPath,
|
||
'file_size' => $totalSize,
|
||
'file_size_human' => $this->formatBytes($totalSize),
|
||
'files_added' => count($diffFiles['added']),
|
||
'files_modified' => count($diffFiles['modified']),
|
||
'files_deleted' => count($diffFiles['deleted']),
|
||
'total_files' => count($diffFiles['added']) + count($diffFiles['modified']),
|
||
'dry_run' => true,
|
||
'message' => '试运行模式 - 未实际创建文件'
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 检查是否应该排除文件
|
||
*/
|
||
private function shouldExclude(string $relativePath): bool
|
||
{
|
||
// 首先检查包含模式
|
||
foreach ($this->config['include_patterns'] as $pattern) {
|
||
if (preg_match($pattern, $relativePath)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 然后检查排除模式
|
||
foreach ($this->config['exclude_patterns'] as $pattern) {
|
||
if (preg_match($pattern, $relativePath)) {
|
||
$this->logMessage('DEBUG', "排除文件: {$relativePath} (匹配模式: {$pattern})");
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 计算文件哈希
|
||
*/
|
||
private function calculateFileHash(string $filePath): string
|
||
{
|
||
$algorithm = $this->config['hash_algorithm'];
|
||
|
||
switch ($algorithm) {
|
||
case 'md5':
|
||
return md5_file($filePath);
|
||
case 'sha1':
|
||
return sha1_file($filePath);
|
||
case 'crc32':
|
||
return hash_file('crc32b', $filePath);
|
||
default:
|
||
return md5_file($filePath);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取相对路径
|
||
*/
|
||
private function getRelativePath(string $path, string $basePath): string
|
||
{
|
||
$basePath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||
return str_replace($basePath, '', $path);
|
||
}
|
||
|
||
/**
|
||
* 格式化字节大小
|
||
*/
|
||
private function formatBytes(int $bytes, int $precision = 2): string
|
||
{
|
||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
$bytes = max($bytes, 0);
|
||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||
$pow = min($pow, count($units) - 1);
|
||
$bytes /= pow(1024, $pow);
|
||
|
||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||
}
|
||
|
||
/**
|
||
* 记录日志
|
||
*/
|
||
private function logMessage(string $level, string $message): void
|
||
{
|
||
$timestamp = date('Y-m-d H:i:s');
|
||
$logEntry = "[{$timestamp}] [{$level}] {$message}";
|
||
|
||
$this->log[] = $logEntry;
|
||
|
||
if ($this->config['verbose'] || in_array($level, ['ERROR', 'SUCCESS', 'WARNING'])) {
|
||
echo $logEntry . PHP_EOL;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取日志
|
||
*/
|
||
public function getLog(): array
|
||
{
|
||
return $this->log;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 增强版部署包生成器
|
||
* 生成包含删除清单的部署包
|
||
*/
|
||
class EnhancedPackageGenerator extends DeployPackageGenerator
|
||
{
|
||
/**
|
||
* 创建部署包(增强版)
|
||
*/
|
||
protected function createDeployPackage(array $diffFiles): array
|
||
{
|
||
$result = parent::createDeployPackage($diffFiles);
|
||
|
||
// 添加删除清单
|
||
if (!empty($diffFiles['deleted'])) {
|
||
$this->addDeleteManifest($diffFiles['deleted']);
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 添加删除清单文件
|
||
*/
|
||
private function addDeleteManifest(array $deletedFiles): void
|
||
{
|
||
if ($this->config['dry_run']) {
|
||
$this->logMessage('DRY_RUN', "将创建删除清单,包含 " . count($deletedFiles) . " 个文件");
|
||
return;
|
||
}
|
||
|
||
$zip = new ZipArchive();
|
||
if ($zip->open($this->outputPath) !== true) {
|
||
throw new RuntimeException("无法打开ZIP文件添加删除清单");
|
||
}
|
||
|
||
// 文本格式删除清单
|
||
$textContent = $this->createTextDeleteList($deletedFiles);
|
||
$zip->addFromString('DELETE_LIST.txt', $textContent);
|
||
|
||
// JSON格式删除清单
|
||
$jsonContent = $this->createJsonDeleteList($deletedFiles);
|
||
$zip->addFromString('delete_manifest.json', $jsonContent);
|
||
|
||
$zip->close();
|
||
|
||
$this->logMessage('INFO', "删除清单已添加: " . count($deletedFiles) . " 个待删除文件");
|
||
}
|
||
|
||
/**
|
||
* 创建文本格式删除清单
|
||
*/
|
||
private function createTextDeleteList(array $deletedFiles): string
|
||
{
|
||
$content = "# 自动部署删除清单\n";
|
||
$content .= "# 生成时间: " . date('Y-m-d H:i:s') . "\n";
|
||
$content .= "# 此文件列出了需要删除的文件列表\n\n";
|
||
|
||
foreach (array_keys($deletedFiles) as $filePath) {
|
||
$content .= $filePath . "\n";
|
||
}
|
||
|
||
$content .= "\n# 注释以 # 开头\n";
|
||
$content .= "# 空行和注释行将被忽略\n";
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 创建JSON格式删除清单
|
||
*/
|
||
private function createJsonDeleteList(array $deletedFiles): string
|
||
{
|
||
$manifest = [
|
||
'version' => '1.0',
|
||
'generated_at' => date('c'),
|
||
'description' => '自动部署删除清单',
|
||
'files_to_delete' => array_keys($deletedFiles),
|
||
'total_files' => count($deletedFiles),
|
||
'metadata' => [
|
||
'safe_deletion' => true,
|
||
'backup_recommended' => true,
|
||
'generator' => 'EnhancedPackageGenerator'
|
||
]
|
||
];
|
||
|
||
return json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||
}
|
||
}
|
||
|
||
// 命令行接口
|
||
class PackageGeneratorCLI
|
||
{
|
||
private array $options;
|
||
|
||
public function __construct(array $argv)
|
||
{
|
||
$this->options = $this->parseOptions($argv);
|
||
}
|
||
|
||
public function run(): void
|
||
{
|
||
try {
|
||
$this->showBanner();
|
||
|
||
if ($this->options['help'] ?? false) {
|
||
$this->showHelp();
|
||
exit(0);
|
||
}
|
||
|
||
// 验证参数
|
||
$sourceDir = $this->options['source'] ?? null;
|
||
$targetDir = $this->options['target'] ?? null;
|
||
$outputFile = $this->options['output'] ?? null;
|
||
|
||
if (!$sourceDir || !$targetDir || !$outputFile) {
|
||
echo "错误: 必须指定 --source, --target 和 --output 参数\n";
|
||
$this->showHelp();
|
||
exit(1);
|
||
}
|
||
|
||
// 构建配置
|
||
$config = [
|
||
'dry_run' => $this->options['dry-run'] ?? false,
|
||
'verbose' => $this->options['verbose'] ?? false,
|
||
'compare_method' => $this->options['compare'] ?? 'content'
|
||
];
|
||
|
||
// 加载配置文件
|
||
if (isset($this->options['config'])) {
|
||
$config = array_merge($config, $this->loadConfigFile($this->options['config']));
|
||
}
|
||
|
||
// 创建生成器实例
|
||
$generator = new DeployPackageGenerator($sourceDir, $targetDir, $outputFile, $config);
|
||
|
||
// 生成部署包
|
||
$result = $generator->generate();
|
||
|
||
// 显示结果
|
||
$this->showResults($result);
|
||
|
||
} catch (Exception $e) {
|
||
echo "错误: " . $e->getMessage() . PHP_EOL;
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
private function parseOptions(array $argv): array
|
||
{
|
||
$options = [
|
||
'help' => false,
|
||
'verbose' => false,
|
||
'dry-run' => false
|
||
];
|
||
|
||
for ($i = 1; $i < count($argv); $i++) {
|
||
$arg = $argv[$i];
|
||
|
||
switch ($arg) {
|
||
case '--help':
|
||
case '-h':
|
||
$options['help'] = true;
|
||
break;
|
||
|
||
case '--verbose':
|
||
case '-v':
|
||
$options['verbose'] = true;
|
||
break;
|
||
|
||
case '--dry-run':
|
||
case '-d':
|
||
$options['dry-run'] = true;
|
||
break;
|
||
|
||
case '--source':
|
||
case '-s':
|
||
$options['source'] = $argv[++$i] ?? null;
|
||
break;
|
||
|
||
case '--target':
|
||
case '-t':
|
||
$options['target'] = $argv[++$i] ?? null;
|
||
break;
|
||
|
||
case '--output':
|
||
case '-o':
|
||
$options['output'] = $argv[++$i] ?? null;
|
||
break;
|
||
|
||
case '--config':
|
||
case '-c':
|
||
$options['config'] = $argv[++$i] ?? null;
|
||
break;
|
||
|
||
case '--compare':
|
||
$options['compare'] = $argv[++$i] ?? 'content';
|
||
break;
|
||
|
||
default:
|
||
if (substr($arg, 0, 1) === '-') {
|
||
echo "警告: 未知选项 {$arg}\n";
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $options;
|
||
}
|
||
|
||
private function loadConfigFile(string $configPath): array
|
||
{
|
||
if (!file_exists($configPath)) {
|
||
throw new InvalidArgumentException("配置文件不存在: {$configPath}");
|
||
}
|
||
|
||
$config = include $configPath;
|
||
|
||
if (!is_array($config)) {
|
||
throw new InvalidArgumentException("无效的配置文件: {$configPath}");
|
||
}
|
||
|
||
return $config;
|
||
}
|
||
|
||
private function showBanner(): void
|
||
{
|
||
echo "========================================\n";
|
||
echo " 部署包生成工具 v1.0\n";
|
||
echo " PHP 7.4 兼容版本\n";
|
||
echo "========================================\n\n";
|
||
}
|
||
|
||
private function showHelp(): void
|
||
{
|
||
echo "用法: php generate_deploy_package.php [选项]\n\n";
|
||
echo "选项:\n";
|
||
echo " --source, -s <path> 源目录(新版本)\n";
|
||
echo " --target, -t <path> 目标目录(旧版本)\n";
|
||
echo " --output, -o <path> 输出ZIP文件路径\n";
|
||
echo " --config, -c <path> 配置文件路径\n";
|
||
echo " --compare <method> 比较方法 (content, size, mtime, hash)\n";
|
||
echo " --dry-run, -d 试运行,不实际创建文件\n";
|
||
echo " --verbose, -v 显示详细输出\n";
|
||
echo " --help, -h 显示此帮助信息\n\n";
|
||
echo "示例:\n";
|
||
echo " php generate_deploy_package.php --source ./new_version --target ./old_version --output update.zip\n";
|
||
echo " php generate_deploy_package.php -s /var/www/new -t /var/www/old -o patch.zip --dry-run\n";
|
||
echo " php generate_deploy_package.php --source ./v2.0 --target ./v1.0 --output deploy.zip --config deploy.conf.php\n";
|
||
}
|
||
|
||
private function showResults(array $result): void
|
||
{
|
||
echo "\n生成结果:\n";
|
||
echo "========================================\n";
|
||
echo "输出文件: {$result['output_path']}\n";
|
||
echo "文件大小: {$result['file_size_human']}\n";
|
||
echo "包含文件: {$result['total_files']} 个\n";
|
||
echo " - 新增: {$result['files_added']} 个\n";
|
||
echo " - 修改: {$result['files_modified']} 个\n";
|
||
echo " - 删除: {$result['files_deleted']} 个\n";
|
||
|
||
if (isset($result['dry_run'])) {
|
||
echo "模式: 试运行(未实际创建文件)\n";
|
||
}
|
||
|
||
echo "========================================\n";
|
||
}
|
||
}
|
||
|
||
// 主程序入口
|
||
if (PHP_SAPI === 'cli') {
|
||
$cli = new PackageGeneratorCLI($argv);
|
||
$cli->run();
|
||
} else {
|
||
echo "此脚本只能在命令行模式下运行\n";
|
||
exit(1);
|
||
}
|
||
?>
|