使用 Meson 构建 Cpp 项目

archive time: 2024-11-22

这也算自己的一个备份,防止自己忘记

有一段时间没更新博客了,主要是一直在瞎忙,导致 SICP 和 99 Questions 这两个系列完结遥遥无期, 反正债多不愁,所以今天还是不更新另外两个系列,而是来看看 Meson 这个编译工具

缘起

Meson 的定位是 CMake,底下使用的是 Ninja,对应 GNU Make

那么我为什么要放着好好的 CMake 不用,而来用 Meson 呢?

因为 Meson 足够简单,比起冗长的 CMakeLists.txtmeson.build 要更加易读, 而且许多的 Linux 项目都是使用 Meson,特别是 Gnome 和 GTK 的项目

入门

我们先来看一个简单示例,来编译一个单文件项目,其目录结构如下:

.
├── main.cpp
└── meson.build

其中 meson.build 内容如下:

project('meson tutorial', 'cpp')
executable('tutorial', 'main.cpp')

初始化

要使用 Meson 来构建项目,首先要使用 Meson 来初始化:

meson setup --prefix="$HOME/.local/sdk/user" --backend="ninja" --buildtype="debug" "./build/debug" "./"

其中 --prefix 指定了软件安装位置,--backend 指定了底层编译工具, 而 --buildtype 指定了编译类型,是调试类型还是发行类型

而如果要优化编译,可以使用发行类型:

meson setup --prefix="$HOME/.local/sdk/user" --backend="ninja" --buildtype="release" --debug "./build/release" "./"

其实这些选项都可以忽略,默认情况的安装位置是 /local/usr,而编译类型则默认是调试类型,所以一般来说可以简化为如下指令:

meson setup "./build/debug" "./"

后面两个是位置参数,分别代表编译到的位置以及源代码所在目录

更为直接的,可以使用 meson build 来初始化项目, 默认编译内容会放到 ./build 目录下,但是为了方便区分,一般还是会放到类似 ./build/debug 这种目录下

编译

初始化项目后就是要编译项目了,指令也非常简单:

meson compile -C "./build/debug"

其中 -C 表示执行编译指令前先切换到对应目录,这是因为 meson compile 只有在 Meson Build 目录下才能编译

也可与手动使用对应的编译后端来编译,例如 ninja

ninja -C "./build/debug"

复杂一点的例子

当然,一般的项目没有这么简单,下面是一个正常项目的结构

.
├── include
│   ├── meson.build
│   └── tut
│       └── tut.hpp
├── meson.build
├── src
│   ├── main.cpp
│   ├── meson.build
│   └── tut
│       ├── meson.build
│       └── tut.cpp
└── subprojects
    └── libtutorial
        ├── include
        │   ├── meson.build
        │   └── tutorial
        │       └── tutorial.hpp
        ├── meson.build
        └── tutorial.cpp

其中 tut 是项目内部库,而 libtutorial 则是子项目,可以是用源码管理的外部项目

我们自顶向下来看看每个 meson.build 文件里写了什么,以及各自的作用

项目配置

# ./meson.build

project(
    'example',
    'cpp',
    version: '0.1',
    default_options: ['cpp_std=c++17'],
)

# headers
subdir('include')
incs = include_directories('include')

# subprojects
libtutorial_dep = dependency('libtutorial', fallback: ['libtutorial', 'libtutorial_dep'])

# sources
subdir('src')

executable(
    'example',
    sources,
    include_directories: incs,
    dependencies: [libtutorial_dep, libtut_dep],
    install: true,
)

project() 中,我们可以设置非常多的东西,例如项目的版本,使用的语言,以及标准等级

然后,通过 subdir()include 目录加入项目, 这是一个推荐做法,也就是将头文件专门用一个目录存放,而不是和源码放在一起, 这样也方便 pkgconfig 之类的工具安装库和设置对应变量

include_directories() 则是为了生成 compile_commands.json 文件,方便 IDE 提示

