#!/usr/bin/env bash # OpsXShell v2.1.x native release builder. # Builds only for the current host OS; macOS and Windows installers must be # produced and verified on their respective systems. set -Eeuo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" VERSION_ARG="" TARGET="auto" CHECK_MODE="full" usage() { cat <<'EOF' OpsXShell 原生安装包构建脚本 用法: ./scripts/build.sh [v2.1.x] [目标] [选项] 目标: auto 根据当前系统自动选择(默认) mac-arm64 macOS Apple Silicon,生成 .app 和 .dmg mac-x64 macOS Intel,生成 .app 和 .dmg win-x64 Windows x86_64,生成 NSIS/MSI 安装包 linux-x64 Linux x86_64,生成 deb/rpm/AppImage linux-arm64 Linux aarch64,生成 deb/rpm/AppImage 选项: --quick 构建前只运行快速门禁 --skip-checks 跳过门禁,仅用于本地重复打包 -h, --help 显示帮助 示例: ./scripts/build.sh ./scripts/build.sh v2.1.0 mac-arm64 ./scripts/build.sh v2.1.1 mac-arm64 --quick 说明: - 显式传入新版本时,会同步 package.json、package-lock.json、 Cargo.toml、Cargo.lock 和 tauri.conf.json,但不会创建或推送 Git tag。 - Windows 包必须在 Windows 上构建;macOS 包必须在 macOS 上构建;Linux 包必须在 Linux 上构建。 - 正式签名构建请先设置 REQUIRE_SIGNING=true 及对应签名环境变量。 EOF } fail() { printf '\n[build] ERROR: %s\n' "$*" >&2 exit 1 } log() { printf '\n[build] %s\n' "$*" } while [ "$#" -gt 0 ]; do case "$1" in -h|--help) usage exit 0 ;; --quick) CHECK_MODE="quick" ;; --skip-checks) CHECK_MODE="none" ;; auto|mac-arm64|mac-x64|win-x64|linux-x64|linux-arm64) TARGET="$1" ;; v[0-9]*|[0-9]*) [ -z "$VERSION_ARG" ] || fail "只能指定一个版本号" VERSION_ARG="${1#v}" ;; *) fail "未知参数:$1(使用 ./scripts/build.sh --help 查看用法)" ;; esac shift done for command in node npm npx cargo rustc; do command -v "$command" >/dev/null 2>&1 || fail "缺少构建依赖:$command" done CURRENT_VERSION="$(node -p "require('./package.json').version")" VERSION="${VERSION_ARG:-$CURRENT_VERSION}" if ! printf '%s' "$VERSION" | grep -Eq '^2\.1\.[0-9]+([.-][0-9A-Za-z.-]+)?$'; then fail "版本必须是 v2.1.x 语义版本,当前输入:$VERSION" fi HOST_OS="$(uname -s)" HOST_ARCH="$(uname -m)" if [ "$TARGET" = "auto" ]; then case "$HOST_OS/$HOST_ARCH" in Darwin/arm64|Darwin/aarch64) TARGET="mac-arm64" ;; Darwin/x86_64) TARGET="mac-x64" ;; MINGW*/*|MSYS*/*|CYGWIN*/*) TARGET="win-x64" ;; Linux/x86_64) TARGET="linux-x64" ;; Linux/aarch64|Linux/arm64) TARGET="linux-arm64" ;; *) fail "当前平台 $HOST_OS/$HOST_ARCH 不支持自动打包,请显式指定目标" ;; esac fi case "$TARGET" in mac-arm64) [ "$HOST_OS" = "Darwin" ] || fail "mac-arm64 必须在 macOS 上构建" RUST_TARGET="aarch64-apple-darwin" RELEASE_PLATFORM="macos" OUTPUT_PLATFORM="macos-arm64" ;; mac-x64) [ "$HOST_OS" = "Darwin" ] || fail "mac-x64 必须在 macOS 上构建" RUST_TARGET="x86_64-apple-darwin" RELEASE_PLATFORM="macos" OUTPUT_PLATFORM="macos-x64" ;; win-x64) case "$HOST_OS" in MINGW*|MSYS*|CYGWIN*) ;; *) fail "win-x64 必须在 Windows 上构建,macOS 交叉编译不能替代 Windows 实机打包" ;; esac RUST_TARGET="x86_64-pc-windows-msvc" RELEASE_PLATFORM="windows" OUTPUT_PLATFORM="windows-x64" ;; linux-x64) [ "$HOST_OS" = "Linux" ] || fail "linux-x64 必须在 Linux 上构建" RUST_TARGET="x86_64-unknown-linux-gnu" RELEASE_PLATFORM="linux" OUTPUT_PLATFORM="linux-x64" ;; linux-arm64) [ "$HOST_OS" = "Linux" ] || fail "linux-arm64 必须在 Linux 上构建" RUST_TARGET="aarch64-unknown-linux-gnu" RELEASE_PLATFORM="linux" OUTPUT_PLATFORM="linux-arm64" ;; esac if command -v rustup >/dev/null 2>&1 && ! rustup target list --installed | grep -Fxq "$RUST_TARGET"; then fail "Rust target 未安装:$RUST_TARGET;请先运行 rustup target add $RUST_TARGET" fi if [ -n "$VERSION_ARG" ] && [ "$VERSION" != "$CURRENT_VERSION" ]; then log "同步版本号到 v$VERSION" node scripts/set-version.mjs "$VERSION" fi node scripts/set-version.mjs --check "$VERSION" if [ ! -d node_modules ]; then log "安装锁定依赖" npm ci fi log "OpsXShell v$VERSION · $TARGET · $(git rev-parse --short HEAD 2>/dev/null || printf unknown)" case "$CHECK_MODE" in full) log "运行完整发布门禁" npm run acceptance:full ;; quick) log "运行快速发布门禁" npm run acceptance:quick ;; none) log "已按参数跳过构建前门禁" ;; esac node scripts/release-preflight.mjs "$RELEASE_PLATFORM" log "构建原生安装包" TAURI_ARGS=(--ci --target "$RUST_TARGET") CODESIGN_WRAP_DIR="" if [ "$RELEASE_PLATFORM" = "macos" ]; then TAURI_ARGS+=(--bundles app,dmg) CODESIGN_WRAP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/opsx-codesign.XXXXXX")" cp "$ROOT_DIR/scripts/macos-codesign-wrapper.sh" "$CODESIGN_WRAP_DIR/codesign" chmod +x "$CODESIGN_WRAP_DIR/codesign" export PATH="$CODESIGN_WRAP_DIR:$PATH" log "macOS 签名使用 HTTP 时间戳(可用 CODESIGN_USE_SYSTEM_TIMESTAMP=1 关闭)" fi if [ -f "$ROOT_DIR/src-tauri/tauri.signing.conf.json" ]; then TAURI_ARGS+=(--config src-tauri/tauri.signing.conf.json) log "使用 src-tauri/tauri.signing.conf.json 签名" fi npx tauri build "${TAURI_ARGS[@]}" if [ -n "$CODESIGN_WRAP_DIR" ]; then rm -rf "$CODESIGN_WRAP_DIR" fi BUNDLE_ROOT="${CARGO_TARGET_DIR:-$ROOT_DIR/src-tauri/target}/$RUST_TARGET/release/bundle" node scripts/verify-release.mjs "$RELEASE_PLATFORM" "$BUNDLE_ROOT" OUTPUT_DIR="$ROOT_DIR/release/v$VERSION/$OUTPUT_PLATFORM" rm -rf "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" shopt -s nullglob COPIED_ARTIFACTS=() if [ "$RELEASE_PLATFORM" = "macos" ]; then SOURCE_ARTIFACTS=("$BUNDLE_ROOT"/dmg/*.dmg) elif [ "$RELEASE_PLATFORM" = "linux" ]; then SOURCE_ARTIFACTS=("$BUNDLE_ROOT"/deb/*.deb "$BUNDLE_ROOT"/rpm/*.rpm "$BUNDLE_ROOT"/appimage/*.AppImage) else SOURCE_ARTIFACTS=("$BUNDLE_ROOT"/nsis/*.exe "$BUNDLE_ROOT"/msi/*.msi "$BUNDLE_ROOT"/wix/*.msi) fi [ "${#SOURCE_ARTIFACTS[@]}" -gt 0 ] || fail "构建完成但未找到安装包:$BUNDLE_ROOT" for artifact in "${SOURCE_ARTIFACTS[@]}"; do destination="$OUTPUT_DIR/$(basename "$artifact")" cp "$artifact" "$destination" COPIED_ARTIFACTS+=("$destination") done CHECKSUM_FILE="$OUTPUT_DIR/SHA256SUMS.txt" if command -v shasum >/dev/null 2>&1; then ( cd "$OUTPUT_DIR" names=() for artifact in "${COPIED_ARTIFACTS[@]}"; do names+=("$(basename "$artifact")"); done shasum -a 256 "${names[@]}" > SHA256SUMS.txt ) elif command -v sha256sum >/dev/null 2>&1; then ( cd "$OUTPUT_DIR" names=() for artifact in "${COPIED_ARTIFACTS[@]}"; do names+=("$(basename "$artifact")"); done sha256sum "${names[@]}" > SHA256SUMS.txt ) else node --input-type=module -e ' import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { basename } from "node:path"; for (const path of process.argv.slice(1)) { console.log(`${createHash("sha256").update(readFileSync(path)).digest("hex")} ${basename(path)}`); } ' "${COPIED_ARTIFACTS[@]}" > "$CHECKSUM_FILE" fi log "构建完成:$OUTPUT_DIR" ls -lh "$OUTPUT_DIR" printf '\n[build] SHA-256\n' cat "$CHECKSUM_FILE"