Python的C/C++三方库移植到鸿蒙PC实战踩坑记

本文档是在实际经历了一整天的排错过程后整理而成。鸿蒙 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. 核心疑问:必须要下载源码吗?)
    • [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)
      • [方式 A(推荐):使用 RUSTFLAGS 环境变量](#方式 A(推荐):使用 RUSTFLAGS 环境变量)
      • [方式 B:手动下载源码 + 添加 build.rs](#方式 B:手动下载源码 + 添加 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.pybuild.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 上的 aptyumbrew 等包管理器,也没有预装 Rust、Clang 等编译工具链。Harmonybrew 是专门为 OpenHarmony 移植的 Homebrew,提供了 Rust、Python、编译工具链等关键软件的鸿蒙适配版本。编译任何 Python C 扩展(包括 bcrypt)之前,必须先完成本节的环境搭建。

0.1 卸载冲突软件

如果 PC 中安装有 GitNextDevBox 这两个应用,请将它们卸载。它们可能与 Harmonybrew 的工具链冲突。

0.2 打开安全开关

鸿蒙 PC 默认禁止运行未经签名的扩展程序,需要手动开启:

  1. 打开开发者选项:进入"设置"→"系统"→"开发者选项",打开"开发者选项"开关。
  2. 允许非应用市场扩展:进入"设置"→"隐私和安全"→"高级",打开"运行来自非应用市场的扩展程序"开关。
  3. 如果找不到"开发者选项":打开"设置"→"关于本机",找到"软件版本",连续点击 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,包含 clangllvm-ar 等工具。其中的 lld 链接器经过封装,默认启用代码签名,编译出的二进制可直接运行。 ✅ 编译 Rust/Python C 扩展必需
llvm-gcc-compat 生成 ccgccld 等软链接,全部指向 ohos-sdk 中的 LLVM 工具链。无需手动指定 CCCXX 等环境变量。 ✅ 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 安装过程会:

  1. 下载依赖(setuptools、setuptools-rust、wheel)
  2. setuptools 解析 pyproject.toml
  3. setuptools-rust 调用 cargo build 编译 Rust 代码
  4. 把生成的 .so 文件复制到 Python 包的安装目录
  5. 打包成 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 这个命令会:

  1. pip 检查 PyPI 上 bcrypt 的可用分发包
  2. --no-binary bcrypt 告诉 pip:不要下载预编译的 .whl 文件 ,只下载源码包(.tar.gz
  3. pip 自动下载源码到临时目录,解压,触发编译,安装
  4. 安装完成后,临时目录会被自动删除

所以,对于大多数"一键安装"的场景,是完全不需要手动下载源码的

那为什么我们之前手动下载了?

因为我们遇到了第 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 输出和错误

什么时候需要手动下载源码?

只有以下两个场景需要手动下载:

  1. 需要永久性修改包的源码(如添加 build.rs、修改 Cargo.toml)
  2. 需要离线安装(目标机器没有网络)

手动下载的方法:

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_outputsubprocess.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 中所有调用子进程的地方,在传入 rustccargo 之前先用 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_subprocesscheck_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* 符号都能解析到。

为什么这是推荐方案?

  1. 不需要下载源码 --- pip 直接从 PyPI 拉取源码编译
  2. 不需要修改任何文件 --- 所有配置通过环境变量传递
  3. 通用性强 --- 对任何使用 PyO3 abi3 的包都有效
  4. 不产生遗留 --- 安装完成后,不需要清理任何修改

方式 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 --versioncargo --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 时使用

核心教训

  1. 鸿蒙 ≠ Linux 。虽然鸿蒙声称兼容 Linux,但底层系统调用(如 posix_spawn)和动态链接器(dlopen)的行为有差异。

  2. abi3 + 鸿蒙 = 需要显式链接 libpython 。可以通过 RUSTFLAGS 环境变量解决,不需要下载源码或修改代码。

  3. PATH 不自动搜索 。鸿蒙上 subprocess.run(['rustc', ...]) 会失败,但 subprocess.run(['/full/path/rustc', ...]) 成功。任何涉及子进程调用的 Python 库都可能受影响。

  4. --no-build-isolation 的重要性。在需要修改 pip 依赖源码的场景下,必须用这个参数,否则 pip 会在隔离环境重新下载未修改的依赖。

  5. 先尝试环境变量,再考虑修改源码RUSTFLAGSLD_PRELOADPYO3_PYTHON 等环境变量往往不需要修改任何源码就能解决问题。

  6. 本地 C 库的版本分裂 (编译时 vs 运行时)。编译时通过 PKG_CONFIG_PATH 找到 OpenSSL 3.x,但运行时动态链接器可能优先加载系统自带的旧版 OpenSSL。必须用 LD_LIBRARY_PATH 确保运行时也使用正确的库。

  7. maturin 也需要打 PATH 补丁 。和 setuptools-rust 的 _resolve_executable 一样,maturin 的 __init__.py 中的子进程调用也需要把 "maturin" 替换为绝对路径。不使用 --no-build-isolation 还无法应用这个补丁。

  8. 模块化问题排查 。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)、orjsonpydantic-coreruff 等。

常见的构建后端:

构建后端 相关问题 修复方式
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

原因分析

cryptographypyproject.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/binPATH 环境变量中,鸿蒙的 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_outputrun),全部都要改。

快速验证
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 的头文件和库文件。它的查找顺序是:

  1. 环境变量 OPENSSL_DIR(最优先)
  2. pkg-config 查询
  3. 系统默认路径(/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-sys crate 用的,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.pybuild.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 wheelpip 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 wheelpip wheel 通常用于:

  • 离线分发 :在一台机器上编译好 .whl,拷贝到其他机器安装
  • 构建产物存档:CI/CD 中保存制品
  • 制作本地 wheelhouse:加速后续安装
为什么有 build.rs 但原版没有?

build.rsRust/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_spawndlopen)上仍有差异。遇到问题时,从"系统调用的行为是否与 Linux 一致"这个角度去排查,往往能找到突破口。

相关推荐
Wild_Pointer.17 小时前
高效工具实战指南:Procexp64进程资源管理器
c++·windows
程序员黑豆17 小时前
鸿蒙应用开发之模拟器安装与使用教程
前端·harmonyos
二宝哥17 小时前
14.Python模块与包完全指南:从定义到实战
python
cjr_xyi17 小时前
AT1202Contest_c binarydigit 题解
c语言·c++·算法
会周易的程序员17 小时前
Libnodave S7 通信库:架构设计与实现解析
linux·c++·物联网·架构·c·s7·工业协议
jinyishu_17 小时前
C++ 多态完全指南:从基础语法到底层原理
开发语言·c++·程序人生·面试
三声三视18 小时前
交互式用够了?用 Agent SDK 把 Claude 塞进 Python Web 服务
人工智能·python·ai·aigc·ai编程
zhanghaha131418 小时前
Python语言基础:4_数据类型转换
java·前端·python
larance18 小时前
机器学习特征预处理之标准化/归一化
开发语言·python·机器学习
tokenova18 小时前
Python 多模型统一调用封装类,一套代码兼容 GPT/Claude/Grok
python