如何实现Python包支持依赖的不同变体(如CPU/GPU版)且避免冲突安装?
如何实现Python包支持依赖的不同变体(如CPU/GPU版)且避免冲突安装?
我完全理解你的痛点——想通过额外依赖(extra)让用户一键切换依赖的CPU/GPU变体,但pip的默认解析逻辑却导致两个冲突变体同时安装,这确实很头疼。下面给你几个比发布两个独立包更优的方案:
方案1:使用带Extra标记的条件依赖(推荐,适配现代pip)
你的尝试方向其实是对的,只是可能因为pip版本过旧导致标记不生效。现代pip(20.3+,推荐21.0+)已经能正确解析基于extra的条件依赖,不会同时安装冲突的包。
在你的pyproject.toml里这样配置:
[project] name = "A" version = "0.1.0" # 仅当未指定cuda extra时,才安装onnxruntime dependencies = [ "onnxruntime; extra != 'cuda'", "pip>=21.0" # 强制用户使用支持条件标记的现代pip ] [project.optional-dependencies] # 指定cuda extra时,安装onnxruntime-gpu cuda = [ "onnxruntime-gpu" ]
为什么之前没生效?
- 如果你用的是pip 20.3之前的旧版本,它的依赖解析器不会正确处理
extra标记,会无视条件安装所有依赖。升级pip到21.0+就能解决这个问题。 - 确认你安装时的命令是
pip install A[cuda],不要出现拼写错误(比如标记里用cuda但安装时写A[gpu])。
验证效果:
- 执行
pip install A:只会安装onnxruntime和包A。 - 执行
pip install A[cuda]:只会安装onnxruntime-gpu和包A,不会安装onnxruntime。
方案2:核心包+元包分离(兼容旧版pip)
如果你的用户可能还在使用旧版pip,不想强制升级,可以把代码和依赖配置分离:
- 发布核心包
A-core:包含所有业务代码,不依赖onnxruntime的任何变体(或仅依赖无冲突的基础依赖)。 - 发布元包
A:作为入口,通过不同extras拉取正确的核心包和依赖变体:
# 元包A的pyproject.toml [project] name = "A" version = "0.1.0" dependencies = ["A-core", "onnxruntime"] [project.optional-dependencies] cuda = [ "A-core", "onnxruntime-gpu", "onnxruntime==0.0.0; python_version>='3.7'" # 虚拟版本,强制pip忽略默认的onnxruntime ]
说明:
- 元包本身没有代码,仅负责拉取正确的核心包和依赖。
- 虚拟版本
onnxruntime==0.0.0是一个小技巧,会让pip优先选择onnxruntime-gpu(因为它是有效版本),从而跳过默认的onnxruntime。
方案3:动态依赖配置(适合复杂场景)
如果需要更灵活的逻辑,可以用setuptools的动态依赖功能,在setup.py里根据用户指定的extras动态设置依赖:
from setuptools import setup def get_dependencies(extras): if 'cuda' in extras: return ["onnxruntime-gpu"] return ["onnxruntime"] setup( name="A", version="0.1.0", install_requires=get_dependencies(set()), extras_require={ "cuda": [] }, dynamic=["install_requires"] )
注意:
- 这种方式需要配合
setup.py使用,且要求setuptools>=61.0支持动态字段。 - 需注意pip解析extras时的逻辑,建议在测试环境验证后再发布。
不推荐的方案
- 发布
A和A-gpu两个独立包:虽然可行,但会大幅增加维护成本,用户也容易混淆变体。 - 用post-install脚本卸载冲突包:属于hacky做法,可能破坏pip的依赖树,导致后续安装出现未知问题。
总结
优先选择方案1:只要用户使用现代pip,就能完美实现你想要的效果——默认安装CPU版,加extra安装GPU版,且不会出现冲突。如果必须兼容旧版pip,再考虑方案2的核心包+元包模式。




