/ 目录内容 -> zip 根 * - public/static/addon// 静态资源 -> zip 的 static/ 前缀(缺失时回退历史 public/static//) * 客户端 download()/local() 解压时按此结构还原。 * * 用法: * php scripts/package_addon.php [version | major|minor|patch] [title] [author] [description] [price] * - version 缺省时按 patch 自动递增 addons//info.php 的版本号并写回;可显式传 x.y.z 或 major/minor/patch * * 例: * php scripts/package_addon.php appmall # 自动 1.0.0 -> 1.0.1 * php scripts/package_addon.php appmall minor # 1.0.0 -> 1.1.0 * php scripts/package_addon.php mqttbroker "MQTT代理" "" "基于 Workerman 自研 MQTT Broker" 0 */ declare(strict_types=1); $root = dirname(__DIR__) . DIRECTORY_SEPARATOR; // d:/wwwroot/ywxapp_dev/ $addon = $argv[1] ?? 'mqttbroker'; // 解析可选版本参数(位于 $argv[2]);若是版本格式则视为版本参数,其余左移一位 $verArg = $argv[2] ?? ''; $isVerArg = ($verArg !== '' && (preg_match('/^\d+\.\d+\.\d+$/', $verArg) || in_array($verArg, ['major', 'minor', 'patch'], true))); if ($isVerArg) { $argv[2] = $argv[3] ?? ''; $argv[3] = $argv[4] ?? ''; $argv[4] = $argv[5] ?? ''; $argv[5] = $argv[6] ?? ''; } $price = $argv[5] ?? '0.00'; $addonDir = $root . 'addon' . DIRECTORY_SEPARATOR . $addon . DIRECTORY_SEPARATOR; if (!is_dir($addonDir)) { fwrite(STDERR, "插件目录不存在: {$addonDir}\n"); exit(1); } $info = include $addonDir . 'info.php'; $curVersion = $info['version'] ?? ''; if (!$curVersion) { fwrite(STDERR, "info.php 缺少 version\n"); exit(1); } // ===================== 确定新版本号(支持自动递增) ===================== if ($verArg === '') { $newVersion = bumpVersion($curVersion, 'patch'); echo "[版本] 未指定版本号,按 patch 自动递增: {$curVersion} -> {$newVersion}\n"; } elseif (preg_match('/^\d+\.\d+\.\d+$/', $verArg)) { $newVersion = $verArg; } else { $newVersion = bumpVersion($curVersion, $verArg); echo "[版本] 按 {$verArg} 递增: {$curVersion} -> {$newVersion}\n"; } // 将新版本写回 info.php(保持插件元数据与产出一致,下次打包在此基础上再 +1) writeAddonVersion($addonDir . 'info.php', $newVersion); echo "[版本] 已写回 addon/{$addon}/info.php: {$newVersion}\n"; $version = $newVersion; // 标题/作者/简介:命令行参数优先,缺省则从 info.php 读取,保持与插件元数据一致 $title = $argv[2] ?? ($info['title'] ?? ''); $author = $argv[3] ?? ($info['author'] ?? ''); $description = $argv[4] ?? ($info['intro'] ?? ''); // 输出到「非 Web 可访问」目录(build/ 不在站点根 public/ 下) $marketDir = $root . 'build' . DIRECTORY_SEPARATOR . 'addons' . DIRECTORY_SEPARATOR; if (!is_dir($marketDir)) { mkdir($marketDir, 0755, true); } $zipFile = $marketDir . $addon . '-' . $version . '.zip'; // 静态资源目录(可选):统一 public/static/addon//,回退历史 public/static// $staticDir = $root . 'public' . DIRECTORY_SEPARATOR . 'static' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $addon . DIRECTORY_SEPARATOR; $legacyStaticDir = $root . 'public' . DIRECTORY_SEPARATOR . 'static' . DIRECTORY_SEPARATOR . $addon . DIRECTORY_SEPARATOR; if (!class_exists('ZipArchive')) { fwrite(STDERR, "ZipArchive 扩展不可用\n"); exit(1); } $zip = new ZipArchive(); if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { fwrite(STDERR, "无法创建 zip: {$zipFile}\n"); exit(1); } addDirToZip($zip, $addonDir, ''); if (is_dir($staticDir)) { addDirToZip($zip, $staticDir, 'static/'); } elseif (is_dir($legacyStaticDir)) { fwrite(STDOUT, "提示:静态资源仍在历史路径 public/static/{$addon}/,已按新口径打包;" . "建议迁移到 public/static/addon/{$addon}/\n"); addDirToZip($zip, $legacyStaticDir, 'static/'); } $zip->close(); // 写入包指纹(与 AddonService::package() 一致):zip 注释 + 同名 .sha256 文件, // 供安装端 AddonService::verifyPackageIntegrity() 校验完整性与防篡改。 $zip->open($zipFile); $zip->setArchiveComment('YWXAPP-SHA256:' . hash_file('sha256', $zipFile)); $zip->close(); @file_put_contents($zipFile . '.sha256', hash_file('sha256', $zipFile) . ' ' . basename($zipFile) . PHP_EOL); $hash = hash_file('sha256', $zipFile); $size = filesize($zipFile); $absPath = realpath($zipFile); // SQL 转义(生成供人工执行的 INSERT) $esc = static function (string $v): string { return str_replace(["\\", "'"], ["\\\\", "\\'"], $v); }; $sql = "INSERT INTO `wxapp_appmall_addon_list` " . "(`name`,`title`,`version`,`price`,`author`,`description`,`logo`,`file_path`,`file_hash`,`status`,`create_at`,`update_at`) VALUES (" . "'" . $esc($addon) . "'," . "'" . $esc($title) . "'," . "'" . $esc($version) . "'," . floatval($price) . "," . "'" . $esc($author) . "'," . "'" . $esc($description) . "'," . "''," . "'" . $esc($absPath) . "'," . "'" . $hash . "'," . "1,UNIX_TIMESTAMP(),UNIX_TIMESTAMP());"; $sqlDir = $root . 'build' . DIRECTORY_SEPARATOR . 'sql' . DIRECTORY_SEPARATOR; if (!is_dir($sqlDir)) { mkdir($sqlDir, 0755, true); } $sqlFile = $sqlDir . $addon . '-' . $version . '-appmall_addon_list.sql'; file_put_contents($sqlFile, "-- 插件市场测试记录(由 scripts/package_addon.php 生成)\n-- zip: {$absPath}\n-- sha256: {$hash}\n" . $sql . "\n"); echo "=== 打包完成 ===\n"; echo "zip : {$absPath}\n"; echo "size : {$size} bytes\n"; echo "sha256: {$hash}\n\n"; echo "=== 测试记录 SQL(已写入 {$sqlFile})===\n"; echo $sql . "\n"; /** * 在 x.y.z 基础上递增指定段(major/minor/patch) */ function bumpVersion(string $ver, string $level = 'patch'): string { if (!preg_match('/^(\d+)\.(\d+)\.(\d+)$/', $ver, $m)) { return $ver; } $major = (int) $m[1]; $minor = (int) $m[2]; $patch = (int) $m[3]; switch ($level) { case 'major': $major++; $minor = 0; $patch = 0; break; case 'minor': $minor++; $patch = 0; break; case 'patch': default: $patch++; break; } return $major . '.' . $minor . '.' . $patch; } /** * 将新版本号写回插件 info.php */ function writeAddonVersion(string $infoFile, string $ver): void { if (!is_file($infoFile)) { return; } $c = file_get_contents($infoFile); $c = preg_replace( "/(['\"])version['\"]\s*=>\s*['\"][^'\"]*['\"]/", "'version' => '" . $ver . "'", $c ); file_put_contents($infoFile, $c); } /** * 将目录递归加入 zip(结构对齐 AddonService::addDirectoryToZip) */ function addDirToZip(ZipArchive $zip, string $dir, string $prefix): void { $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($files as $file) { $filePath = $file->getRealPath(); $rel = ltrim(str_replace(DIRECTORY_SEPARATOR, '/', substr($filePath, strlen($dir))), '/'); if ($file->isDir()) { $zip->addEmptyDir($prefix . $rel); } else { $zip->addFile($filePath, $prefix . $rel); } } }