pip install 在写入 installed-files.txt 时遗漏了一些生成文件

3 投票
3 回答
1997 浏览
提问于 2025-04-20 00:53

当你用 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 个回答

1

我遇到了和这个帖子里描述的类似的问题。上面提到的解决方案都没用。我在我的setup.py文件的安装后步骤中创建了一个符号链接。

为了搞清楚发生了什么,我查看了pip的源代码,特别是它是如何读取installed-files.txt的。结果发现,由于我的文件是一个符号链接,所以它被忽略了。在pip/req/req_uninstall.py的第50行,调用normalise_path时,它跟随了这个符号链接,简单地把链接指向的文件添加到了要删除的路径列表中。链接本身并没有被添加到要删除的文件列表中,因此被忽略了,安装过程就把它留了下来。

作为一个解决办法,我把符号链接改成了硬链接,现在pip可以完全卸载我的包了。

1

需要重写一下 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)
2

比起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方法可以让我们更接近文件生成的地方来管理更改的文件列表,这样在运行时生成的文件情况也会更容易处理。

撰写回答