#!/bin/bash
# Copyright (c) 2026 Huawei Technologies Co., Ltd.
# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
# CANN Open Software License Agreement Version 2.0 (the "License").
# Please refer to the License for details. You may not use this file except in compliance with the License.
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
# See LICENSE in the root of the software repository for the full text of the License.
# HCCL-VM 一键安装:装依赖 -> 检测 CANN -> 拉取公开源码(hcomm/hccl) -> 编译。
# 支持架构:x86_64 / aarch64
#
# ============================ 依赖关系说明 ============================
# 组件角色:
# CANN 包(用户提供) : 终态发布包;内含编译好的 hcomm/hccl 库(.so)、对外导出头
# (如 hccl_res_expt.h 中的 ThreadType 等类型)、ACL runtime。
# hcomm 源码(脚本 clone) : ① 承载 checker 本体(即 test/hccl_vm);② 提供 hcomm 内部
# 源码(src/platform、framework、base_comm…)供 checker 编译。
# hccl 源码(脚本 clone) : 提供 hccl 内部源码(src/ops、hcomm_dlsym…)供 checker 编译
# device/AICPU/AIV。上述内部源码 CANN 包里不包含。
#
# 官方(正常)配套方向:CANN 版本为锚 -> hcomm/hccl 源码用与该 CANN 版本一致的发布
# tag(见 release-management 的 cann-hcomm/cann-hccl 行)。即“源码跟随 CANN”。
#
# 本工具的配套:checker 存在于 hcomm 的竞赛/主线分支(发布 tag 里没有 checker)。默认按
# profile main 取 hcomm/hccl 的 master 主线;具体分支映射见下方 profile 注册表。源码与 CANN
# 需同代配套,不配套时 device 侧会因缺新接口
# (如 ThreadType)编译失败,本脚本会在失败时提示更换配套 CANN。
# ====================================================================
set -euo pipefail
# 颜色(仅 stdout 为终端时启用;被管道/重定向时关闭,避免日志里出现乱码转义)
color_enabled() { [ -t 1 ]; }
if color_enabled; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; NC=''
fi
# 全局变量(可由环境变量或命令行覆盖)
WORKSPACE=${WORKSPACE:-}
ASCEND_PATH=${ASCEND_PATH:-/usr/local/Ascend}
HCOMM_PATH=${HCOMM_PATH:-}
HCCL_PATH=${HCCL_PATH:-}
CHECKER_PATH=${CHECKER_PATH:-}
# 分支引用:不在此写死默认,默认由 profile 落定(见 apply_defaults)。
# 记录“环境变量是否显式提供”,用于优先级链 CLI > env > profile。
ENV_HCOMM_REF="${HCOMM_REF:-}"
ENV_HCCL_REF="${HCCL_REF:-}"
CLI_HCOMM_REF=""
CLI_HCCL_REF=""
HCOMM_REF=""
HCCL_REF=""
HCOMM_REF_SRC=""
HCCL_REF_SRC=""
PROFILE=""
PROFILE_CUSTOM=false
VERBOSE=false
CANN_MODE="auto" # 内部 CANN 策略:auto(默认) | download(--reinstall-cann) | keep(--offline)
ASCEND_PATH_EXPLICIT=false # --ascend-path 是否由用户显式传入
SKIP_HCCL_TEST=false # --skip-hccl-test
WILL_DL=false # 阶段①决策结果:稍后是否下载 CANN
CANN_INSTALLED="" # detect_cann 写入:已探测到的现有 CANN 根目录(空=未探测到)
CANN_SETENV="" # detect_cann 写入:对应的 set_env.sh 全路径(空=未探测到)
CANN_TARGET="" # 解析出的安装目标目录(决定下载/覆盖时由 resolve_cann_target 赋值)
CANN_CACHE="" # 下载缓存目录,apply_defaults 里赋 ${WORKSPACE}/.cann-download
CANN_RUN_FILE="" # 实际下载到的 .run 文件全路径(收尾展示用)
OPS_RUN_FILE="" # 实际下载到的 ops 包全路径(收尾展示用)
CANN_CHANNEL="master" # CANN 镜像下载通道,apply_defaults 按 profile 落定(campus=legacy / main=master)
HCCL_TEST_RESULT="" # build_hccl_test 写入:built | skipped | failed:<原因>
HCCL_TEST_BIN_DIR="" # build_hccl_test 写入:hccl_test 可执行文件所在目录(原地编译)
PKG_MGR="" # detect_pkg_mgr 写入:apt(Ubuntu/Debian)或 dnf(HCE/openEuler/EulerOS)
SUDO_PREFIX="" # setup_sudo 解析:root 为空;非 root 有 sudo 为 "sudo"
SUDO_KEEPALIVE_PID="" # sudo 授权保活后台进程 PID(cleanup_sudo 负责回收)
STEP_TOTAL=9 # 编译 HCCL-VM(7) → 编译安装子包(8) → 编译 hccl_test(9)
LOG_DIR=""
RUN_TS=""
# 版本标识(发布时更新;SCRIPT_COMMIT 可由同步流程回填,未知则保持 unknown)
SCRIPT_VERSION="1.0.0"
SCRIPT_COMMIT="unknown"
# 显示信息
info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
# 显示警告
warn() {
echo -e "${YELLOW}[WARN]${NC} $1" >&2
}
# 显示错误并退出
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
exit 1
}
# 步骤进度:[时:分:秒] [n/total] 消息
step() {
echo -e "${GREEN}[$(date +%H:%M:%S)] [$1/$2]${NC} $3"
}
# 版本串:commit 已回填才带 commit,否则只显示版本号(避免露 "commit unknown")
version_line() {
if [ "${SCRIPT_COMMIT}" = "unknown" ]; then
echo "v${SCRIPT_VERSION}"
else
echo "v${SCRIPT_VERSION} (commit ${SCRIPT_COMMIT})"
fi
}
# 起始信息块(非 ASCII art)
banner() {
info "HCCL-VM 一键安装 $(version_line)(工作目录: ${WORKSPACE})"
info "配套方案: ${PROFILE}"
info "将分 ${STEP_TOTAL} 步自动完成依赖安装、源码拉取、CANN 配置与编译;总耗时取决于网络与机器性能。"
}
success_summary() {
local bin="${CHECKER_PATH}/hccl_vm_install/bin/hccl-vm"
info "HCCL-VM 安装完成。"
info "checker 产物: ${bin}"
info "CANN 路径: ${ASCEND_HOME_PATH:-未知}"
info "HCCL/HCOMM 子包已编译安装到: ${ASCEND_HOME_PATH:-未知}"
info "驱动依赖库已拷贝到: ${CHECKER_PATH}/hccl_vm_install/lib/aarch64/"
case "${HCCL_TEST_RESULT}" in
built) info "hccl_test: 已编译 ${HCCL_TEST_BIN_DIR:-${WORKSPACE}/hccl_test/bin}/" ;;
skipped) info "hccl_test: 已跳过(--skip-hccl-test)" ;;
failed:*) warn "hccl_test: 未编译(${HCCL_TEST_RESULT#failed:})" ;;
*) : ;;
esac
[ -n "${CANN_RUN_FILE}" ] && info "CANN 安装包(已保留): ${CANN_RUN_FILE}"
[ -n "${OPS_RUN_FILE:-}" ] && info "CANN ops 包(已保留): ${OPS_RUN_FILE}"
local inst="${CHECKER_PATH}/hccl_vm_install"
local setenv="${ASCEND_HOME_PATH:-<CANN>/ascend-toolkit}/set_env.sh"
local mpirun_extra=""
if [ "$(normalize_arch "$(uname -m)" 2>/dev/null)" = "x86_64" ]; then
mpirun_extra="--allow-run-as-root --oversubscribe"
fi
if [ "${HCCL_TEST_RESULT}" = built ]; then
info "后续步骤(可直接复制,运行一个 hccl_test 用例并由 checker 校验):"
echo " source ${setenv}" >&2
echo " source ${inst}/script/hccl_config.sh # 设 RANK_TABLE_FILE 等运行变量" >&2
echo " cd ${inst}/bin && ./hccl-vm start ascend950_cluster_32_server_normal.yaml" >&2
echo " (hvm)\$> hccl-vm mock-comm 112" >&2
echo " (hvm)\$> mpirun ${mpirun_extra} -np 2 ${HCCL_TEST_BIN_DIR:-${WORKSPACE}/hccl_test/bin}/alltoall_test -b 64 -e 64 -d int32 -o sum -w 0 -n 1 -c 1" >&2
echo " (hvm)\$> hccl-vm plugin run @checker" >&2
echo " (hvm)\$> exit" >&2
else
info "后续步骤(可直接复制):"
echo " cd ${CHECKER_PATH} && source ${setenv} && ./hccl_vm_install/bin/hccl-vm start <集群配置文件>" >&2
fi
if [ -f "${CHECKER_PATH}/README-Competition.md" ]; then
echo " 更多集群配置与用例见 ${CHECKER_PATH}/README-Competition.md §4" >&2
elif [ -f "${CHECKER_PATH}/README.md" ]; then
echo " 集群配置示例见 ${CHECKER_PATH}/README.md" >&2
fi
}
# 检查系统包是否已安装:apt 系(Ubuntu/Debian)用 dpkg-query 精确查询;
# dnf 系(HCE/openEuler/EulerOS/CentOS)用 rpm -q 查询。需在 check_env 中调用 detect_pkg_mgr 后使用。
check_package() {
if [ "${PKG_MGR:-}" = "apt" ]; then
local status
status="$(dpkg-query -W -f='${Status}' "$1" 2>/dev/null)" || return 1
[ "$status" = "install ok installed" ]
elif [ "${PKG_MGR:-}" = "dnf" ]; then
rpm -q "$1" &>/dev/null
else
return 1
fi
}
# 检测包管理器:apt(Ubuntu/Debian)或 dnf(HCE/openEuler/EulerOS/CentOS/RHEL);均未找到留空
detect_pkg_mgr() {
if command -v apt-get &>/dev/null; then
PKG_MGR="apt"
elif command -v dnf &>/dev/null; then
PKG_MGR="dnf"
else
PKG_MGR=""
fi
}
# 判断操作系统是否受支持(仅 Linux)
is_supported_os() {
[[ "$1" == "Linux" ]]
}
# 归一化 CPU 架构;未知架构返回非零
normalize_arch() {
case "$1" in
x86_64|amd64) echo "x86_64" ;;
aarch64|arm64) echo "aarch64" ;;
*) return 1 ;;
esac
}
# ── Profile 注册表:一个 profile 只映射 hcomm/hccl 分支;首行为默认;新增版本加一行即可 ──
# 每行格式:hcomm_ref|hccl_ref|说明
profile_all_names() {
printf '%s\n' "main" "campus-2026"
}
# 每行格式:hcomm_ref|hccl_ref|说明|是否允许 weekly|CANN 下载通道(master=滚动最新 / legacy=归档)
profile_row() {
case "$1" in
main) echo "master|master|主线最新,随官方更新|true|master" ;;
campus-2026) echo "competition/campus-2026|competition/campus-2026|竞赛固定,CANN 从 build.md 提取|false|legacy" ;;
*) return 1 ;;
esac
}
profile_default() { profile_all_names | head -n 1; }
profile_exists() { profile_row "$1" >/dev/null 2>&1; }
profile_hcomm_ref() { profile_row "$1" | cut -d'|' -f1; }
profile_hccl_ref() { profile_row "$1" | cut -d'|' -f2; }
profile_desc() { profile_row "$1" | cut -d'|' -f3; }
profile_allow_weekly() { profile_row "$1" | cut -d'|' -f4; } # 空/false 均视为不允许
profile_cann_channel() { profile_row "$1" | cut -d'|' -f5; } # CANN 下载通道;空则由 apply_defaults 兜底 master
# 显示帮助信息
show_help() {
echo "HCCL-VM 一键安装脚本 $(version_line)"
echo ""
echo "新手直接运行,无需任何参数(依赖自动安装;需确认的操作有终端时提示、超时按默认继续):"
echo " curl -fsSL https://raw.gitcode.com/cann/hcomm/raw/master/test/hccl_vm/hccl_vm_installer | bash"
echo " curl -fsSL <URL> | bash -s -- --workspace /root/hvm # 一键并指定工作目录"
echo " (或下载后本地运行: bash hccl_vm_installer)"
echo ""
echo "脚本将自动完成以下 9 步:"
echo " 1-3. 检查环境、磁盘、安装依赖"
echo " 4-6. 拉取 hcomm/hccl 源码、检测/下载安装 CANN"
echo " 7. 编译 HCCL-VM 工具"
echo " 8. 编译安装 HCCL/HCOMM 子包(build_pkg.sh 执行)"
echo " 9. 编译 hccl_test(--skip-hccl-test 可跳过)"
echo ""
echo "用法:"
echo " bash hccl_vm_installer [选项]"
echo ""
echo "常用选项:"
echo " --profile <名称> 配套方案,默认: main(主线最新)"
echo " --list-profiles 查看所有可选配套方案"
echo " --workspace <路径> 工作目录 (默认: 当前目录)"
echo " --ascend-path <路径> 指定 CANN 目录:有则复用、没有则装到这里(不指定时探测 <工作目录>/Ascend)"
echo " --reinstall-cann 重新下载并覆盖现有 CANN(用于现有 CANN 与配套版本不匹配;默认保留现有 CANN,不覆盖)"
echo " --offline 从不下载 CANN,只用现有;没有则打印自备引导并退出(自备/离线场景)"
echo " 默认(不加以上):探测 <工作目录>/Ascend,有配套就复用、没有就下载安装到那里"
echo " --skip-hccl-test 不编译 hccl_test,也不安装 OpenMPI(默认会编译)"
echo " --version 显示版本"
echo " -h, --help 显示此帮助"
echo " 提示: CANN 若装在工作目录之外,请用 --ascend-path 指定,避免重复下载(不再自动采用已 source 的环境变量)。"
echo ""
echo "高级选项(自定义分支组合,可能与 CANN 不配套):"
echo " --hcomm-ref <分支/标签> 覆盖 profile 的 hcomm 分支(仅支持分支/标签名,非 commit)"
echo " --hccl-ref <分支/标签> 覆盖 profile 的 hccl 分支(仅支持分支/标签名,非 commit)"
echo " --hcomm-path/--hccl-path/--checker-path <路径> 自定义源码路径"
echo " --verbose 打印全过程日志(含 apt 与编译输出)"
}
# 打印可选 profile(供客户查看)
list_profiles() {
echo "可用配套方案(--profile 可选值):"
echo ""
local first=true name row hcomm hccl desc tag
while IFS= read -r name; do
row="$(profile_row "${name}")"
hcomm="$(echo "${row}" | cut -d'|' -f1)"
hccl="$(echo "${row}" | cut -d'|' -f2)"
desc="$(echo "${row}" | cut -d'|' -f3)"
if [ "${first}" = true ]; then tag=" (默认)"; first=false; else tag=""; fi
echo " ${name}${tag}"
echo " hcomm 分支: ${hcomm}"
echo " hccl 分支: ${hccl}"
echo " 说明: ${desc}"
echo ""
done < <(profile_all_names)
echo "注: profile 名 main 对应 git 分支 master。"
echo "用法示例: --profile main"
}
# 校验带值选项确有参数值(避免 set -u 下末位缺值报晦涩的 $2 unbound)
require_val() {
# $1=选项名 $2=剩余参数个数
[[ "$2" -ge 2 ]] || error "选项 $1 需要一个参数值"
}
# 解析命令行参数
parse_args() {
while [[ $# -gt 0 ]]; do
# 归一化 --opt=val 为 --opt val(现有选项也一并受益,如 --profile=main)
case "$1" in
--*=*)
local __opt="${1%%=*}" __val="${1#*=}"
shift
set -- "${__opt}" "${__val}" "$@"
;;
esac
case "$1" in
--workspace)
require_val "$1" "$#"
WORKSPACE="$2"
shift 2
;;
--ascend-path)
require_val "$1" "$#"
ASCEND_PATH="$2"
ASCEND_PATH_EXPLICIT=true
shift 2
;;
--hcomm-path)
require_val "$1" "$#"
HCOMM_PATH="$2"
shift 2
;;
--hccl-path)
require_val "$1" "$#"
HCCL_PATH="$2"
shift 2
;;
--checker-path)
require_val "$1" "$#"
CHECKER_PATH="$2"
shift 2
;;
--hcomm-ref)
require_val "$1" "$#"
CLI_HCOMM_REF="$2"
shift 2
;;
--hccl-ref)
require_val "$1" "$#"
CLI_HCCL_REF="$2"
shift 2
;;
--profile)
require_val "$1" "$#"
PROFILE="$2"
shift 2
;;
--reinstall-cann)
[ "${CANN_MODE}" = keep ] && error "--reinstall-cann 与 --offline 互斥(一个要下载覆盖、一个绝不下载)。"
CANN_MODE="download" # 内部策略:强制重下覆盖现有 CANN
shift 1
;;
--offline)
[ "${CANN_MODE}" = download ] && error "--offline 与 --reinstall-cann 互斥(一个绝不下载、一个要下载覆盖)。"
CANN_MODE="keep" # 内部策略:从不下载,用现有否则退出给引导
shift 1
;;
--skip-hccl-test)
SKIP_HCCL_TEST=true
shift 1
;;
--version)
echo "HCCL-VM 一键安装脚本 $(version_line)"
exit 0
;;
--list-profiles)
list_profiles
exit 0
;;
--verbose)
VERBOSE=true
shift 1
;;
-h|--help)
show_help
exit 0
;;
*)
error "未知参数: $1。运行 bash hccl_vm_installer --help 查看可用选项。"
;;
esac
done
}
# 相对路径转绝对(基于当前工作目录);已是绝对路径则原样返回。
to_abs() { case "$1" in /*) printf '%s\n' "$1" ;; *) printf '%s\n' "$(pwd)/$1" ;; esac; }
# 统一设置依赖其他变量的默认值(parse_args 之后调用)
apply_defaults() {
WORKSPACE=${WORKSPACE:-$(pwd)}
ASCEND_PATH=${ASCEND_PATH:-/usr/local/Ascend}
HCOMM_PATH=${HCOMM_PATH:-${WORKSPACE}/hcomm}
HCCL_PATH=${HCCL_PATH:-${WORKSPACE}/hccl}
CHECKER_PATH=${CHECKER_PATH:-${HCOMM_PATH}/test/hccl_vm}
# 路径统一绝对化:相对路径会在后续 cd 与 precheck_cann_path 的 dirname 回溯中出错
WORKSPACE="$(to_abs "${WORKSPACE}")"
ASCEND_PATH="$(to_abs "${ASCEND_PATH}")"
HCOMM_PATH="$(to_abs "${HCOMM_PATH}")"
HCCL_PATH="$(to_abs "${HCCL_PATH}")"
CHECKER_PATH="$(to_abs "${CHECKER_PATH}")"
# profile 落定(默认=注册表首行)
PROFILE="${PROFILE:-$(profile_default)}"
if ! profile_exists "${PROFILE}"; then
error "未知 profile: ${PROFILE}。运行 --list-profiles 查看可选方案,或去掉 --profile 使用默认。"
fi
# CANN 下载通道按 profile 落定:campus=legacy(版本已归档)、main=master(滚动最新);未定则兜底 master
CANN_CHANNEL="$(profile_cann_channel "${PROFILE}")"; CANN_CHANNEL="${CANN_CHANNEL:-master}"
# 分支优先级:CLI --*-ref > 环境变量 HCOMM_REF/HCCL_REF > profile 映射
if [ -n "${CLI_HCOMM_REF}" ]; then
HCOMM_REF="${CLI_HCOMM_REF}"; HCOMM_REF_SRC="--hcomm-ref"; PROFILE_CUSTOM=true
elif [ -n "${ENV_HCOMM_REF}" ]; then
HCOMM_REF="${ENV_HCOMM_REF}"; HCOMM_REF_SRC="环境变量 HCOMM_REF"; PROFILE_CUSTOM=true
else
HCOMM_REF="$(profile_hcomm_ref "${PROFILE}")"; HCOMM_REF_SRC="profile ${PROFILE}"
fi
if [ -n "${CLI_HCCL_REF}" ]; then
HCCL_REF="${CLI_HCCL_REF}"; HCCL_REF_SRC="--hccl-ref"; PROFILE_CUSTOM=true
elif [ -n "${ENV_HCCL_REF}" ]; then
HCCL_REF="${ENV_HCCL_REF}"; HCCL_REF_SRC="环境变量 HCCL_REF"; PROFILE_CUSTOM=true
else
HCCL_REF="$(profile_hccl_ref "${PROFILE}")"; HCCL_REF_SRC="profile ${PROFILE}"
fi
LOG_DIR="${WORKSPACE}/.hccl_vm_install_logs"
RUN_TS="$(date +%Y%m%d-%H%M%S)"
CANN_CACHE="${WORKSPACE}/.cann-download"
}
# 自定义分支组合时提示一次(逐仓标注实际来源);已定义 profile(含默认)不提示
notice_custom_refs() {
[ "${PROFILE_CUSTOM}" = true ] || return 0
warn "使用自定义分支组合,可能与 CANN 或彼此不配套:"
echo " hcomm=${HCOMM_REF}(来自 ${HCOMM_REF_SRC})" >&2
echo " hccl=${HCCL_REF}(来自 ${HCCL_REF_SRC})" >&2
}
# 第 1 步早期决策:先探测;未探测到 CANN 时按 CANN 策略(--offline/默认)与终端有无决定 下载/退出。
# 探测到则置好 CANN_INSTALLED/CANN_SETENV 后直接返回(校验留到第 5 步)。
cann_phase1_decide() {
detect_cann || true
[ -n "${CANN_INSTALLED}" ] && return 0 # 已探测到,阶段①跳过
resolve_cann_target "" # 未探测到:先算下载目标 <target>
case "${CANN_MODE}" in
keep)
print_cann_guidance; exit 1 ;;
download)
info "未探测到 CANN,将于第 5 步下载安装到 ${CANN_TARGET}"
WILL_DL=true ;;
auto)
if [ -t 0 ] || [ -e /dev/tty ]; then
if prompt_yn "未探测到 CANN,将自动下载安装配套版 CANN(下载约 1.5G,安装后约占 4.3G,装到 ${CANN_TARGET})。15 秒内无响应视为同意。继续? [Y/n]" Y; then
WILL_DL=true
else
print_cann_guidance; exit 1
fi
else
warn "未探测到 CANN,将自动下载并安装配套版本(下载约 1.5G,安装后约占 4.3G,安装至 ${CANN_TARGET});当前无现有 CANN,不会覆盖任何已有安装。如需禁用自动下载,请使用 --offline。"
WILL_DL=true
fi ;;
esac
return 0
}
# 运行环境自检:必须 Linux + 具备 apt 或 dnf + 受支持架构(x86_64 / aarch64)
check_env() {
if ! is_supported_os "$(uname -s)"; then
error "本工具仅支持 Linux。Windows 请在 WSL2 或远端 Linux 中运行。"
fi
# 检测包管理器:apt(Ubuntu/Debian)或 dnf(HCE/openEuler/EulerOS);均未找到则报错
detect_pkg_mgr
if [ -z "${PKG_MGR}" ]; then
error "未检测到 apt-get 或 dnf。本工具支持 Ubuntu 22.04+(apt)或 Huawei Cloud EulerOS(dnf)。
其他发行版请手动安装等价依赖后用 --hcomm-path/--hccl-path 复用已备源码:
$(dependency_list | tr '\n' ' ')"
fi
if ! normalize_arch "$(uname -m)" &> /dev/null; then
error "不支持的架构: $(uname -m)(仅 x86_64 / aarch64)。请换用 x86_64 或 aarch64 环境。"
fi
# 非受支持架构软警告(不阻断):仅 x86_64 / aarch64 为受支持架构,其他架构报错已在上面处理
# Ubuntu 版本检查仅对 apt 系有意义;dnf 系(HCE/openEuler)无 Ubuntu 版本号,跳过
if [ "${PKG_MGR}" = "apt" ] && ! ubuntu_version_ok; then
warn "当前系统非 Ubuntu 22.04+(官方验证 x86_64 / aarch64 Ubuntu 22.04+);可继续,但遇到编译问题请优先换官方环境。"
fi
# gcc 版本软警告(不阻断,与架构/OS 警告同档):hcomm build.md 前置依赖要求 gcc 7.3.0–13.3.x。
# x86_64 宿主该约束同时管宿主 gcc 与 aarch64 交叉编译器(device_arm 用系统交叉 gcc 编);
# aarch64 本机构筑仅管系统 gcc。超范围(如 Ubuntu 25.04 gcc14 / 26.04 gcc15)可能因新 gcc 移除
# 头文件间接包含、第三方(yaml-cpp 0.8.0 等)未适配而编译失败——属超出官方工具链范围,非本脚本缺陷。
# 此处仅告警并继续尝试(编译器会在真不兼容时报真错)。
local _gcc_major
_gcc_major="$(gcc -dumpversion 2>/dev/null | cut -d. -f1 || true)"
if [ -n "${_gcc_major}" ] && [ "${_gcc_major}" -ge 15 ] 2>/dev/null; then
warn "检测到 gcc ${_gcc_major},超出 build.md 前置依赖要求的 gcc 7.3.0–13.3.x。gcc 15(如 Ubuntu 26.04)已实测会因第三方 yaml-cpp 0.8.0 未适配新 gcc(缺 <cstdint>)编译失败;可继续尝试,如需编译成功请在满足该 gcc 范围的环境(如 Ubuntu 22.04 / 24.04)上运行。"
elif [ -n "${_gcc_major}" ] && { [ "${_gcc_major}" -ge 14 ] || [ "${_gcc_major}" -lt 7 ]; } 2>/dev/null; then
warn "检测到 gcc ${_gcc_major},超出 build.md 前置依赖要求的 gcc 7.3.0–13.3.x;可继续尝试,如遇编译问题请在满足该范围的环境(如 Ubuntu 22.04 / 24.04)上运行。"
fi
# cmake 版本软警告(同档):build.md 要求 cmake >= 3.16.0;另 CMake 4.x 移除了对 <3.5 策略的兼容,
# 会使第三方 yaml-cpp 老 CMakeLists 配置失败(如 Ubuntu 26.04 自带 cmake 4.x),故有效上限视为 <4.0。
# 仅当 cmake 已存在时才判(未装时依赖安装阶段会装,此处跳过)。
local _cmake_ver _cmake_major _cmake_minor
_cmake_ver="$(cmake --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || true)"
if [ -n "${_cmake_ver}" ]; then
_cmake_major="${_cmake_ver%%.*}"
_cmake_minor="$(echo "${_cmake_ver}" | cut -d. -f2)"
if [ "${_cmake_major}" -ge 4 ] 2>/dev/null; then
warn "检测到 cmake ${_cmake_ver},CMake 4.x 移除了对 <3.5 策略的兼容,会使第三方 yaml-cpp 老 CMakeLists 配置失败(如 Ubuntu 26.04);build.md 支持 cmake >= 3.16.0(3.x 系列),可继续尝试,如遇配置失败请改用 cmake 3.16–3.x(如 Ubuntu 22.04 / 24.04 自带版本)。"
elif { [ "${_cmake_major}" -lt 3 ] || { [ "${_cmake_major}" -eq 3 ] && [ "${_cmake_minor:-0}" -lt 16 ]; }; } 2>/dev/null; then
warn "检测到 cmake ${_cmake_ver},低于 build.md 要求的 cmake >= 3.16.0;请升级 cmake 后重试。"
fi
fi
# 内存预警(同档,不阻断):子包编译用 -j nproc 全核并行,内存不足会被 OOM killer 杀 cc1plus。
# 阈值来自实测:Ubuntu 24.04 / nproc=24 满负荷编译峰值≈13G(单进程 cc1plus 峰值≈1.3G),
# 折算约 2G 基底 + 0.5G/核;内存+swap 低于该估算即告警。
local _np _mem_kb _swap_kb _total_mb _need_mb
_np="$(nproc 2>/dev/null || echo 1)"
_mem_kb="$(awk '/^MemTotal:/{print $2}' /proc/meminfo 2>/dev/null || true)"
_swap_kb="$(awk '/^SwapTotal:/{print $2}' /proc/meminfo 2>/dev/null || true)"
if [ -n "${_mem_kb}" ] && [ "${_np}" -ge 1 ] 2>/dev/null; then
_total_mb=$(( (_mem_kb + ${_swap_kb:-0}) / 1024 ))
_need_mb=$(( 2048 + 512 * _np ))
if [ "${_total_mb}" -lt "${_need_mb}" ] 2>/dev/null; then
warn "内存偏低:本机 内存+swap 约 $(( _total_mb/1024 ))G,而 ${_np} 核并行编译预计约需 $(( _need_mb/1024 ))G(实测 24 核峰值≈13G)。内存不足会在编译期触发 OOM(cc1plus 被杀、报 'Killed signal terminated program cc1plus')。建议增大内存/swap(WSL 改 .wslconfig 的 memory=/swap= 后 wsl --shutdown 重启)再运行。"
fi
fi
# 管道模式(curl|bash)下 stdin 非终端:明确非交互
if [ ! -t 0 ]; then
info "检测到管道模式(curl|bash):系统依赖自动安装(${PKG_MGR});需确认的操作会在检测到终端时提示(超时按默认值继续),无终端时按默认值自动执行。"
fi
cann_phase1_decide
}
# 判断是否 Ubuntu 22.04 及以上(读 /etc/os-release,非 Ubuntu 或版本低返回非零);仅 apt 系(x86_64 / aarch64)调用
ubuntu_version_ok() {
local id ver_major
[ -r /etc/os-release ] || return 1
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}" || true)"
ver_major="$(. /etc/os-release 2>/dev/null && echo "${VERSION_ID:-}" | cut -d. -f1 || true)"
[ "${id}" = "ubuntu" ] || return 1
[ -n "${ver_major}" ] && [ "${ver_major}" -ge 22 ] 2>/dev/null
}
# 回显 <path> 所在分区可用 GB(逐级 dirname 找到存在的挂载点后 df -Pk);取不到回显空。
avail_gb_of() {
local target="$1" avail_kb
while [ ! -d "${target}" ] && [ "${target}" != "/" ] && [ -n "${target}" ]; do
target="$(dirname "${target}")"
done
avail_kb="$(df -Pk "${target}" 2>/dev/null | awk 'NR==2{print $4}' || true)"
[ -n "${avail_kb}" ] || { echo ""; return 0; }
echo $(( avail_kb / 1024 / 1024 ))
}
# 决定下载 CANN 后调用。目标分区 <target> ≥4.3G、缓存分区 <cache> ≥1.5G,任一不足即 exit 1。
check_cann_disk() {
local target="$1" cache="$2" t_gb c_gb
t_gb="$(avail_gb_of "${target}")"; c_gb="$(avail_gb_of "${cache}")"
if [ -n "${t_gb}" ] && [ "${t_gb}" -lt 5 ]; then
error "CANN 安装目标分区(${target})可用约 ${t_gb}G,低于所需 ~4.3G。请用 --ascend-path 指向更大分区后重试。"
fi
if [ -n "${c_gb}" ] && [ "${c_gb}" -lt 2 ]; then
error "下载缓存分区(${cache})可用约 ${c_gb}G,低于所需 ~1.5G。请用 --workspace 指向更大分区后重试。"
fi
}
# 磁盘空间预检:源码 + --full 编译需要一定空间,不足时提前告警(非阻断)
check_disk() {
local min_gb=10 avail_gb
avail_gb="$(avail_gb_of "${WORKSPACE}")"
if [ -z "${avail_gb}" ]; then
return 0
fi
if [ "${avail_gb}" -lt "${min_gb}" ]; then
warn "工作目录所在磁盘可用空间约 ${avail_gb}G,低于建议的 ${min_gb}G;源码拉取 + 编译可能空间不足。"
echo " 可用 --workspace 指向更大分区,或清理后重试。" >&2
else
info "磁盘可用空间约 ${avail_gb}G(建议 ≥ ${min_gb}G)"
fi
}
# 选可用的 qemu 用户态模拟包名:默认传统名 qemu-user-static(22.04/24.04/25.04 均有);
# 新版 Ubuntu(26.04+)该名变虚拟包无安装候选,仅当 apt-cache 确认此情形时回退到 qemu-user-binfmt。
pick_qemu_pkg() {
command -v apt-cache >/dev/null 2>&1 || { echo qemu-user-static; return; }
# 命令替换取值 + || true + 判空;不在 pipefail 管道里用 grep -q(-q 早退致上游 SIGPIPE 会误判失败)
local static_cand binfmt_cand
static_cand="$(apt-cache policy qemu-user-static 2>/dev/null | grep -E 'Candidate: [0-9]' || true)"
binfmt_cand="$(apt-cache policy qemu-user-binfmt 2>/dev/null | grep -E 'Candidate: [0-9]' || true)"
if [ -z "${static_cand}" ] && [ -n "${binfmt_cand}" ]; then
echo qemu-user-binfmt
else
echo qemu-user-static
fi
}
# 依赖清单:编译基础包 + 运行时 python-yaml(hccl-vm start 生成组网拓扑需);按 PKG_MGR 输出对应包名。
# apt 系(Ubuntu):build-essential / -dev 后缀;dnf 系(HCE/openEuler):gcc gcc-c++ make / -devel 后缀。
# x86_64 宿主额外装 device(arm) 交叉编译工具链(仅 apt 有此包);aarch64 本机直接编译用系统 gcc 无需交叉链。
# sudo:build_pkg.sh 内部用 sudo 关 pip 保护锁并 chmod CANN 目录;标准 Ubuntu/HCE 自带,极简 root 环境需补装。
dependency_list() {
local deps=()
if [ "${PKG_MGR}" = "apt" ]; then
deps=("git" "sudo" "build-essential" "cmake" "libsqlite3-dev" "libboost-all-dev" "rdma-core" "libibverbs-dev" "pkg-config" "python3" "python3-yaml")
[ "${SKIP_HCCL_TEST}" = false ] && deps+=("openmpi-bin" "libopenmpi-dev")
# curl 列举镜像/取头、wget 下载 .run;仅下载 CANN 时需要(keep 从不下载)
[ "${CANN_MODE}" != keep ] && deps+=("curl" "wget" "python3-pip")
# x86_64 宿主编译 device 侧(ARM)代码需交叉编译工具链 + qemu 模拟执行 binfmt;
# aarch64 本机直接编译,用系统 gcc 即可,无需交叉链和 qemu。
if [ "$(uname -m)" = "x86_64" ]; then
deps+=("gcc-aarch64-linux-gnu" "g++-aarch64-linux-gnu" "$(pick_qemu_pkg)" "binfmt-support")
fi
elif [ "${PKG_MGR}" = "dnf" ]; then
# HCE/openEuler/EulerOS:gcc gcc-c++ make 替代 build-essential;-devel 替代 -dev
deps=("git" "sudo" "gcc" "gcc-c++" "make" "cmake" "sqlite-devel" "boost-devel" "rdma-core" "libibverbs-devel" "pkgconf-pkg-config" "python3" "python3-pyyaml")
[ "${SKIP_HCCL_TEST}" = false ] && deps+=("mpich" "mpich-devel")
[ "${CANN_MODE}" != keep ] && deps+=("curl" "wget" "python3-pip")
# aarch64 在 HCE 上本机编译,无需交叉链;x86_64 在 HCE 上的交叉链包名暂不内置
fi
printf '%s\n' "${deps[@]}"
}
# 确保 universe 源已启用(仅 apt 系):精简/被裁过的 Ubuntu 只留 main 时,libboost-all-dev、openmpi-bin、
# libopenmpi-dev、qemu-user-static 等(均在 universe 组件)会报 "Unable to locate package"。
# dnf 系(HCE/openEuler)所有包均在官方源,无需额外启用;调用时自动跳过。
# 装依赖前调用;启用后不在此 apt-get update,交由 install_deps 紧接着的 update 统一刷新。
ensure_universe_enabled() {
[ "${PKG_MGR:-}" = "apt" ] || return 0 # dnf 系无需处理,直接返回
local apt_prefix="$1" apt_log="$2" cand base_line
# 命令替换取值 + || true + 判空;不在 pipefail 管道里用 grep -q(-q 早退致上游 SIGPIPE 会误判失败)
cand="$(apt-cache policy libboost-all-dev 2>/dev/null | grep -E 'Candidate: [0-9]' || true)"
[ -n "${cand}" ] && return 0 # universe 已可用,无需处理
info "启用 universe 源(libboost、openmpi 等依赖所需)..."
if command -v add-apt-repository >/dev/null 2>&1; then
${apt_prefix} add-apt-repository universe -y >> "${apt_log}" 2>&1 || true
elif ${apt_prefix} apt-get update -y >> "${apt_log}" 2>&1 \
&& ${apt_prefix} env DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common >> "${apt_log}" 2>&1; then
${apt_prefix} add-apt-repository universe -y >> "${apt_log}" 2>&1 || true
else
# 兜底:add-apt-repository 不可用且装不上 software-properties-common 时,
# 复用系统现有 main 源行、仅追加 universe 组件(尊重内网镜像 URI,不硬编码官方源)
base_line="$(grep -hE '^[[:space:]]*deb[[:space:]].*\bmain\b' /etc/apt/sources.list 2>/dev/null | grep -viE 'universe' | head -n1 || true)"
if [ -n "${base_line}" ]; then
echo "${base_line} universe" | ${apt_prefix} tee /etc/apt/sources.list.d/hccl-vm-universe.list >> "${apt_log}" 2>&1 || true
else
warn "无法自动启用 universe 源,请手动执行: sudo add-apt-repository universe && sudo apt-get update"
fi
fi
}
# 安装第三方依赖:按 PKG_MGR 走 apt 或 dnf 分支
install_deps() {
info "检查第三方依赖..."
local dependencies=() dep_line
while IFS= read -r dep_line; do
dependencies+=("${dep_line}")
done < <(dependency_list)
local missing_deps=()
for dep in "${dependencies[@]}"; do
if ! check_package "$dep"; then
missing_deps+=("$dep")
fi
done
if [ ${#missing_deps[@]} -eq 0 ]; then
info "所有第三方依赖已安装,跳过安装步骤"
return
fi
info "安装缺失的依赖: ${missing_deps[*]}"
# 提权前缀由 setup_sudo 已提前解析并认证(root 为空、非 root 为 sudo);curl|bash 的密码经 /dev/tty 输入,不再误判
local pkg_prefix="${SUDO_PREFIX}"
if [ "$(id -u)" -ne 0 ] && [ -z "${pkg_prefix}" ]; then
warn "缺少系统依赖,但当前非 root 且无 sudo,无法自动安装:"
echo " ${missing_deps[*]}" >&2
echo " 请让管理员安装后重试,或改用 root 运行,或手动执行:" >&2
if [ "${PKG_MGR}" = "apt" ]; then
echo " sudo apt-get update && sudo apt-get install -y ${missing_deps[*]}" >&2
else
echo " sudo dnf install -y ${missing_deps[*]}" >&2
fi
exit 1
fi
# 输出留存到日志文件(不再 >/dev/null 吞掉);--verbose 时同时回显到终端,失败时回显便于定位
mkdir -p "${LOG_DIR}"
local pkg_log="${LOG_DIR}/${PKG_MGR}-install-${RUN_TS}.log"
info "包安装日志: ${pkg_log}"
# apt 系:精简/被裁过的 Ubuntu 可能未启用 universe 源(libboost-all-dev、openmpi 等在该组件中);
# 装依赖前先确保 universe 可用,避免 "Unable to locate package"。dnf 系所有包均在官方源,跳过。
ensure_universe_enabled "${pkg_prefix}" "${pkg_log}"
local rc=0
if [ "${PKG_MGR}" = "apt" ]; then
if [ "${VERBOSE}" = true ]; then
if { ${pkg_prefix} apt-get update -y \
&& ${pkg_prefix} env DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing_deps[@]}"; } 2>&1 | tee "${pkg_log}"; then
rc=0
else
rc=${PIPESTATUS[0]}
fi
else
{ ${pkg_prefix} apt-get update -y \
&& ${pkg_prefix} env DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing_deps[@]}"; } > "${pkg_log}" 2>&1 &
local install_pid=$!
spin_on_pid "${install_pid}" "安装依赖中"
wait "${install_pid}" || rc=$?
fi
else
# dnf 系(HCE/openEuler/EulerOS):直接 dnf install -y,无需 update
if [ "${VERBOSE}" = true ]; then
if ${pkg_prefix} dnf install -y "${missing_deps[@]}" 2>&1 | tee "${pkg_log}"; then
rc=0
else
rc=${PIPESTATUS[0]}
fi
else
${pkg_prefix} dnf install -y "${missing_deps[@]}" > "${pkg_log}" 2>&1 &
local install_pid=$!
spin_on_pid "${install_pid}" "安装依赖中"
wait "${install_pid}" || rc=$?
fi
fi
if [ "${rc}" -ne 0 ]; then
warn "依赖安装失败,末尾日志:"
tail -n 20 "${pkg_log}" >&2
echo " 仍无法解决可带完整日志到 https://gitcode.com/cann/hcomm/issues 反馈。" >&2
error "${PKG_MGR} 安装依赖失败,请检查网络 / 源 / 磁盘 / 锁后重试(完整日志: ${pkg_log})。"
fi
}
# source CANN 的 set_env.sh(其未按 nounset/errexit 编写,临时关闭再恢复)
source_cann_env() {
info "检测到 CANN: $1,加载环境变量"
set +u +e
# $1 为运行时探测到的 CANN set_env.sh 绝对路径(见 cann_setenv_candidates), 无固定路径可供
# ShellCheck 静态跟随, 指定按空来源处理即可(正当抑制, 非掩盖缺陷)。
# shellcheck source=/dev/null
# 部分 CANN 版本的 set_env.sh 会打 banner;输出收进日志,终端只留统一 [INFO]
source "$1" > /dev/null 2>&1
set -eu
}
# 从 build.md 提取 CANN 推荐快照号(cann-run-mirror/software/<通道>/<10位以上数字>)。
# 通道无关:build.md 写 master 或 legacy 都能抠出快照号(实际下载通道由 profile 的 CANN_CHANNEL 决定)。
# 命中 echo 数字 + rc0;未固定/无匹配/文件缺失 echo 空 + rc1。
extract_cann_snapshot() {
local buildmd="$1" snap
[ -f "${buildmd}" ] || return 1
snap="$(grep -oE 'cann-run-mirror/software/[a-z]+/[0-9]{10,}' "${buildmd}" 2>/dev/null | head -n 1 | grep -oE '[0-9]{10,}' || true)"
[ -n "${snap}" ] || return 1
echo "${snap}"
}
# 按序列出可能的 CANN set_env.sh 位置(兼容 ascend-toolkit / cann 两种布局)。
# 只探测 workspace 内的 CANN;用户显式 --ascend-path 时才额外探测该路径。
# root 与非 root 一致,不扫描 /usr/local、/home/workspace、$HOME 等系统位置。
cann_setenv_candidates() {
printf '%s\n' \
"${WORKSPACE}/Ascend/ascend-toolkit/set_env.sh" \
"${WORKSPACE}/Ascend/cann/set_env.sh"
if [ "${ASCEND_PATH_EXPLICIT}" = true ]; then
printf '%s\n' \
"${ASCEND_PATH}/ascend-toolkit/set_env.sh" \
"${ASCEND_PATH}/cann/set_env.sh"
fi
}
default_cann_target() {
# 统一落在 workspace 下,保证工作目录自包含(root / 非 root 相同)
echo "${WORKSPACE}/Ascend"
}
resolve_cann_target() {
local installed="${1:-}"
if [ "${ASCEND_PATH_EXPLICIT}" = true ]; then
CANN_TARGET="${ASCEND_PATH}"
elif [ -n "${installed}" ]; then
CANN_TARGET="${installed}"
else
CANN_TARGET="$(default_cann_target)"
fi
}
# 校验以 root 安装 CANN 到 <target> 时各级父目录是否合规:CANN 的 ge-executor 对 root 安装要求
# 每一级父目录 ① 权限 755(group/other 可读可执行)② 属主为 root。任一不满足都会被拒绝安装。
# 非 root 不受此限制(装进自有目录,owner 有权即可)。stat -L 跟随软链按真实目录判定。
check_cann_parent_perms() {
local target="$1" d perm owner ownername
[ "$(id -u)" -eq 0 ] || return 0 # 仅 root 安装受父目录 755 + 属主 root 限制
d="$(dirname "${target}")"
while :; do
if [ -d "${d}" ]; then
perm="$(stat -L -c %a "${d}" 2>/dev/null || echo 000)"
owner="$(stat -L -c %u "${d}" 2>/dev/null || echo -1)"
# 属主必须是 root(uid 0);否则 ge-executor 报 "permission isn't right, it should belong to root"
if [ "${owner}" != 0 ]; then
ownername="$(stat -L -c %U "${d}" 2>/dev/null || echo "${owner}")"
error "以 root 安装 CANN 到 ${target},但父目录 ${d} 的属主是 ${ownername}(非 root),不满足 CANN 官方要求(root 安装时各级父目录须归 root 所有)。
请改用 root 拥有的安装路径(如 --workspace /opt/hvm),或改以该目录属主身份(非 root)运行本脚本。"
fi
# 权限:group 和 other 都要 r-x,即 (perm & 055) == 055;否则 ge-executor 报 permission invalid
if [ "$(( 8#${perm} & 8#55 ))" -ne "$(( 8#55 ))" ]; then
error "以 root 安装 CANN 到 ${target},但父目录 ${d} 权限为 ${perm},不满足 CANN 官方要求(安装路径各级父目录需 755,即 group/other 可读可执行)。
请执行后重跑: chmod 755 ${d}
或用 --workspace / --ascend-path 指向各级父目录均为 755 的路径(如 /opt/hvm)。"
fi
fi
# 到根即止;对 "." 或空也止(防相对路径 dirname 收敛到 "." 永不到 "/" 的死循环兜底)
case "${d}" in /|.|"") break ;; esac
d="$(dirname "${d}")"
done
return 0
}
# 第 1 步早期预检:将要下载安装 CANN(WILL_DL)时,提前校验目标父目录权限,
# 不满足即在开头报错,避免跑到第 5 步装 CANN 才失败、白费前面的下载与编译。
# 覆盖不到的场景(探测到旧 CANN、第 5 步才决定覆盖重装)由 download_and_install_cann 内的同一校验兜底。
precheck_cann_path() {
[ "${WILL_DL}" = true ] || return 0 # 不下载安装 CANN 则无需检查
check_cann_parent_perms "${CANN_TARGET}"
}
cann_setenv_under() {
if [ -f "$1/ascend-toolkit/set_env.sh" ]; then echo "$1/ascend-toolkit/set_env.sh"
elif [ -f "$1/cann/set_env.sh" ]; then echo "$1/cann/set_env.sh"; fi
}
pick_cann_snapshot() {
local fixed="$1" snap mirror="https://ascend.devcloud.huaweicloud.com/artifactory/cann-run-mirror/software/${CANN_CHANNEL}"
if [ -n "${fixed}" ]; then echo "${fixed}"; return 0; fi
snap="$(curl -fsSL "${mirror}/" 2>/dev/null | grep -oE '[0-9]{17}' | sort -n | tail -1)" || true
[ -n "${snap}" ] || return 1
warn "本 profile 未固定 CANN 版本,将下载 ${CANN_CHANNEL} 通道最新快照 ${snap}(未经固定配套校验,可能与代码不配套;如需可复现,请用 --ascend-path 复用一次装好的 CANN)。"
echo "${snap}"
}
# $1=快照号 $2=是否允许 weekly(true=主线 profile;否则只取正式发布版)
pick_toolkit_file() {
local snap="$1" allow_weekly="${2:-false}" base all file arch
arch="$(normalize_arch "$(uname -m)" 2>/dev/null || echo x86_64)"
base="https://ascend.devcloud.huaweicloud.com/artifactory/cann-run-mirror/software/${CANN_CHANNEL}/${snap}"
all="$(curl -fsSL "${base}/" 2>/dev/null | grep -oE "Ascend-cann-toolkit_[^\"< ]*linux-${arch}\\.run" | sort -u)" || true
if [ "${allow_weekly}" = true ]; then
# 主线 profile(随官方更新):允许 weekly,优先 weekly(最新),无则退回正式版。
file="$(printf '%s\n' "${all}" | grep -i weekly | sort -V | tail -1)"
[ -n "${file}" ] || file="$(printf '%s\n' "${all}" | sort -V | tail -1)"
else
# 固定配套 profile(如竞赛):只取正式发布版(版本号纯数字点分,如 9.1.0),排除 weekly 等未测构建;取最高版本。
file="$(printf '%s\n' "${all}" | grep -E "^Ascend-cann-toolkit_[0-9]+(\\.[0-9]+)*_linux-${arch}\\.run$" | sort -V | tail -1)"
fi
[ -n "${file}" ] || return 1
echo "${file}"
}
# $1=快照号 $2=是否允许 weekly(true=主线 profile;否则只取正式发布版)
pick_ops_file() {
local snap="$1" allow_weekly="${2:-false}" base all file arch
arch="$(normalize_arch "$(uname -m)" 2>/dev/null || echo x86_64)"
base="https://ascend.devcloud.huaweicloud.com/artifactory/cann-run-mirror/software/${CANN_CHANNEL}/${snap}"
all="$(curl -fsSL "${base}/" 2>/dev/null | grep -oE "Ascend-cann-950-ops_[^\"< ]*linux-${arch}\\.run" | sort -u)" || true
if [ "${allow_weekly}" = true ]; then
file="$(printf '%s\n' "${all}" | grep -i weekly | sort -V | tail -1)"
[ -n "${file}" ] || file="$(printf '%s\n' "${all}" | sort -V | tail -1)"
else
file="$(printf '%s\n' "${all}" | grep -E "^Ascend-cann-950-ops_[0-9]+(\\.[0-9]+)*_linux-${arch}\\.run$" | sort -V | tail -1)"
fi
[ -n "${file}" ] || return 1
echo "${file}"
}
detect_cann() {
CANN_INSTALLED=""; CANN_SETENV=""
local cand se
if [ "${ASCEND_PATH_EXPLICIT}" = true ]; then
se="$(cann_setenv_under "${ASCEND_PATH}" || true)"
if [ -n "${ASCEND_HOME_PATH:-}" ] && [ "${ASCEND_HOME_PATH%/ascend-toolkit*}" != "${ASCEND_PATH}" ]; then
warn "已 source 的 ASCEND_HOME_PATH=${ASCEND_HOME_PATH} 与 --ascend-path ${ASCEND_PATH} 不一致,将以 --ascend-path 为准。"
fi
[ -n "${se}" ] && { CANN_INSTALLED="${ASCEND_PATH}"; CANN_SETENV="${se}"; return 0; }
return 1
fi
# 只按 workspace 候选探测(显式 --ascend-path 已在上面处理),不信任继承的
# ASCEND_HOME_PATH/ASCEND_TOOLKIT_HOME——避免用户 source 的外部/旧 CANN 污染 workspace 自包含。
while IFS= read -r cand; do
[ -f "${cand}" ] && { CANN_SETENV="${cand}"; CANN_INSTALLED="${cand%/*/*}"; return 0; }
done < <(cann_setenv_candidates)
# workspace 未找到 CANN;若环境里 source 了外部 CANN,提示可用 --ascend-path 复用(否则将下载到 workspace)
local sourced=""
[ -n "${ASCEND_HOME_PATH:-}" ] && sourced="${ASCEND_HOME_PATH%/ascend-toolkit*}"
[ -z "${sourced}" ] && [ -n "${ASCEND_TOOLKIT_HOME:-}" ] && sourced="${ASCEND_TOOLKIT_HOME%/ascend-toolkit*}"
[ -n "${sourced}" ] && warn "检测到已 source 的 CANN(${sourced});如需复用请加 --ascend-path ${sourced},否则将下载配套版到 workspace。"
return 1
}
# 缺 CANN 时的安装引导:优先原样打印 build.md 的“安装CANN软件包”章节;抠不到则回退精简步骤。
print_cann_guidance() {
local buildmd="${HCOMM_PATH}/docs/zh/build/build.md" arch section dlpage
arch="$(normalize_arch "$(uname -m)" 2>/dev/null || echo x86_64)"
echo -e "${RED}[ERROR]${NC} 未检测到 CANN(编译必需)" >&2
echo " 说明: 按下方步骤装好 CANN 后,重跑本脚本时用 --ascend-path 指向其安装目录即可复用(不再自动采用已 source 的环境变量)" >&2
if [ -f "${buildmd}" ]; then
# 抠“### 安装CANN软件包”到下一个 ## / ### 标题之前(先判后印,避免多打下一节标题;用 ###? 规避 mawk 区间语法)
section="$(awk '/^###? /{if(f)exit} /^###? *安装.*CANN/{f=1} f{print}' "${buildmd}" 2>/dev/null)"
# 提取下载页 URL,用于回退步骤拼真实命令
dlpage="$(grep -oE 'https://[^ )]*cann-run-mirror/software/[a-z]+/[0-9]*/?' "${buildmd}" 2>/dev/null | head -n 1 || true)"
fi
if [ -n "${section:-}" ]; then
echo " ── 摘自 build.md(本机架构 ${arch})──" >&2
printf '%s\n' "${section}" >&2
echo " ──" >&2
else
echo " 按 4 步安装(本机架构 ${arch}):" >&2
echo " 1) wget ${dlpage:-https://ascend.devcloud.huaweicloud.com/artifactory/cann-run-mirror/software/${CANN_CHANNEL}/}Ascend-cann-toolkit_<版本>_linux-${arch}.run" >&2
echo " (下载页里认准带 weekly 日期、结尾 linux-${arch}.run 的 Toolkit 包)" >&2
echo " 2) chmod +x 上述文件 && ./上述文件 --install --install-path=<目录> # root 默认 /usr/local/Ascend,非 root 用 ~/Ascend" >&2
echo " 3) 重跑本脚本时加 --ascend-path <上面的安装目录> 复用" >&2
fi
echo " 提示: ① 只需 Toolkit 包(驱动/固件/ops 编译不用装) ② .run 自解压,无需手动解压" >&2
echo " ③ CANN 已装在别处 → 用 --ascend-path <含 set_env.sh 的目录> 指定,无需重装" >&2
echo " 更多: https://www.hiascend.com/document/redirect/CannCommunityInstWizard" >&2
}
# 正在克隆的目标目录;中断时由 trap 清理半成品
CURRENT_CLONE_DIR=""
# 当前正在等待的后台子进程 PID(apt/clone/编译/下载均经 spin_on_pid* 记录);中断时由 trap 结束它,避免孤儿占锁
CURRENT_BG_PID=""
# 克隆或复用一个仓库:$1=名称 $2=git地址 $3=分支/标签 $4=目标目录
clone_repo() {
local name="$1" url="$2" ref="$3" dest="$4"
if [ -d "${dest}" ]; then
if git -C "${dest}" rev-parse --is-inside-work-tree > /dev/null 2>&1; then
local cur
# 用 branch --show-current:对"无提交(unborn)分支"与正常仓库都可靠返回分支名(rev-parse --abbrev-ref 在 unborn 上返回 "HEAD")
cur="$(git -C "${dest}" branch --show-current 2>/dev/null || echo unknown)"
[ -n "${cur}" ] || cur="detached@$(git -C "${dest}" rev-parse --short HEAD 2>/dev/null || echo unknown)"
if [ "${cur}" != "${ref}" ]; then
error "目录 ${dest} 当前在分支 ${cur},与本次期望 ${ref} 不符(可能上次用了别的 profile)。请删除后重跑:rm -rf '${dest}',或改 --workspace。"
fi
info "${name} 已存在(分支 ${ref},commit $(git -C "${dest}" rev-parse --short HEAD 2>/dev/null || echo unknown)),跳过下载"
return
fi
error "目录已存在但不是完整 git 仓库(疑似上次下载中断):${dest}。请删除后重跑:rm -rf '${dest}'"
fi
info "克隆 ${name}(${ref})……"
mkdir -p "$(dirname "${dest}")" "${LOG_DIR}"
CURRENT_CLONE_DIR="${dest}"
# 重试最多 3 次;用 http 低速阈值作为“卡住即中止”的超时(<1KB/s 持续 60s 判失败)。
# git 原始输出收进日志,终端只显示统一风格的活动指示。
local attempt=1 max_attempts=3 clone_log="${LOG_DIR}/clone-${name}-${RUN_TS}.log"
while true; do
git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=60 \
clone --branch "${ref}" "${url}" "${dest}" > "${clone_log}" 2>&1 &
local clone_pid=$!
spin_on_pid "${clone_pid}" "拉取 ${name} 源码中"
if wait "${clone_pid}"; then break; fi
rm -rf "${dest}" # 清理半成品,避免下次被“已存在”复用
if [ "${attempt}" -ge "${max_attempts}" ]; then
CURRENT_CLONE_DIR=""
warn "克隆 ${name} 失败,末尾日志:"
tail -n 15 "${clone_log}" >&2
error "克隆 ${name} 失败(分支/标签 ${ref},已重试 ${max_attempts} 次)。请检查网络/代理、确认 ${ref} 存在;也可手动 git clone 到 ${dest} 后重跑(脚本会自动复用)。"
fi
warn "克隆 ${name} 第 ${attempt}/${max_attempts} 次尝试失败,3 秒后重试……"
attempt=$((attempt + 1))
sleep 3
done
CURRENT_CLONE_DIR=""
info "${name} 就绪(commit $(git -C "${dest}" rev-parse --short HEAD 2>/dev/null || echo unknown))"
}
# 读已装 CANN 的安装信息(用于 best-effort 版本比对);找不到返回空
installed_cann_version_id() {
local f
# 拼接所有可能含版本/日期的 info(install.info 有 version/innerversion;version.info 常含快照日期)
# ${ASCEND_HOME_PATH:-} 兜底:某些 CANN 布局只导出 ASCEND_TOOLKIT_HOME,此处在其兜底赋值前被调用,
# 不加 :- 会在 set -u 下把"ASCEND_HOME_PATH: unbound variable"漏到用户终端(虽被 || true 吞但仍刺眼)。
for f in \
"${ASCEND_HOME_PATH:-}"/ascend-toolkit/latest/*/ascend_toolkit_install.info \
"${ASCEND_HOME_PATH:-}"/ascend-toolkit/latest/*/version.info \
"${ASCEND_HOME_PATH:-}"/*/ascend_toolkit_install.info \
"${ASCEND_HOME_PATH:-}"/*/version.info \
"${ASCEND_HOME_PATH:-}"/ascend_toolkit_install.info; do
[ -f "${f}" ] && cat "${f}" 2>/dev/null || true
done
return 0
}
# 入参 $1=<snapshot>(可空)。调用前须已 source CANN(读 ASCEND_HOME_PATH 下版本信息)。
# 返回:0=配套(命中快照日期) 1=不配套(有快照但未命中) 2=无法校验(无快照或读不到已装版本)
cann_match_status() {
local snapshot="$1" date8 info
[ -n "${snapshot}" ] || return 2
info="$(installed_cann_version_id)" || true
[ -n "${info}" ] || return 2
date8="${snapshot:0:8}"
if grep -q "${date8}" <<<"${info}"; then return 0; else return 1; fi
}
# $1=提示语 $2=默认(Y|N) → 0=继续 / 1=拒绝
# Y:空/超时/无终端→继续(0),输入 n→拒绝(1); N:空/超时/无终端→拒绝(1),输入 y→继续(0)
prompt_yn() {
printf '%b[INPUT]%b %s ' "${YELLOW}" "${NC}" "$1" >&2
local reply="" rc=0
{ read -t 15 -r reply < /dev/tty; } 2>/dev/null || rc=$?
# 超时或无终端时 read 不回显换行,补一个,避免后续步骤输出黏在提示行末尾
[ "${rc}" -ne 0 ] && { reply=""; printf '\n' >&2; }
if [ "$2" = "N" ]; then
case "${reply}" in [yY]*) return 0 ;; *) return 1 ;; esac
else
case "${reply}" in [nN]*) return 1 ;; *) return 0 ;; esac
fi
}
# 后台进程活动指示:stdout 为终端时单行动画(每 0.8s 转圈),否则每 10s 一行心跳;均附已用时长。
# 不 wait,调用方 wait 取退出码。
spin_on_pid() {
local pid="$1" label="$2" start=${SECONDS} elapsed
CURRENT_BG_PID="${pid}" # 记录在途子进程,供中断 trap 结束它
local tty=false; color_enabled && tty=true
local frames='|/-\' i=0 tick=0
while kill -0 "${pid}" 2>/dev/null; do
elapsed=$(( SECONDS - start ))
if ${tty}; then
printf '\r%b[INFO]%b %s %s(已用 %d 分 %02d 秒)\033[K' \
"${GREEN}" "${NC}" "${label}" "${frames:i%4:1}" $((elapsed / 60)) $((elapsed % 60))
i=$((i + 1))
sleep 0.8
else
tick=$((tick + 1))
if [ "${tick}" -ge 10 ]; then
tick=0
info "${label}(已用 $((elapsed / 60)) 分 $((elapsed % 60)) 秒)"
fi
sleep 1
fi
done
${tty} && printf '\r\033[K'
CURRENT_BG_PID=""
return 0
}
# 后台进程下载进度指示:显示百分比/速率/ETA(total已知),或仅显示已下载MB(total未知)
# 不 wait,调用方 wait 取退出码。
spin_on_pid_pct() {
local pid="$1" label="$2" total="$3" file="$4"
CURRENT_BG_PID="${pid}" # 记录在途子进程,供中断 trap 结束它
local tty=false; color_enabled && tty=true
local frames='|/-\' i=0 start=${SECONDS} cur pct rate eta line prev=0 prevt=${SECONDS}
while kill -0 "${pid}" 2>/dev/null; do
cur="$(stat -c%s "${file}" 2>/dev/null || echo 0)"
if [ -n "${total}" ] && [ "${total}" -gt 0 ] 2>/dev/null; then
pct=$(( cur * 100 / total )); [ "${pct}" -gt 100 ] && pct=100
local dt=$(( SECONDS - prevt )); [ "${dt}" -lt 1 ] && dt=1
rate=$(( (cur - prev) / dt / 1024 / 1024 )); [ "${rate}" -lt 1 ] && rate=1
eta=$(( (total - cur) / 1024 / 1024 / rate ))
line="$(printf '下载 CANN %d%% (%dM / %dM) ~%dMB/s 剩 ~%d 分' \
"${pct}" "$((cur/1024/1024))" "$((total/1024/1024))" "${rate}" "$((eta/60))")"
prev="${cur}"; prevt=${SECONDS}
else
line="$(printf '下载 CANN 已下载 %dM' "$((cur/1024/1024))")"
fi
if ${tty}; then
printf '\r%b[INFO]%b %s %s\033[K' "${GREEN}" "${NC}" "${line}" "${frames:i%4:1}"; i=$((i+1)); sleep 0.8
else
info "${line}"; sleep 10
fi
done
${tty} && printf '\r\033[K'
CURRENT_BG_PID=""
return 0
}
# $1=安装目标 <target>;$2=build.md 提取的固定快照 <fixed_snapshot>(可空)
download_and_install_cann() {
local target="$1" fixed="$2"
# 0. 幂等复用
if [ -f "${target}/ascend-toolkit/set_env.sh" ]; then
source_cann_env "${target}/ascend-toolkit/set_env.sh"
if cann_match_status "${fixed}"; then
info "目标已存在配套 CANN,直接复用:${target}"
return 0
fi
fi
# 0.5 权限预检(root 装 CANN 要求各级父目录 755):兜底第 5 步才决定覆盖重装的路径(第 1 步预检未覆盖),失败即报错不白装
check_cann_parent_perms "${target}"
# 1. 磁盘(阻断)
check_cann_disk "${target}" "${CANN_CACHE}"
# 2. 快照
local snap; snap="$(pick_cann_snapshot "${fixed}")" || true
[ -n "${snap}" ] || { warn "无法确定 CANN 快照。"; print_cann_guidance; exit 1; }
# 3. 文件(是否允许 weekly 由 profile 决定:主线可用 weekly,竞赛等固定配套只取正式发布版)
local aw; aw="$(profile_allow_weekly "${PROFILE}")"
local file; file="$(pick_toolkit_file "${snap}" "${aw}")" || true
local _arch_for_msg
_arch_for_msg="$(normalize_arch "$(uname -m)" 2>/dev/null || echo x86_64)"
[ -n "${file}" ] || { warn "快照目录 ${snap} 未找到可用的 ${_arch_for_msg} Toolkit 包(当前 profile=${PROFILE}$([ "${aw}" = true ] || echo ',仅取正式发布版、不取 weekly'))。"; print_cann_guidance; exit 1; }
# 4. 下载
local base="https://ascend.devcloud.huaweicloud.com/artifactory/cann-run-mirror/software/${CANN_CHANNEL}/${snap}"
local url="${base}/${file}" total
local inst_log="${LOG_DIR}/cann-install-${RUN_TS}.log" dl_log="${LOG_DIR}/cann-download-${RUN_TS}.log"
mkdir -p "${CANN_CACHE}" "${LOG_DIR}" || { warn "无法创建缓存/日志目录"; print_cann_guidance; exit 1; }
total="$(curl -fsSI "${url}" 2>/dev/null | tr -d '\r' | awk 'tolower($1)=="content-length:"{print $2}' | tail -1)" || true
# 缓存复用:上次已下完整包(按 content-length 校验大小一致)则跳过下载,避免装失败重跑又重下 ~1.5G。
# 拿不到 total(HEAD 无 content-length)时无法判定完整性,稳妥起见仍重新下载。
if [ -f "${CANN_CACHE}/${file}" ] && [ -n "${total}" ] \
&& [ "$(stat -c%s "${CANN_CACHE}/${file}" 2>/dev/null || echo 0)" = "${total}" ]; then
info "复用已下载的 CANN 安装包(${CANN_CACHE}/${file},$(( total / 1024 / 1024 ))M),跳过下载"
else
local attempt=1
while :; do
wget -q -O "${CANN_CACHE}/${file}" "${url}" </dev/null 2>>"${dl_log}" &
local dpid=$!
spin_on_pid_pct "${dpid}" "下载 CANN" "${total:-0}" "${CANN_CACHE}/${file}"
if wait "${dpid}" && { [ -z "${total}" ] || [ "$(stat -c%s "${CANN_CACHE}/${file}" 2>/dev/null || echo 0)" = "${total}" ]; }; then
break
fi
rm -f "${CANN_CACHE}/${file}" || true
if [ "${attempt}" -ge 2 ]; then
warn "CANN 下载失败,末尾日志:"; tail -n 20 "${dl_log}" >&2
echo " 请检查网络/代理/防火墙;持续失败可带日志到 https://gitcode.com/cann/hcomm/issues 反馈。" >&2
print_cann_guidance; exit 1
fi
warn "CANN 下载失败,3 秒后重试一次……"; attempt=2; sleep 3 || true
done
fi
# 5. 备份(覆盖场景)
if [ -e "${target}" ]; then
if ! mv "${target}" "${target}.bak-${RUN_TS}"; then warn "备份原 CANN 失败:${target}"; print_cann_guidance; exit 1; fi
info "已将原 CANN 备份为 ${target}.bak-${RUN_TS}(安装成功后可自行删除)"
fi
# 6. 赋可执行权限
if ! chmod +x "${CANN_CACHE}/${file}"; then warn "无法给安装包加执行权限:${CANN_CACHE}/${file}"; print_cann_guidance; exit 1; fi
# 7. 安装
if ! LANG=C.UTF-8 LC_ALL=C.UTF-8 "${CANN_CACHE}/${file}" --install --install-path="${target}" --quiet </dev/null >>"${inst_log}" 2>&1; then
warn "CANN 安装失败,末尾日志:"; tail -n 20 "${inst_log}" >&2
print_cann_guidance; exit 1
fi
# 8. 加载
source_cann_env "${target}/ascend-toolkit/set_env.sh"
# 9. 记账
CANN_RUN_FILE="${CANN_CACHE}/${file}"
}
# $1=安装目标 <target>
# 下载并安装 CANN ops 算子包(与 toolkit 安装策略一致,失败则退出)
download_and_install_cann_ops() {
local target="$1"
# 1. 幂等检查(ops 包已装则跳过)
if [ -d "${target}/ascend-toolkit/latest/ops" ] || [ -d "${target}/ops" ]; then
info "检测到 ops 包已安装:${target},跳过下载"
return 0
fi
# 2. 选择 ops 包
local snap; snap="$(pick_cann_snapshot "" || true)"
[ -n "${snap}" ] || { warn "无法确定 CANN 快照。"; print_cann_guidance; exit 1; }
local aw; aw="$(profile_allow_weekly "${PROFILE}")"
local file; file="$(pick_ops_file "${snap}" "${aw}")" || true
local _arch_for_msg
_arch_for_msg="$(normalize_arch "$(uname -m)" 2>/dev/null || echo x86_64)"
[ -n "${file}" ] || { warn "快照目录 ${snap} 未找到可用的 ${_arch_for_msg} ops 包(当前 profile=${PROFILE}$([ "${aw}" = true ] || echo ',仅取正式发布版、不取 weekly'))。"; print_cann_guidance; exit 1; }
# 3. 下载(缓存逻辑同 toolkit)
local base="https://ascend.devcloud.huaweicloud.com/artifactory/cann-run-mirror/software/${CANN_CHANNEL}/${snap}"
local url="${base}/${file}" total
local dl_log="${LOG_DIR}/ops-download-${RUN_TS}.log"
mkdir -p "${CANN_CACHE}" "${LOG_DIR}" || { warn "无法创建缓存/日志目录"; print_cann_guidance; exit 1; }
total="$(curl -fsSI "${url}" 2>/dev/null | tr -d '\r' | awk 'tolower($1)=="content-length:"{print $2}' | tail -1)" || true
if [ -f "${CANN_CACHE}/${file}" ] && [ -n "${total}" ] \
&& [ "$(stat -c%s "${CANN_CACHE}/${file}" 2>/dev/null || echo 0)" = "${total}" ]; then
info "复用已下载的 ops 包(${CANN_CACHE}/${file},$(( total / 1024 / 1024 ))M),跳过下载"
else
info "下载 ops 包..."
local attempt=1
while :; do
wget -q -O "${CANN_CACHE}/${file}" "${url}" </dev/null 2>>"${dl_log}" &
local dpid=$!
spin_on_pid_pct "${dpid}" "下载 ops" "${total:-0}" "${CANN_CACHE}/${file}"
if wait "${dpid}" && { [ -z "${total}" ] || [ "$(stat -c%s "${CANN_CACHE}/${file}" 2>/dev/null || echo 0)" = "${total}" ]; }; then
break
fi
rm -f "${CANN_CACHE}/${file}" || true
if [ "${attempt}" -ge 2 ]; then
warn "ops 包下载失败,末尾日志:"; tail -n 20 "${dl_log}" >&2
echo " 请检查网络/代理/防火墙;持续失败可带日志到 https://gitcode.com/cann/hcomm/issues 反馈。" >&2
print_cann_guidance; exit 1
fi
warn "ops 包下载失败,3 秒后重试一次……"; attempt=2; sleep 3 || true
done
fi
# 4. 安装(安装到同一 target 目录)
local inst_log="${LOG_DIR}/ops-install-${RUN_TS}.log"
if ! chmod +x "${CANN_CACHE}/${file}"; then warn "无法给安装包加执行权限:${CANN_CACHE}/${file}"; print_cann_guidance; exit 1; fi
if ! LANG=C.UTF-8 LC_ALL=C.UTF-8 "${CANN_CACHE}/${file}" --install --install-path="${target}" --quiet </dev/null >>"${inst_log}" 2>&1; then
warn "ops 包安装失败,末尾日志:"; tail -n 20 "${inst_log}" >&2
print_cann_guidance; exit 1
fi
OPS_RUN_FILE="${CANN_CACHE}/${file}"
info "ops 包安装完成:${file}"
return 0
}
# 第 5 步:复用第 1 步写好的 CANN_INSTALLED/CANN_SETENV,按四情况处理。$1=build.md 快照(可空)
resolve_cann() {
local snapshot="$1"
if [ -n "${CANN_INSTALLED}" ]; then
[ -n "${CANN_SETENV}" ] && source_cann_env "${CANN_SETENV}"
local st=0; cann_match_status "${snapshot}" || st=$?
if [ "${st}" -eq 0 ]; then
info "使用已装 CANN:${CANN_INSTALLED}"
elif [ "${st}" -eq 2 ]; then
warn "本 profile 未固定 CANN 版本,无法校验配套性;若编译报类型未声明(如 ThreadType)请换配套 CANN。"
info "使用已装 CANN:${CANN_INSTALLED}"
else
warn "已装 CANN(${CANN_INSTALLED})与配套快照(${snapshot})不一致,可能不配套。"
case "${CANN_MODE}" in
keep)
warn "已按 --offline 保留现有 CANN;请自行确认其与所需配套版本一致。" ;;
download)
resolve_cann_target "${CANN_INSTALLED}"
download_and_install_cann "${CANN_TARGET}" "${snapshot}"
download_and_install_cann_ops "${CANN_TARGET}" ;;
auto)
resolve_cann_target "${CANN_INSTALLED}"
if { [ -t 0 ] || [ -e /dev/tty ]; } && prompt_yn "下载配套版并覆盖升级 ${CANN_TARGET} 的现有 CANN? 15 秒内无响应视为保留。[y/N]" N; then
download_and_install_cann "${CANN_TARGET}" "${snapshot}"
download_and_install_cann_ops "${CANN_TARGET}"
else
warn "保留现有 CANN(未覆盖);如需覆盖请加 --reinstall-cann。"
fi ;;
esac
fi
else
info "下载 CANN ${snapshot} 并安装到 ${CANN_TARGET}"
download_and_install_cann "${CANN_TARGET}" "${snapshot}"
download_and_install_cann_ops "${CANN_TARGET}"
fi
# ASCEND_HOME_PATH 兜底:从 ASCEND_TOOLKIT_HOME 或探测到的 CANN 目录推导
if [ -z "${ASCEND_HOME_PATH:-}" ]; then
if [ -n "${ASCEND_TOOLKIT_HOME:-}" ]; then
export ASCEND_HOME_PATH="${ASCEND_TOOLKIT_HOME}"
else
error "CANN 已识别但未导出 ASCEND_HOME_PATH。请先 source <CANN>/ascend-toolkit/set_env.sh 后重试。"
fi
fi
}
# 清理 third_party 下载缓存:build.sh 的第三方 cmake 按"文件是否存在"判定是否重下,
# 一次下载失败留下的半包会被下次直接复用导致持续失败。编译失败时清掉下载归档,让重跑重新下载。
clean_thirdparty_dl_cache() {
local tp="${CHECKER_PATH}/third_party"
[ -d "${tp}" ] || return 0
rm -f "${tp}"/*.tar.gz "${tp}"/*.tgz "${tp}"/*.zip 2>/dev/null || true
info "已清理 ${tp} 下的第三方下载归档(重跑本脚本会重新下载,规避半包缓存)。"
}
# 编译 HCCL-VM 工具(对应 README §3.1 的 bash ./build.sh --full)
build_hccl_vm() {
info "编译 HCCL-VM 工具..."
if [ ! -d "${CHECKER_PATH}" ]; then
error "未找到 checker 源码目录: ${CHECKER_PATH}。checker 存在于 hcomm 的竞赛/主线分支,不在发布 tag;若用 --hcomm-ref 指向了发布 tag,请改回 competition/campus-2026 或 master,或用 --checker-path 指定。"
fi
if [ ! -f "${CHECKER_PATH}/build.sh" ]; then
error "未找到 ${CHECKER_PATH}/build.sh,无法编译。请确认 checker 源码完整。"
fi
export HCOMM_CODE_HOME="${HCOMM_PATH}"
export HCCL_CODE_HOME="${HCCL_PATH}"
cd "${CHECKER_PATH}" || error "无法进入 checker 目录: ${CHECKER_PATH}"
mkdir -p "${LOG_DIR}"
local build_log="${LOG_DIR}/build-${RUN_TS}.log"
info "编译日志: ${build_log}"
local rc=0
if [ "${VERBOSE}" = true ]; then
if bash build.sh --package-path "${ASCEND_HOME_PATH}" --hcomm-path "${HCOMM_CODE_HOME}" --hccl-path "${HCCL_CODE_HOME}" --pkg --full 2>&1 | tee "${build_log}"; then
rc=0
else
rc=${PIPESTATUS[0]}
fi
else
info "开始编译,预计数分钟。实时日志: tail -f ${build_log}"
bash build.sh --package-path "${ASCEND_HOME_PATH}" --hcomm-path "${HCOMM_CODE_HOME}" --hccl-path "${HCCL_CODE_HOME}" --pkg --full > "${build_log}" 2>&1 &
local build_pid=$!
spin_on_pid "${build_pid}" "编译中"
wait "${build_pid}" || rc=$?
fi
if [ "${rc}" -ne 0 ]; then
warn "编译失败(日志: ${build_log})。常见原因:"
tail -n 20 "${build_log}" >&2
echo " ① 第三方库下载失败(网络/SSL):已清理下载缓存,重跑本脚本即可重新下载;" >&2
echo " ② gcc 超出 build.md 前置依赖范围(7.3.0–13.3.x,管宿主+交叉):Ubuntu 25.04/26.04 的 gcc14/15 会使 yaml-cpp 等第三方编译失败,请在满足该范围的环境(如 22.04/24.04)运行;" >&2
echo " ③ CANN 与 hccl 不配套(device 侧报 ThreadType 未声明等):换配套 CANN,见 https://gitcode.com/cann/release-management/ ;" >&2
echo " ④ 架构适配问题(aarch64):确认 CANN 安装包与当前架构一致(linux-aarch64),交叉编译工具链已正确安装(x86_64 宿主需 gcc-aarch64-linux-gnu);" >&2
echo " 仍无法解决可带完整日志到 https://gitcode.com/cann/hcomm/issues 反馈。" >&2
clean_thirdparty_dl_cache
error "编译未通过(完整日志: ${build_log})。"
fi
# 产物落地校验:build.sh 退 0 不代表二进制一定生成
local bin="${CHECKER_PATH}/hccl_vm_install/bin/hccl-vm"
if [ ! -x "${bin}" ]; then
error "编译进程已结束但未找到可执行产物: ${bin}。请查看日志 ${build_log} 后重试。"
fi
}
# 执行 build_pkg.sh 获得aicpu在device侧的符号(使用 hcomm 仓库自带的 build_pkg.sh)
# build_pkg.sh 位于 CHECKER_PATH(即 hcomm/test/hccl_vm/)下,步骤 4 已 clone 完成
# 需要以下环境变量:
# ASCEND_HOME_PATH - CANN 安装目录(source CANN set_env.sh 后导出)
# HCCL_CODE_HOME - hccl 源码目录(build_hccl_vm 已导出)
# HCOMM_CODE_HOME - hcomm 源码目录(build_hccl_vm 已导出)
# 通过 --tool_path 传入 checker 安装路径(即 CHECKER_PATH)
build_hcomm_hccl_pkg() {
info "执行 build_pkg.sh 获得 aicpu 运行在 device 侧的符号..."
local pkg_script="${CHECKER_PATH}/build_pkg.sh"
local pkg_log="${LOG_DIR}/build-pkg-${RUN_TS}.log"
# build_pkg.sh 随 hcomm 仓库一起下发(步骤 4 clone),直接使用本地副本
if [ ! -f "${pkg_script}" ]; then
error "未找到 build_pkg.sh: ${pkg_script}。请确认 hcomm 源码(分支 ${HCOMM_REF})中包含 test/hccl_vm/build_pkg.sh。"
fi
# 执行 build_pkg.sh:编译 hccl + hcomm 子包并安装到 CANN 目录
info "编译日志: ${pkg_log}"
local rc=0
if [ "${VERBOSE}" = true ]; then
bash "${pkg_script}" --tool_path "${CHECKER_PATH}" 2>&1 | tee "${pkg_log}" || rc=${PIPESTATUS[0]}
else
info "开始执行 build_pkg.sh。实时日志: tail -f ${pkg_log}"
bash "${pkg_script}" --tool_path "${CHECKER_PATH}" > "${pkg_log}" 2>&1 &
local pkg_pid=$!
spin_on_pid "${pkg_pid}" "编译子包中"
wait "${pkg_pid}" || rc=$?
fi
if [ "${rc}" -ne 0 ]; then
warn "执行 build_pkg.sh 失败,末尾日志:"
tail -n 20 "${pkg_log}" >&2
echo " 完整日志: ${pkg_log}" >&2
echo " 若为第三方库下载失败(网络/SSL):已清理下载缓存,重跑即可重新下载;若为 gcc 超出 build.md 前置依赖范围(7.3.0–13.3.x)请在满足该范围的环境(如 22.04/24.04)运行。" >&2
echo " 也可手动执行: bash ${pkg_script} --tool_path ${CHECKER_PATH}" >&2
clean_thirdparty_dl_cache
error "执行 build_pkg.sh 失败,请检查日志后重试。"
fi
info "执行 build_pkg.sh 完成!"
}
# 按 README §4.7 准备 /etc/hccl_rootinfo.json:HCCL 初始化通信域时从该文件读 topo.json 路径,
# 缺它会报 GetTopoFilePath failed。topo_file_path 指向本安装的 data/topo.json(首次 start 时生成)。
# 用 sudo 写入并覆盖更新(幂等;换 workspace 重跑也会刷新为新的 topo 路径);无 root/sudo 才 WARN。
setup_hccl_rootinfo() {
local topo="${CHECKER_PATH}/hccl_vm_install/data/topo.json"
local rootinfo="/etc/hccl_rootinfo.json"
if printf '{\n "version": "2.0",\n "topo_file_path": "%s"\n}\n' "${topo}" \
| ${SUDO_PREFIX} tee "${rootinfo}" > /dev/null 2>&1; then
info "已配置 ${rootinfo}(topo_file_path=${topo})"
else
warn "无法写入 ${rootinfo}(需 root/sudo)。运行 hccl_test 用例前请以 root 配置,内容见 README §4.7:"
echo " {\"version\": \"2.0\", \"topo_file_path\": \"${topo}\"}" >&2
fi
}
# 第 9 步:编译 hccl_test(增值步骤,失败只 WARN 不阻断)。
build_hccl_test() {
if [ "${SKIP_HCCL_TEST}" = true ]; then
HCCL_TEST_RESULT="skipped"
info "已按 --skip-hccl-test 跳过 hccl_test 编译"
return 0
fi
local cann_root="${ASCEND_HOME_PATH%/ascend-toolkit*}" src=""
local c
for c in "${ASCEND_HOME_PATH}/tools/hccl_test" "${cann_root}/tools/hccl_test" "${cann_root}/ascend-toolkit/latest/tools/hccl_test"; do
[ -d "${c}" ] && { src="${c}"; break; }
done
if [ -z "${src}" ]; then
HCCL_TEST_RESULT="failed:源码缺失"
warn "未在 CANN 下找到 hccl_test 源码(该 CANN 可能不含 tools 目录),跳过编译。"
echo " 可手动到 ${cann_root}/tools/hccl_test 按 README §4.2.1 编译,或用 --ascend-path 指向含该目录的 CANN。" >&2
return 0
fi
# 原地编译:默认 CANN 在 workspace 下、当前用户可写,直接在 tools/hccl_test 编,产物落 ${src}/bin。
# 复用别人装的外部 CANN(--ascend-path 指向非本人目录)时源目录不可写,无法原地编:告警后跳过。
local work="${src}"
if [ ! -w "${src}" ]; then
HCCL_TEST_RESULT="failed:源目录不可写"
warn "hccl_test 源目录不可写(复用的 CANN 不属当前用户):${src}"
echo " hccl_test 需原地编译。请改用自装的 CANN(默认装入 workspace),或对该目录赋写权限后重试。" >&2
return 0
fi
chmod -R u+w "${work}" 2>/dev/null || true
[ -n "${CANN_SETENV}" ] && source_cann_env "${CANN_SETENV}"
# ── Makefile 幂等保护 ──────────────────────────────────────────────
# 首次运行:把已污染的 Makefile 清理为干净状态(-lmpi_cxxcxx / -lmpicxxcxx 等所有历史残留均消除),
# 保存为 Makefile.orig;重跑:从干净 Makefile.orig 恢复(任何旧污染都被覆盖)。
# 清理链(6 步,覆盖所有已知污染模式):
# 1) -lmpi_cxxcxx → -lmpi(消除级联替换产生的畸形标志)
# 2) -lmpi_cxx → -lmpi(消除历史 -lmpi_cxx 残留)
# 3) -lmpicxxcxx → -lmpi(消除级联替换产生的畸形标志)
# 4) -lmpicxx → -lmpi(消除历史 -lmpicxx 残留)
# 5) -lmpi -lmpi → -lmpi(消除重复追加,跑两次以覆盖 2+、3+ 连续重复)
if [ ! -f "${work}/Makefile.orig" ]; then
{ sed -e 's/-lmpi_cxxcxx/-lmpi/g' \
-e 's/-lmpi_cxx/-lmpi/g' \
-e 's/-lmpicxxcxx/-lmpi/g' \
-e 's/-lmpicxx/-lmpi/g' \
-e 's/ -lmpi -lmpi/ -lmpi/g' \
-e 's/ -lmpi -lmpi/ -lmpi/g' "${work}/Makefile" > "${work}/Makefile.cl" \
&& mv -- "${work}/Makefile.cl" "${work}/Makefile"; } 2>/dev/null || true
cp -- "${work}/Makefile" "${work}/Makefile.orig"
else
cp -- "${work}/Makefile.orig" "${work}/Makefile"
fi
mkdir -p "${LOG_DIR}"
local ht_log="${LOG_DIR}/hccl_test-build-${RUN_TS}.log" rc=0
local arch
arch="$(normalize_arch "$(uname -m)" 2>/dev/null || echo x86_64)"
if [ "${arch}" = "x86_64" ]; then
# ── x86_64:OpenMPI ──────────────────────────────────────────────
# Ubuntu:/usr/lib/<triplet>/openmpi/{lib,include}
# HCE/openEuler:/usr/lib64/openmpi-<arch>/{lib,include} 或 /usr/lib/<triplet>/openmpi/
# 优先用 ompi_info 探测真实路径(所有发行版通用),不可用时回退到已知候选路径列表。
local mpi_home="" _mpi_lib _mpi_inc
if command -v ompi_info &>/dev/null; then
_mpi_lib="$(ompi_info --path libs 2>/dev/null | grep -v '^libs:' | head -1 | tr -d ' ')" || true
_mpi_inc="$(ompi_info --path headers 2>/dev/null | grep -v '^headers:' | head -1 | tr -d ' ')" || true
if [ -n "${_mpi_lib}" ] && [ -n "${_mpi_inc}" ]; then
mpi_home="${_mpi_lib%/lib}"
[ -d "${mpi_home}/lib" ] && [ -d "${mpi_home}/include" ] || mpi_home=""
fi
fi
if [ -z "${mpi_home}" ]; then
local _probe_path
for _probe_path in \
"/usr/lib/x86_64-linux-gnu/openmpi" \
"/usr/lib64/openmpi" \
"/usr/lib/openmpi"; do
if [ -d "${_probe_path}/lib" ] && [ -d "${_probe_path}/include" ]; then
mpi_home="${_probe_path}"
break
fi
done
fi
if [ -z "${mpi_home}" ]; then
HCCL_TEST_RESULT="failed:找不到 OpenMPI 安装路径"
warn "找不到 OpenMPI 安装路径(ompi_info 不可用,且已知候选路径均不存在)。"
echo " 请手动安装 openmpi 或确认安装路径后用 MPI_HOME 环境变量指定。" >&2
return 0
fi
# 补 -lmpi_cxx
perl -pi -e 's/(?<!\w)(-lmpi)(?!_cxx)/${1} -lmpi_cxx/g' "${work}/Makefile" 2>/dev/null || true
( cd "${work}" && MPI_HOME="${mpi_home}" make ASCEND_DIR="${ASCEND_HOME_PATH}" ) > "${ht_log}" 2>&1 &
else
# ── aarch64: mpich ──────────────────────────────────────────────
local mpi_home="/usr/local/mpich"
if [ ! -d "${mpi_home}" ]; then
HCCL_TEST_RESULT="failed:找不到 mpich 安装路径"
warn "找不到 mpich 安装路径:${mpi_home} 不存在。"
echo " 请手动安装 mpich 或确认安装路径后用 MPI_HOME 环境变量指定。" >&2
return 0
fi
# 补 -lmpicxx
perl -pi -e 's/(?<!\w)(-lmpi)(?!cxx)/${1} -lmpicxx/g' "${work}/Makefile" 2>/dev/null || true
local ld_path="/usr/lib/mpich/lib/:${ASCEND_HOME_PATH}/lib64/:${ASCEND_HOME_PATH}/aarch64-linux/devlib"
( cd "${work}" && export LD_LIBRARY_PATH="${ld_path}:${LD_LIBRARY_PATH:-}" && MPI_HOME="${mpi_home}" make ASCEND_DIR="${ASCEND_HOME_PATH}" ) > "${ht_log}" 2>&1 &
fi
local bpid=$!
spin_on_pid "${bpid}" "编译 hccl_test 中"
wait "${bpid}" || rc=$?
if [ "${rc}" -ne 0 ]; then
HCCL_TEST_RESULT="failed:编译失败,日志 ${ht_log}"
warn "hccl_test 编译失败,末尾日志(不影响 checker,已跳过):"
tail -n 20 "${ht_log}" >&2
return 0
fi
HCCL_TEST_RESULT="built"
HCCL_TEST_BIN_DIR="${work}/bin"
info "hccl_test 已编译:${HCCL_TEST_BIN_DIR}/"
setup_hccl_rootinfo
}
# 结束进程及其所有子孙:apt/编译经包装子 shell 启动({ apt-get …; } / bash build.sh / ( … make )),
# 真正占 dpkg 锁、耗 CPU 的是孙子进程(dpkg/make/cc1plus),只杀直接子进程杀不到。优先用 pgrep 递归;
# 无 pgrep 时退化为只结束直接子进程。
kill_process_tree() {
local pid="$1" child
if command -v pgrep >/dev/null 2>&1; then
for child in $(pgrep -P "${pid}" 2>/dev/null); do
kill_process_tree "${child}"
done
fi
kill "${pid}" 2>/dev/null || true
}
# 中断(Ctrl-C/kill)时结束在途后台子进程树并清理正在克隆的半成品目录。
# 收到仅发给本脚本的 SIGTERM(如 kill <pid>)时,前台进程组不会自动收到信号,
# 需主动结束 apt/clone/编译/下载子进程(连同其子孙),避免其成为孤儿继续占用 dpkg 锁。
cleanup_on_interrupt() {
[ -n "${CURRENT_BG_PID:-}" ] && kill_process_tree "${CURRENT_BG_PID}"
if [ -n "${CURRENT_CLONE_DIR:-}" ] && [ -d "${CURRENT_CLONE_DIR}" ]; then
rm -rf "${CURRENT_CLONE_DIR}"
fi
echo "" >&2
warn "已中断。已完整下载的源码会保留,重跑本脚本可自动复用。"
exit 130
}
# 临时放开 git safe.directory(不永久改客户 ~/.gitconfig):
# WSL /mnt 等挂载盘上,git 2.35.2+ 会因仓库目录属主与运行用户不符判为“属主可疑”,
# 导致子包 build_pkg 里 FetchContent 拉 cann-cmake 的 git checkout 失败(Failed to checkout tag)。
# git 出于安全只认 system/global 配置文件里的 safe.directory(不认 -c/内联 env),故用
# GIT_CONFIG_GLOBAL 指向一个临时文件:置 safe.directory=* 且 include 用户原 global 配置。
# 仅本进程及其子进程(含 build_pkg)可见,脚本退出即失效,不落盘到客户 ~/.gitconfig。
GIT_SAFE_TMP=""
setup_git_safe_directory() {
local tmp; tmp="$(mktemp 2>/dev/null)" || { warn "无法创建临时 git 配置,挂载盘上编译可能因 git 属主可疑失败。"; return 0; }
{
printf '[safe]\n\tdirectory = *\n'
[ -f "${HOME}/.gitconfig" ] && printf '[include]\n\tpath = %s\n' "${HOME}/.gitconfig"
} > "${tmp}" 2>/dev/null
export GIT_CONFIG_GLOBAL="${tmp}"
GIT_SAFE_TMP="${tmp}"
# 内部实现细节,仅 --verbose 时输出,正常运行对用户不可见
[ "${VERBOSE}" = true ] && info "构建环境已临时配置 git safe.directory(仅本进程生效,不修改 ${HOME}/.gitconfig)"
return 0
}
# 提前获取并保活 sudo 授权:全程只问一次密码、结束即注销,避免 curl|bash 长任务里反复提示。
# root → 前缀空;非 root 无 sudo → 前缀空并告警;非 root 有 sudo → 前缀 sudo,必要时经 /dev/tty 认证。
setup_sudo() {
if [ "$(id -u)" -eq 0 ]; then
SUDO_PREFIX=""
return 0
fi
if ! command -v sudo >/dev/null 2>&1; then
SUDO_PREFIX=""
warn "当前非 root 且未安装 sudo:安装依赖、写系统配置等需要管理员权限的步骤可能失败;建议改用 root 运行。"
return 0
fi
SUDO_PREFIX="sudo"
# 已是免密或近期已认证 → 直接保活
if sudo -n true 2>/dev/null; then
start_sudo_keepalive
return 0
fi
# 需要密码:仅当能真正打开控制终端 /dev/tty 时才可输入(-e 判断不可靠,故实测打开)
if { : <>/dev/tty; } 2>/dev/null; then
local sudo_prompt
sudo_prompt="$(printf '%b[INPUT]%b 请输入 %s 的密码(sudo): ' "${YELLOW}" "${NC}" "$(id -un)")"
if sudo -v -p "${sudo_prompt}"; then # sudo 自带最多 3 次重试,从 /dev/tty 读密码,支持 curl|bash
start_sudo_keepalive
return 0
fi
warn "sudo 认证失败(密码错误或不在 sudoers)。"
else
warn "需要 sudo 但无可用控制终端输入密码(完全非交互环境,如 cron/nohup)。"
fi
# 拿不到 sudo 授权:前缀置空,让下游各步据此走明确报错/引导,而非静默用无效 sudo(后台任务里密码提示会被
# 进度动画盖住导致假死)。装依赖缺权限时 install_deps 会给出清晰指引后退出。
SUDO_PREFIX=""
echo " 可选:① 改用 root 运行;② 下载脚本后 sudo 运行:sudo bash hccl_vm_installer [选项];" >&2
echo " ③ curl 前先在同终端执行 sudo -v 预认证;④ 让管理员授予 sudo 权限。" >&2
return 0
}
# sudo 授权保活:每 50 秒刷新一次时间戳;父进程一旦退出自动结束,不留孤儿。
start_sudo_keepalive() {
( while true; do
sudo -n true 2>/dev/null || exit
sleep 50
kill -0 "$$" 2>/dev/null || exit
done ) &
SUDO_KEEPALIVE_PID=$!
}
# 退出时(trap EXIT):杀掉保活循环并注销 sudo 授权,等价“内存里的授权,脚本结束即销毁”。
cleanup_sudo() {
[ -n "${SUDO_KEEPALIVE_PID}" ] && kill "${SUDO_KEEPALIVE_PID}" 2>/dev/null || true
command -v sudo >/dev/null 2>&1 && sudo -k 2>/dev/null || true
}
# 退出时统一清理(trap EXIT):注销 sudo + 删除 setup_git_safe_directory 建的临时 git 配置文件,不留残留。
cleanup_on_exit() {
cleanup_sudo
[ -n "${GIT_SAFE_TMP:-}" ] && rm -f "${GIT_SAFE_TMP}" 2>/dev/null || true
}
# 清理上次被 kill 的半成品,保证失败后重跑干净:不完整的 clone 目录 + 被中断的 dpkg。
# 安全约束:只清理脚本默认 clone 位置(${WORKSPACE}/hcomm|hccl)且用户未用 --hcomm-path/--hccl-path 覆盖;
# 再要求“有 .git 但仓库不完整(疑似中断的 clone)或空目录”,避免误删用户自备的非 git 源码。
cleanup_residue() {
local d
for d in "${WORKSPACE}/hcomm" "${WORKSPACE}/hccl"; do
{ [ "${d}" = "${HCOMM_PATH}" ] || [ "${d}" = "${HCCL_PATH}" ]; } || continue # 用户覆盖了路径 → 不碰
[ -d "${d}" ] || continue
git -C "${d}" rev-parse --is-inside-work-tree >/dev/null 2>&1 && continue # 完整仓库交给 clone_repo 复用
if [ -d "${d}/.git" ] || [ -z "$(ls -A "${d}" 2>/dev/null)" ]; then
warn "清理疑似上次中断的半成品目录:${d}"
rm -rf "${d}" 2>/dev/null || true
fi
done
# dpkg 曾被中断时自愈(需要提权;无权限则静默跳过)
if command -v dpkg >/dev/null 2>&1; then
${SUDO_PREFIX} dpkg --configure -a >/dev/null 2>&1 || true
fi
}
# 主函数
main() {
trap cleanup_on_interrupt INT TERM
trap cleanup_on_exit EXIT
parse_args "$@"
apply_defaults
setup_git_safe_directory
banner
notice_custom_refs
step 1 "${STEP_TOTAL}" "检查运行环境"
check_env
precheck_cann_path # root 安装 CANN 的父目录 755 预检:不满足开头即报错,避免第 5 步才失败
setup_sudo # 提前获取并保活 sudo 授权,供装依赖/build_pkg/写 rootinfo 全程复用
cleanup_residue # 清上次被中断的半成品,保证重跑干净
step 2 "${STEP_TOTAL}" "检查磁盘空间"
check_disk
[ "${WILL_DL}" = true ] && check_cann_disk "${CANN_TARGET}" "${CANN_CACHE}"
step 3 "${STEP_TOTAL}" "检查/安装第三方依赖"
install_deps
step 4 "${STEP_TOTAL}" "拉取 hcomm 源码(${HCOMM_REF})"
clone_repo "hcomm" "https://gitcode.com/cann/hcomm.git" "${HCOMM_REF}" "${HCOMM_PATH}"
local snapshot=""
snapshot="$(extract_cann_snapshot "${HCOMM_PATH}/docs/zh/build/build.md" || true)"
step 5 "${STEP_TOTAL}" "检测/下载 CANN"
resolve_cann "${snapshot}"
step 6 "${STEP_TOTAL}" "拉取 hccl 源码(${HCCL_REF})"
clone_repo "hccl" "https://gitcode.com/cann/hccl.git" "${HCCL_REF}" "${HCCL_PATH}"
step 7 "${STEP_TOTAL}" "编译 HCCL-VM"
build_hccl_vm
step 8 "${STEP_TOTAL}" "解析 aicpu_hcxx.tar.gz 得到 device 侧符号"
build_hcomm_hccl_pkg
step 9 "${STEP_TOTAL}" "编译 hccl_test"
build_hccl_test
success_summary
}
# 仅当脚本被直接执行时运行主函数;被 source(单测)时不执行
if [[ "${BASH_SOURCE[0]:-$0}" == "${0}" ]]; then
main "$@"
fi