本文档是在实际经历了一整天的排错过程后整理而成。鸿蒙 PC 虽然对 Linux 生态兼容性在持续改善,但在底层细节(如 posix_spawn、dlopen)上仍有差异。遇到问题时,从"系统调用的行为是否与 Linux 一致"这个角度去排查,往往能找到突破口。
以 bcrypt 5.0.0 和cryptography移植为例:
适用场景:在 HarmonyOS(OpenHarmony)设备上,通过源码编译安装需要 Rust 编译器参与的 Python 第三方库。
目标读者:有基础 Linux/Python 使用经验,但不熟悉鸿蒙底层差异的开发者。
环境:HarmonyOS NEXT(API 15),aarch64 架构,Python 3.12,Rust 1.97
更多交流学习,欢迎加入开源鸿蒙PC社区 :https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目 :https://atomgit.com/OpenHarmonyPCDeveloper
猫哥的博客 :https://blog.csdn.net/qq8864
文章目录
-
- [0. 前置准备:Harmonybrew 环境搭建](#0. 前置准备:Harmonybrew 环境搭建)
-
- [0.1 卸载冲突软件](#0.1 卸载冲突软件)
- [0.2 打开安全开关](#0.2 打开安全开关)
- [0.3 安装 Harmonybrew(Homebrew)](#0.3 安装 Harmonybrew(Homebrew))
- [0.4 配置环境变量](#0.4 配置环境变量)
- [0.5 常用 brew 命令速查](#0.5 常用 brew 命令速查)
- [0.6 Harmonybrew 特色软件包](#0.6 Harmonybrew 特色软件包)
- [0.7 安装编译环境(编译 bcrypt 需要)](#0.7 安装编译环境(编译 bcrypt 需要))
- [0.8 验证整体环境](#0.8 验证整体环境)
- [1. 基础概念](#1. 基础概念)
-
- [什么是 bcrypt?](#什么是 bcrypt?)
- [什么是 setuptools-rust?](#什么是 setuptools-rust?)
- [2. 环境确认](#2. 环境确认)
-
- [2.1 确认 Rust 和 Python](#2.1 确认 Rust 和 Python)
- [2.2 记录 libpython 路径(后面用到)](#2.2 记录 libpython 路径(后面用到))
- [2.3 确认 setuptools-rust 已安装](#2.3 确认 setuptools-rust 已安装)
- [3. 核心疑问:必须要下载源码吗?](#3. 核心疑问:必须要下载源码吗?)
-
- 问题
- 答案:**不一定需要下载源码!分为两种情况**
- 那为什么我们之前手动下载了?
- [pip install --no-binary 的完整工作原理](#pip install --no-binary 的完整工作原理)
- 关键参数说明
- 什么时候需要手动下载源码?
- [4. 第一个坑:pip 找不到 rustc](#4. 第一个坑:pip 找不到 rustc)
- [5. 修复 setuptools-rust 的 _utils.py](#5. 修复 setuptools-rust 的 _utils.py)
- [6. 第二个坑:C 扩展找不到 Python C API 符号](#6. 第二个坑:C 扩展找不到 Python C API 符号)
- [7. 解决 abi3 问题:RUSTFLAGS(推荐) vs build.rs](#7. 解决 abi3 问题:RUSTFLAGS(推荐) vs build.rs)
- [8. 最终安装命令](#8. 最终安装命令)
-
- [方式 A(推荐):RUSTFLAGS 一条命令](#方式 A(推荐):RUSTFLAGS 一条命令)
- [方式 B:手动下载源码](#方式 B:手动下载源码)
- 编译输出关键行
- [验证 .so 的链接状态](#验证 .so 的链接状态)
- [9. 验证测试](#9. 验证测试)
- [10. 总结](#10. 总结)
- [11. 附:完整测试脚本](#11. 附:完整测试脚本)
- [12. cryptography 移植实战(maturin + OpenSSL)](#12. cryptography 移植实战(maturin + OpenSSL))
-
- [12.1 背景:cryptography 的构建方式](#12.1 背景:cryptography 的构建方式)
- [12.2 第 5 个坑:maturin 也找不到自己(pip 构建隔离问题)](#12.2 第 5 个坑:maturin 也找不到自己(pip 构建隔离问题))
- [12.3 第 6 个坑:编译时找不到 OpenSSL](#12.3 第 6 个坑:编译时找不到 OpenSSL)
- [12.4 第 7 个坑:链接阶段找不到 libpython3.12.so](#12.4 第 7 个坑:链接阶段找不到 libpython3.12.so)
- [12.5 第 8 个坑:运行时找不到 OpenSSL(OSSL_get_max_threads)](#12.5 第 8 个坑:运行时找不到 OpenSSL(OSSL_get_max_threads))
- [12.6 cryptography 完整安装命令(总结)](#12.6 cryptography 完整安装命令(总结))
- [12.7 验证测试](#12.7 验证测试)
- [12.8 踩坑清单速查](#12.8 踩坑清单速查)
- [附录:常见问题 FAQ](#附录:常见问题 FAQ)
-
- [Q1: 怎么知道一个包是否用了 setuptools-rust?](#Q1: 怎么知道一个包是否用了 setuptools-rust?)
- [Q2: 我怎么知道一个包使用了 abi3?(从而需要 RUSTFLAGS)](#Q2: 我怎么知道一个包使用了 abi3?(从而需要 RUSTFLAGS))
- [Q3: 怎么检查 .so 文件是否需要 libpython?](#Q3: 怎么检查 .so 文件是否需要 libpython?)
- [Q4: 其他 Python 包也适用这套步骤吗?](#Q4: 其他 Python 包也适用这套步骤吗?)
- [Q5: 如果升级 setuptools-rust,修改会被覆盖吗?](#Q5: 如果升级 setuptools-rust,修改会被覆盖吗?)
- [Q6: 为什么这个示例里没有 `setup.py`、`build.py`?其他文章说的 `pip wheel . --no-build-isolation -w dist/` 又是什么?](#Q6: 为什么这个示例里没有
setup.py、build.py?其他文章说的pip wheel . --no-build-isolation -w dist/又是什么?) -
- 各类构建配置文件的角色对比
- [为什么 bcrypt 不需要 `setup.py`?](#为什么 bcrypt 不需要
setup.py?) - [`pip wheel . --no-build-isolation -w dist/` 是什么?](#
pip wheel . --no-build-isolation -w dist/是什么?) - [为什么有 `build.rs` 但原版没有?](#为什么有
build.rs但原版没有?)
- [Q7: maturin 也要打 PATH 补丁吗?和 setuptools-rust 有什么不同?](#Q7: maturin 也要打 PATH 补丁吗?和 setuptools-rust 有什么不同?)
0. 前置准备:Harmonybrew 环境搭建
为什么需要 Harmonybrew? 鸿蒙 PC 没有 Linux/macOS 上的
apt、yum、brew等包管理器,也没有预装 Rust、Clang 等编译工具链。Harmonybrew 是专门为 OpenHarmony 移植的 Homebrew,提供了 Rust、Python、编译工具链等关键软件的鸿蒙适配版本。编译任何 Python C 扩展(包括 bcrypt)之前,必须先完成本节的环境搭建。
0.1 卸载冲突软件
如果 PC 中安装有 GitNext 和 DevBox 这两个应用,请将它们卸载。它们可能与 Harmonybrew 的工具链冲突。
0.2 打开安全开关
鸿蒙 PC 默认禁止运行未经签名的扩展程序,需要手动开启:
- 打开开发者选项:进入"设置"→"系统"→"开发者选项",打开"开发者选项"开关。
- 允许非应用市场扩展:进入"设置"→"隐私和安全"→"高级",打开"运行来自非应用市场的扩展程序"开关。
- 如果找不到"开发者选项":打开"设置"→"关于本机",找到"软件版本",连续点击 7 次,弹窗后点击"确认重启并开启"。
0.3 安装 Harmonybrew(Homebrew)
bash
zsh -c "$(curl -fsSL https://harmonybrew.atomgit.com/install.sh)"
安装过程中会提示输入确认信息,按提示操作即可。
0.4 配置环境变量
安装完成后,按照脚本输出的指引,将 Homebrew 加入到 PATH 中。
对于 zsh 用户:
bash
echo >> ~/.zshrc
echo 'eval "$(/storage/Users/currentUser/.harmonybrew/bin/brew shellenv)"' >> ~/.zshrc
eval "$(/storage/Users/currentUser/.harmonybrew/bin/brew shellenv)"
对于 mksh(鸿蒙 PC 默认 shell)用户:
bash
echo >> ~/.mkshrc
echo 'eval "$(/storage/Users/currentUser/.harmonybrew/bin/brew shellenv)"' >> ~/.mkshrc
eval "$(/storage/Users/currentUser/.harmonybrew/bin/brew shellenv)"
验证安装:
bash
brew --version
# Homebrew 4.x.x
0.5 常用 brew 命令速查
| 命令 | 说明 |
|---|---|
brew update |
更新 Homebrew 包管理器和包索引 |
brew formulae |
列出软件仓库中可用的软件包列表 |
brew search [keyword] |
在软件仓库中通过关键词搜索软件包 |
brew install [formula] |
安装软件包 |
brew uninstall [formula] |
卸载软件包 |
brew list |
查看已安装的软件包列表 |
rm -rf $(brew --cache) |
清除缓存 |
rm -rf /storage/Users/currentUser/.harmonybrew |
彻底删除 Homebrew 安装目录 |
0.6 Harmonybrew 特色软件包
Harmonybrew 除了包管理,还专门维护了一系列针对鸿蒙平台适配的特色软件包,解决了系统环境中的诸多限制(如代码签名、平台标识等)。以下是编译 Python C 扩展相关的关键包:
| 名称 | 说明 | 与本教程的关系 |
|---|---|---|
| ohos-sdk | 鸿蒙原生 SDK,包含 clang、llvm-ar 等工具。其中的 lld 链接器经过封装,默认启用代码签名,编译出的二进制可直接运行。 |
✅ 编译 Rust/Python C 扩展必需 |
| llvm-gcc-compat | 生成 cc、gcc、ld 等软链接,全部指向 ohos-sdk 中的 LLVM 工具链。无需手动指定 CC、CXX 等环境变量。 |
✅ rustc 默认用 cc 作为 linker,装上它就不用配 config.toml |
| devel-base | 类似 Debian 的 build-essential,级联依赖安装 ohos-sdk、llvm-gcc-compat、make、coreutils 等。 |
✅ 一键装完整个编译环境 |
| uname-is-linux | 系统标识伪装工具,劫持 uname() 函数使系统标识始终为 Linux。编译传统 C/C++ 软件时常用。 |
❌ 本教程不需要 |
| ohos-pip-autosign | 自动对 pip 包中的 .so 文件进行代码签名。鸿蒙 PC 要求所有加载的 .so 必须有签名,否则 Python import 会报错。 |
✅ pip 安装 Python C 扩展必需 |
| musl-compat | 为 OpenHarmony 补充 musl libc 缺失符号的兼容垫片库。 | ⚠️ Python 3.13+ 可能需要 |
代码签名是什么? 鸿蒙 PC 有一个安全机制:所有可执行文件和动态库(
.so)必须带有数字签名才能加载运行。ohos-pip-autosign在 pip 安装过程中自动补全签名,否则import时会失败。这就是为什么在鸿蒙 PC 上装 Python 三方库需要额外配签名工具。
0.7 安装编译环境(编译 bcrypt 需要)
按以下顺序,通过 Harmonybrew 安装编译 Python C 扩展所需的全部工具链。
① 安装基础编译工具链
bash
brew install -y devel-base
这会级联安装 ohos-sdk、llvm-gcc-compat、make、coreutils 等。安装后验证:
bash
cc --version # → 指向 ohos-sdk 里面的 clang
ld --version # → 经过签名的 lld 链接器
② 安装 Rust
bash
brew install -y rust
# 验证
rustc --version # rustc 1.x.x
cargo --version # cargo 1.x.x
which rustc # /storage/Users/currentUser/.harmonybrew/bin/rustc
注意 :鸿蒙 PC 没有
rustup(Rust 版本管理工具),Rust 是通过 Harmonybrew 直接安装的。which rustup返回not found是正常的。
③ 安装 Python 和自动签名工具
Harmonybrew 提供的 Python 已将平台三元组硬编码为 aarch64-linux-musl,这使 pip 可以直接下载 PyPI 上已编译好的原生二进制包(如 numpy、scipy 等)。ohos-pip-autosign 会自动为 pip 安装的 .so 文件补全代码签名。
安装 Python 和签名工具:
bash
brew install -y python ohos-pip-autosign
# 验证
python3 --version # Python 3.x.x
pip3 --version # pip 24.x.x
建议:使用虚拟环境(venv)隔离项目依赖。venv 是 Python 的标准虚拟环境工具,创建一个隔离的 Python 环境,避免不同项目之间依赖冲突。后续所有 pip 操作都应在 venv 中进行。
创建并进入 venv(可选但推荐):
bash
# 在项目的上级目录创建虚拟环境(例如 ~/projects/bcrypt-env)
python3 -m venv ~/projects/bcrypt-env
# 激活虚拟环境(每次新开终端都需要执行这一步)
source ~/projects/bcrypt-env/bin/activate
# 激活后,终端提示符会变成 (bcrypt-env) $,且 python3/pip3 都指向 venv 内的版本
激活自动签名工具(与 venv 搭配使用):
bash
# 在 venv 激活状态下执行,确保自动签名与 venv 环境绑定
ohos-pip-autosign activate
激活后,后续在该终端中执行 pip install 时,所有下载的 .so 文件都会自动补全代码签名。
如果不使用 venv ,直接在系统 Python 中执行
ohos-pip-autosign activate也可行,但建议先用 venv 隔离,避免依赖冲突。
为什么要用 venv?
| 场景 | 无 venv | 有 venv |
|---|---|---|
| 依赖隔离 | 所有项目共用一套包,版本冲突需手动解决 | 每个项目独立依赖,互不影响 |
| 权限问题 | pip install 全局安装可能需要提权 | venv 内的 pip install 无需提权 |
| 清理 | 卸载困难,需逐一手动 pip uninstall | 删掉 venv 目录即可彻底清除 |
④ 安装编译 bcrypt 所需的 Python 构建依赖
bash
# setuptools-rust:pip 安装 Rust 扩展时调用 cargo 编译的桥梁
pip3 install setuptools-rust
0.8 验证整体环境
bash
echo "=== 编译工具链 ===" && cc --version && echo "=== Rust ===" && rustc --version && cargo --version && echo "=== Python ===" && python3 --version && pip3 --version && pip3 list 2>/dev/null | grep setuptools-rust
全部通过后,你的鸿蒙 PC 就已经具备了编译 Rust 类 Python C 扩展的完整条件。
1. 基础概念
什么是 bcrypt?
bcrypt 是一个 Python 密码哈希库,用于安全地存储用户密码。它基于 Blowfish 密码算法,内置 salt 和可调节的计算成本(rounds 参数)。
为什么需要 C 扩展? bcrypt 5.0.0 完全用 Rust 编写核心逻辑,通过 PyO3 框架生成 Python C 扩展(.so 文件),性能远优于纯 Python 实现。
什么是 setuptools-rust?
setuptools-rust 是一个 Python 打包工具插件,它的作用是:
Python 包(.whl)
│
├── Python 源码 → 由 setuptools 处理(纯 Python 部分)
│
└── Rust 扩展 → 由 setuptools-rust 处理
├── 调用 cargo/rustc 编译 Rust 代码
├── 生成 .so(共享库)文件
└── 放到 Python 包的对应位置
简单说,setuptools-rust 是 pip install 过程中负责编译 Rust 代码的"桥梁" 。当 pyproject.toml 中有 [[tool.setuptools-rust.ext-modules]] 配置时,pip 安装过程会:
- 下载依赖(setuptools、setuptools-rust、wheel)
- setuptools 解析
pyproject.toml - setuptools-rust 调用
cargo build编译 Rust 代码 - 把生成的
.so文件复制到 Python 包的安装目录 - 打包成 wheel 安装
--no-build-isolation 的作用 :默认情况下 pip 会在一个隔离的临时环境中安装构建依赖。加上 --no-build-isolation 后,pip 会使用当前 Python 环境已安装的包,而不是下载一套新的。这在我们修改了 setuptools-rust 源码后尤为重要。
2. 环境确认
如果你已完成 [第 0 节](#第 0 节),以下内容已就绪。本节只做快速确认,并记录后面多次用到的关键路径。
2.1 确认 Rust 和 Python
bash
rustc --version # rustc 1.x.x
python3 --version # Python 3.12.x
2.2 记录 libpython 路径(后面用到)
bash
python3 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))"
# /storage/Users/currentUser/usr/local/lib
这个路径在[第 7 节](#第 7 节)的 RUSTFLAGS 中会用到。
2.3 确认 setuptools-rust 已安装
bash
pip3 list 2>/dev/null | grep setuptools-rust
# setuptools-rust x.x.x
3. 核心疑问:必须要下载源码吗?
问题
使用
pip install --no-binary bcrypt bcrypt --no-build-isolation不是会自动下载源码并触发编译吗?为什么还要手动下载源码?
答案:不一定需要下载源码!分为两种情况
你的理解是完全正确的。pip install --no-binary bcrypt bcrypt 这个命令会:
- pip 检查 PyPI 上 bcrypt 的可用分发包
--no-binary bcrypt告诉 pip:不要下载预编译的 .whl 文件 ,只下载源码包(.tar.gz)- pip 自动下载源码到临时目录,解压,触发编译,安装
- 安装完成后,临时目录会被自动删除
所以,对于大多数"一键安装"的场景,是完全不需要手动下载源码的。
那为什么我们之前手动下载了?
因为我们遇到了第 7 节 要讲的第二个坑(abi3 符号可见性问题),需要给 bcrypt 的 Cargo 项目添加一个链接参数。
有两种修复方式:
| 方式 | 需要手动下载源码? | 原理 | 推荐度 |
|---|---|---|---|
| A. RUSTFLAGS 环境变量 | ❌ 不需要 | 通过环境变量传给 cargo,cargo 再传给 rustc 链接器 | ⭐ 推荐 |
| B. 修改源码加 build.rs | ✅ 需要下载源码 | 在项目里创建 build.rs,cargo 自动读取 | 仅用于永久性修复 |
方式 A 才是正确的做法,一条命令搞定,无需下载源码。我们当时绕了弯路。
pip install --no-binary 的完整工作原理
bash
pip install --no-binary bcrypt bcrypt --no-build-isolation -v
执行这条命令后,pip 内部做的事情:
pip install --no-binary bcrypt bcrypt --no-build-isolation -v
│
├── ① 查询 PyPI 上 bcrypt 的可用版本
│ └── 找到 bcrypt-5.0.0(源码 .tar.gz)
│
├── ② 下载 bcrypt-5.0.0.tar.gz 到临时目录
│ └── /tmp/pip-xxx/bcrypt-5.0.0/
│
├── ③ 解压源码,读取 pyproject.toml
│ └── 发现 [build-system] 中有 setuptools-rust
│
├── ④ 从 pyproject.toml 读取 setuptools-rust 配置
│ └── [[tool.setuptools-rust.ext-modules]]
│ target = "bcrypt._bcrypt"
│ path = "src/_bcrypt/Cargo.toml"
│
├── ⑤ setuptools-rust 调用 cargo build
│ └── cargo 编译 Rust 代码 → 生成 .so 文件
│
├── ⑥ setuptools 把 .so 复制到 Python 包目录
│ └── bcrypt/_bcrypt.cpython-312-aarch64-linux-ohos.so
│
└── ⑦ 安装到 site-packages,清理临时目录
└── pip install 完成
关键参数说明
| 参数 | 作用 | 为什么需要 |
|---|---|---|
--no-binary bcrypt |
不要预编译的 .whl,下载源码编译 | 因为 PyPI 上没有鸿蒙 aarch64 的预编译包 |
--no-build-isolation |
使用当前环境的构建依赖 | 因为我们修改了 setuptools-rust 源码,隔离环境里没有这个修改 |
-v(verbose) |
显示详细编译日志 | 方便调试时看到 cargo 输出和错误 |
什么时候需要手动下载源码?
只有以下两个场景需要手动下载:
- 需要永久性修改包的源码(如添加 build.rs、修改 Cargo.toml)
- 需要离线安装(目标机器没有网络)
手动下载的方法:
bash
mkdir -p ~/python-packages/bcrypt_src && cd ~/python-packages/bcrypt_src
pip3 download bcrypt==5.0.0 --no-binary bcrypt --no-deps
tar xzf bcrypt-5.0.0.tar.gz
cd bcrypt-5.0.0
4. 第一个坑:pip 找不到 rustc
现象
执行安装命令:
bash
pip3 install . --no-build-isolation
报错:
cargo rustc --lib --manifest-path src/_bcrypt/Cargo.toml ...
error: failed to run `rustc` to learn about target-specific attributes
Caused by:
No such file or directory (os error 2)
或者:
Error: Failed to find rustc. Check that the Rust toolchain is installed.
原因分析
鸿蒙系统的 os.posix_spawn(Python subprocess 模块在 Linux 上的底层实现)不搜索 PATH 环境变量 。即使 PATH 环境变量中包含了 rustc 的目录,subprocess 仍然会报 FileNotFoundError。
正常情况下在 Linux 上:
python
import subprocess
# Linux:subprocess 会搜索 PATH,找到 rustc
subprocess.run(["rustc", "--version"], env={"PATH": "/usr/bin:/storage/.../bin"}) # ✅ 成功
鸿蒙上的行为:
python
import subprocess
# 鸿蒙:os.posix_spawn 不搜索 PATH,直接报错
subprocess.run(["rustc", "--version"], env={"PATH": "/usr/bin:/storage/.../bin"}) # ❌ 失败
# FileNotFoundError: [Errno 2] No such file or directory: 'rustc'
setuptools-rust 在调用子进程编译 Rust 时,使用的是 subprocess.check_output 和 subprocess.run,传入的可执行文件名为 "rustc"(不带路径),期望操作系统从 PATH 中查找。这个行为在标准 Linux 上没问题,但在鸿蒙上失败了。
快速验证方法
bash
# 验证 PATH 设置了但还是找不到
python3 -c "
import subprocess, os
# 设置 PATH
env = os.environ.copy()
env['PATH'] = '/storage/Users/currentUser/.harmonybrew/bin:' + env.get('PATH', '')
# 方法一:用绝对路径(能成功)
result = subprocess.run(
['/storage/Users/currentUser/.harmonybrew/bin/rustc', '--version'],
capture_output=True, text=True, env=env
)
print('绝对路径:', result.stdout)
# 方法二:只用名字(鸿蒙上会失败)
try:
result = subprocess.run(
['rustc', '--version'],
capture_output=True, text=True, env=env
)
print('相对路径:', result.stdout)
except FileNotFoundError as e:
print('相对路径失败:', e)
"
5. 修复 setuptools-rust 的 _utils.py
原理
我们需要修改 setuptools-rust 中所有调用子进程的地方,在传入 rustc 或 cargo 之前先用 shutil.which() 解析出完整的绝对路径。
修改的文件
找到 setuptools-rust 的 _utils.py:
bash
# 找到安装路径
python3 -c "import setuptools_rust._utils; print(setuptools_rust._utils.__file__)"
# /storage/Users/currentUser/usr/local/lib/python3.12/site-packages/setuptools_rust/_utils.py
具体修改
在文件开头附近添加辅助函数 _resolve_executable ,然后在 run_subprocess 和 check_subprocess_output 两个函数中调用它。
原始的 _utils.py(只显示关键部分):
python
import os
import shutil
import subprocess
from typing import Any, Optional, Union, cast
def run_subprocess(
*args: Any, env: Union[Env, dict[str, str], None], **kwargs: Any
) -> subprocess.CompletedProcess:
"""Wrapper around subprocess.run that requires a decision to pass env."""
if isinstance(env, Env):
env = env.env
kwargs["env"] = env
return subprocess.run(*args, **kwargs)
def check_subprocess_output(
*args: Any, env: Union[Env, dict[str, str], None], **kwargs: Any
) -> str:
"""Wrapper around subprocess.run that requires a decision to pass env."""
if isinstance(env, Env):
env = env.env
kwargs["env"] = env
return cast(str, subprocess.check_output(*args, **kwargs))
修改后的 _utils.py (新增的代码用 ← 新增 标出):
python
import os
import shutil
import subprocess
from typing import Any, Optional, Union, cast
def _resolve_executable( # ← 新增
cmd: Any, env: Optional[dict[str, str]] # ← 新增
) -> Any: # ← 新增
"""Resolve the first argument to an absolute path using shutil.which().""" # ← 新增
if isinstance(cmd, (list, tuple)) and cmd: # ← 新增
exe = cmd[0] # ← 新增
if isinstance(exe, str) and os.sep not in exe: # ← 新增
resolved = shutil.which(exe, path=env.get("PATH") if env else None) # ← 新增
if resolved: # ← 新增
cmd = list(cmd) # ← 新增
cmd[0] = resolved # ← 新增
return cmd # ← 新增
def run_subprocess(
*args: Any, env: Union[Env, dict[str, str], None], **kwargs: Any
) -> subprocess.CompletedProcess:
"""Wrapper around subprocess.run that requires a decision to pass env."""
if isinstance(env, Env):
env = env.env
kwargs["env"] = env
args = tuple(_resolve_executable(a, env) for a in args) # ← 修改:解析绝对路径
return subprocess.run(*args, **kwargs)
def check_subprocess_output(
*args: Any, env: Union[Env, dict[str, str], None], **kwargs: Any
) -> str:
"""Wrapper around subprocess.run that requires a decision to pass env."""
if isinstance(env, Env):
env = env.env
kwargs["env"] = env
args = tuple(_resolve_executable(a, env) for a in args) # ← 修改:解析绝对路径
return cast(str, subprocess.check_output(*args, **kwargs))
代码逐行解读
| 代码 | 解释 |
|---|---|
if isinstance(cmd, (list, tuple)) and cmd: |
确保 cmd 是非空列表/元组,subprocess.run(["rustc", ...]) 中的 ["rustc", ...] |
exe = cmd[0] |
取第一个元素,即可执行文件名,如 "rustc" |
if isinstance(exe, str) and os.sep not in exe: |
os.sep not in exe 判断是否不包含 / 。"rustc" 不包含 /,需要解析;"/usr/bin/rustc" 包含 /,已经是绝对路径,跳过 |
shutil.which(exe, path=...) |
在指定的 PATH 中查找可执行文件,返回第一个找到的绝对路径,找不到返回 None |
cmd = list(cmd); cmd[0] = resolved |
把裸命令名替换为绝对路径,如 ["rustc", ...] → ["/storage/.../rustc", ...] |
args = tuple(...) |
把修改后的参数重新打包成元组,传给 subprocess.run |
修改后验证
bash
python3 -c "
import subprocess, os
# 现在即使只用名字,也能找到 rustc 了
env = os.environ.copy()
result = subprocess.run(
['rustc', '--version'],
capture_output=True, text=True, env=env
)
print('找到 rustc:', result.stdout.strip())
"
6. 第二个坑:C 扩展找不到 Python C API 符号
现象
经过第一步修复后,cargo build 能成功编译,生成的 _bcrypt.cpython-312-aarch64-linux-ohos.so 也存在。但运行时:
python
import bcrypt
报错:
ImportError: .../bcrypt/_bcrypt.cpython-312-aarch64-linux-ohos.so:
undefined symbol: PyBytes_Type
或者:
undefined symbol: PyEval_SaveThread
这些 Py* 开头的符号都是 Python C API ,由 libpython3.12.so 提供。
原因分析
bcrypt 的 Rust C 扩展使用了 PyO3 的 abi3 特性 (查看 src/_bcrypt/Cargo.toml):
toml
[dependencies]
pyo3 = { version = "0.26", features = ["abi3"] }
什么是 abi3?
abi3 = Application Binary Interface,版本 3(对应 Python 3.2+)。它的核心思想是:编译出的 .so 文件不链接 libpython3.12.so ,只暴露 C API 符号的引用,期望运行时从进程的全局符号表中解析。
这样做的好处:
- 编译出的
.so可以在多个 Python 3.x 版本间通用(3.8~3.14 都能用) - 不需要在系统上安装特定版本的 libpython-dev
但这也意味着:任何提供全局符号表的东西,必须包含 Python C API 符号。
为什么标准 Linux 上能工作?
在标准 Linux 上,python3 可执行文件在启动时用 -Xlinker -export-dynamic 链接,会把自己链接的 libpython3.12.so 中的所有符号导出到全局符号表。当 Python 用 dlopen() 加载 .so 时,.so 引用的 Py* 符号都能从主程序的全局符号表中找到。
为什么鸿蒙上失败?
鸿蒙的 dlopen 实现与 glibc 的 dlopen 行为不同:
- glibc Linux:
dlopen默认使用RTLD_LAZY | RTLD_GLOBAL,主程序符号对动态库可见 - 鸿蒙:
dlopen不自动搜索主程序的符号表,.so引用的 Py* 符号解析不到
🔬 这个差异是鸿蒙对 Linux 兼容性的一个典型边界案例。鸿蒙的 musl-libc 变种与标准 glibc 在动态链接器行为上存在差异。
临时解决方案(验证用)
bash
# 用 LD_PRELOAD 强制预加载 libpython3.12.so
LD_PRELOAD=/storage/Users/currentUser/usr/local/lib/libpython3.12.so \
python3 -c "import bcrypt; print('OK')"
如果这样能成功,说明问题确实出在符号解析上。请在实际操作前先执行这个验证,确认你的环境的确有这个表现。
7. 解决 abi3 问题:RUSTFLAGS(推荐) vs build.rs
解决这个问题有两种方式:
方式 A(推荐):使用 RUSTFLAGS 环境变量
不需要下载源码,一条命令搞定。
原理:通过 RUSTFLAGS 环境变量,告诉 rustc 链接器:链接 libpython3.12.so。
bash
PYO3_PYTHON=/storage/Users/currentUser/usr/local/bin/python3.12 \
RUSTFLAGS="-L /storage/Users/currentUser/usr/local/lib -l python3.12" \
pip install --no-binary bcrypt bcrypt --no-build-isolation -v
RUSTFLAGS 各参数含义:
| 参数 | 作用 | 解释 |
|---|---|---|
-L /storage/.../lib |
添加库搜索路径 | 告诉 rustc 到哪里找 libpython3.12.so |
-l python3.12 |
链接指定的库 | 告诉链接器生成 NEEDED 条目,引用这个库 |
生成的 .so 文件中会多一个 NEEDED 条目:
NEEDED libpython3.12.so ← 新增,之前没有
NEEDED libc.so
NEEDED libgcc_s.so.1
这样动态加载器在加载 .so 时,会自动先加载 libpython3.12.so,所有 Py* 符号都能解析到。
为什么这是推荐方案?
- 不需要下载源码 --- pip 直接从 PyPI 拉取源码编译
- 不需要修改任何文件 --- 所有配置通过环境变量传递
- 通用性强 --- 对任何使用 PyO3 abi3 的包都有效
- 不产生遗留 --- 安装完成后,不需要清理任何修改
方式 B:手动下载源码 + 添加 build.rs
适用于需要永久性保留修改的场景(比如你要打包成自己的发行版)。
步骤 1:下载源码
bash
cd ~/python-packages/bcrypt_src
pip3 download bcrypt==5.0.0 --no-binary bcrypt --no-deps
tar xzf bcrypt-5.0.0.tar.gz
cd bcrypt-5.0.0
步骤 2:创建 build.rs
在 src/_bcrypt/ 目录下创建 build.rs:
rust
// src/_bcrypt/build.rs
fn main() {
// 打印 cargo 指令,告诉链接器链接 libpython3.12.so
println!("cargo:rustc-link-lib=python3.12");
println!("cargo:rustc-link-search=/storage/Users/currentUser/usr/local/lib");
}
为什么叫 build.rs? 这是 Cargo 的约定:项目根目录或
src/_bcrypt/下的build.rs文件,会在编译主 crate 之前被 Cargo 自动编译和执行。这个文件通过println!("cargo:...")向 Cargo 发出指令。
步骤 3:安装
bash
PYO3_PYTHON=/storage/Users/currentUser/usr/local/bin/python3.12 \
pip3 install . --no-build-isolation -v
两种方式对比:
| 对比维度 | 方式 A:RUSTFLAGS | 方式 B:build.rs |
|---|---|---|
| 下载源码 | ❌ 不需要 | ✅ 需要 |
| 修改文件 | ❌ 不需要 | ✅ 需要创建 build.rs |
| 命令行长度 | 略长(RUSTFLAGS 较长) | 短 |
| 永久性 | 每次安装都要带 RUSTFLAGS | 修改保留在源码中 |
| 通用性 | 对所有 abi3 包通用 | 每改一个包都要加 build.rs |
| 适用场景 | 日常安装 | 打包/发行 |
8. 最终安装命令
方式 A(推荐):RUSTFLAGS 一条命令
bash
# 第一步(只需做一次):修复 setuptools-rust 的 PATH 问题
# 手动编辑 _utils.py(见第 5 节)
# 第二步(每次安装):一条命令搞定
PYO3_PYTHON=/storage/Users/currentUser/usr/local/bin/python3.12 \
RUSTFLAGS="-L /storage/Users/currentUser/usr/local/lib -l python3.12" \
pip install --no-binary bcrypt bcrypt --no-build-isolation -v
方式 B:手动下载源码
bash
# 第一步:修复 setuptools-rust
# 第二步:下载源码,创建 build.rs
cd ~/python-packages/bcrypt_src
pip3 download bcrypt==5.0.0 --no-binary bcrypt --no-deps
tar xzf bcrypt-5.0.0.tar.gz
cd bcrypt-5.0.0
cat > src/_bcrypt/build.rs << 'EOF'
fn main() {
println!("cargo:rustc-link-lib=python3.12");
println!("cargo:rustc-link-search=/storage/Users/currentUser/usr/local/lib");
}
EOF
# 第三步:安装
PYO3_PYTHON=/storage/Users/currentUser/usr/local/bin/python3.12 \
pip3 install . --no-build-isolation -v
编译输出关键行
成功时你会看到类似这样的输出:
cargo rustc --lib ... --release -v ...
Compiling pyo3 v0.26.5
Compiling bcrypt-rust v0.1.0
Running `rustc --crate-name bcrypt_rust ... -L /storage/Users/currentUser/usr/local/lib -l python3.12`
Finished release profile [optimized] target(s) in 23.90s
Copying rust artifact from .../target/release/libbcrypt_rust.so
to .../bcrypt/_bcrypt.cpython-312-aarch64-linux-ohos.so
Successfully built bcrypt
Installing collected packages: bcrypt
Successfully installed bcrypt-5.0.0
注意看 rustc 命令最后有没有 -L /storage/.../lib -l python3.12 ------ 这说明链接参数生效了。
验证 .so 的链接状态
bash
# 检查生成的 .so 是否有 python3.12 依赖
python3 -c "
import bcrypt
so_path = bcrypt.__file__.replace('__init__.py', '_bcrypt.cpython-312-aarch64-linux-ohos.so')
with open(so_path, 'rb') as f:
data = f.read()
if b'python3.12' in data or b'libpython' in data:
print('✅ .so 包含 libpython3.12 依赖')
else:
print('⚠️ .so 不包含 libpython3.12 依赖,可能需要 LD_PRELOAD')
"
9. 验证测试
安装完成后,立即测试:
bash
python3 -c "
import bcrypt
# 基本信息
print('版本:', bcrypt.__version__)
# 生成 salt 并哈希密码
password = b'mysecretpassword'
salt = bcrypt.gensalt(rounds=10)
print('Salt:', salt)
hashed = bcrypt.hashpw(password, salt)
print('哈希值:', hashed)
# 验证密码
assert bcrypt.checkpw(password, hashed), '验证失败!'
print('密码验证:通过')
# 验证错误密码被拒绝
assert not bcrypt.checkpw(b'wrongpassword', hashed), '错误密码应该被拒绝!'
print('错误密码拒绝:通过')
print()
print('所有测试通过!')
"
成功输出示例:
版本: 5.0.0
Salt: b'\$2b\$10\$KVEUkvExSTKPH/3PtXqZfe'
哈希值: b'\$2b\$10\$KVEUkvExSTKPH/3PtXqZfeVGOzG1kmS./M6sVZZ4m62E5sNtStiN6'
密码验证:通过
错误密码拒绝:通过
所有测试通过!
10. 总结
鸿蒙安装 Python C 扩展的检查清单
| 步骤 | 检查项 | 常见问题 |
|---|---|---|
| ① | Rust 工具链是否安装? | rustc --version 和 cargo --version |
| ② | PATH 是否包含 Rust 目录? | `echo $PATH |
| ③ | 构建后端是否已打 PATH 补丁? | setuptools-rust 改 _utils.py,maturin 改 __init__.py |
| ④ | 如果使用 maturin:maturin/__init__.py 是否已修复? |
检查子进程调用处是否用了绝对路径 |
| ⑤ | 是否设置了 RUSTFLAGS?(对有 abi3 的包) |
-L <libdir> -l python3.12 |
| ⑥ | 是否指定了 PYO3_PYTHON? |
避免指向错误的 Python 版本 |
| ⑦ | 是否使用了 --no-build-isolation? |
让 pip 使用当前环境的修改后的包 |
| ⑧ | 有本地 C 依赖的包:是否安装了对应库? | openssl → brew install openssl + PKG_CONFIG_PATH |
| ⑨ | 链接阶段:是否设置了 LIBRARY_PATH? |
找不到 libpython3.12 时使用 |
| ⑩ | 运行时:本地 C 库是否在 LD_LIBRARY_PATH 中? |
运行时 undefined symbol 时使用 |
核心教训
-
鸿蒙 ≠ Linux 。虽然鸿蒙声称兼容 Linux,但底层系统调用(如
posix_spawn)和动态链接器(dlopen)的行为有差异。 -
abi3+ 鸿蒙 = 需要显式链接libpython。可以通过RUSTFLAGS环境变量解决,不需要下载源码或修改代码。 -
PATH 不自动搜索 。鸿蒙上
subprocess.run(['rustc', ...])会失败,但subprocess.run(['/full/path/rustc', ...])成功。任何涉及子进程调用的 Python 库都可能受影响。 -
--no-build-isolation的重要性。在需要修改 pip 依赖源码的场景下,必须用这个参数,否则 pip 会在隔离环境重新下载未修改的依赖。 -
先尝试环境变量,再考虑修改源码 。
RUSTFLAGS、LD_PRELOAD、PYO3_PYTHON等环境变量往往不需要修改任何源码就能解决问题。 -
本地 C 库的版本分裂 (编译时 vs 运行时)。编译时通过
PKG_CONFIG_PATH找到 OpenSSL 3.x,但运行时动态链接器可能优先加载系统自带的旧版 OpenSSL。必须用LD_LIBRARY_PATH确保运行时也使用正确的库。 -
maturin也需要打 PATH 补丁 。和 setuptools-rust 的_resolve_executable一样,maturin 的__init__.py中的子进程调用也需要把"maturin"替换为绝对路径。不使用--no-build-isolation还无法应用这个补丁。 -
模块化问题排查 。cryptography 的移植暴露出四个独立阶段的问题:构建环境(maturin)、编译链接(OpenSSL + libpython3.12)、运行时动态库搜索(LD_LIBRARY_PATH)。每个阶段有独立的错误特征和解决方式,需要分阶段诊断。
其他可能出问题的包
只要一个 Python 包满足以下条件,就可能遇到类似问题:
- 包含 Rust C 扩展(使用 PyO3、maturin、setuptools-rust 等构建后端)
- 使用 PyO3 的 abi3 特性
- 需要 调用子进程编译 (任何构建工具都可能,鸿蒙上
posix_spawn不搜索 PATH)
典型例子:cryptography(maturin + OpenSSL)、tokenizers(Hugging Face)、orjson、pydantic-core、ruff 等。
常见的构建后端:
| 构建后端 | 相关问题 | 修复方式 |
|---|---|---|
| setuptools-rust | 子进程找不到 rustc |
修改 _utils.py 中的 _resolve_executable |
| maturin | 子进程找不到 maturin 自身 |
修改 __init__.py 中的子进程调用路径 |
11. 附:完整测试脚本
保存为 test_bcrypt.py:
python
#!/usr/bin/env python3
"""
bcrypt 5.0.0 功能测试脚本
在 HarmonyOS 上验证编译安装的 bcrypt C 扩展是否正常工作
"""
import bcrypt
def test_bcrypt_basic():
"""基本功能测试:生成 salt、哈希、验证"""
print("=" * 60)
print("bcrypt 基本功能测试")
print("=" * 60)
# 包信息
print(f"版本: {bcrypt.__version__}")
print(f"位置: {bcrypt.__file__}")
print()
password = b"my_secret_password_123"
# 测试 gensalt
print("[测试 1] gensalt() - 生成 salt")
salt = bcrypt.gensalt()
print(f" Salt: {salt}")
assert salt.startswith(b"$2b$"), f"Sail 应以 $2b$ 开头"
print(f" 通过")
print()
# 测试 hashpw
print("[测试 2] hashpw() - 哈希密码")
hashed = bcrypt.hashpw(password, salt)
print(f" 原始密码: {password}")
print(f" 哈希值: {hashed}")
assert hashed.startswith(b"$2b$"), f"哈希值应以 $2b$ 开头"
print(f" 通过")
print()
# 测试 checkpw(正确密码)
print("[测试 3] checkpw() - 验证正确密码")
result = bcrypt.checkpw(password, hashed)
print(f" 验证结果: {result}")
assert result is True, "正确密码应返回 True"
print(f" 通过")
print()
# 测试 checkpw(错误密码)
print("[测试 4] checkpw() - 拒绝错误密码")
wrong_result = bcrypt.checkpw(b"wrong_password", hashed)
print(f" 错误密码验证结果: {wrong_result}")
assert wrong_result is False, "错误密码应返回 False"
print(f" 通过")
print()
print("✓ 基本功能全部通过!")
def test_bcrypt_rounds():
"""测试不同的 rounds 参数"""
print("=" * 60)
print("rounds 参数测试(不同成本因子)")
print("=" * 60)
password = b"test_password"
for rounds in [4, 8, 12]:
salt = bcrypt.gensalt(rounds=rounds)
hashed = bcrypt.hashpw(password, salt)
verified = bcrypt.checkpw(password, hashed)
# 从哈希值中提取 rounds 确认
parts = hashed.split(b"$")
actual_rounds = int(parts[2])
status = "✓" if verified else "✗"
print(f" [{status}] rounds={rounds:2d}, 哈希中 rounds={actual_rounds:2d}, 验证={verified}")
assert verified, f"rounds={rounds} 验证失败"
assert actual_rounds == rounds, f"rounds 不一致:预期 {rounds},实际 {actual_rounds}"
print()
print("✓ rounds 参数测试全部通过!")
def test_bcrypt_prefix():
"""测试不同的 prefix 参数"""
print("=" * 60)
print("prefix 参数测试(不同版本前缀)")
print("=" * 60)
password = b"test_password"
for prefix in [b"2a", b"2b"]:
salt = bcrypt.gensalt(prefix=prefix)
hashed = bcrypt.hashpw(password, salt)
verified = bcrypt.checkpw(password, hashed)
# 确认前缀
actual_prefix = hashed.split(b"$")[1]
status = "✓" if verified else "✗"
print(f" [{status}] prefix={prefix}, 哈希前缀={actual_prefix}, 验证={verified}")
assert verified, f"prefix={prefix} 验证失败"
assert actual_prefix == prefix, f"前缀不一致:预期 {prefix},实际 {actual_prefix}"
print()
print("✓ prefix 参数测试全部通过!")
def test_bcrypt_72byte_limit():
"""测试密码长度限制(bcrypt 最多 72 字节)"""
print("=" * 60)
print("密码长度限制测试(72 字节上限)")
print("=" * 60)
# 71 字节密码应该可以
password_71 = b"a" * 71
try:
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_71, salt)
print(f" 71 字节密码: ✓ 成功,验证={bcrypt.checkpw(password_71, hashed)}")
except ValueError as e:
print(f" 71 字节密码: ✗ 异常: {e}")
# 72 字节密码应该可以(bcrypt 的精确上限)
password_72 = b"a" * 72
try:
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_72, salt)
print(f" 72 字节密码: ✓ 成功,验证={bcrypt.checkpw(password_72, hashed)}")
except ValueError as e:
print(f" 72 字节密码: ✗ 异常: {e}")
# 73 字节密码应该被拒绝
password_73 = b"a" * 73
try:
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_73, salt)
print(f" 73 字节密码: ✗ 应该抛出异常但没有")
except ValueError as e:
print(f" 73 字节密码: ✓ 正确拒绝了: {e}")
print()
print("✓ 长度限制测试完成!")
if __name__ == "__main__":
print()
print("🌟 bcrypt 5.0.0 HarmonyOS 兼容性测试 🌟")
print()
test_bcrypt_basic()
print()
test_bcrypt_rounds()
print()
test_bcrypt_prefix()
print()
test_bcrypt_72byte_limit()
print()
print("=" * 60)
print("🎉 全部测试通过!bcrypt 5.0.0 在 HarmonyOS 上工作正常!")
print("=" * 60)
print()
运行测试:
bash
python3 test_bcrypt.py
12. cryptography 移植实战(maturin + OpenSSL)
前面的章节以 bcrypt(setuptools-rust)为例,讲解了 Rust C 扩展在鸿蒙上的通用移植方法。
本节以
cryptography为例,展示一个更复杂的真实场景 :它使用 maturin 作为构建后端,且依赖 OpenSSL 本地 C 库。这涉及了构建隔离、本地库链接、运行时动态链接等多个额外坑点。
12.1 背景:cryptography 的构建方式
cryptography 不同于 bcrypt:
| 对比维度 | bcrypt | cryptography |
|---|---|---|
| Rust 构建后端 | setuptools-rust |
maturin(PyO3 官方推荐的构建工具) |
| 是否使用 abi3 | ✅ 是 | ✅ 是 |
| 本地 C 依赖 | ❌ 无 | ✅ OpenSSL(编译和运行都需要) |
| 构建方式 | pip 直接调 cargo | pip 调 maturin,maturin 再调 cargo |
| 是否需要编译 | 全程 Rust | Rust + C(OpenSSL bindings) |
12.2 第 5 个坑:maturin 也找不到自己(pip 构建隔离问题)
现象
text
$ pip3 install cryptography --no-build-isolation
...
Preparing metadata (pyproject.toml) ... error
error: maturin failed
Caused by: Failed to find maturin. Is maturin installed?
注意:maturin Python 包已经安装 了(pip3 list | grep maturin 能看见),maturin 命令也在 PATH 中(which maturin 能返回绝对路径),但 pip 的构建后端还是报找不到 maturin。
原因分析
cryptography 的 pyproject.toml 中声明了两种构建后端:
toml
[build-system]
requires = ["maturin>=1.5,<2.0"]
build-backend = "maturin"
pip 在构建时,会调用 maturin.prepare_metadata_for_build_wheel()(在 maturin 包的 __init__.py 中)。这个函数内部通过 subprocess 启动 maturin 命令行工具来执行实际的 Rust 编译:
python
# maturin/__init__.py(简化)
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
command = ["maturin", "pep517", "write-metadata", ...]
subprocess.check_output(command, env=env) # ← 子进程启动 maturin
问题出在两层调用:
pip (parent)
└─ maturin 构建后端 (Python 函数调用)
└─ subprocess.run(["maturin", ...]) (子进程,需要用 PATH 找 maturin 命令)
即使路径 /storage/Users/currentUser/usr/local/bin 在 PATH 环境变量中,鸿蒙的 posix_spawn 仍然不搜索 PATH ,直接报找不到 maturin 可执行文件。
解决方法
和 setuptools-rust 的思路一样------把 maturin 可执行文件的路径解析为绝对路径,传给子进程。
修改 maturin/__init__.py:
bash
# 找到文件位置
python3 -c "import maturin; print(maturin.__file__)"
找到 _get_env() 函数(约第 70-90 行),修改调用 subprocess 的地方,将 "maturin"(裸命令名)替换为 "/full/path/to/maturin" 或使用 shutil.which() 解析。
具体修改(在 maturin/__init__.py 中,找到 _get_env() 函数内的子进程调用处):
python
# 修改前(约第 240 行附近)
command = ["maturin", "pep517", "write-metadata", ...]
subprocess.check_output(command, env=env)
# 修改后
import shutil
MATURIN_PATH = shutil.which("maturin") # 或直接写:"/storage/.../usr/local/bin/maturin"
command = [MATURIN_PATH, "pep517", "write-metadata", ...]
subprocess.check_output(command, env=env)
注意 :如果 maturin 有多处子进程调用(
check_output和run),全部都要改。
快速验证
bash
python3 -c "
import subprocess, shutil
# 用绝对路径确保子进程能找到
maturin_path = shutil.which('maturin')
result = subprocess.run([maturin_path, '--version'], capture_output=True, text=True)
print('maturin 版本:', result.stdout.strip())
"
12.3 第 6 个坑:编译时找不到 OpenSSL
现象
修复 maturin PATH 问题后,继续安装:
text
$ pip3 install cryptography --no-build-isolation
...
cargo: rustc --edition 2021 ...
error: failed to run custom build command for `openssl-sys v0.9.xx`
Caused by:
Could not find directory of OpenSSL installation, and this `-sys` crate
cannot proceed without this knowledge. If OpenSSL is installed in a
non-standard path, set the `OPENSSL_DIR` environment variable.
或者:
text
run pkg_config fail: "Could not run `pkg-config --libs --cflags openssl`"
原因分析
cryptography 的 Rust 部分依赖 openssl-sys crate,这是一个 Rust FFI 绑定,需要在编译时找到 OpenSSL 的头文件和库文件。它的查找顺序是:
- 环境变量
OPENSSL_DIR(最优先) pkg-config查询- 系统默认路径(
/usr/lib、/usr/local/lib等)
鸿蒙系统没有自带 OpenSSL 或版本过旧(低于 3.x),而 cryptography 需要 OpenSSL 3.x+。需要手动安装并通过环境变量指定路径。
解决方法
步骤 1:通过 Harmonybrew 安装 OpenSSL
bash
brew install openssl
步骤 2:安装 pkg-config(如果还没有)
bash
brew install pkg-config
步骤 3:确认 pkg-config 能找到 OpenSSL
bash
export PKG_CONFIG_PATH="/storage/Users/currentUser/.harmonybrew/opt/openssl/lib/pkgconfig:$PKG_CONFIG_PATH"
pkg-config --libs --cflags openssl
# 应输出类似:
# -I/storage/.../include -L/storage/.../lib -lssl -lcrypto
步骤 4:安装时传递 OpenSSL 路径
bash
PKG_CONFIG_PATH="/storage/Users/currentUser/.harmonybrew/opt/openssl/lib/pkgconfig:$PKG_CONFIG_PATH" \
OPENSSL_DIR="/storage/Users/currentUser/.harmonybrew/opt/openssl" \
pip3 install cryptography --no-build-isolation
注意:
OPENSSL_DIR是给openssl-syscrate 用的,PKG_CONFIG_PATH是给 pkg-config 用的。两者都设最保险。
12.4 第 7 个坑:链接阶段找不到 libpython3.12.so
现象
OpenSSL 编译通过后,链接阶段报错:
text
= note: rust-lld: error: unable to find library -lpython3.12
collect2: error: ld returned 1 exit status
原因分析
cryptography 使用 PyO3 的 abi3 特性,生成的 .so 需要在链接时与 libpython3.12.so 进行符号解析(虽然运行时通过 dlopen 加载,但编译期仍然需要找到库文件)。
鸿蒙的链接器(rust-lld)搜索路径中不包含 Python 的 lib 目录。
解决方法
通过 LIBRARY_PATH 环境变量告诉链接器去哪里找 libpython3.12.so:
bash
LIBRARY_PATH="/storage/Users/currentUser/usr/local/lib:$LIBRARY_PATH" \
PKG_CONFIG_PATH="..." \
OPENSSL_DIR="..." \
pip3 install cryptography --no-build-isolation
12.5 第 8 个坑:运行时找不到 OpenSSL(OSSL_get_max_threads)
现象
cryptography 安装成功后,运行测试报错:
text
$ python3 -c "from cryptography.fernet import Fernet"
...
ImportError: /storage/.../cryptography/hazmat/bindings/_openssl.abi3.so:
undefined symbol: OSSL_get_max_threads
原因分析
这是编译时与运行时的 OpenSSL 版本不一致导致的。
-
编译时:通过
PKG_CONFIG_PATH找到了 Harmonybrew 安装的 OpenSSL 3.6.3,链接了它的符号。 -
运行时:Python 进程加载
.so时,动态链接器会搜索系统默认路径(/usr/lib、/system/lib等),找到的是鸿蒙系统自带的旧版 OpenSSL (可能只有 1.x 或 3.0.x),其中没有OSSL_get_max_threads这个较新的函数。Python 进程
│
├─ 加载 _openssl.abi3.so
│ │
│ ├─ 需要符号 OSSL_get_max_threads ← OpenSSL 3.6+
│ └─ 动态链接器查找 libssl.so / libcrypto.so
│ │
│ ├─ 优先找系统路径: /usr/lib/libcrypto.so(旧版) ❌ 没有这个符号
│ └─ Harmonybrew 的 OpenSSL(新版)✅ 有这个符号,但搜索优先级低
│
└─ ImportError: undefined symbol
解决方法
运行前设置 LD_LIBRARY_PATH 优先搜索 Harmonybrew 的 OpenSSL 库:
bash
LD_LIBRARY_PATH="/storage/Users/currentUser/.harmonybrew/opt/openssl/lib:$LD_LIBRARY_PATH" python3
或者把这条加入 shell 配置文件(.bashrc / .zshrc):
bash
export LD_LIBRARY_PATH="/storage/Users/currentUser/.harmonybrew/opt/openssl/lib:$LD_LIBRARY_PATH"
验证修复
bash
LD_LIBRARY_PATH="/storage/Users/currentUser/.harmonybrew/opt/openssl/lib:$LD_LIBRARY_PATH" \
python3 -c "from cryptography.fernet import Fernet; print('cryptography 导入成功')"
12.6 cryptography 完整安装命令(总结)
综合以上所有坑点,一条命令完成安装(以下是成功编译时的部分输出示例):
text
$ LIBRARY_PATH="..." PKG_CONFIG_PATH="..." OPENSSL_DIR="..." pip3 install cryptography --no-build-isolation
Processing ./cryptography
Preparing metadata (pyproject.toml) ... done
Running command for build wheel ...
️ maturin: 正在编译 cryptography...
️ cargo: 正在下载依赖 openssl-sys v0.9.xx
️ cargo: 正在编译 openssl-sys(找到 OpenSSL 3.6.3 ✓)
️ cargo: 正在编译 cryptography-rust
️ rustc: 链接 libpython3.12.so(通过 LIBRARY_PATH ✓)
Building wheel ... done
Installing ... done
Successfully installed cryptography-xx.x
bash
LIBRARY_PATH="/storage/Users/currentUser/usr/local/lib:$LIBRARY_PATH" \
PKG_CONFIG_PATH="/storage/Users/currentUser/.harmonybrew/opt/openssl/lib/pkgconfig:$PKG_CONFIG_PATH" \
OPENSSL_DIR="/storage/Users/currentUser/.harmonybrew/opt/openssl" \
pip3 install cryptography --no-build-isolation
运行前始终设置:
bash
export LD_LIBRARY_PATH="/storage/Users/currentUser/.harmonybrew/opt/openssl/lib:$LD_LIBRARY_PATH"
12.7 验证测试
保存为 test_cryptography.py:
python
#!/usr/bin/env python3
"""
cryptography 功能测试脚本
在 HarmonyOS 上验证编译安装的 cryptography 包是否正常工作
"""
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
def test_symmetric_encryption():
"""对称加密(Fernet)"""
print("=" * 60)
print("1. 对称加密 (Fernet)")
print("-" * 60)
key = Fernet.generate_key()
cipher = Fernet(key)
original = b"Hello, HarmonyOS! cryptography works!"
encrypted = cipher.encrypt(original)
decrypted = cipher.decrypt(encrypted)
assert decrypted == original
print(f" 原文: {original}")
print(f" 密文: {encrypted}")
print(f" 解密: {decrypted}")
print(" ✓ 对称加密测试通过\n")
def test_hash():
"""哈希函数"""
print("=" * 60)
print("2. 哈希 (SHA-256)")
print("-" * 60)
digest = hashes.Hash(hashes.SHA256())
digest.update(b"test data for hashing")
result = digest.finalize()
print(f" SHA-256: {result.hex()}")
print(f" 长度: {len(result)} bytes")
print(" ✓ 哈希测试通过\n")
def test_asymmetric_encryption():
"""非对称加密(RSA)"""
print("=" * 60)
print("3. 非对称加密 (RSA 2048 + OAEP)")
print("-" * 60)
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048,
)
public_key = private_key.public_key()
message = b"Secret message for RSA test"
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(), label=None,
),
)
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(), label=None,
),
)
assert plaintext == message
print(f" 密文长度: {len(ciphertext)} bytes")
print(f" 解密结果: {plaintext}")
print(" ✓ 非对称加密测试通过\n")
def test_sign_and_verify():
"""数字签名"""
print("=" * 60)
print("4. 数字签名 (RSA-PSS)")
print("-" * 60)
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048,
)
public_key = private_key.public_key()
data = b"Important document to sign"
signature = private_key.sign(
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
public_key.verify(
signature, data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
print(f" 签名长度: {len(signature)} bytes")
print(" ✓ 签名验证通过\n")
def main():
print("\n" + "★" * 30)
print(" cryptography 安装测试")
print("★" * 30 + "\n")
test_symmetric_encryption()
test_hash()
test_asymmetric_encryption()
test_sign_and_verify()
print("=" * 60)
print("🎉 全部测试通过!cryptography 在 HarmonyOS 上工作正常!")
print("=" * 60 + "\n")
if __name__ == "__main__":
main()
运行(记得设 LD_LIBRARY_PATH):
bash
LD_LIBRARY_PATH="/storage/Users/currentUser/.harmonybrew/opt/openssl/lib:$LD_LIBRARY_PATH" \
python3 test_cryptography.py
预期输出:
★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★
cryptography 安装测试
★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★
============================================================
1. 对称加密 (Fernet)
------------------------------------------------------------
原文: b'Hello, HarmonyOS! cryptography works!'
密文: b'gAAAAAB...'
解密: b'Hello, HarmonyOS! cryptography works!'
✓ 对称加密测试通过
...
============================================================
🎉 全部测试通过!cryptography 在 HarmonyOS 上工作正常!
============================================================
12.8 踩坑清单速查
| # | 坑 | 阶段 | 错误特征 | 解决方式 |
|---|---|---|---|---|
| ⑤ | maturin 子进程找不到自己 | 构建 | Failed to find maturin.Is maturin installed? |
修改 maturin/__init__.py,用绝对路径调 maturin 命令 |
| ⑥ | 找不到 OpenSSL | 编译 | Could not find directory of OpenSSL installation |
brew install openssl + PKG_CONFIG_PATH + OPENSSL_DIR |
| ⑦ | libpython3.12 找不到 | 链接 | unable to find library -lpython3.12 |
LIBRARY_PATH= 指向 Python lib 目录 |
| ⑧ | 运行时符号未定义 | 运行 | undefined symbol: OSSL_get_max_threads |
LD_LIBRARY_PATH= 指向 brew OpenSSL lib 目录 |
附录:常见问题 FAQ
Q1: 怎么知道一个包是否用了 setuptools-rust?
查看它的 pyproject.toml:
bash
# 方法一:查看 PyPI 上的源码包信息
pip download <包名> --no-binary :all: --no-deps --dry-run 2>&1 | grep -i rust
# 方法二:手动查看
# 去 https://pypi.org/project/<包名>/#files 下载 .tar.gz 展开
grep -r "setuptools-rust\|\[tool.setuptools-rust" pyproject.toml
如果有 [[tool.setuptools-rust.ext-modules]] 配置,就需要 setuptools-rust。
Q2: 我怎么知道一个包使用了 abi3?(从而需要 RUSTFLAGS)
查看它的 Cargo.toml 或 pyproject.toml:
bash
# 下载源码包后
grep -r "abi3\|py-limited-api" .
# 如果有输出,说明使用了 abi3
或者看 PyPI 上分发的 .whl 文件名,如果包含 abi3(如 bcrypt-5.0.0-cp312-abi3-manylinux_2_28_x86_64.whl),说明使用了 abi3。
Q3: 怎么检查 .so 文件是否需要 libpython?
bash
# 如果在标准 Linux 上有 readelf
readelf -d /path/to/xxx.so | grep NEEDED
# 如果没有 readelf(鸿蒙),可以用 Python
python3 -c "
import sys
with open('/path/to/xxx.so', 'rb') as f:
data = f.read()
if b'python' in data.lower():
print('可能需要 libpython(有 python 相关字符串)')
else:
print('可能使用 abi3(不直接依赖 libpython)')
"
Q4: 其他 Python 包也适用这套步骤吗?
是的,只要那个包满足以下任一条件:
- 包含 Rust 编写的 C 扩展(PyO3、maturin)
- 构建时需要调用子进程编译
- 使用了
abi3特性
核心修改点只有两个:
| 问题 | 修复 | 适用场景 |
|---|---|---|
| 子进程找不到命令 | 改 setuptools-rust/_utils.py |
所有需编译的包 |
| abi3 符号不可见 | 设 RUSTFLAGS="-L <libdir> -l python3.12" |
只对使用 abi3 的包 |
Q5: 如果升级 setuptools-rust,修改会被覆盖吗?
会的。 每次 pip install -U setuptools-rust 都会重新安装 _utils.py,之前的修改会丢失。
建议做法:
bash
# 备份修改后的文件
cp /path/to/site-packages/setuptools_rust/_utils.py \
~/patches/setuptools_rust_utils.py.patched
# 升级后重新打补丁
pip install -U setuptools-rust
cp ~/patches/setuptools_rust_utils.py.patched \
/path/to/site-packages/setuptools_rust/_utils.py
Q6: 为什么这个示例里没有 setup.py、build.py?其他文章说的 pip wheel . --no-build-isolation -w dist/ 又是什么?
这是个很好的问题,涉及 Rust 扩展项目 和 C 扩展项目 的区别。
一句话:bcrypt 是 Rust 扩展(通过 setuptools-rust + pyo3),不是传统的 C 扩展。配置文件的形式因此不同。
各类构建配置文件的角色对比
| 文件 | bcrypt 中有吗 | 作用 | 属于 |
|---|---|---|---|
setup.py |
❌ 无 | 传统 Python 扩展的构建入口(C 扩展时代标准) | setuptools |
setup.cfg |
✅ 有(仅 3 行) | 额外的 egg-info 配置,现代版已很少用 | setuptools |
pyproject.toml |
✅ 核心配置文件 | PEP 517 标准,所有构建元数据 + Rust 扩展声明 | PEP 517 |
build.py |
❌ 无 | 某些 C 项目自定义的构建辅助脚本 | 非标准 |
Cargo.toml |
✅ Rust 构建配置 | Rust 依赖、编译目标、链接选项 | Cargo |
build.rs |
⚠️ 原版无(我们之前创建的) | Cargo 构建脚本,用于设置链接参数 | Cargo |
为什么 bcrypt 不需要 setup.py?
传统 C 扩展项目(如 cryptography、numpy)会有一个 setup.py,里面调用 setuptools.Extension() 告诉 setuptools 如何编译 C 代码:
python
# setup.py(传统 C 扩展)
from setuptools import Extension, setup
setup(
ext_modules=[
Extension('mymod', sources=['mymod.c']),
]
)
而 bcrypt 是 Rust 扩展 ,它通过 setuptools-rust 集成到 Python 构建系统。配置信息全部写在 pyproject.toml 的 [[tool.setuptools-rust.ext-modules]] 段落中:
toml
# pyproject.toml(bcrypt 实际内容)
[[tool.setuptools-rust.ext-modules]]
target = "bcrypt._bcrypt" # Python 模块名
path = "src/_bcrypt/Cargo.toml" # Rust crate 路径
py-limited-api = "auto" # 使用 abi3 特性 ← 这就是问题的根源!
这一行等同于传统 setup.py 里写 Extension("bcrypt._bcrypt", ...)。
Rust 的具体编译细节(依赖、编译目标、链接选项)则写在 Cargo.toml 中:
toml
# Cargo.toml(bcrypt 实际内容)
[dependencies]
pyo3 = { version = "0.26", features = ["abi3"] } # ← abi3 导致了 dlopen 问题
bcrypt = "0.17"
# ...
[lib]
name = "bcrypt_rust"
crate-type = ["cdylib"] # 编译成动态库 .so
当 pip 执行 pip install . --no-build-isolation 时,完整的构建链是:
pyproject.toml
│
▼
setuptools.build_meta (build-backend)
│
▼
setuptools-rust 插件(读取 [[tool.setuptools-rust.ext-modules]])
│
▼
cargo build --manifest-path src/_bcrypt/Cargo.toml
│
▼
生成的 .so → 打包成 wheel → 安装到 site-packages
pip wheel . --no-build-isolation -w dist/ 是什么?
pip wheel 是 pip install 的"只构建、不安装"模式,生成 .whl 文件到指定目录:
bash
# 在 bcrypt 源码目录下执行:
pip wheel . --no-build-isolation -w dist/
流程完全一样,只是最后一步从"安装到 site-packages"变成"保存 .whl 到 dist/":
pip install . --no-build-isolation pip wheel . --no-build-isolation -w dist/
│ │
├── 下载/解压源码 ├── 下载/解压源码
├── 编译 Rust 扩展 ├── 编译 Rust 扩展
├── 打包成 .whl ├── 打包成 .whl
├── 安装到 site-packages ←不同→ └── 保存 .whl 到 dist/,不安装
└── 清理
对于我们这个场景,直接 pip install 即可,不需要单独用 pip wheel。pip wheel 通常用于:
- 离线分发 :在一台机器上编译好
.whl,拷贝到其他机器安装 - 构建产物存档:CI/CD 中保存制品
- 制作本地 wheelhouse:加速后续安装
为什么有 build.rs 但原版没有?
build.rs 是 Rust/Cargo 的构建脚本(不是 Python 的)。我们之前为了修复 abi3 链接问题,在 src/_bcrypt/ 下手动创建了它:
rust
// build.rs(我们创建的,原版 bcrypt 没有这个文件)
fn main() {
println!("cargo:rustc-link-lib=python3.12");
println!("cargo:rustc-link-search=/storage/Users/currentUser/usr/local/lib");
}
但正如我们验证过的,用 RUSTFLAGS 环境变量可以完全替代 build.rs,所以不需要手动创建它。
📝 验证记录 :文章中的"方式 A(RUSTFLAGS 一条命令)"在写完后经过了完整验证。具体流程:卸载已安装的 bcrypt → 确认
_utils.py补丁存在 → 执行上文命令 → pip 自动下载源码(25 kB .tar.gz)→ cargo 编译 23.41 秒 → 成功安装 →import bcrypt及所有功能测试通过。验证结论:无需手动下载源码,无需创建 build.rs,一条命令即可完成。
最后更新:2025 年 7 月
Q7: maturin 也要打 PATH 补丁吗?和 setuptools-rust 有什么不同?
是的,maturin 也需要打 PATH 补丁,但补丁的位置不同。
| 对比维度 | setuptools-rust | maturin |
|---|---|---|
| 需要修改的文件 | setuptools_rust/_utils.py |
maturin/__init__.py |
| 修改的函数 | _resolve_executable() |
_get_env() 内的子进程调用 |
| 修改内容 | 把命令名变绝对路径 | 把命令名变绝对路径 |
是否需 --no-build-isolation |
✅ 是 | ✅ 是 |
具体步骤(maturin 版):
bash
# 1. 找到 maturin 安装位置
python3 -c "import maturin; print(maturin.__file__)"
# 输出示例:/storage/.../site-packages/maturin/__init__.py
# 2. 找到 maturin 命令的绝对路径
which maturin
# 输出示例:/storage/Users/currentUser/usr/local/bin/maturin
# 3. 修改 maturin/__init__.py 中的子进程调用
# 把 'command = ["maturin", ...]' 改为 'command = ["/full/path/to/maturin", ...]'
# 或用 shutil.which("maturin") 动态解析
# 4. 安装时一定要用 --no-build-isolation
pip3 install <包名> --no-build-isolation
注意 :如果不加
--no-build-isolation,pip 会在临时目录重新下载一份未修改的 maturin,补丁不生效。
怎么判断一个包用 setuptools-rust 还是 maturin?
看它的 pyproject.toml:
toml
# setuptools-rust
[build-system]
requires = ["setuptools", "setuptools-rust"]
build-backend = "setuptools.build_meta"
# maturin
[build-system]
requires = ["maturin>=1.5,<2.0"]
build-backend = "maturin"
使用 maturin 的常见包:
cryptography(Rust 加密库)tokenizers(Hugging Face 分词器)pydantic-core(Pydantic v2 核心)ruff(Python linter)uv(下一代包管理器)
作者注 :本文档是在实际经历了一整天的排错过程后整理而成。鸿蒙 PC 虽然对 Linux 生态兼容性在持续改善,但在底层细节(如
posix_spawn、dlopen)上仍有差异。遇到问题时,从"系统调用的行为是否与 Linux 一致"这个角度去排查,往往能找到突破口。