pip安装PyPI包时执行shell脚本失效及requirements.txt找不到问题
问题
我发布了一个依赖额外Apt包的PyPI包,于是在GitHub仓库根目录放了setup.sh脚本,想让用户执行pip install mypackage时自动运行这个脚本。我在setup.py里通过cmdclass自定义了CustomInstall类,代码如下:
from setuptools import setup, find_packages from setuptools.command.install import install import subprocess import codecs import os class CustomInstall(install): def run(self): subprocess.check_call("bash setup.sh", shell=True) install.run(self) with open('requirements.txt') as f: requirements = f.read().splitlines() setup( ... cmdclass={'install': CustomInstall}, install_requires=requirements, ... )
本地执行python setup.py sdist bdist_wheel时,CustomInstall类能正常运行,但用pip安装时这个类被忽略。后来我只执行python setup.py sdist生成tar.gz包,用pip安装时又报错:
with open('requirements.txt') as f: ^^^^^^^^^^^^^^^^^^^^^^^^ FileNotFoundError: [Errno 2] No such file or directory: 'requirements.txt'
requirements.txt在仓库根目录,想知道哪里错了?是路径问题吗?
解决方案
1. 为什么pip安装时CustomInstall被忽略?
当你用bdist_wheel生成wheel包时,pip安装wheel包不会执行setup.py里的自定义install命令——wheel包是预构建的,安装过程跳过了setup.py的执行阶段,直接解压文件到目标路径。这就是本地运行setup.py有效,但pip装wheel包时自定义逻辑失效的原因。
2. requirements.txt找不到的原因
生成sdist源码包时,默认只会把setup.py、setup.cfg、pyproject.toml和包的源码文件打包进去,requirements.txt和setup.sh不会被自动包含到tar.gz包里。所以pip安装源码包时,运行setup.py找不到这两个文件,就会报错。
3. 修复步骤
(1)确保必要文件被打包进源码包
在仓库根目录创建MANIFEST.in文件,添加以下内容,把requirements.txt和setup.sh包含到源码包中:
include requirements.txt include setup.sh
这样生成sdist时,这两个文件会被加入tar.gz包。
(2)调整setup.py中的路径处理
因为安装时当前工作目录可能不是包的解压目录,所以要通过__file__获取setup.py所在的绝对路径,再拼接其他文件的路径,避免相对路径错误:
from setuptools import setup, find_packages from setuptools.command.install import install import subprocess import os # 获取setup.py所在目录的绝对路径 BASE_DIR = os.path.abspath(os.path.dirname(__file__)) class CustomInstall(install): def run(self): # 拼接setup.sh的绝对路径 setup_sh_path = os.path.join(BASE_DIR, 'setup.sh') subprocess.check_call(["bash", setup_sh_path], shell=False) install.run(self) # 读取requirements.txt的绝对路径 with open(os.path.join(BASE_DIR, 'requirements.txt')) as f: requirements = f.read().splitlines() setup( ... cmdclass={'install': CustomInstall}, install_requires=requirements, ... )
这里把shell=True改成shell=False并传入列表形式的命令,更安全,也避免路径解析问题。
(3)关于wheel包的替代方案
如果想支持wheel包的自动脚本执行,wheel本身不允许安装时运行自定义脚本(这是设计上的安全限制)。你可以:
- 只发布源码包(sdist),这样用户安装时会执行
setup.py的自定义逻辑; - 把Apt依赖的检查和提示放到包的初始化代码里(比如
__init__.py),当用户第一次导入包时检测依赖是否存在,引导用户手动安装; - 使用
post-install钩子,但这需要依赖setuptools的特定机制,且同样不适用于wheel包。
内容的提问来源于stack exchange,提问作者Ayus Chatterjee

