为 Flet 提供 Python 3.14 构建支持

Creeper19472
对本文时效性的说明

由于 Flet 在 0.83.0 版本对构建模板的托管方式做了破坏性改动,因此本文的一些内容可能已经过时。

上篇文章中我曾提到:为了能够在构建好的应用中使用高版本 Python 引入的新特性,需要自行实现对高版本构建的支持。本文将更加详细地讨论实现此目标的方法,但由于这个问题本身的复杂性,本文也仅能介绍一种可以基本达到目的的方法,而无法给出能够兼顾方方面面的完全解。

0. 分析与准备

要令 Flet 能基于高版本的 Python 构建应用程序,显然地,我们至少需要完成以下几个步骤:

  1. 封装 Python 运行时
  2. 编译适配新版本的 .whl 文件
  3. 修改内部实现,使得 Flet 能够使用新的资源构建应用

而经过分析发现,参与整个 flet build 过程的除了 flet-cli 自身之外,还有两个托管于 github:flet-dev 的仓库:flet-build-templateserious-pythonflet-build-template 为构建流程提供了模板,并指定了可构建的所有架构;serious-python 则实际负责将 Python 部分的代码进行打包,并将适配各架构的 pip 包放入对应的目录。值得注意的是,serious-python 还会在执行 pip 命令的同时追加 --extra-index-url 参数(位于 src\serious_python\bin\package_command.dart),以使 pip 从 flet-dev 自有的包索引库中查找为 Android 和 iOS 构建的包,因为发布于 PyPI 的大多数包都是没有针对这些平台的构建版的。

为了进行之后的改动,我们需要 fork flet-build-templateserious-python两个代码仓库。同时,还需准备好需要的 Android SDK,如果你希望多平台构建仍然至少能对 Android 有效的话。由于需要编译 .whl 文件,建议在 Linux 下进行之后的工作,确只能在 Windows 下进行的,也请尽可能使用 WSL。

说明

不 fork python-build 的原因是 flet-dev 已提供了我们需要的 Python 3.14.3 构建——我们也会发现,前述准备部分的第一个步骤实际已经完成。仅当你需要为其他版本进行封装时才需要对这个仓库进行改动,但此处不做更多讨论。

上述步骤的难点实际在于为移动端架构构建二进制文件。仅 Android 而言,交叉编译的存在就使得整个构建过程十分复杂且容易出错。如果像我一样此前对编译和构建原理并不熟悉,接下来的过程将很可能令人十分头痛。

1. 编译 .whl 文件和创建自有包索引

说明

如果你完全不打算为移动端构建应用,则此部分可以跳过。

由于移动端编译是一个十分复杂的过程,因此我强烈建议你应先将注意力集中于那些正由你的项目使用的、因为并非纯 Python 实现所以需要编译的包,而不是试图将上游的 mobile-forge 所含的全部包都移植一遍。在移植过程中,不断请教 AI 将是个好主意,因为它能够较为清楚地分析你正遇上的问题。你可以让 AI 帮助你编写一段编译脚本,Claude Sonnet 4.6 在这方面有不错的效果。

尽管 cibuildwheel 作为一种工具已被推出以简化构建过程,但由于其可供参考的资料实在太少,所以并不一定推荐使用。

在你取得你需要的所有 .whl 文件之后,便可着手创建属于你自己的包索引。你可以利用 GitHub Pages 来托管你构建好的 .whl 文件,但需要注意站点的结构应当符合 pip 的要求。你还可以利用 GitHub Actions 来自动地根据已有的 .whl 文件创建站点。在这方面,你可以参考 CFMS 采用的 workaround

2. 修改 flet-build-template

这一步骤的目的是从构建模板中移除对 armeabi-v7ax86 架构的构建支持,因为 Python 3.14 已不再对这两个架构提供支持。

