从 Miniconda 迁移到 Miniforge3 全记录:安装、配置、环境迁移与排错实录

从 Miniconda 迁移到 Miniforge3 全记录:安装、配置、环境迁移与排错实录

之前公司团队下的一台共享服务器还在跑 Miniconda,直到法务发来通知,提醒 Anaconda 的 defaults 频道在 200 人以上的组织内使用需要商业授权,我才认真对待这件事。Anaconda 在 2020 年改了授权条款,defaults 频道涉及商用合规,而 Miniforge 由 conda-forge 社区官方背书,默认只用 conda-forge 频道,完全绕开这个问题。

这台服务器上跑着十几个项目的虚拟环境,迁移不能出岔子。整个过程断断续续做了两天,踩了不少坑------频道配置的合并规则、zsh 与 bash 的行为差异、base 保留名报错、glibc 版本限制------最后写成一份验证脚本跑通 18 项检查。本文是完整记录,所有命令在 Linux 环境下验证过,安装路径分别是:

  • 老 Miniconda:/usr/local/app/miniconda3
  • 新 Miniforge3:/usr/local/app/miniforge3

一、先理清三者关系

动手前值得花两分钟弄明白要装的是什么:

  • Anaconda:完整发行版,带 GUI 和预装的科学计算包,体积大,defaults 频道要商业授权
  • Miniconda:Anaconda 的精简版,只有 conda 包管理器加 Python,但依然默认连 defaults 频道------精简不等于免授权
  • Miniforge :社区维护,conda-forge 官方出品,默认只用 conda-forge 频道,channel_priority 出厂就是 strict
    另外提一句,2024 年 7 月起 Mambaforge 已被官方弃用,统一并入 Miniforge3,不用再纠结选哪个。

二、安装 Miniforge3

下载命令解析

官方给的下载命令看起来像一行魔法:

bash 复制代码
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
bash Miniforge3-Linux-x86_64.sh

拆开看其实很简单。$(uname) 输出操作系统名(Linux 上是 Linux),$(uname -m) 输出 CPU 架构,两个变量拼接后自动得到匹配当前机器的安装包:

机器 $(uname) $(uname -m) 实际下载文件
Linux x86_64 服务器 Linux x86_64 Miniforge3-Linux-x86_64.sh
Apple Silicon Darwin arm64 Miniforge3-Darwin-arm64.sh
AWS Graviton / ARM 服务器 Linux aarch64 Miniforge3-Linux-aarch64.sh
curl 的两个参数都不能省:-L 跟随重定向(GitHub releases 会 302 跳到 CDN,不加会下到 HTML 页面),-O 用远端文件名保存(大写 O,小写 -o 需要自己指定文件名)。

两种安装方式

交互模式就是直接跑脚本,一路回车加输 yes,提示安装路径时填入自定义目录即可。但要把 Miniforge 装到 /usr/local/app/miniforge3,更推荐批处理模式:

bash 复制代码
sudo bash Miniforge3-Linux-x86_64.sh -b -p /usr/local/app/miniforge3

-b(batch)跳过所有交互问题,自动接受协议;-p(prefix)指定安装目录。注意批处理模式不会自动执行 conda init,装完要手动初始化(见后文第五节)。CI/CD 流水线和 Docker 镜像构建也推荐这种方式。

/usr/local/app 的权限问题

root 装完之后,普通用户默认只能读不能写,conda install 会直接报权限错误。多用户共享服务器需要额外处理,我们用的是组权限方案:

bash 复制代码
sudo groupadd conda-users
sudo usermod -aG conda-users sietium    # 加用户进组,重新登录生效
sudo chgrp -R conda-users /usr/local/app/miniforge3
sudo chmod -R g+rwX /usr/local/app/miniforge3
sudo find /usr/local/app/miniforge3 -type d -exec chmod g+s {} \;

更省事的做法是系统目录保持只读,把虚拟环境和包缓存重定向到用户家目录------这个放在后文配置一节讲。

三、频道配置

如果 .condarc 里还是 defaults 时代的写法,需要整体替换。我最初配了一份典型的「清华镜像加速」配置,看起来很完整,实际上埋着雷:

yaml 复制代码
channel_alias: https://mirrors.tuna.tsinghua.edu.cn/anaconda
channel_priority: strict
channels:
  - conda-forge
custom_channels:
  conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
