上周被拉去看一台内网服务器,业务方要跑一个数据清洗脚本,脚本里用了 3.8 之后才引入的语法特性,而机器上python3 -V打出来是 3.6.8。运维同事的第一反应是yum install python39,源里翻了个遍没有;第二反应是把系统 Python 升上去,我当场拦住了——那台机器上压着一堆运维脚本和包管理工具,它们全都吊在系统自带的那个解释器上,动一下可能就是整个机器的运维链路失灵。最后走的就是最笨也最稳的那条路:Linux 安装 Python 3.9.2,用源码编译,把它整个扔到/usr/local下面,跟系统自带的那个彻底物理隔离。整个过程看起来只是三条命令,真正卡人的地方全在细节里——依赖少一个包,装完之后import ssl直接崩;--enable-shared一加,不配动态库路径就跑不起来;make install和make altinstall差一个字母,后果是系统里多出一个会惹祸的python3。
这篇内容适合三类人看:一是手上有一台不能重装、不能动系统环境的服务器,需要在上面跑新版本 Python 的;二是被各种"模块缺失""编译报错"折磨过、想搞清楚每一步到底在干什么的;三是想给团队沉淀一份可复现的标准操作流程的。我会把每一步的选择理由、参数的实际含义、以及那些文档里不写但实际会踩的坑,全部摊开讲。
1. 动手之前先想清楚:Linux 上装 Python 3.9.2,为什么大概率要走源码编译
1.1 三条安装路线摆在桌面上比较
在 Linux 上搞一个指定版本的 Python,能走的路其实就三条,很多人一上来就问"哪条最好",这个问题没有普适答案,得看你手上机器的约束条件。
第一条是包管理器。apt install python3.9或者yum install python39,这是最省事的。它的上限完全取决于你的软件源里有没有这个版本,以及源里的包是不是还能用。老一些的发行版,源里最新就是 3.6 或 3.8,你再怎么update也变不出 3.9.2 这个具体的小版本号。而且包管理器装出来的 Python,安装路径、编译选项都是打包者替你定死的,--enable-shared有没有开、ssl 链接的哪个版本,你无从干预。
第二条是版本管理工具,比如 pyenv、conda 这一类。它们本质上是帮你把"源码编译"这一步自动化了,底层还是同一套./configure && make。好处是切换版本方便,一个命令换一个解释器;坏处是多了一层需要维护的东西,而且在离线环境、受限权限的内网机器上,这类工具本身的安装和依赖下载都可能成问题。我见过不止一次,内网机器装 pyenv 时卡在下载源码包那一步,最后还是老老实实手工编译。
第三条就是源码编译,自己下载Python-3.9.2.tgz,自己决定 prefix、自己决定编译选项、自己承担编译失败。它最麻烦,但可控性最高,而且完全离线可复现——源码包拷进去,依赖包用本地源装好,剩下的就是纯本地操作。
我一般的判断顺序是:源里有对应版本、且不需要特殊编译选项,直接用包管理器;需要精确到小版本号、或者需要--enable-shared、或者机器完全离线,走源码编译;需要频繁在多个版本之间切换的研发机,才考虑版本管理工具。
1.2 源码编译真正解决的是"版本锁定"和"隔离"这两件事
这两个词听起来抽象,落到实际场景里非常具体。
版本锁定指的是你必须精确地跑 3.9.2 这个版本,不能是 3.9.7,也不能是 3.10。这种情况在交付场景里很常见:某个模型推理代码、某个第三方 SDK,官方文档上写的就是"已在 3.9.2 上验证",你换了小版本号,行为可能就不一样。包管理器给你的永远是源里那个版本,管不了这么细。
隔离则更重要。系统自带的 Python 是被操作系统深度绑定的,包管理器本身、大量系统工具、甚至防火墙配置脚本,全都跑在它上面。你在它身上做任何改动——升级、装包、改site-packages——风险都是全局的。我亲眼见过有人往系统 Python 里pip install了一个新版本的 requests,结果某个依赖旧版 requests 的系统工具直接起不来。源码编译到独立目录,装出来的解释器有自己的bin、自己的lib、自己的site-packages,跟系统那个互不知道对方存在,这才叫真隔离。
这里有个判断标准:如果这台机器上
python3 -c "import sys; print(sys.prefix)"打出来的路径是/usr,那这个 Python 你基本就别碰了,把它当成系统组件看待。
1.3 编译安装的代价,得提前认下来
源码编译不是没有成本的,这几点在动手前要有心理准备。
时间成本。一次完整的编译,去掉优化选项大概 10 到 20 分钟;加上--enable-optimizations做 PGO(按配置文件引导优化),编译过程会额外跑一遍内置的基准测试,时间直接翻两三倍,低配机器上四十分钟起步。
维护成本。包管理器装的 Python,安全补丁跟着系统更新一起走。你自己编译的这个,OpenSSL 出了漏洞、需要重新链接,得你自己重新编一遍。所以生产环境里我会在文档里明确记下"这个 Python 是源码编译的,路径在哪,编译选项是什么,升级需要重编",避免半年后接手的人一脸茫然。
空间成本。源码包解压后大概 100MB 出头,编译中间产物会膨胀到几百 MB,安装目录完整装下来(含 pip、setuptools、基础库)大概 200MB 上下。磁盘不紧张的话这不算事,但在容器镜像里就要注意清理。
2. 依赖清单、资源预估与三个前置检查
2.1 编译依赖一次装齐,别反复重编
源码编译 Python 最典型的翻车方式是:编译顺利结束,make altinstall也成功了,一跑python3.9 -c "import ssl"报ModuleNotFoundError: No module named '_ssl'。原因是 Python 的很多标准库模块是 C 扩展,编译时如果系统里没有对应的头文件,configure 阶段会静默跳过这些模块,不报错,只在最后打印一堆The necessary bits to build these optional modules were not found的提示,那条提示很容易被刷屏淹掉。
所以正确的做法是,装依赖这一步一次装齐,宁可多装几个用不上的,也别为了省事重编一遍。下面这份清单覆盖了日常开发里 95% 会用到的模块:
| 系统库(Debian 系) | 系统库(RHEL 系) | 对应 Python 模块 |
|---|---|---|
| build-essential | gcc / gcc-c++ / make | 编译骨架,缺了直接 configure 失败 |
| zlib1g-dev | zlib-devel | zlib、zipimport,pip 装 whl 包必需 |
| libssl-dev | openssl-devel | _ssl、hashlib,pip 联网必需 |
| libffi-dev | libffi-devel | _ctypes,大量第三方库依赖它 |
| libbz2-dev | bzip2-devel | _bz2 |
| liblzma-dev | xz-devel | _lzma,解压 .xz 包用 |
| libreadline-dev | readline-devel | readline,交互式解释器的方向键和历史记录 |
| libsqlite3-dev | sqlite-devel | _sqlite3,Django 初始化数据库必备 |
| libncurses5-dev / libncursesw5-dev | ncurses-devel | _curses |
| libgdbm-dev | gdbm-devel | _dbm、_gdbm |
| uuid-dev | libuuid-devel | _uuid |
| tk-dev | tk-devel | tkinter,画图脚本会用 |
| wget / curl | wget / curl | 下载源码包 |
Debian 系一条命令搞定:
apt-get update apt-get install -y build-essential zlib1g-dev libssl-dev libffi-dev \ libbz2-dev liblzma-dev libreadline-dev libsqlite3-dev \ libncursesw5-dev libgdbm-dev uuid-dev tk-dev wgetRHEL 系:
yum install -y gcc gcc-c++ make zlib-devel openssl-devel libffi-devel \ bzip2-devel xz-devel readline-devel sqlite-devel \ ncurses-devel gdbm-devel libuuid-devel tk-devel wget注意一个细节:
libssl-dev这个名字在不同发行版上可能叫libssl1.1-dev、libssl3-dev,取决于系统的 OpenSSL 主版本。装之前先跑openssl version看一眼,别装了个版本对不上的开发包,白忙一场。
2.2 内存、磁盘、时间的实际预估
这三个数字决定了你的编译参数该怎么定,值得花两分钟估一下。
内存是最容易出问题的。编译 CPython 时,单个gcc进程处理某些大文件(比如ceval.c)峰值内存能吃到 300MB 到 500MB。make -j N意味着同时开 N 个这样的进程。一台 2GB 内存的机器上你要是习惯性来一句make -j$(nproc),八个核心并发,瞬间就是几个 G 的内存需求,然后gcc就被内核的 OOM killer 干掉了,报错信息是一句让人摸不着头脑的internal compiler error: Killed (program cc1)。
我的经验公式是:可用内存每 1GB 对应 1 到 1.5 个并发。2GB 内存用-j2,4GB 用-j4,8GB 以上才敢用-j$(nproc)。另外,如果机器上还跑着别的服务,把可用内存再减掉一部分。
磁盘方面,源码目录加上编译中间产物,按源码包大小的 5 到 6 倍估算。Python 3.9.2 的 tar 包大约 26MB,解压后 100MB 左右,完整编译完的源码目录大概 500MB 到 600MB。安装目录按 250MB 算。也就是说/usr/local所在分区至少留出 1GB 的空余,别卡在最后一个链接步骤上因为磁盘满失败。
时间上,四核机器不带优化大约 8 到 15 分钟,带--enable-optimizations大约 30 到 45 分钟。如果只是给一个脚本临时用,我会直接砍掉优化选项,省下来的时间远比那 10% 左右的性能提升值钱。
2.3 前置检查一:OpenSSL 版本决定 _ssl 模块能不能编出来
这是我在老机器上踩得最狠的一个坑,单独拎出来讲。
Python 3.9 这一代对 OpenSSL 的要求是:支持 1.1.1 和 1.0.2 两条线,而到了 3.10 就只认 1.1.1 及以上了。所以对于 3.9.2,如果系统 OpenSSL 是 1.0.2,是能编出_ssl的;但如果是更老的 1.0.1,或者系统里根本没有 OpenSSL 头文件,_ssl就编不出来。而_ssl编不出来,直接后果是 pip 装不了任何东西,因为 pip 访问包索引走的就是 HTTPS。
检查方法很简单:
openssl version如果输出是OpenSSL 1.1.1x或OpenSSL 1.0.2x,基本可以放心往下走。如果版本低于这个,或者你希望 Python 链接的是一个独立编译的高版本 OpenSSL(不想动系统那个),有两个方案。
方案一,动系统 OpenSSL。风险高,因为系统里大量组件链接它,升级可能引发连锁反应,我一般不推荐在生产机上这么干。
方案二,自己编译一个 OpenSSL 到独立目录,然后告诉 Python 去哪儿找:
# 编译 OpenSSL 到 /opt/openssl11 ./config --prefix=/opt/openssl11 --openssldir=/opt/openssl11 shared zlib make -j4 make install # Python configure 时通过 --with-openssl 指过去 ./configure --prefix=/usr/local/python3.9 --with-openssl=/opt/openssl11 ...用--with-openssl指定之后,Python 会优先链接你指定的那个版本。但要注意,运行时动态库的查找路径还是得配,要么加rpath,要么把/opt/openssl11/lib加进ld.so.conf,否则运行时会报找不到libssl.so.1.1。
判断
_ssl有没有编出来的方法:configure结束后往上翻输出,找checking for SSL/TLS...和结尾那段The necessary bits to build these optional modules were not found列表。列表里出现_ssl就是没编出来,别抱侥幸心理往下走,先解决它。
2.4 前置检查二:源码目录能不能执行,临时空间够不够
这两个属于偏门但确实会遇到的坑。
第一个是挂载选项。如果/home或者某个数据盘是以noexec挂载的,你在上面编译,make中途要执行一些临时生成的辅助程序,就会报Permission denied。检查办法是mount | grep noexec看一眼,有的话换到/usr/local/src或/tmp下面编译。
第二个是/tmp的空间和权限。有些加固过的机器会把/tmp单独挂一个小分区,或者挂成noexec、nosuid。configure 阶段的一些检测程序、编译时的临时文件都往/tmp写。如果/tmp太小或者不可执行,编译会很诡异地失败。稳妥起见,我习惯把TMPDIR指到源码目录旁边:
mkdir -p /usr/local/src/pybuild_tmp export TMPDIR=/usr/local/src/pybuild_tmp这一步做了之后,后面无数个莫名其妙的临时文件相关报错都能绕开。
3. 编译全流程:configure 参数逐条拆解到 make altinstall
3.1 下载、解压与校验
源码包从哪里拿,取决于机器能不能联网。
能联网的,用官方发布的地址下载,下载完做一次校验:
cd /usr/local/src wget https://www.python.org/ftp/python/3.9.2/Python-3.9.2.tgz wget https://www.python.org/ftp/python/3.9.2/Python-3.9.2.tgz.sha256 sha256sum -c Python-3.9.2.tgz.sha256校验这一步看着多余,但在内网环境里非常有价值。源码包经过邮件、U 盘、跳板机反复转手,损坏的概率比想象中高。我一个同事就遇到过压缩包传输时截断,tar解压报了个无关痛痒的 warning,后面编译到一半才炸,排查了半小时才反应过来是包坏了。
不能联网的机器,把 tgz 包拷进去,同样做一次sha256sum手工比对,然后:
tar -xzf Python-3.9.2.tgz cd Python-3.9.2解压后的目录建议保留着别删,后面万一要重新编译、或者要对比编译日志,都用得上。
3.2 configure 参数逐条拆解,每个都有存在的理由
这一步是整个流程里最值得花时间的。命令本身不复杂,但每个参数都在影响最终装出来的东西。
./configure \ --prefix=/usr/local/python3.9 \ --enable-optimizations \ --with-lto \ --enable-shared \ --with-ensurepip=install--prefix=/usr/local/python3.9。这是最重要的一条,决定了安装根目录。为什么不用默认的/usr/local?因为默认安装会把python3.9、pip3.9这些散落到/usr/local/bin,把库散落到/usr/local/lib,跟系统里其他源码编译的软件混在一起,以后想单独卸载或者单独升级都很难。给一个带版本号的独立前缀,好处是这个目录就是整个 Python 的完整边界——想删就rm -rf,干净利落,不留残余。
--enable-optimizations。开启 PGO 优化。原理是先用一个不带优化的解释器跑一遍内置的测试套件,收集哪些代码路径最热,然后让编译器针对这些热点重新优化。代价是编译时间翻两三倍,收益是运行速度通常能提升 10% 到 20%。我的取舍是:长期跑服务的解释器加,临时用一下的脚本解释器不加。另外这个选项要求源码目录可写、测试能跑通,如果系统环境有奇怪的限制导致测试失败,编译会中断,这时候去掉这个参数重来就行,不影响功能。
--with-lto。链接时优化,让编译器在链接阶段跨文件做优化。能带来额外的几个百分点性能,但对编译器版本有要求,老版本 gcc 可能直接报错。我通常是先试着加上,报错了就去掉,不影响主线。
--enable-shared。生成libpython3.9.so这样的动态链接库。这一条要不要加,取决于你后面要干什么。如果会用 PyInstaller 把脚本打包成单文件可执行程序,或者会有 C 扩展嵌入 Python 解释器,或者某些第三方库需要链接动态库,那就必须加。代价是装完之后要额外配置动态链接器,否则运行python3.9就报error while loading shared libraries: libpython3.9.so.1.0: cannot open shared object file。如果你只是纯跑脚本、装纯 Python 包,完全可以不加,少一层麻烦。
--with-ensurepip=install。安装时顺带把 pip 装上。这个是 3.4 之后 configure 的默认行为,写出来是为了明确表达意图。
有一个参数很多人不知道:
--with-ssl-default-suites=openssl。它的作用是把 SSL 的默认加密套件交给 OpenSSL 管理,而不是用 Python 内置那份。在跟一些老旧的内部服务对接时,这个参数能省掉不少握手失败的麻烦。不是必需项,记着有这么个东西就行。
3.3 make 的并行度、日志和失败后的处理
configure 跑完之后,最后几行会打印一份总结,包括检测到的编译器版本、会构建哪些模块、哪些模块因为缺依赖被跳过。这一段务必扫一眼,尤其是被跳过的模块列表。
然后开始编译:
make -j4 2>&1 | tee /usr/local/src/build.logtee把日志存一份,这是我强烈建议的习惯。编译失败时,屏幕上滚过去的报错往往已经找不到了,翻日志比重新编译一遍快得多。
并行度按前面 2.2 节的经验公式来定。如果编译途中发现机器负载失控、或者 dmesg 里出现 OOM killer 的记录,中断重来,把-j调小。
编译失败最常见的三类原因,以及对应的处理思路:
- 报
internal compiler error: Killed或者cc1: out of memory,内存不够,减小-j。 - 报某个头文件
No such file or directory,依赖没装齐,回到 2.1 节的清单补。 - 带
--enable-optimizations时在 PGO 阶段报测试失败,去掉这个参数重来。
make中途失败之后再重跑,建议先make clean一下,否则可能有残留的目标文件导致行为不一致。
3.4 make altinstall 与 make install:一字之差,后果完全不同
编译成功之后,装的时候有两个选择,这一步是新手最容易犯错的地方。
make install会安装所有东西,包括不带版本号后缀的命令别名。也就是说,它会在$prefix/bin下面创建python3和pip3。如果你的$prefix是/usr/local,而/usr/local/bin又在PATH里排在/usr/bin前面(很多发行版默认就是这样),那么系统里所有敲python3的人,拿到的都会是 3.9.2。这听起来好像是你想要的,但问题在于:某些系统脚本写死了用python3,某些包管理器工具在特定环节会调用它,它们从来没在 3.9 上测过。轻则行为异常,重则包管理直接罢工。
make altinstall只安装带版本号的命令:python3.9、pip3.9,不碰python3这个通用名字。这样装完之后,系统里同时存在两套解释器,谁也不干扰谁,你想用新的就显式敲python3.9。
所以除非你非常清楚自己在干什么、并且这台机器上python3本来就该指向新版本,否则一律用:
make altinstall这台内网服务器上我用的就是altinstall,装完之后python3 -V依然是 3.6.8,所有系统工具毫无感知,而python3.9 -V输出 3.9.2,业务脚本指定用后者跑。
3.5 装完必做的五项验证,一项都不能省
安装结束不等于成功,下面的检查按顺序做一遍:
# 1. 版本对不对 /usr/local/python3.9/bin/python3.9 -V # 2. 默认编码是不是 UTF-8 /usr/local/python3.9/bin/python3.9 -c "import sys; print(sys.getdefaultencoding(), sys.stdout.encoding)" # 3. 关键扩展模块能不能导入 /usr/local/python3.9/bin/python3.9 -c "import ssl, zlib, bz2, lzma, sqlite3, ctypes, readline, hashlib; print('all ok')" # 4. pip 能不能用 /usr/local/python3.9/bin/pip3.9 -V /usr/local/python3.9/bin/pip3.9 install --upgrade pip # 5. 联网能不能通(有网环境) /usr/local/python3.9/bin/python3.9 -c "import urllib.request; print(urllib.request.urlopen('https://www.python.org', timeout=5).status)"第 3 项是最关键的。我把日常最常用的几个 C 扩展模块一次性import了,任何一个失败,说明对应的系统依赖当初没装上,必须回去补依赖、make clean、重新编译。别想着"反正我现在不用 sqlite3"就跳过去,等到某天 Django 初始化数据库时报错,你还得再来一遍完整流程。
第 5 项如果报CERTIFICATE_VERIFY_FAILED,说明 SSL 模块编出来了但找不到根证书。这是另一个常见问题,后面 5.3 节会讲怎么处理。
4. 装完还差三件事:动态库路径、pip 配置、虚拟环境
4.1 开了 --enable-shared 就必须配动态链接器
如果你在 configure 阶段加了--enable-shared,现在敲一下python3.9:
/usr/local/python3.9/bin/python3.9 -V大概率会看到:
error while loading shared libraries: libpython3.9.so.1.0: cannot open shared object file: No such file or directory这不是装坏了,是系统的动态链接器不知道去/usr/local/python3.9/lib找这个库。解决办法是把这个路径注册进去:
echo "/usr/local/python3.9/lib" > /etc/ld.so.conf.d/python39.conf ldconfigldconfig会重建系统的动态库缓存。做完再跑一次python3.9 -V,应该就正常了。
还有一种不改系统配置的办法,用rpath在编译时就写死在二进制里。但那个需要在 configure 时设置LDFLAGS="-Wl,-rpath,/usr/local/python3.9/lib",对已经编完的这次来说来不及了,知道有这条路就行。
一个验证方法:
ldd /usr/local/python3.9/bin/python3.9 | grep libpython,能解析出实际路径就说明配置生效了。如果显示not found,说明ldconfig没执行或者路径写错了。
4.2 pip 源与全局配置,内网机器尤其重要
pip 默认走官方索引,在国内的网络条件下经常慢到让人怀疑人生,内网机器更是直接超时。配置一个网络条件更好的镜像源,是装完之后第一件该做的事。
配置文件的位置,按优先级从高到低:当前项目下的pip.conf、用户级的~/.config/pip/pip.conf(老版本是~/.pip/pip.conf)、全局的/etc/pip.conf。我一般配全局的,所有用户共享,省得每个人配一遍。
# /etc/pip.conf [global] index-url = <your-mirror-url>/simple trusted-host = <your-mirror-host> timeout = 60 retries = 3 [install] upgrade-strategy = only-if-neededindex-url具体指向哪家源,各家的公开文档里都写得很清楚,我这里只给配置结构,不贴具体域名。trusted-host这一项在镜像源用 HTTP 协议时是必需的,否则 pip 会因为证书校验失败拒绝安装。超时和重试次数调大一点,内网环境下的瞬时抖动很常见。
配置完验证一下:
/usr/local/python3.9/bin/pip3.9 config list /usr/local/python3.9/bin/pip3.9 install --upgrade pip第二条命令如果能在十几秒内完成,说明源配通了。如果卡在那里不动,先看trusted-host有没有配对,再看机器的出网策略。
4.3 venv 的标准用法,别把包装进解释器本体
源码编译装好的这个 Python,site-packages目录是干净的,只有 pip 和 setuptools。这个状态要保持住,别图省事直接往里pip install一堆业务依赖——一旦装进去,这个解释器就绑定了那套依赖,以后想给另一个项目用,依赖冲突会把你逼疯。
正确的姿势是每个项目一个虚拟环境:
# 创建 /usr/local/python3.9/bin/python3.9 -m venv /opt/venvs/data-clean # 激活 source /opt/venvs/data-clean/bin/activate python -V # 这里输出的 python 就是 3.9.2 # 装依赖 pip install -r requirements.txt # 退出 deactivate虚拟环境的路径我习惯放在/opt/venvs下面,跟项目代码分开,好处是环境可以复用、也可以整体删掉重建,不会跟代码目录混在一起。
有一个细节值得注意:python3.9 -m venv创建出来的环境里,python这个名字是指向 3.9.2 的软链。这是 venv 的机制,跟系统里的python没关系,激活状态下敲python就是新版本,deactivate之后就恢复原样。这也是为什么我一直推荐altinstall——它让"全局的 python3"和"环境里的 python"两件事彻底分开。
如果后面的业务要长期跑,环境建好之后记得把依赖冻结下来:
pip freeze > requirements.lockrequirements.txt写的是你要求的版本范围,requirements.lock记的是实际装上的精确版本。重装环境时用 lock 文件,能保证两次装出来的东西完全一致。
4.4 多版本共存与切换的几种做法
机器上同时存在 3.6、3.9 甚至更多版本是很正常的事,怎么优雅地切换,有几种做法。
最直接的是绝对路径调用,/usr/local/python3.9/bin/python3.9 script.py。笨但最可靠,脚本里、crontab 里、systemd 的ExecStart里我都推荐这种写法,因为它不依赖任何环境变量,谁来跑都一样。
稍微方便一点的是改PATH,把/usr/local/python3.9/bin加到用户级~/.bashrc里。但要注意altinstall装出来的命令名是python3.9,加不加PATH对你敲python3的结果没影响——这正好是隔离的好处。
如果机器上版本特别多、需要经常切,可以用update-alternatives统一管理:
update-alternatives --install /usr/local/bin/python3 python3 /usr/local/python3.9/bin/python3.9 30 update-alternatives --config python3最后这条命令会列出所有候选版本让你选。这套机制在 Debian 系上是标配,RHEL 系可能没有,需要手动建软链。要提醒一句:用update-alternatives把默认python3切到 3.9 之后,系统里所有依赖python3的脚本行为都会变,动手前先确认没有关键工具受影响。
5. 踩坑实录:从编译到运行的问题速查
5.1 模块缺失类问题,一张表查完
这类问题的表现高度一致:编译顺利结束,运行时报ModuleNotFoundError: No module named '_xxx'。根因永远是同一个——对应的系统开发包没装。下面这张表是我这些年攒下来的对照关系,遇到直接查。
| 报错信息 | 缺失的系统包(Debian 系) | 缺失的系统包(RHEL 系) |
|---|---|---|
No module named '_ssl' | libssl-dev | openssl-devel |
No module named '_ctypes' | libffi-dev | libffi-devel |
No module named 'zlib'/ zipimport 报错 | zlib1g-dev | zlib-devel |
No module named '_bz2' | libbz2-dev | bzip2-devel |
No module named '_lzma' | liblzma-dev | xz-devel |
No module named '_sqlite3' | libsqlite3-dev | sqlite-devel |
No module named 'readline',方向键打出^[[A | libreadline-dev | readline-devel |
No module named '_curses' | libncursesw5-dev | ncurses-devel |
No module named '_uuid' | uuid-dev | libuuid-devel |
No module named '_gdbm' | libgdbm-dev | gdbm-devel |
No module named 'tkinter' | tk-dev | tk-devel |
补装依赖之后必须重新configure和make,运行时pip install是补不回来的,因为这是编译期链接的 C 扩展。
有个容易误判的情况:
import _sqlite3成功了,但import sqlite3报错。前者是 C 扩展,后者是纯 Python 的封装层。如果扩展能导、封装层报错,通常是sys.path被污染了,检查一下有没有同名文件或者环境变量干扰。
5.2 编译期报错,多半是资源问题
gcc: internal compiler error: Killed (program cc1),翻译成人话就是编译器进程被系统的 OOM killer 强制杀掉了。这是编译 Python 最常见的失败原因,尤其是在内存不大的云主机上。处理办法就一个:调小-j的数值,或者临时加 swap。想知道是不是这个原因,dmesg | tail -20里能看到Out of memory: Killed process的记录。
configure: error: no acceptable C compiler found in $PATH,机器上连 gcc 都没有。装build-essential或者gcc-c++。
undefined reference to 'libintl_gettext'这类链接错误,通常是系统里装了多个版本的 gettext 或者编译选项冲突。处理办法是先make clean,然后检查LDFLAGS和CPPFLAGS环境变量里有没有残留的路径设置——很多时候是之前编译别的软件时export的变量忘了清,污染了这次编译。
带--enable-optimizations时在 PGO 阶段报测试失败,报错通常出现在make profile-opt那一步。原因可能是 locale 配置不完整、/tmp权限异常、或者某个测试用例在特定内核上不通过。最快的处理方式是去掉这个参数重来,先保证能装出来,性能优化后面再说。
5.3 运行期的几个诡异现象
pip报SSL: CERTIFICATE_VERIFY_FAILED。SSL 模块本身是好的,是找不到可信的根证书。检查/etc/ssl/certs/ca-certificates.crt存不存在,不存在就装ca-certificates这个包。如果存在还报错,可能是 Python 编译时没找到证书目录,可以用环境变量临时指过去:
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt内网自签证书的场景,把企业根证书追加到那个 crt 文件里就能解决。
中文输出乱码或者UnicodeEncodeError。这是 locale 没配好。检查locale命令的输出,如果LANG是空或者C,Python 的sys.stdout.encoding可能是ascii。解决办法是装好 locale 数据、设置LANG=en_US.UTF-8或者zh_CN.UTF-8并写进/etc/profile。如果是容器环境,还要注意镜像里可能根本没生成 locale 数据,需要locale-gen。
交互式解释器里方向键打出^[[A这种字符,历史记录也不能用。这就是 5.1 节表里那个 readline 的问题,装readline-devel重编。
python3.9启动特别慢,要好几秒。检查PYTHONHOME、PYTHONPATH这两个环境变量有没有被设置成奇怪的路径,导致解释器启动时去扫描一堆网络挂载的目录。我遇到过一次,PYTHONPATH指向了一个已经下线的 NFS 路径,每次启动都要等到超时。
make altinstall装完了,但敲python3.9提示command not found。这说明$prefix/bin不在PATH里。要么写全路径,要么在~/.bashrc里加上export PATH=/usr/local/python3.9/bin:$PATH。
6. 卸载、回滚与几条我觉得值得记下来的经验
6.1 干净卸载的完整步骤
源码编译装的 Python 没有登记在包管理器的数据库里,卸载只能手工来。按顺序做这几步,基本不会有残留。
先删软链和 alternatives 记录(如果配过):
update-alternatives --remove-all python3 # 只在用过 alternatives 时执行再删动态库配置(如果加过--enable-shared):
rm -f /etc/ld.so.conf.d/python39.conf ldconfig然后删安装目录:
rm -rf /usr/local/python3.9最后清掉PATH和PYTHONPATH里相关的配置,删掉/etc/pip.conf(如果这个 Python 是机器上唯一一个的话),再检查一下/opt/venvs下面有没有基于它的虚拟环境,那些也一并删掉——留着也没用,里面的bin/python是断链。
删完之后用find / -name "*python3.9*" 2>/dev/null扫一遍,看看有没有漏网的。
6.2 我踩过之后才明白的几件事
第一件,编译日志一定要留。tee build.log这个习惯,帮我省过至少三次重编。一次是半年后要排查某个模块的行为差异,翻日志发现当初_ssl链接的是 1.0.2 而不是预期的 1.1.1;一次是同事照着同样的步骤在另一台机器上装,某个模块没编出来,两边日志一对比,一眼看出是依赖包版本差异。日志不占多少空间,价值却很高。
第二件,虚拟环境不要建在用户家目录里。我以前习惯~/venv,结果机器换人接手、或者某个账号被清理,环境跟着没了,重建要重新装一遍依赖。放到/opt/venvs这种固定的、跟账号无关的位置,稳定得多。
第三件,--enable-shared这个决定要一次做对。如果一开始没加,后面发现某个工具需要动态库,唯一的办法是重新编译一遍。所以如果对后续用途有一点不确定,我倾向于直接加上,多配一次ldconfig的成本远低于重编一次。当然,如果确定是纯脚本环境,不加更省心。
第四件,给编译出来的这个 Python 在文档里留一个明确的记录。我的做法是在安装目录下放一个README.install,写清楚编译日期、configure 参数、依赖包版本、编译人。半年后接手的人(可能就是未来的你自己)看到这个文件,能省掉大量考古时间。
第五件,别在业务高峰期编译。--enable-optimizations那种跑满几个核心跑四十分钟的操作,如果在共享机器上做,隔壁跑着的服务会因为 CPU 抢占出现各种抖动,排查起来又是一堆误会。挑个空闲时段,或者干脆限一下并发数,安安静静地编完。
最后补一个小技巧,关于机器上存在多个源码编译 Python 时怎么快速确认自己在用哪个。在每个安装目录的bin下放一个可执行的whichpy.sh,内容是echo "$(dirname $(dirname $0))",敲whichpy.sh就直接告诉你是哪个前缀。比sys.prefix还直观一点,尤其在没有进入解释器的时候。
这套流程我在这台内网服务器上完整跑了一遍,从拉源码到业务脚本跑起来,实际耗时三十五分钟,其中二十分钟花在等--enable-optimizations的 PGO 阶段。业务方原本以为要重装系统才能解决,看到python3.9 -V输出 3.9.2 的时候还挺惊讶。实际上这件事的门槛从来不在命令本身,而在那些"装完了不能用"的细节上——依赖没装齐、动态库没配、证书找不到,每一个都能让你在装完之后还卡上一两个小时。把这些提前想清楚,整件事就变得很平淡了。