对于外部依赖,如果是通过源码管理的,我们可以使用 subproject() 来加入依赖, 但是由于需求非常常见,所以我们可以直接使用 dependency()fallback 选项快捷地添加依赖

而后就是将源文件目录加入项目,通过 executable() 从而编译生成一个可执行文件

头文件管理

include 目录中,文件内容如下:

# ./include/meson.build

install_subdir('tut', install_dir: get_option('includedir'))

而在 subprojects/libtutorial/include 文件夹中的内容大同小异,只是将目录名改成了 tutorial

install_subdir() 指定了要安装的头文件,方便之后寻找头文件

源码管理

这里源码分成了两部分,一个是可执行文件对应的 main.cpp,另一个就是项目自己的库 tut

# ./src/meson.build

# all source file for executable
sources = files('main.cpp')

# local libs
subdir('tut')

内容非常简单,通过 files 指定有哪些文件,然后在 ./meson.build 中就可以通过 sources 变量来使用这些文件,然后将 tut 加入项目

# ./src/tut/meson.build

# sources
tut_sources = files('tut.cpp')

# math dependency
cxx = meson.get_compiler('cpp')
m_dep = cxx.find_library('m', required: true)

# declare a shared library
# can use static_library() for static libraries
# or just use library() and declare by options
libtut = shared_library(
    'tut',
    tut_sources,
    include_directories: incs,
    dependencies: [m_dep],
    install: true,
)

# declare as a dependency
libtut_dep = declare_dependency(include_directories: incs, link_with: libtut)

# generate pkgconfig files
pkg_config = import('pkgconfig')
pkg_config.generate(
    name: 'Tut',
    filebase: 'tut',
    description: 'a meson tutorial dependcies',
    libraries: libtut,
)

tut 中我们定义了一个库,库的定义一般使用 library() 就可以了, 但是如果要指定库的形式,那么也可以使用 static_library()shared_library() 来指定

编译器有时候并不会自动链接一些库,例如数学库 m 和线程库, 所以我们需要通过编译器来链接对应的库依赖,即 cxx.find_library(), 其中 required 选项如果是 true,那么在找不到库的情况下编译过程就会停止,而 false 则不会

为了方便别人链接我们的库,我们可以自己暴露一些变量,例如 libtut_dep,而其他人就可以通过 get_variable() 来获取这些内容

如果要更进一步使用 pkgconfig 管理, 我们可以使用 import 加载 pkgconfig 的相关内容, 然后通过 generate 为我们的库生成对应的 .pc 文件

子项目

子项目一定要放在 ./subprojects 文件夹下,而 ./subprojects/libtutorial/meson.build 文件则是项目文件,也就是需要有 project()

# ./subprojects/libtutorial/meson.build

# library project declare
project('libtutorial', 'cpp', default_options: ['cpp_std=c++17'])

# headers
subdir('include')
headers = include_directories('include')

# sources
sources = files('tutorial.cpp')

# declare a static library
# can use shared_library() for shared libraries
# or just use library() and declare by options
libtutorial = static_library(
    'tutorial',
    sources,
    include_directories: headers,
    install: true,
)

# declare as a dependency
libtutorial_dep = declare_dependency(include_directories: headers, link_with: libtutorial)

# generate pkgconfig files
pkg_config = import('pkgconfig')
pkg_config.generate(
    name: 'Tutorial',
    filebase: 'tutorial',
    description: 'a meson tutorial subproject',
    libraries: libtutorial,
)

编译和安装

准备好这些文件后,我们就可以开始编译了,初始化过程和普通项目一样,编译可以使用 meson compile -C "./build/debug" 来编译

meson setup --prefix="$HOME/.local/sdk/user" --backend="ninja" --buildtype="debug" "./build/debug" "./"
meson compile -C "./build/debug"
meson install -C "./build/debug"

后记

Meson 看起来比较繁琐,但是功能上比较直观,并且非常方便配合 pkgconfig 工具使用,所以还是非常值得一学的