编辑 {{cookiecutter.out_dir}}\android\app\build.gradle.kts 并按以下内容注释指定行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[...]
android {
namespace = "{{ cookiecutter.org_name_2 }}.{{ cookiecutter.package_name }}"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion

packaging {
jniLibs {
useLegacyPackaging = true
keepDebugSymbols += listOf(
"*/arm64-v8a/libpython*.so",
// "*/armeabi-v7a/libpython*.so",
// "*/x86/libpython*.so",
"*/x86_64/libpython*.so",
)
}
}
[...]
ndk {
{% if cookiecutter.options.target_arch %}
abiFilters += listOf({% for arch in cookiecutter.options.target_arch %}"{{ arch }}"{% if not loop.last %}, {% endif %}{% endfor %})
{% else %}
abiFilters += listOf("arm64-v8a", "x86_64") // "armeabi-v7a",
[...]

3. 修改 serious-python

注释、增添、查找和替换

对于 serious-python,我们需要按与上一步类似的做法全局查找并移除与 armeabi-v7ax86 有关的部分。在此之后,编辑 src\serious_python\bin\package_command.dart

1
2
3
4
const buildPythonVersion = "3.14.3";
// 你要使用的 Python 版本
const buildPythonReleaseDate = "20260203";
// 这个常量需根据 https://github.com/astral-sh/python-build-standalone/releases/ 确定。

并在此处添加:

1
2
3
4
5
6
7
8
9
10
11
12
[...]
var junkFiles = isMobile ? junkFilesMobile : junkFilesDesktop;

// Extra indexes
List<String> extraPyPiIndexes = [mobilePyPiUrl];

// 在这里添加
extraPyPiIndexes.add("https://cfms-dev.github.io/platform-wheels/"); // 添加你自有的包索引
// 添加部分结束
if (platform == "Pyodide") {
pyodidePyPiServer = await startSimpleServer();
[...]

修改 src\serious_python_android\android\build.gradle

1
def python_version = '3.14'

按类似的方法全局查找 3.12 的出现位置,并将确为 Python 版本号的部分修改为 3.14,如 "${PYTHON_PACKAGE}/lib/libpython3.12.so.1.0" -> "${PYTHON_PACKAGE}/lib/libpython3.14.so.1.0"

重新运行 ffigen

转到 src/serious_python_android,编辑 python_ffigen.yaml,修改其中内容以指向你准备好的构建版的头文件。它们可以从 github:flet-dev/python-build 的 Releases 中取得(python-android-mobile-forge-3.14.tar.gz)。

之后,运行 dart run ffigen --config python_ffigen.yaml。视情况而定,你可能还需要增加对 llvm 的引用。以下是一份示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Run with `dart run ffigen --config python_ffigen.yaml`.

output: "lib/src/gen.dart"
# enums:
# rename:
# "_(.*)": "$1"
# member-rename:
# "_(.*)":
# "_(.*)": "$1"
globals:
rename:
"^class (\\w+) extends ffi.Struct": "final class $1 extends ffi.Struct"
"^class (\\w+) extends ffi.Opaque": "final class $1 extends ffi.Opaque"
"^class (\\w+) extends ffi.Union": "final class $1 extends ffi.Union"
headers:
entry-points:
- "C:\\Users\\[username]\\Downloads\\py314\\install\\android\\arm64-v8a\\python-3.14.3\\include\\python3.14\\Python.h"
include-directives:
- "C:\\Users\\[username]\\Downloads\\py314\\install\\android\\arm64-v8a\\python-3.14.3\\include\\python3.14\\**"

name: "CPython"
compiler-opts:
- "--target=aarch64-linux-android21"
- "-IC:/Users/[username]/AppData/Local/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/19/include"
- "-IC:/Users/[username]/AppData/Local/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include/aarch64-linux-android"
- "--sysroot=C:/Users/[username]/AppData/Local/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/windows-x86_64/sysroot"
description: "Bindings to Python C interface\nignore_for_file: unused_field, unused_element\n"
#array-workaround: true

4. 指向你修改好的 flet-build-templateserious-python

编辑你项目的 pyproject.toml

1
2
3
4
5
[tool.flet.template]
url = "gh:{你的账户名}/flet-build-template"

[tool.flet.flutter.pubspec.dependency_overrides]
serious_python = { git = { url = "https://github.com/{你的账户名}/serious-python.git", ref = "main", path = "src/serious_python" } }
评论