default_channels:
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free

三个问题:清华源 2026 年 8 月起停止同步 pkgs/* 系列,default_channels 指向的地址直接 404;channel_alias 会劫持所有短名频道,副作用大;default_channels 本质上就是 defaults 频道的镜像展开,违背 Miniforge「仅 conda-forge」的设计初衷。

修正后的最小化配置:

yaml 复制代码
channel_priority: strict
channels:
  - conda-forge
  - nodefaults
show_channel_urls: true
custom_channels:
  conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud

nodefaults 是显式兜底,防止某些环境 yml 或命令参数里冒出 defaults 时被意外解析。清华源只是停了 pkgs 系列,cloud/conda-forge 仍在正常同步,所以镜像映射可以保留。

由于是多人共用的服务器,我选择把这份配置放到系统级路径,统一所有人的行为:

bash 复制代码
sudo tee /usr/local/app/miniforge3/.condarc << 'EOF'
channel_priority: strict
channels:
  - conda-forge
  - nodefaults
show_channel_urls: true
custom_channels:
  conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
envs_dirs:
  - /usr/local/app/miniforge3/envs
  - ~/.conda/envs
pkgs_dirs:
  - /usr/local/app/miniforge3/pkgs
  - ~/.conda/pkgs
EOF

四、配置文件的合并规则:最容易翻车的地方

conda 配置文件的优先级链是:

复制代码
命令行参数 --config > 环境变量 CONDARC > 用户级 ~/.condarc > 系统级安装目录/.condarc

真正的坑在于合并规则:列表合并,标量覆盖 。系统级写 channels: [conda-forge],用户级写 channels: [defaults],最终生效的是 [defaults, conda-forge]------不是覆盖关系。这意味着哪怕系统级配置写得再干净,任何用户在自己家目录里加一行 defaults,就又把授权风险拉回来了。

排查配置来源用这条命令:

bash 复制代码
conda config --show-sources

输出按来源分组,凡是没出现在任何文件里的字段,全部来自 conda 内置默认值(源码硬编码),不必也无法清除。

读懂 conda config --show

配置完成后跑一次 conda config --show 做体检。输出很长,我按实际经验分成三类:

第一类:核心项,必须正确。 channels 应该是 [conda-forge, nodefaults],channel_priority 应该是 strict,solver 应该是 libmamba(Miniforge 默认,性能远胜 classic),root_prefix 指向 miniforge3。这几项对了,大方向就没问题。

第二类:内置默认值,看着碍眼但不用动。 default_channels 里那串 repo.anaconda.com/pkgs/main、custom_multichannels.defaults 里的展开定义,都是 conda 出厂硬编码------因为 channels 里有 nodefaults,这串 URL 根本不会被请求。channel_alias、repodata_use_zst、sat_solver 等也同理,不值得花时间去「清理」。

第三类:按需微调的几项。

default_python: 3.14------新建环境不指定版本时用的默认值。3.14 已经稳定,但不少科学计算包的 cp314 wheel 还没跟上,如果团队主流是 3.11/3.12,建议 conda config --set default_python 3.11。

envs_dirs 的第一项是系统目录。conda 的规则是新建环境写到第一个可写目录,所以 root 建的环境落在 /usr/local/app/miniforge3/envs,普通用户自动落到 ~/.conda/envs。同一台机器上环境散落两处,conda env list 都能看到但目录层级混乱。如果磁盘紧张想让所有人共享一份包缓存,给 pkgs_dirs 的系统目录配好组权限即可。

croot 指向 /usr/local/app/miniforge3/conda-bld,普通用户跑 conda build 会往系统目录写,直接 Permission denied。需要构建的用户在自己 ~/.condarc 里加 croot: ~/.conda/conda-bld。

auto_activate: True 决定开终端是否自动进 base。不喜欢提示符被 (base) 占据的话,conda config --set auto_activate_base false。

五、迁移虚拟环境

先看整体流程:
#mermaid-svg-FczagLZeBQc2Bw1Z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FczagLZeBQc2Bw1Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FczagLZeBQc2Bw1Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FczagLZeBQc2Bw1Z .error-icon{fill:#552222;}#mermaid-svg-FczagLZeBQc2Bw1Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FczagLZeBQc2Bw1Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FczagLZeBQc2Bw1Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FczagLZeBQc2Bw1Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FczagLZeBQc2Bw1Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FczagLZeBQc2Bw1Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FczagLZeBQc2Bw1Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FczagLZeBQc2Bw1Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FczagLZeBQc2Bw1Z .marker.cross{stroke:#333333;}#mermaid-svg-FczagLZeBQc2Bw1Z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FczagLZeBQc2Bw1Z p{margin:0;}#mermaid-svg-FczagLZeBQc2Bw1Z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FczagLZeBQc2Bw1Z .cluster-label text{fill:#333;}#mermaid-svg-FczagLZeBQc2Bw1Z .cluster-label span{color:#333;}#mermaid-svg-FczagLZeBQc2Bw1Z .cluster-label span p{background-color:transparent;}#mermaid-svg-FczagLZeBQc2Bw1Z .label text,#mermaid-svg-FczagLZeBQc2Bw1Z span{fill:#333;color:#333;}#mermaid-svg-FczagLZeBQc2Bw1Z .node rect,#mermaid-svg-FczagLZeBQc2Bw1Z .node circle,#mermaid-svg-FczagLZeBQc2Bw1Z .node ellipse,#mermaid-svg-FczagLZeBQc2Bw1Z .node polygon,#mermaid-svg-FczagLZeBQc2Bw1Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FczagLZeBQc2Bw1Z .rough-node .label text,#mermaid-svg-FczagLZeBQc2Bw1Z .node .label text,#mermaid-svg-FczagLZeBQc2Bw1Z .image-shape .label,#mermaid-svg-FczagLZeBQc2Bw1Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-FczagLZeBQc2Bw1Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FczagLZeBQc2Bw1Z .rough-node .label,#mermaid-svg-FczagLZeBQc2Bw1Z .node .label,#mermaid-svg-FczagLZeBQc2Bw1Z .image-shape .label,#mermaid-svg-FczagLZeBQc2Bw1Z .icon-shape .label{text-align:center;}#mermaid-svg-FczagLZeBQc2Bw1Z .node.clickable{cursor:pointer;}#mermaid-svg-FczagLZeBQc2Bw1Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FczagLZeBQc2Bw1Z .arrowheadPath{fill:#333333;}#mermaid-svg-FczagLZeBQc2Bw1Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FczagLZeBQc2Bw1Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FczagLZeBQc2Bw1Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FczagLZeBQc2Bw1Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FczagLZeBQc2Bw1Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FczagLZeBQc2Bw1Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FczagLZeBQc2Bw1Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FczagLZeBQc2Bw1Z .cluster text{fill:#333;}#mermaid-svg-FczagLZeBQc2Bw1Z .cluster span{color:#333;}#mermaid-svg-FczagLZeBQc2Bw1Z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FczagLZeBQc2Bw1Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FczagLZeBQc2Bw1Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-FczagLZeBQc2Bw1Z .icon-shape,#mermaid-svg-FczagLZeBQc2Bw1Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FczagLZeBQc2Bw1Z .icon-shape p,#mermaid-svg-FczagLZeBQc2Bw1Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FczagLZeBQc2Bw1Z .icon-shape .label rect,#mermaid-svg-FczagLZeBQc2Bw1Z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FczagLZeBQc2Bw1Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FczagLZeBQc2Bw1Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FczagLZeBQc2Bw1Z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 失败
全部通过
只删虚拟环境
彻底卸载老 miniconda
bash
zsh
开始迁移
确认两套 conda 路径

/usr/local/app/miniconda3

/usr/local/app/miniforge3
第 1 步: 批量导出老环境

conda env export --no-builds

  • pip freeze 兜底
    第 2 步: 在 miniforge3 重建

conda env create -f xxx.yml

  • pip install -r xxx.pip.txt
    第 3 步: 逐环境验证

import 测试 + 业务代码
查看构建日志

--from-history 重导

或改用 pip 补装缺失包
第 4 步: 选择清理力度
conda env remove -n xxx

conda clean -a -y
sudo rm -rf /usr/local/app/miniconda3

清理 ~/.conda 残留
清理 environments.txt

避免幽灵环境条目
按 shell 类型接管
conda init bash

清理 ~/.bashrc 旧块
conda init zsh

清理 ~/.zshrc 旧块
最终自检清单
迁移完成

核心原则就一条:验证全部通过之前,老 miniconda 始终是回滚保底,千万别先删。

导出

两套 conda 全程用绝对路径区分,不要依赖 PATH 里的 conda 命令,否则极易搞混操作对象:

bash 复制代码
mkdir -p ~/conda_migrate_backup
cd ~/conda_migrate_backup
for env in $(/usr/local/app/miniconda3/bin/conda env list | awk 'NR>2 {print $1}' | grep -v '^$'); do
    /usr/local/app/miniconda3/bin/conda env export -n "$env" --no-builds \
        | sed '/^prefix:/d' > "$env.yml"
    /usr/local/app/miniconda3/bin/conda run -n "$env" pip freeze > "$env.pip.txt" 2>/dev/null || true
done

两个参数都有讲究。--no-builds 是迁移关键:defaults 的 build 字符串(如 py39h6a678d5_0)在 conda-forge 里不存在,保留会导致重建时解析失败。pip freeze 是兜底:conda export 对 pip 安装的包记录不可靠,单独存一份以防万一。

重建

bash 复制代码
source /usr/local/app/miniforge3/etc/profile.d/conda.sh
which conda    # 确认是 miniforge3
for yml in ~/conda_migrate_backup/*.yml; do
    env=$(basename "$yml" .yml)
    # base 是保留名,根环境随安装自带,不重建
    if [ "$env" = "base" ]; then
        echo ">> 跳过 base(保留名;老 base 的包清单见 base.yml)"
        continue
    fi
    conda env create -f "$yml"
    [ -s "$HOME/conda_migrate_backup/$env.pip.txt" ] \
        && conda run -n "$env" pip install -r "$HOME/conda_migrate_backup/$env.pip.txt"
done

关于 base 环境的保留名报错

这里是我踩的第一个真坑。批量导出会把 base 也导出成 base.yml,重建循环跑到它时直接报错:

复制代码
CondaValueError: 'base' is a reserved environment name

原因想明白后很简单:base 物理上就是安装目录本身(root_prefix),不是 envs/ 下的普通环境,随安装自动存在。如果允许再建一个叫 base 的环境,conda activate base 就不知道该指向谁,所以 conda 在入口处硬拦截。同理被拦截的还有 conda rename -n base xxx。

正确姿势是:base 不迁移、不重命名,只往里补装 。把 base.yml 移出重建队列留作参考,老 base 里显式装过的工具(jupyterlab、ipython 之类)从 yml 里挑出来手动装:

bash 复制代码
grep -E "^- " ~/conda_migrate_backup/base.yml | head -40
conda install -n base jupyterlab -y

千万别图省事执行 conda env update -n base -f base.yml------全量 yml 里带着老 base 的 Python 版本钉和依赖树,会把新 base 的 Python 连带 conda 自身一起搅乱,那才是真的灾难。

验证

每个环境做导入测试,跑一遍实际业务代码更稳妥:

bash 复制代码
conda activate my_env
python -c "import numpy, pandas; print('OK')"
which python   # 应指向 /usr/local/app/miniforge3/envs/my_env/bin/python
pip list | wc -l

包级对比可以用迁移前的 pip 备份文件:

bash 复制代码
conda run -n my_env pip list --format=freeze 2>/dev/null | sort > /tmp/now.txt
sort ~/conda_migrate_backup/my_env.pip.txt > /tmp/old.txt
comm -23 /tmp/old.txt /tmp/now.txt
# 无输出 = 老环境有的包新环境全都有

清理

验证全部通过后,直接卸载老 miniconda:

bash 复制代码
sudo rm -rf /usr/local/app/miniconda3
rm -rf ~/.conda ~/.condarc ~/.continuum ~/.anaconda_backup 2>/dev/null

六、Shell 接管:bash 与 zsh 双方案

服务器默认 shell 是 bash,我个人终端用 zsh,两套都得处理。这一节的差异比想象中多。

清理旧 init 块

老 conda 在配置文件里留下的 init 代码块格式统一,都在 # >>> conda initialize >>> 与 # <<< conda initialize <<< 之间:

bash 复制代码
# bash 用户
cp ~/.bashrc ~/.bashrc.bak.$(date +%Y%m%d_%H%M%S)
sed -i '/>>> conda initialize >>>/,/<<< conda initialize <<</d' ~/.bashrc
# zsh 用户
cp ~/.zshrc ~/.zshrc.bak.$(date +%Y%m%d_%H%M%S)
sed -i '/>>> conda initialize >>>/,/<<< conda initialize <<</d' ~/.zshrc
sed -i '/miniconda3\/bin/d' ~/.zshrc ~/.zprofile ~/.zshenv 2>/dev/null

注册新包装

bash 复制代码
/usr/local/app/miniforge3/bin/conda init bash   # bash 用户
/usr/local/app/miniforge3/bin/conda init zsh    # zsh 用户

init 之后两种 shell 都会在 PATH 里加上同一个入口:/usr/local/app/miniforge3/condabin。这是 conda 专门设计的命令目录,保证即使函数包装未加载(非交互 shell、CI 脚本),conda 命令也能用。

两种 shell 的行为差异

这里有个经典的误判场景。zsh 下执行 which conda,看到的是一大段函数定义:

zsh 复制代码
conda () {
    \local cmd="${1-__missing__}"
    case "$cmd" in
        (activate | deactivate) __conda_activate "$@" ;;
        (install | update | upgrade | remove | uninstall) __conda_exe "$@" || \return
            __conda_activate reactivate ;;
        (*) __conda_exe "$@" ;;
    esac
}

很多人第一反应是「出问题了」。恰恰相反,zsh 的 which 是内置命令,会主动解析并打印函数定义------这段函数体正是 conda init 注册的包装正常工作的证据 。bash 的 which 是外部命令,查不到函数就不输出,所以同样命令在 bash 下只显示可执行文件路径。

正确的验证方式是两种 shell 通用的:

bash 复制代码
type conda                            # conda is a shell function
conda info | grep "base environment"  # 确认指向 miniforge3

PATH 检查也有差异。zsh 的 PATH 数组是小写 $path(与 $PATH 自动绑定),用 print -l $path | grep -i conda 查;bash 直接 echo $PATH | tr ':' '\n' | grep -i conda。健康状态都只有一条 condabin。

其他细节汇总成表:

项目 bash zsh
交互配置文件 ~/.bashrc ~/.zshrc
登录配置文件 ~/.bash_profile ~/.zprofile
conda init 目标 conda init bash conda init zsh
刷新命令缓存 hash -r rehash
环境切换后偶发 command not found 少见 rehash 手动刷新
两个 shell 特有的坑值得单独记。bash 侧:ssh 进服务器后 conda: command not found,但本地终端正常------原因是 conda init 写了 ~/.bashrc,而 ~/.bash_profile 里没 source 它,检查 .bash_profile 末尾是否有标准的 source 块。zsh 侧:用了 oh-my-zsh 的话,plugins=(...) 里的 conda 插件会与 init 块冲突,二选一;另外设置了 ZDOTDIR 的 dotfiles 管理方案,init 块会写去非默认位置,用 echo $ZDOTDIR 确认。

七、迁移后的两次典型报错

环境跑起来后我又遇到两个报错,都挺典型,记录在此。

报错一:strict priority 把包全排除了

某次安装时突然爆出一长串:

复制代码
- package babel-2.14.0-pyhd8ed1ab_0 is excluded by strict repo priority
- package openssl-3.3.2-hb9d3cd8_0 is excluded by strict repo priority
...

看报错头部的 Channels 列表就明白了:

复制代码
Channels:
 - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
 - conda-forge
 - defaults
 - nodefaults

channels 里混进了 pkgs/main 和 defaults。strict 模式下只有最高优先级频道里的包才能参与求解,pkgs/main 排在第一位,conda-forge 的包(build string 都是 pyhd8ed1ab 之类)全部被剔除。

排查来源用三条命令:conda config --show-sources 看配置文件是否被改,history | grep conda 看安装命令是否带了 -c defaults,如果是按 yml 创建则检查 yml 里的 channels 段。我的情况是某个同事在命令行里临时加了 -c pkgs/main 想加速,重新执行时去掉,或者显式用 conda install --override-channels -c conda-forge xxx 强制只走 conda-forge。

同一份报错里还夹着两条不同性质的:

复制代码
- nothing provides __glibc >=2.28,<3.0.a0 needed by python-3.14.0

这是系统 glibc 版本问题,与频道配置无关。conda-forge 从 2024 年起逐步迁移到 glibc 2.28 ABI(RHEL 8 基线),Python 3.14 和新版核心库都按新 ABI 构建。CentOS 7(glibc 2.17)这类老系统上,strict priority 修好后依然装不上。用 ldd --version 查系统 glibc,低于 2.28 的话要么降 Python 版本(3.11/3.12 的老 build 还兼容),要么升级操作系统------后者是长期正解,CentOS 7 也早就 EOL 了。

报错二:base 保留名

前文已经讲过,根源在迁移循环没跳过 base.yml。修复就是循环里加一个 continue,再吸取一个教训:老 base 的 yml 只当包清单用,别整灌。

八、验证:18 项检查脚本

迁移完不代表结束,得有证据。我把验证逻辑写成了一个脚本,覆盖五个维度:老环境移除干净、miniforge3 完全接管、频道配置正确、环境迁移无遗漏、功能可用。

bash 复制代码
#!/usr/bin/env bash
# migrate_verify.sh ------ Miniconda -> Miniforge3 迁移完成度验证
# 用法: bash migrate_verify.sh [备份目录]
set -u
OLD="/usr/local/app/miniconda3"
NEW="/usr/local/app/miniforge3"
BACKUP="${1:-$HOME/conda_migrate_backup}"
PASS=0; FAIL=0
FAILED_ITEMS=()
ok()  { printf "  [PASS] %s\n" "$1"; PASS=$((PASS+1)); }
bad() { printf "  [FAIL] %s\n" "$1"; FAIL=$((FAIL+1)); FAILED_ITEMS+=("$1"); }
echo "=============================================="
echo " Miniconda -> Miniforge3 迁移验证报告"
echo " 时间: $(date '+%F %T')   用户: $(whoami)"
echo "=============================================="
echo ""
echo "[1] 老 Miniconda 移除检查"
if [ ! -d "$OLD" ]; then
    ok "老安装目录已删除: $OLD"
else
    bad "老安装目录仍存在: $OLD"
fi
LEFTOVER=0
for f in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.zshenv" "$HOME/.bashrc" "$HOME/.bash_profile"; do
    if [ -f "$f" ] && grep -qi "miniconda\|anaconda" "$f"; then
        LEFTOVER=1; bad "$f 存在旧 conda 残留"
    fi
done
[ "$LEFTOVER" -eq 0 ] && ok "shell 配置文件无 miniconda/anaconda 残留"
if [ -f "$HOME/.conda/environments.txt" ] && grep -qi "miniconda" "$HOME/.conda/environments.txt"; then
    bad "~/.conda/environments.txt 存在旧路径条目"
else
    ok "environments.txt 无旧路径条目"
fi
echo ""
echo "[2] Miniforge3 接管检查"
BASE=$(conda info --base 2>/dev/null)
if [ "$BASE" = "$NEW" ]; then
    ok "conda base 指向 $NEW"
else
    bad "conda base 指向异常: ${BASE:-未获取}"
fi
# 注意: 必须以交互模式启动同类型子 shell 才能探测到函数包装,
# 非交互进程加载不到 rc 文件,会误报
USER_SHELL=$(basename "${SHELL:-/bin/bash}")
FUNC_FOUND=0
case "$USER_SHELL" in
    zsh)
        zsh -ic 'type conda' 2>/dev/null | grep -qi "function" && FUNC_FOUND=1
        RC_FILE="$HOME/.zshrc"
        ;;
    bash)
        bash -ic 'type conda' 2>/dev/null | grep -qi "function" && FUNC_FOUND=1
        RC_FILE="$HOME/.bashrc"
        ;;
esac
if [ "$FUNC_FOUND" -eq 1 ]; then
    ok "conda shell 函数包装正常($USER_SHELL 交互模式探测)"
elif [ -f "$RC_FILE" ] && grep -q ">>> conda initialize >>>" "$RC_FILE"; then
    ok "conda init 代码块存在于 $RC_FILE"
else
    bad "conda 函数包装缺失(执行 conda init $USER_SHELL 后重开 shell)"
fi
PY_PATH=$(conda run -n base which python 2>/dev/null)
case "$PY_PATH" in
    "$NEW"/*) ok "python 指向 miniforge3: $PY_PATH" ;;
    *)        bad "python 指向异常: ${PY_PATH:-未获取}" ;;
esac
ok "conda 版本: $(conda --version 2>/dev/null)"
ok "solver: $(conda config --show solver 2>/dev/null | awk -F': *' '{print $2}')"
echo ""
echo "[3] 频道配置检查"
CH=$(conda config --show channels 2>/dev/null)
echo "$CH" | grep -q "conda-forge" && ok "channels 包含 conda-forge" || bad "channels 缺少 conda-forge"
echo "$CH" | grep -q "nodefaults"  && ok "channels 含 nodefaults(屏蔽 defaults)" || bad "channels 缺少 nodefaults"
# conda config --show 输出带键名前缀,必须剥离后再比较
PRIO=$(conda config --show channel_priority 2>/dev/null | awk -F': *' '{print $2}')
[ "$PRIO" = "strict" ] && ok "channel_priority: strict" || bad "channel_priority=${PRIO:-未获取}(期望 strict)"
conda config --show custom_channels 2>/dev/null | grep -q "tuna.tsinghua" \
    && ok "conda-forge 已配置清华镜像" \
    || echo "  [INFO] 未配置镜像(海外网络可忽略)"
echo ""
echo "[4] 虚拟环境完整性检查"
ENV_LIST=$(conda env list 2>/dev/null)
ENV_COUNT=$(echo "$ENV_LIST" | awk 'NF && $1 !~ /^#/' | wc -l)
ok "当前共 $ENV_COUNT 个环境(含 base)"
if [ -d "$BACKUP" ]; then
    YML_N=$(ls "$BACKUP"/*.yml 2>/dev/null | wc -l)
    MISSING=0
    for yml in "$BACKUP"/*.yml; do
        [ -e "$yml" ] || continue
        env=$(basename "$yml" .yml)
        [ "$env" = "base" ] && continue    # base 是保留名,不参与重建比对
        if ! echo "$ENV_LIST" | awk '{print $1}' | grep -qx "$env"; then
            MISSING=$((MISSING+1)); bad "备份环境 $env 未在 miniforge3 中重建"
        fi
    done
    [ "$MISSING" -eq 0 ] && ok "备份的 $YML_N 个环境全部已重建(base 除外)"
else
    echo "  [INFO] 未找到备份目录 $BACKUP,跳过环境比对(可用参数指定)"
fi
BROKEN=0
for e in $(echo "$ENV_LIST" | awk 'NF && $1 !~ /^[#\/]/ {print $1}' | grep -vx base); do
    conda run -n "$e" python -c "pass" >/dev/null 2>&1 \
        || { BROKEN=$((BROKEN+1)); bad "环境 $e 的 python 无法启动"; }
done
[ "$BROKEN" -eq 0 ] && ok "所有命名环境 python 均可正常启动"
echo ""
echo "[5] 功能冒烟测试"
if conda create -n __verify_smoke python=3.11 -y >/dev/null 2>&1; then
    ok "conda create 新建环境成功"
    if conda run -n __verify_smoke python -c "import sys; assert sys.prefix.startswith('$NEW')" >/dev/null 2>&1; then
        ok "新环境 sys.prefix 正确指向 miniforge3"
    else
        bad "新环境 sys.prefix 异常"
    fi
    if conda install -n __verify_smoke numpy -y >/dev/null 2>&1; then
        ok "conda install 安装包成功"
    else
        bad "conda install 失败(检查网络/镜像)"
    fi
    conda env remove -n __verify_smoke -y >/dev/null 2>&1 \
        && ok "测试环境已清理" || bad "测试环境清理失败"
else
    bad "conda create 失败(检查目录写权限或网络)"
fi
echo ""
echo "=============================================="
echo " 结果: $PASS 项通过, $FAIL 项失败"
if [ "$FAIL" -gt 0 ]; then
    echo " 失败项:"
    for item in ${FAILED_ITEMS[@]+"${FAILED_ITEMS[@]}"}; do echo "   - $item"; done
    echo "=============================================="
    exit 1
else
    echo " 迁移验证全部通过,迁移完成"
    echo "=============================================="
    exit 0
fi

执行与留档:

bash 复制代码
bash migrate_verify.sh | tee ~/migrate_verify_$(date +%Y%m%d).log
# 退出码 0 = 全部通过

这个脚本本身也修过两版。初版有两个误报:函数包装检查在非交互子进程里跑,永远探测不到 rc 文件里的函数,恒报 FAIL;channel_priority 直接拿 channel_priority: strict 整串和 strict 比,必然不等。后来改成 zsh -ic / bash -ic 交互模式探测,输出解析用 awk 剥掉键名前缀。如果你也在写类似的检查脚本,这两处是最容易踩的。

如果不想跑脚本,手动核查就按五组来:

bash 复制代码
# 组 1: 老环境移除
ls /usr/local/app/miniconda3                                # 期望 No such file or directory
grep -in "miniconda\|anaconda" ~/.zshrc ~/.bashrc 2>/dev/null  # 期望无输出
# 组 2: 新环境接管
conda info --base                                           # /usr/local/app/miniforge3
type conda                                                  # conda is a shell function
# 组 3: 频道配置
conda config --show channels            # conda-forge + nodefaults
conda config --show channel_priority    # strict
# 组 4: 环境完整
conda env list                                               # 与备份数量一致
for env in $(conda env list | awk 'NF && $1 !~ /^[#\/]/ {print $1}' | grep -vx base); do
    printf "%-20s" "$env"; conda run -n "$env" python --version
done
# 组 5: 冒烟测试
conda create -n smoke python=3.11 -y && conda activate smoke && python -c "import sys; print(sys.prefix)"

九、写在最后

回头看整个迁移,真正花时间的不是命令本身,而是三件容易被忽视的事。

一是配置文件合并规则 。channels 哪怕写错了,大多数时候不会立刻报错,而是默默把 defaults 拉回来------等哪天 CI 被拦截、或者法务发通知才发现。系统级配置统一所有人行为,配合定期跑 conda config --show-sources 抽查,能避免大部分意外。

二是shell 差异 。同一个 which conda,bash 和 zsh 输出完全不同;同样是配置文件,bash 的 .bash_profile 与 .bashrc 有加载链关系,zsh 的 .zshenv/.zprofile/.zshrc 又是另一套逻辑。多 shell 用户建议两套配置都过一遍清理命令,否则会出现「这台机器明明修好了,换台机器又复发」的怪事。

三是版本边界的硬约束 。conda-forge 的 glibc 2.28 基线意味着老操作系统装不了新包,这不是配置能绕过的。规划迁移时先把 ldd --version 查了,CentOS 7 一类的机器要么降 Python 版本,要么先升系统。

另外提醒一下还在用旧镜像配置的:清华源 2026 年 8 月起停止同步 Anaconda 官方 pkgs 仓库,但 cloud/conda-forge 等社区频道镜像正常维护。旧的镜像配置文件即使没有授权压力也需要主动更新,否则 default_channels 指向的地址会直接 404。

迁移完成后,Miniforge3 的使用体验和 Miniconda 几乎一致,conda create、conda install、conda activate 全部通用,唯一区别是所有包默认来自 conda-forge。验证报告和备份目录我留了两周,确认日常开发无异常后才清理备份。建议照做的读者也留一段时间------毕竟迁移这种事,稳妥永远比快重要。

文章标签 :Python Conda Miniforge 环境管理 迁移实战 Zsh Bash Linux运维 开发者工具链

相关推荐
happylifetree14 小时前
Python09:核心语法-数据存储与运算-字面量
python
心之语歌14 小时前
Tkinter 画布基本梳理
运维·服务器·python
Wx-bishekaifayuan15 小时前
django个性化旅游路线推荐平台49005-计算机课程设计、毕业设计
spring boot·后端·python·django·课程设计·express·旅游
小白快快跑哦16 小时前
python-字符串全解(六):正则表达式-量词
python·正则表达式·字符串
禹凕16 小时前
机器学习之Selenium(Machina Learning about Selenium)
爬虫·python·selenium·测试工具·机器学习
ebiobiz18 小时前
基于 GD32 Embedded Builder (GEB) 与 Nimmake 的 MCU 工程搭建指南
c++·python·单片机·嵌入式硬件·mcu
老歌老听老掉牙19 小时前
穿越时空的物流密码:VRP与VRPTW的深度剖析及实战求解
python·物流·vrp
FYKJ_201019 小时前
django电影推荐系统55066-计算机课程设计、毕业设计
vue.js·spring boot·python·mysql·typescript·spark·django
ShyanZh19 小时前
【Python3基础】13-Python 常用设计模式
开发语言·python·设计模式
小静AI工程实验室19 小时前
RSA 算法详解:从模幂原理到 OAEP 加密、PSS 签名与 Python 实验
python·算法