diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a81e670..b07cb62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,6 +59,15 @@ jobs: needs: resolve_version if: github.event.inputs.build_linux == 'true' || github.event_name == 'push' || github.event_name == 'release' runs-on: ubuntu-latest + strategy: + matrix: + include: + - arch: x64 + package_command: npm run package:linux:x64 + artifact_name: gsm3-management-panel-linux-x64-build + - arch: arm64 + package_command: npm run package:linux:arm64 + artifact_name: gsm3-management-panel-linux-arm64-build steps: - uses: actions/checkout@v4 - name: Setup Node.js for build @@ -72,17 +81,17 @@ jobs: npm install cd client && npm install cd ../server && npm install - - name: Build package - run: npm run package:linux + - name: Build Linux ${{ matrix.arch }} package + run: ${{ matrix.package_command }} env: CI: true NODE_ENV: production APP_VERSION: ${{ needs.resolve_version.outputs.version }} VITE_APP_VERSION: ${{ needs.resolve_version.outputs.version }} - - name: Upload artifacts + - name: Upload Linux ${{ matrix.arch }} artifact uses: actions/upload-artifact@v4 with: - name: gsm3-management-panel-linux-build + name: ${{ matrix.artifact_name }} path: dist/package/ retention-days: 30 @@ -205,29 +214,33 @@ jobs: needs: [resolve_version, build_linux, build_windows] runs-on: ubuntu-latest steps: - - name: Download Linux artifact + - name: Download Linux x64 artifact uses: actions/download-artifact@v4 with: - name: gsm3-management-panel-linux-build - path: linux-build/ + name: gsm3-management-panel-linux-x64-build + path: linux-x64-build/ + - name: Download Linux arm64 artifact + uses: actions/download-artifact@v4 + with: + name: gsm3-management-panel-linux-arm64-build + path: linux-arm64-build/ - name: Download Windows artifact uses: actions/download-artifact@v4 with: name: gsm3-management-panel-windows-build path: windows-build/ - - name: Create Linux tar.gz - run: | - cd linux-build - tar -czf ../gsm3-management-panel-linux-v${{ needs.resolve_version.outputs.version }}.tar.gz * - - name: Create Windows zip + - name: Create platform archives run: | + tar -czf gsm3-management-panel-linux-x64-v${{ needs.resolve_version.outputs.version }}.tar.gz -C linux-x64-build . + tar -czf gsm3-management-panel-linux-arm64-v${{ needs.resolve_version.outputs.version }}.tar.gz -C linux-arm64-build . cd windows-build - zip -r ../gsm3-management-panel-windows-v${{ needs.resolve_version.outputs.version }}.zip * + zip -r ../gsm3-management-panel-windows-v${{ needs.resolve_version.outputs.version }}.zip . - name: Create Release uses: softprops/action-gh-release@v1 with: files: | - gsm3-management-panel-linux-v${{ needs.resolve_version.outputs.version }}.tar.gz + gsm3-management-panel-linux-x64-v${{ needs.resolve_version.outputs.version }}.tar.gz + gsm3-management-panel-linux-arm64-v${{ needs.resolve_version.outputs.version }}.tar.gz gsm3-management-panel-windows-v${{ needs.resolve_version.outputs.version }}.zip generate_release_notes: true draft: false diff --git a/Dockerfile b/Dockerfile index 7746e6c..bc059a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -228,11 +228,11 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then \ npm config set fetch-retry-maxtimeout 120000 && \ npm config set fetch-timeout 300000 && \ npm run install:all && \ - npm run package:linux:no-zip; \ + npm run package:linux:arm64:no-zip; \ else \ echo "AMD64架构构建,使用标准配置..." && \ npm run install:all && \ - npm run package:linux:no-zip; \ + npm run package:linux:x64:no-zip; \ fi # ---------- 运行阶段(最终镜像) ---------- @@ -297,18 +297,17 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \ chmod 755 /root/server/data/lib/${BINARY_7Z} && \ echo "7z 下载完成: ${BINARY_7Z}" -# 下载 PTY 二进制文件(从 GitHub Releases latest,构建时预置) +# 通过打包产物中的固定资产 CLI 校验并预置当前架构的 PTY RUN if [ "$TARGETARCH" = "amd64" ]; then \ - PTY_NAME="pty_linux_x64"; \ - elif [ "$TARGETARCH" = "arm64" ]; then \ - PTY_NAME="pty_linux_arm64"; \ - fi && \ - echo "正在下载 PTY (${PTY_NAME})..." && \ - wget -t 3 --retry-connrefused --waitretry=2 --read-timeout=30 --timeout=15 \ - -O /root/server/data/lib/${PTY_NAME} \ - "https://github.com/MCSManager/PTY/releases/download/latest/${PTY_NAME}" && \ - chmod 755 /root/server/data/lib/${PTY_NAME} && \ - echo "PTY 下载完成: ${PTY_NAME}" + PTY_ASSET="linux-x64"; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + PTY_ASSET="linux-arm64"; \ + else \ + echo "不支持的 PTY 架构: $TARGETARCH" >&2; exit 1; \ + fi && \ + node /root/server/utils/ptyAssetCli.js ensure \ + --asset "$PTY_ASSET" \ + --target-dir /root/server/data/lib # 拷贝 Python 依赖清单并安装 COPY --from=builder /app/server/src/Python/requirements.txt /tmp/requirements.txt # 安装Python依赖并配置最终权限 diff --git a/install-gsm3.sh b/install-gsm3.sh index ef0cf15..5d8ba61 100755 --- a/install-gsm3.sh +++ b/install-gsm3.sh @@ -105,10 +105,27 @@ mkdir -pv "$install_path" cd "$install_path" if test "$install_type" = "1"; then + ARCH=$(uname -m) + case "$ARCH" in + x86_64|amd64) + ARCHIVE_NAME="gsm3-management-panel-linux-x64.tar.gz" + PTY_ASSET="linux-x64" + ;; + aarch64|arm64) + ARCHIVE_NAME="gsm3-management-panel-linux-arm64.tar.gz" + PTY_ASSET="linux-arm64" + ;; + *) + echo -e "\x1b[31m不支持的系统架构: $ARCH\x1b[0m" + exit 1 + ;; + esac + DOWNLOAD_URL="https://ghfast.top/https://github.com/GSManagerXZ/GameServerManager/releases/latest/download/$ARCHIVE_NAME" + if command -v curl &>/dev/null;then - curl -Lo gsm3.tgz https://ghfast.top/https://github.com/GSManagerXZ/GameServerManager/releases/latest/download/gsm3-management-panel-linux.tar.gz + curl -Lo gsm3.tgz "$DOWNLOAD_URL" elif command -v wget &>/dev/null; then - wget -O gsm3.tgz https://ghfast.top/https://github.com/GSManagerXZ/GameServerManager/releases/latest/download/gsm3-management-panel-linux.tar.gz + wget -O gsm3.tgz "$DOWNLOAD_URL" else echo -e "\x1b[31m错误:既没有安装curl也没有安装wget,无法下载gsm3程序,请安装这俩其中一个工具后再次执行该脚本!" exit 1 @@ -123,51 +140,16 @@ if test "$install_type" = "1"; then rm -rf gsm3.tgz chmod 755 "$install_path/node/bin/node" "$install_path/start.sh" 2>/dev/null || true - # 验证并修复PTY二进制文件 - ARCH=$(uname -m) - if [ "$ARCH" = "x86_64" ]; then - PTY_NAME="pty_linux_x64" - elif [ "$ARCH" = "aarch64" ]; then - PTY_NAME="pty_linux_arm64" + # 通过打包产物中的固定资产 CLI 校验或修复 PTY + mkdir -p "$install_path/data/lib" + echo -e "\x1b[33m正在校验固定 PTY 资产...\x1b[0m" + if "$install_path/node/bin/node" \ + "$install_path/server/utils/ptyAssetCli.js" ensure \ + --asset "$PTY_ASSET" \ + --target-dir "$install_path/data/lib"; then + echo -e "\x1b[32mPTY 资产校验完成\x1b[0m" else - PTY_NAME="" - fi - - if [ -n "$PTY_NAME" ]; then - PTY_FILE="$install_path/data/lib/$PTY_NAME" - PTY_VALID=false - - # 检查PTY文件是否存在且为有效的ELF二进制文件 - if [ -f "$PTY_FILE" ]; then - if file "$PTY_FILE" 2>/dev/null | grep -q "ELF"; then - PTY_VALID=true - else - echo -e "\x1b[33mPTY文件无效(非ELF二进制),将重新下载...\x1b[0m" - rm -f "$PTY_FILE" - fi - fi - - if [ "$PTY_VALID" = "false" ]; then - echo -e "\x1b[33m正在下载PTY二进制文件...\x1b[0m" - mkdir -p "$install_path/data/lib" - PTY_URL="https://github.com/MCSManager/PTY/releases/download/latest/$PTY_NAME" - if command -v curl &>/dev/null; then - curl -Lo "$PTY_FILE" "$PTY_URL" - elif command -v wget &>/dev/null; then - wget -O "$PTY_FILE" "$PTY_URL" - fi - if [ -f "$PTY_FILE" ] && file "$PTY_FILE" 2>/dev/null | grep -q "ELF"; then - echo -e "\x1b[32mPTY下载完成\x1b[0m" - else - echo -e "\x1b[33mPTY下载失败,终端功能将在服务启动时自动重试下载\x1b[0m" - rm -f "$PTY_FILE" 2>/dev/null - fi - fi - - # 设置可执行权限 - if [ -f "$PTY_FILE" ]; then - chmod 755 "$PTY_FILE" - fi + echo -e "\x1b[33mPTY 资产校验或下载失败;在运行时校验成功前,终端创建功能将保持不可用\x1b[0m" fi # 设置其他lib文件权限 diff --git a/package.json b/package.json index d515f0f..1926d14 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,14 @@ "build:client": "cd client && npm run build", "package": "npm run build && node scripts/package.js", "package:create": "node scripts/package.js", - "package:linux": "npm run build && node scripts/package.js --target=linux", + "package:linux": "npm run package:linux:x64", + "package:linux:x64": "npm run build && node scripts/package.js --target=linux-x64", + "package:linux:arm64": "npm run build && node scripts/package.js --target=linux-arm64", "package:windows": "npm run build && node scripts/package.js --target=windows", "package:no-zip": "npm run build && node scripts/package.js --no-zip", - "package:linux:no-zip": "npm run build && node scripts/package.js --target=linux --no-zip", + "package:linux:no-zip": "npm run package:linux:x64:no-zip", + "package:linux:x64:no-zip": "npm run build && node scripts/package.js --target=linux-x64 --no-zip", + "package:linux:arm64:no-zip": "npm run build && node scripts/package.js --target=linux-arm64 --no-zip", "package:windows:no-zip": "npm run build && node scripts/package.js --target=windows --no-zip", "start": "cd server && npm start", "start:prod": "cd dist && node index.js", diff --git a/scripts/package.js b/scripts/package.js index 052d31d..db6b15c 100644 --- a/scripts/package.js +++ b/scripts/package.js @@ -1,7 +1,7 @@ const fs = require('fs-extra') const path = require('path') const archiver = require('archiver') -const { execSync } = require('child_process') +const { execFileSync, execSync } = require('child_process') const https = require('https') const { pipeline } = require('stream') const { promisify } = require('util') @@ -16,9 +16,23 @@ const packageDir = path.join(distDir, 'package') // 获取命令行参数 const args = process.argv.slice(2) -const buildTarget = args.find(arg => arg.startsWith('--target='))?.split('=')[1] +const requestedBuildTarget = args.find(arg => arg.startsWith('--target='))?.split('=')[1] const skipZip = args.includes('--no-zip') || args.includes('--skip-zip') -const outputFile = buildTarget + +function resolveBuildTarget(target) { + if (!target || target === 'windows' || target === 'linux-x64' || target === 'linux-arm64') { + return target + } + if (target === 'linux') { + if (process.arch === 'x64') return 'linux-x64' + if (process.arch === 'arm64') return 'linux-arm64' + throw new Error(`不支持当前架构的 Linux 打包: ${process.arch}`) + } + throw new Error(`不支持的打包目标: ${target}`) +} + +const buildTarget = resolveBuildTarget(requestedBuildTarget) +const outputFile = buildTarget ? path.join(distDir, `${packageName}-${buildTarget}-v${version}.zip`) : path.join(distDir, `${packageName}-v${version}.zip`) @@ -27,21 +41,14 @@ const nodeVersion = '22.17.0' // Zip-Tools GitHub 下载配置(始终使用最新版本) const ZIP_TOOLS_GITHUB_URL = 'https://github.com/MCSManager/Zip-Tools/releases/latest/download/' -// PTY GitHub 下载配置(tag 名为 latest) -const PTY_GITHUB_URL = 'https://github.com/MCSManager/PTY/releases/download/latest/' - /** - * 获取目标平台对应的 Zip-Tools 二进制文件名列表 - * 打包时下载所有该平台支持的架构版本 + * 获取目标包对应的 Zip-Tools 二进制文件名列表 */ -function getZipToolsBinaries(platform) { - if (platform === 'linux') { - return ['file_zip_linux_x64', 'file_zip_linux_arm64'] - } else if (platform === 'windows') { - // GitHub Releases 上 Zip-Tools 只有 win32_x64 版本 - return ['file_zip_win32_x64.exe'] - } - // 未指定平台时下载所有版本 +function getZipToolsBinaries(target) { + if (target === 'linux-x64') return ['file_zip_linux_x64'] + if (target === 'linux-arm64') return ['file_zip_linux_arm64'] + if (target === 'windows') return ['file_zip_win32_x64.exe'] + // 未指定目标时下载所有版本 return [ 'file_zip_linux_x64', 'file_zip_linux_arm64', @@ -52,16 +59,13 @@ function getZipToolsBinaries(platform) { } /** - * 获取目标平台对应的 7z 二进制文件名列表 - * 打包时下载所有该平台支持的架构版本 + * 获取目标包对应的 7z 二进制文件名列表 */ -function get7zBinaries(platform) { - if (platform === 'linux') { - return ['7z_linux_x64', '7z_linux_arm64'] - } else if (platform === 'windows') { - return ['7z_win32_x64.exe', '7z_win32_arm64.exe'] - } - // 未指定平台时下载所有版本 +function get7zBinaries(target) { + if (target === 'linux-x64') return ['7z_linux_x64'] + if (target === 'linux-arm64') return ['7z_linux_arm64'] + if (target === 'windows') return ['7z_win32_x64.exe', '7z_win32_arm64.exe'] + // 未指定目标时下载所有版本 return [ '7z_linux_x64', '7z_linux_arm64', '7z_linux_386', '7z_linux_arm', '7z_win32_x64.exe', '7z_win32_arm64.exe', @@ -213,79 +217,51 @@ async function download7z(platform) { } /** - * 获取目标平台对应的 PTY 二进制文件名列表 - * 打包时下载所有该平台支持的架构版本 + * 获取目标包对应的固定 PTY 资产键列表 */ -function getPtyBinaries(platform) { - if (platform === 'linux') { - return ['pty_linux_x64', 'pty_linux_arm64'] - } else if (platform === 'windows') { - return ['pty_win32_x64.exe'] - } - // 未指定平台时下载所有版本 - return [ - 'pty_linux_x64', - 'pty_linux_arm64', - 'pty_win32_x64.exe', - ] +function getPtyAssetKeys(target) { + if (target === 'linux-x64') return ['linux-x64'] + if (target === 'linux-arm64') return ['linux-arm64'] + if (target === 'windows') return ['win32-x64'] + return ['linux-x64', 'linux-arm64', 'win32-x64'] } /** - * 下载 PTY 二进制文件到打包目录的 data/lib/ - * 从 GitHub Releases 下载,确保打包产物内置 PTY + * 通过服务端固定资产 CLI 校验或下载 PTY 到打包目录 */ -async function downloadPty(platform) { - const binaries = getPtyBinaries(platform) +async function ensurePtyAssets(target) { const libDir = path.join(packageDir, 'data', 'lib') await fs.ensureDir(libDir) - console.log('📥 正在从 GitHub 下载 PTY (latest)...') - let hasSuccess = false - - for (const binaryName of binaries) { - const url = `${PTY_GITHUB_URL}${binaryName}` - const destPath = path.join(libDir, binaryName) - - console.log(` 下载: ${binaryName}`) - try { - await downloadFile(url, destPath) - // 非 Windows 二进制文件设置可执行权限 - if (!binaryName.endsWith('.exe')) { - try { - execSync(`chmod +x "${destPath}"`) - } catch (e) { - // Windows 构建环境无法 chmod,忽略 - } - } - console.log(` ✅ ${binaryName} 下载完成`) - hasSuccess = true - } catch (err) { - console.error(` ⚠️ ${binaryName} 下载失败(跳过): ${err.message}`) - } + console.log('📥 正在校验固定 PTY 资产...') + for (const assetKey of getPtyAssetKeys(target)) { + execFileSync(process.execPath, [ + path.join(packageDir, 'server', 'utils', 'ptyAssetCli.js'), + 'ensure', + '--asset', assetKey, + '--target-dir', libDir + ], { stdio: 'inherit' }) } - - if (!hasSuccess) { - throw new Error('所有 PTY 文件下载均失败') - } - console.log('✅ PTY 下载完成') + console.log('✅ PTY 资产校验完成') } -async function downloadNodejs(platform) { +async function downloadNodejs(target) { const nodeUrls = { - linux: `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-x64.tar.xz`, + 'linux-x64': `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-x64.tar.xz`, + 'linux-arm64': `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-arm64.tar.xz`, windows: `https://nodejs.org/download/release/latest-v22.x/win-x64/node.exe` } - - const url = nodeUrls[platform] + + const url = nodeUrls[target] if (!url) { - throw new Error(`不支持的平台: ${platform}`) + throw new Error(`不支持的 Node.js 打包目标: ${target}`) } - + const fileName = url.split('/').pop() const filePath = path.join(__dirname, '..', fileName) - - console.log(`📥 正在下载 Node.js ${nodeVersion} for ${platform}...`) - + + console.log(`📥 正在下载 Node.js ${nodeVersion} for ${target}...`) + return new Promise((resolve, reject) => { const file = fs.createWriteStream(filePath) https.get(url, (response) => { @@ -293,7 +269,7 @@ async function downloadNodejs(platform) { reject(new Error(`下载失败: ${response.statusCode}`)) return } - + response.pipe(file) file.on('finish', () => { file.close() @@ -308,33 +284,36 @@ async function downloadNodejs(platform) { } // 解压和部署Node.js -async function deployNodejs(platform, downloadedFile) { +async function deployNodejs(target, downloadedFile) { const projectRoot = path.join(__dirname, '..') - - if (platform === 'linux') { + + if (target === 'linux-x64' || target === 'linux-arm64') { console.log('📦 正在解压 Linux Node.js...') // 解压到临时目录 execSync(`tar -xf "${downloadedFile}"`, { cwd: projectRoot }) - - // 重命名为node文件夹 - const extractedDir = path.join(projectRoot, `node-v${nodeVersion}-linux-x64`) + + const extractedDirNames = { + 'linux-x64': `node-v${nodeVersion}-linux-x64`, + 'linux-arm64': `node-v${nodeVersion}-linux-arm64` + } + const extractedDir = path.join(projectRoot, extractedDirNames[target]) const targetDir = path.join(packageDir, 'node') - + if (await fs.pathExists(extractedDir)) { await fs.move(extractedDir, targetDir) console.log('✅ Linux Node.js 部署到项目根目录/node') } else { throw new Error('Linux Node.js 解压失败') } - } else if (platform === 'windows') { + } else if (target === 'windows') { console.log('📦 正在部署 Windows Node.js...') // 复制node.exe到打包根目录(start.bat不再cd server,cwd为根目录) const targetFile = path.join(packageDir, 'node.exe') - + await fs.copy(downloadedFile, targetFile) console.log('✅ Windows Node.js 部署到打包根目录/node.exe') } - + // 清理下载的文件 await fs.remove(downloadedFile) } @@ -360,7 +339,7 @@ async function createPackage() { path.join(packageDir, 'server', 'package.json') ) - // PTY 文件不再从本地复制,改为从 GitHub 下载到 data/lib/ 目录 + // PTY 文件不从本地复制,待生产依赖安装后由固定资产 CLI 写入 data/lib/ // 复制环境变量配置文件 await fs.copy( @@ -425,6 +404,9 @@ async function createPackage() { console.error('❌ 服务端依赖安装失败:', error) throw error } + + // 服务端构建文件和生产依赖就绪后,通过固定清单校验或下载 PTY + await ensurePtyAssets(buildTarget) console.log('🎨 复制前端文件...') // 复制前端构建文件 @@ -457,14 +439,6 @@ async function createPackage() { console.log(' 用户启动时会自动从镜像站下载') } - // 下载 PTY 二进制文件(从 GitHub Releases) - try { - await downloadPty(buildTarget) - } catch (error) { - console.error('⚠️ PTY 下载失败,打包产物中将不包含 PTY:', error.message) - console.log(' 用户启动时会自动从镜像站下载') - } - console.log('📝 创建启动脚本...') // 根据目标平台创建启动脚本 if (buildTarget === 'windows') { @@ -473,7 +447,7 @@ async function createPackage() { path.join(__dirname, 'start.bat'), path.join(packageDir, 'start.bat') ) - } else if (buildTarget === 'linux') { + } else if (buildTarget === 'linux-x64' || buildTarget === 'linux-arm64') { const startShScript = `#!/bin/bash echo "正在启动GSM3管理面板..." # PTY 文件已迁移到 data/lib/ 目录,启动时由服务端自动检测