pip install 在写入 installed-files.txt 时遗漏了一些生成文件
当你用 pip install
安装一个项目时,如果这个项目有一个自定义的 build_py
命令,它会在构建目录中生成一个额外的文件。可是,pip 在安装时生成的 installed-files.txt 文件并没有列出这个生成的文件。因此,当我卸载这个分发包时,它会留下我生成的文件。
我想我可能没有正确注册这个生成的文件,但我找不到相关的文档来说明该怎么做。
我需要做什么才能让 pip 的 installed-files.txt 列出我的生成文件呢?
重现步骤
创建以下文件系统条目。
installed-files-missing-project ├── install-entry-missing │ └── __init__.py └── setup.py
在 setup.py 中放入以下内容。
import os
from setuptools import setup
from setuptools.command.build_py import build_py
def touch(fname, times=None):
with open(fname, 'a'):
os.utime(fname, times)
class my_build_py(build_py):
def run(self):
if not self.dry_run:
target_dir = os.path.join(self.build_lib, "install-entry-missing")
self.mkpath(target_dir)
touch(os.path.join(target_dir, "my_file.txt"))
# TODO: missing registration of "my_file.txt"?
build_py.run(self)
setup_args = dict(
name='install-entry-missing',
version='1.0.0',
description='',
author='author',
author_email='author@example.com',
packages = ["install-entry-missing"],
cmdclass={'build_py': my_build_py}
)
if __name__ == '__main__':
setup(**setup_args)
从 install-entry-missing-project
目录运行 pip install .
。
安装后的目录会包含 my_file.txt 和 __init__.py。不过,查看 egg-info 目录中的 installed-files.txt 会发现 my_file.txt 并没有被列出。因此,运行 pip uninstall install-entry-missing
会删除 __init__.py,但不会删除 my_file.txt。
3 个回答
我遇到了和这个帖子里描述的类似的问题。上面提到的解决方案都没用。我在我的setup.py文件的安装后步骤中创建了一个符号链接。
为了搞清楚发生了什么,我查看了pip的源代码,特别是它是如何读取installed-files.txt
的。结果发现,由于我的文件是一个符号链接,所以它被忽略了。在pip/req/req_uninstall.py的第50行,调用normalise_path
时,它跟随了这个符号链接,简单地把链接指向的文件添加到了要删除的路径列表中。链接本身并没有被添加到要删除的文件列表中,因此被忽略了,安装过程就把它留了下来。
作为一个解决办法,我把符号链接改成了硬链接,现在pip可以完全卸载我的包了。
需要重写一下 install_egg_info
这个命令,并在它的 self.outputs
列表中添加一个条目。这个 self.outputs
列表里的每一个条目,都会在最终的 installed-files.txt 文件中生成一个对应的条目。
修改上面的代码,正确的解决方案是:
import os
from setuptools import setup
from setuptools.command.build_py import build_py
from setuptools.command.install_egg_info import install_egg_info
def touch(fname, times=None):
with open(fname, 'a'):
os.utime(fname, times)
class my_build_py(build_py):
def run(self):
if not self.dry_run:
target_dir = os.path.join(self.build_lib, "install-entry-missing")
self.mkpath(target_dir)
touch(os.path.join(target_dir, "my_file.txt"))
# this file will be registered in my_install_egg_info
build_py.run(self)
class my_install_egg_info(install_egg_info):
def run(self):
install_egg_info.run(self)
target_path = os.path.join(self.install_dir, "my_file.txt")
self.outputs.append(target_path)
setup_args = dict(
name='install-entry-missing',
version='1.0.0',
description='',
author='author',
author_email='author@example.com',
packages = ["install-entry-missing"],
cmdclass={'build_py': my_build_py,
'install_egg_info': my_install_egg_info}
)
if __name__ == '__main__':
setup(**setup_args)
比起Eric的回答中提到的方法,我们可以通过重写build_py
中的get_outputs
方法来写出更简洁的代码,而不是使用install_egg_info
:
import os
from setuptools import setup
from setuptools.command.build_py import build_py
def touch(fname, times=None):
with open(fname, 'a'):
os.utime(fname, times)
class my_build_py(build_py):
def run(self):
self.my_outputs = []
if not self.dry_run:
target_dir = os.path.join(self.build_lib, "install-entry-missing")
self.mkpath(target_dir)
output = os.path.join(target_dir, "my_file.txt")
touch(output)
self.my_outputs.append(output)
build_py.run(self)
def get_outputs(self):
outputs = build_py.get_outputs(self)
outputs.extend(self.my_outputs)
return outputs
setup_args = dict(
name='install-entry-missing',
version='1.0.0',
description='',
author='author',
author_email='author@example.com',
packages = ["install-entry-missing"],
cmdclass={'build_py': my_build_py}
)
if __name__ == '__main__':
setup(**setup_args)
虽然Eric的回答中提到的方法是可行的,但重写install_egg_info
来更新installed-files.txt其实有点问题,因为你是在更新与egg-info目录中安装的文件相关的列表。相反,重写build_py
中的get_outputs
方法可以让我们更接近文件生成的地方来管理更改的文件列表,这样在运行时生成的文件情况也会更容易处理。