This article was last updated on <span id="expire-date"></span> days ago, the information described in the article may be outdated.
最近在重构组里的代码上遇到一件稍微棘手的事情。导师的旧代码是使用Fortran编写的,其对一些Python无法方便读取的二进制数据做了非常精细的操作,导致将代码完全Python化的难度直线上升。折衷的方案是通过使用numpy.f2py
工具,将Fortran代码转换成Python扩展,这样就引入了新的问题,如何将扩展的编译融合进Python包的打包和安装过程中。
meson构建系统是我在几个月前无意中了解到的新工具,其包含了能够帮助编译和打包的Python模块,并且现有的一些项目都已完全将meson作为后端,例如scipy,numpy。因此我决定将这作为一个练手的机会,使用meson来帮助构建这个Python包。
meson的简单介绍
meson是一个使用Python实现的项目构建系统,作用类似于Makefile或者CMake,提供了对多种平台、多种语言的支持。如果你的主要工作语言是Python,那么安装meson会变得非常方便,通过pip
命令就能安装meson以及其默认的编译后端ninja。
1 | pip install meson ninja |
如果你的Python项目需要编译扩展,则你需要自己再安装对应的编译器,如gcc,gfortran等。
meson的配置文件是名为meson.build
的文件,该文件类似于CMakeLists.txt
,里面定义了项目的信息以及编译的信息等。
1 | project('simple', 'c') |
使用meson来编译其他语言不是这篇文章的重点,如有需要可以直接去meson官网手册查看。
Python项目的meson配置
无扩展
假设你有以下这一个Python项目。
1 | . |
mypyproj
目录内是真正的Python包代码。Python并不是编译型语言,该项目也没有需要编译的扩展,则完全用不到meson的编译功能,但是你依然可以将meson作为其打包的后端。首先在mypyproj
同级目录添加pyproject.toml
,配置项目信息、后端和依赖。
1 | [build-system] |
build-backend
字段指定了使用meson作为后端,但是由于meson并不会随Python一起安装,所以需要在requires
中指定依赖,这样打包和安装时会先安装meson-python
。pyproject.toml
的配置完成后,我们需要再配置meson,指定需要打包的文件以及安装的位置。不同于setuptools
,meson不会自动找寻和检测Python包的代码,而是需要我们在配置文件中详细列出需要包含的文件。这样的做法比setuptools
更繁琐一些,但是能让我们更细粒度控制Python包内包含的东西。
在pyproject.toml
的同级目录下添加meson.build
,同样写入项目配置。
1 | project( |
project()
定义项目的名称,版本号,授权证书,meson版本等。py3 = import('python').find_installation(pure: false)
导入Python模块,并找到安装的Python。subdir()
指定了子目录,随后meson会去子目录下寻找子目录的meson.build
,并再读取相应的配置。可以看到当前的meson.build
并没有配置Python包的内容,这些是由子目录下的meson.build
来配置的。
添加
pyproject.toml
和meson.build
后的目录结构。
1 | . |
在mypyproj
下添加新的meson.build
,该文件将用于定义Python包的内容。
1 | py3.install_sources( |
py3.install_sources()
定义了哪些文件会被包含到Python包中,以及他们在包中对应的位置subdir
。py3
是上一级的meson.build
中获取,通过使用subdir()
包含,我们可以在下级的meson.build
中使用上级中的变量。由于我们的项目还包含了子目录core
,所以我们进一步使用subdir()
包含了core
。同样的道理,在core
目录下,我们同样需要添加meson.build
,并定义需要被包含的文件。
1 | py3.install_sources( |
完整的目录结构如下
1 | . |
至此,meson的配置我们就完成了,每一个目录下都包含有对应的meson.build
,他们分别用于定义项目信息、Python包包含的文件,并通过subdir()
互相联系起来。
完成pyproject.toml
和meson.build
的配置后,我们就可以使用build来打包Python了。
1 | python -m build |
呃,好吧。除了配置pyproject.toml
和meson.build
外,你还需要使用Git来配置一下项目,将需要打包的文件添加到版本控制系统,这是使用meson打包文件时meson要求必须做的工作。需要注意的是,meson进行源代码打包时,会将Git中包含的所有文件打包进来。你可以使用.gitattributes
文件来排除不想打包的东西:
1 | .gitea/ export-ignore |
将文件都添加到Git后,再次build,就可以在dist/
下看到打包好的Python包了。
有扩展
现在我们想添加一个使用Fortran编写的扩展d.f90
,它位于mypyproj/core/d.f90
。
1 | . |
对于Fortran扩展的编译,我们首先需要使用numpy.f2py
对其处理,生成C语言的wrap代码,然后再将生成的C wrap代码与原始Fortran文件一起编译,生成Python可直接import的扩展库。除了C wrap代码的生成需要我们额外进行控制,代码的编译过程直接由meson接管。
由于编译的过程需要使用numpy
,我们还需要对pyproject.toml
文件进行修改,在requires
中添加numpy
。
1 | [build-system] |
编译的相关设置则都需要在mypyproj/core/meson.build
文件中进行,因为d.f90
扩展在mypyproj/core/meson.build
所控制的目录下。首先调用numpy.f2py
的配置如下。
1 | add_languages('fortran') |
add_languages()
设置了项目语言包含Fortran和C,然后使用custom_target()
命令定义自定义构建目标。这里我们定义输入文件为['d.f90']
,自定义的构建命令是numpy.f2py
的构建命令,只不过将Python解释器替换为上级meson.build
找到的py3
,输入文件名设置为@INPUT@
。@INPUT@
是meson的语法,meson会自动将@INPUT@
替换为正确的输入文件名。我们使用了--build-dir
来指定了生成文件存放的目录,保证生成的文件位于d.f90
同目录下。output
参数表示该构建目标的输出,输出文件的命名是numpy.f2py
固定的命名格式。
由于我们是使用numpy.f2py
来生成扩展,这部分涉及到numpy
的C-API,因此需要知道numpy
和numpy.f2py
的头文件路径。可以使用meson的run_command
函数来执行Python命令,获取头文件目录路径。然后使用include_directories()
来设置包含路径。
1 | incdir_numpy = run_command( |
同样,我们需要知道编译Python扩展所依赖的Python链接库,以及编译链接的参数等,这里使用py3.dependency()
来获取相关的设置。以上的准备工作做完以后,就可以使用py3.extension_module()
来真正设置编译扩展。
1 | py3.extension_module( |
第一个参数'd'
是设置的独一无二的构建目标名称,['d.f90', d, incdir_f2py / 'fortranobject.c']
是输入文件,包含了原始Fortran文件,通过custom_target()
编译得到的文件,以及numpy.f2py
中定义的文件。include_directories
设置头文件路径,dependencies
设置链接库路径,install: true
设置编译好后将扩展放入subdir
指定的路径中。
到现在为止mypyproj/core/meson.build
文件更改完毕,所有内容如下
1 | py3.install_sources( |
可以先单独运行meson的命令来测试扩展是否可以编译成功。
1 | meson setup build |
如果没有问题,则可以将所有更改提交到Git,然后运行打包命令打包,打包完成后就可以看到含有扩展的.whl
Python包了。
Author: Syize
Permalink: https://blog.syize.cn/2024/10/03/build-python-package-with-meson/
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Syizeのblog!
Comments