包管理工具的使用技巧

从编程语言特定的 pip, conda,到操作系统级的 apt, pacman,我们在构建工程时离不开包管理工具为我们处理依赖。本文旨在总结这些工具的使用技巧。

如果你也经常用包管理工具,可能能从本文中获得启发。然而,本文不保证全面性(不保证这是认识它们的最佳视角/使用它们的最佳实践)和准确性(以工具的文档为准)

本系列的更新策略:标注 X月X日更新

系统级包管理工具的使用技巧

一些小知识:

  • pacman -S package_name1 package_name2 安装的是包和包的依赖项
  • pacman 数据库中依据安装理由将所有包分为两类:显式命令安装(explicit) 和 作为依赖安装(deps)
  • 可以通过 pacman -Q 选项来查询数据库,如 -Qe 列出所有显式命令安装的包;-Qi <package-name> 列出所选的性质。

用同时升级解决版本死锁

考虑下面的例子:

$ sudo pacman -S pipewire
error: failed to prepare transaction (could not satisfy dependencies)
installing libcamera (0.3.0-1) breaks dependency 'libcamera.so=0.2-64' required by libcamera-ipa
$ sudo pacman -S libcamera libcamera-ipa
installing libcamera (0.3.0-1) breaks dependency 'libcamera.so=0.2-64' required by pipewire

说明当下版本的(很可能是作为依赖安装的) libcamera 同时被 pipewire 和 libcamera-ipa 所依赖。新版本的 pipewire 依赖新版本的 libcamera,升级 pipewire 必须自动升级 libcamera,而这会让旧版本的 libcamera-ipa 损坏(而这个旧版本可能是显式安装的,包管理器不敢自作主张升级)。反之同理。这形成了一个版本死锁。

libcamera <- libcamera-ipa
           \_ pipewire
-- means version tied

如此场景下,只能同时升级 libcamera-ipa 和 pipewire,这种情况下包管理器知晓两个包都要升级,从而可以给出一个同时升级三个包的计划:

sudo pacman -S libcamera-ipa pipewire

先更新包索引,再下载包或更新系统

Ubuntu 安装包的时候,经常遇到 ”Error 404… E: Unable to fetch some archives, maybe run apt-get update or try with –fix-missing?” 的报错,而在 apt-get update 之后重新安装,就能够成功。为什么?

apt-get update 的说明是 “Used to re-synchronize the package index files from their sources. The indexes of available packages are fetched from the location(s) specified in /etc/apt/sources.list” 简单来说,报错的原因很可能是本地的数据库存储的链接过期失效了或者没有所需最新版本的地址,apt-get update 通过从远端拉去最新的包索引解决了这一问题。

这对应 pacman 的 -y 选项,它 “updates the local package database that pacman maintains and prevents pacman from trying to install & requesting an outdated version from mirror sites”,是解决 “Error retrieving file from… the requested URL returned error 404” 的办法。

杂项

一些报错的原因可能是包管理器自身依赖的包版本过低没有更新。这种情况下,先设法升级这个依赖。例如,pacman 报错 ”corrupted package – PGP signature“,则可能是 archlinux-keyring 版本过低的问题,解决方案是 pacman -Sy --needed archlinux-keyring

小实验:用 pacman -Qi 查询数据库

以 pacman -Qi dbus 为例,其输出是:

Name            : dbus
Version         : 1.14.10-1
Description     : Freedesktop.org message bus system
Architecture    : x86_64
URL             : https://wiki.freedesktop.org/www/Software/dbus/
Licenses        : GPL  custom
Groups          : None
Provides        : libdbus  libdbus-1.so=3-64
Depends On      : audit  expat  systemd-libs  libaudit.so=1-64  libsystemd.so=0-64
Optional Deps   : None
Required By     : avahi  bluez  jack2  libdecor  libnvme  libpcap  libpulse  libteam  pipewire  pipewire-audio  pipewire-pulse  rtkit  systemd  wpa_supplicant  xorg-server
Optional For    : None
Conflicts With  : libdbus
Replaces        : libdbus
Installed Size  : 911.37 KiB
Packager        : Jan Alexander Steffens (heftig) <heftig@archlinux.org>
...
Install Reason  : Installed as a dependency for another package

