#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "$0")" && pwd -P)" project_root="$(cd "$script_dir/.." && pwd -P)" # shellcheck source=macos_helpers.sh source "$script_dir/macos_helpers.sh" macos_install_exit_trap 1 macos_require_darwin /bin/bash "$script_dir/check_macos_entrypoints.sh" NODE_BIN="" NPM_BIN="" use_node_pair() { local node_candidate="$1" local npm_candidate="$2" local major version [[ -x "$node_candidate" && -x "$npm_candidate" ]] || return 1 version="$("$node_candidate" --version 2>/dev/null || true)" major="${version#v}" major="${major%%.*}" case "$major" in ''|*[!0-9]*) return 1 ;; esac [[ "$major" -ge 20 ]] || return 1 NODE_BIN="$node_candidate" NPM_BIN="$npm_candidate" export PATH="$(dirname "$NODE_BIN"):$PATH" } locate_node() { local bin_dir node_candidate npm_candidate node_candidate="$(command -v node 2>/dev/null || true)" npm_candidate="$(command -v npm 2>/dev/null || true)" if use_node_pair "$node_candidate" "$npm_candidate"; then return 0 fi for bin_dir in /opt/homebrew/bin /usr/local/bin; do if use_node_pair "$bin_dir/node" "$bin_dir/npm"; then return 0 fi done if [[ -n "${HOME:-}" ]]; then for node_candidate in "$HOME"/.nvm/versions/node/*/bin/node; do [[ -x "$node_candidate" ]] || continue npm_candidate="$(dirname "$node_candidate")/npm" if use_node_pair "$node_candidate" "$npm_candidate"; then return 0 fi done fi return 1 } install_local_node() { local architecture archive archive_path cache_root curl_bin download_dir local expected extracted install_dir node_version shasums_path actual node_version="${DOCTOR_NODE_VERSION:-22.14.0}" case "$(uname -m)" in arm64) architecture="arm64" ;; x86_64) architecture="x64" ;; *) macos_die "不支持的 Mac CPU 架构:$(uname -m)" ;; esac if [[ -n "${XDG_CACHE_HOME:-}" ]]; then cache_root="$XDG_CACHE_HOME/DoctorWorkstation/tools" elif [[ -n "${HOME:-}" ]]; then cache_root="$HOME/Library/Caches/DoctorWorkstation/tools" else macos_die "无法确定 Node 工具缓存目录:HOME 与 XDG_CACHE_HOME 均未设置。" fi install_dir="$cache_root/node-v$node_version-darwin-$architecture" if use_node_pair "$install_dir/bin/node" "$install_dir/bin/npm"; then return 0 fi if [[ -e "$install_dir" ]]; then macos_die "Node 缓存不完整,请删除后重试:$install_dir" fi curl_bin="$(command -v curl 2>/dev/null || true)" [[ -n "$curl_bin" ]] || macos_die "未找到用于下载 Node.js 的 curl。" mkdir -p "$cache_root" download_dir="$(mktemp -d "$cache_root/doctor-node.XXXXXX")" macos_register_temp_dir "$download_dir" archive="node-v$node_version-darwin-$architecture.tar.gz" archive_path="$download_dir/$archive" shasums_path="$download_dir/SHASUMS256.txt" printf '未检测到 Node.js 20+ 与 npm,正在下载 Node.js %s(%s)…\n' \ "$node_version" "$architecture" if ! "$curl_bin" --proto '=https' --tlsv1.2 -fsSL \ "https://nodejs.org/dist/v$node_version/$archive" -o "$archive_path"; then macos_die "Node.js 下载失败,请检查网络后重试。" fi if ! "$curl_bin" --proto '=https' --tlsv1.2 -fsSL \ "https://nodejs.org/dist/v$node_version/SHASUMS256.txt" -o "$shasums_path"; then macos_die "Node.js 校验文件下载失败。" fi expected="$(/usr/bin/awk -v file="$archive" '$2 == file { print $1; exit }' "$shasums_path")" actual="$(/usr/bin/shasum -a 256 "$archive_path" | /usr/bin/awk '{print $1}')" [[ -n "$expected" && "$actual" == "$expected" ]] || macos_die "Node.js 下载包 SHA-256 校验失败。" /usr/bin/tar -xzf "$archive_path" -C "$download_dir" extracted="$download_dir/node-v$node_version-darwin-$architecture" [[ -d "$extracted" ]] || macos_die "Node.js 下载包结构无效。" if [[ ! -e "$install_dir" ]]; then /bin/mv "$extracted" "$install_dir" fi use_node_pair "$install_dir/bin/node" "$install_dir/bin/npm" || \ macos_die "Node.js 安装完成,但 node/npm 无法执行。" } macos_ensure_uv cd "$project_root" printf '正在同步 Python 与 PyInstaller 构建依赖…\n' "$MACOS_UV_BIN" sync --locked --extra build python_bin="$project_root/.venv/bin/python" [[ -x "$python_bin" ]] || macos_die "uv 未生成可用的构建 Python:$python_bin" locate_node || install_local_node printf 'Node.js: %s;npm: %s\n' \ "$("$NODE_BIN" --version)" "$("$NPM_BIN" --version)" printf '正在安装锁定的 companion 依赖…\n' "$NPM_BIN" ci --prefix "$project_root/video_companion" --no-audit --no-fund printf '正在构建 DoctorWorkstation.app…\n' PYINSTALLER_PYTHON="$python_bin" SKIP_FRONTEND_INSTALL=1 \ /bin/bash "$script_dir/build_macos.sh" artifact="$project_root/dist/DoctorWorkstation.app" project_version="$(/usr/bin/awk -F '"' '/^__version__ = "/ { print $2; exit }' \ "$project_root/src/doctor_workstation/__init__.py")" [[ -n "$project_version" ]] || macos_die "无法从 doctor_workstation.__version__ 读取版本号。" case "$(uname -m)" in arm64) release_arch="arm64" ;; x86_64) release_arch="x64" ;; *) macos_die "不支持的 Mac CPU 架构:$(uname -m)" ;; esac release_zip="$project_root/dist/DoctorWorkstation-macOS-$release_arch-$project_version.zip" checksum_file="$release_zip.sha256" rm -f -- "$release_zip" "$checksum_file" printf '正在生成可分发 ZIP…\n' /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$artifact" "$release_zip" release_hash="$(/usr/bin/shasum -a 256 "$release_zip" | /usr/bin/awk '{print $1}')" printf '%s %s\n' "$release_hash" "$(basename "$release_zip")" >"$checksum_file" printf '\n打包成功:%s\n' "$artifact" printf '分发包:%s\n' "$release_zip" printf 'SHA-256:%s\n' "$release_hash"