dbus 是进程间通信系统,通常作为 systemd (系统和服务管理器) 的依赖安装。可以看到,包依赖的可能是包,也可能还额外包括包对应的 .so 文件 (shared object, 共享库,动态链接库)。同时,包提供的也可以是包和 .so 文件。你能看到那些包依赖这个包,以及这个包是通过命令行显式安装的还是作为依赖安装的。

编程语言的包管理工具:以 Python pip 为例

pip 的一些常用指令:

查询包信息 python -m pip show package-name

用同时升级解决版本死锁

考虑下面的例子:

>>> import openai
...
File "C:\miniconda3\envs\genir\lib\site-packages\pydantic_core\__init__.py", line 6, in <module>
    from ._pydantic_core import (
ModuleNotFoundError: No module named 'pydantic_core._pydantic_core

这个环境下,pydantic 和 pydantic core 都是作为依赖安装的。它们出现了问题,需要重新安装或者升级,记得带上依赖它的、显式安装的包:

pip install pydantic pydantic_core openai

pip install 从哪里安装包?

”包“的指代

Python 的一个分发包(distribution package)是一个可供下载的软件。例如,pip install pkg 就是下载名为 pkg 的分发包。

Python 的一个导入包(import package)是一个 Python 编程语言模块(module)。Python Modules 文档 将模块解释为”为了长程序的可维护性,将定义放到一个文件中,以便脚本或者交互式解释器中使用“、Python Packaging Glossary 将其定义为”代码复用的最基本单元 (basic unit of code reusability)“、Javascript 文档将模块定义为”复杂工程中将 JS 程序拆分为独立的文件,在使用时导入“。程序中的 import pkg 就是导入一个模块。

附注:Python中的扩展模块(extension module)定义为用 C, C++ 等低层语言写成的模块。

下载分发包就可以使用对应的导入包,但二者未必重名,例如图像处理的 Pillow 分发包提供 PIL 模块。看到 import foopip install foo 是易错和危险的。不同的分发包可能提供重名的导入包(比如源于代码仓库的fork),罕见地,一个分发包可能提供多个导入包。

Python 分发包的格式

Pypi(Python 包索引)上的包以两种格式存在:源分发包(Source Distribution, sdist)和二进制分发包(Binary Distribution,又称 Wheel)。前者形如 pip-23.3.1.tar.gz,后者形如 pip-23.3.1-py3-none-any.whl

源分发包的 .tar.gz 压缩包中含有源代码和存储元信息的 PKG-INFO 文件。作为小实验,可以从 Pypi 中下载 pip-24.3.1.tar.gz,然后解压之 python -m tarfile -e pip-24.3.1.tar.gz,查验其中的文件。

二进制分发包是安装时需要拷贝的文件。典型地,对含有扩展模块的包,Wheel 含有平台相关的二进制,即 DLL 或 .so。对于纯 Python 语言编写的模块,Wheel 剥离了测试、文档等 sdist 中常有的文件。Wheel 是一个 zip 文件,作为小实验,可以从 Pypi 中下载 pip-24.3.1-py3-none-any.whl,然后解压之 python -m zipfile -e pip-24.3.1-py3-none-any.whl pip-wheel。Wheel 名中,py3 代表 Python 实现,none 代表不依赖 Python 版本,any 代表不依赖平台。

egg 是一个在 2023 年 8 月被弃用(用 wheel 取代的)格式。

pip 可以直接安装 sdist 和 wheel 格式的分发包:python -m pip install {sampleproject-1.0.tar.gz, sampleproject-1.0-py3-none-any.whl}

查看安装了哪些包

python -m pip list 查看环境中安装了哪些分发包

python -m pip show package-name 查看分发包的信息

动手创建一个 Python 分发包

创建一个纯 Python 语言的包

参见本篇官方教程

构建前端(build frontend),如 pip 和 build,会把构建分发包的任务交给构建后端(build backend),如 setuptools。

创建如下文件树,在 example.py 和 pyproject.toml 中填入适合的内容。下面假设 __init__.py 留空。

packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── src/
│   └── mypkg/
│       ├── __init__.py
│       └── example.py
└── tests/

在该文件夹下运行 python -m build,会得到 dist 文件夹,生成了分发包。

安装该分发包:python -m pip install ./dist/mypkg-0.0.1-py3-none-any.whl,验证之:from mypkg import example; example.add_one(2);

创建一个含有扩展模块的包

把含有扩展模块的包加入 Python 中,可以参考这篇教程(它对 python setup.py build 等命令的使用可能是过时的 as of 2024)。


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *