本文以Arch Linux为例介绍如何基于VS Code和Meson搭建一套C语言开发环境。VS Code和Meson都是跨平台的,其他平台的读者可以参考本文自行摸索,大体流程都是差不多的。C++用户也可以参考本文,只有个别参数的不同,需要变动的地方我都会标出来。
Meson介绍
依据其官方网站的介绍:
Meson 是一个开源构建系统,不仅速度极快,而且更重要的是,尽可能用户友好。
Meson 的设计要点是,开发人员花在编写或调试构建定义上的每一刻都是浪费。等待构建系统实际开始编译代码的每一秒也是如此。
同样依据其官方网站的介绍,Meson有以下特性:
- 对 Linux、macOS、Windows、GCC、Clang、Visual Studio 等的多平台支持
- 支持的语言包括 C、C++、D、Fortran、Java、Rust
- 以非常可读且用户友好的非图灵完备 DSL 构建定义
- 对许多操作系统以及裸机的交叉编译支持
- 针对完整和增量构建进行了优化,速度极快而不牺牲正确性
- 内置的多平台依赖提供程序,可与发行版包一起使用
- 乐趣!
其中有两点很重要,一是跨平台,与CMake看齐;二是配置文件非图灵完备,这是用户友好的重要原因。非图灵完备简单来说,就是它的配置文件更像是配置文件,而不是编程语言。
Meson使用Apache 2许可证。
Meson的默认后端是Ninja,也支持Visual Studio、Xcode后端,但不支持Make。因为Make太慢,Makefile语法不行,支持成本高。
Meson是用Python写的,配置文件的语法也基于Python。用Python编写算是Meson的一个劣势,意味着依赖比较庞大。目前有一些其他语言的实现,但还没达到普遍可用的状态。
目前使用Meson的主要是Gnome、Systemd等红帽系项目,但PostgreSQL、QEMU、mpv等多个无关项目也在使用,说明其能力还是被广泛认可的。
环境安装
首先需要安装VS Code与Meson,在Arch Linux上,可以通过以下命令完成:
sh
yay -Sy visual-studio-code-bin meson
如果没有yay ,可以前往AUR手动安装VS Code,参考Linux下VS Code安装与C编程环境配置。Meson可以直接通过pacman
安装。
如果还没有编译器和调试器,在Arch Linux上,可以通过以下命令安装:
sh
sudo pacman -Sy gcc gdb
之后安装必需的VS Code扩展:
- C/C++: ms-vscode.cpptools
- Meson: mesonbuild.mesonbuild
Meson的VS Code扩展是官方推出的,使用体验很差,但没有更好的替代品。要使用扩展的格式化功能,还必需安装另一个程序muon。
Meson扩展的内嵌提示建议关掉,没有价值,还影响阅读。
第一个项目
VS Code以目录为工作区,先创建一个目录,以demo为例,然后用VS Code打开这个目录。
再创建源文件,以main.c为例,随便写点什么,比如著名的"hello, world"。
c
#include <stdio.h>
int main(void) {
puts("hello, world");
return 0;
}
现在我们已经有了完备的源代码,可以构建出一个可执行程序。接下来编写构建配置文件。
首先创建一个名为meson.build的文件,这是Meson的配置文件,并且文件名是特定的。
这时Meson扩展会弹出一条提示,询问你是否配置Meson项目,其实就是是否执行meson setup
命令。这时先不用管,因为我们的配置文件还没写完,肯定会失败。
和Python一样,Meson的行注释也是以#
开头。除注释以外,Meson配置文件必须以project()
开头,表明项目的名字、语言、版本、默认选项等。其中名字 和语言是必需的,依次是函数的前两个位置参数。
python
# Meson configuration for demo
project('demo', 'c',
version: '0.0.1',
default_options: {
'c_std': 'c17',
'warning_level': '3',
'werror': true,
'optimization': 'g',
'strip': true,
},
)
如果项目存在多个语言,可以同时指定,比如project('demo', 'c', 'cpp')
。
Meson的值是有类型的,基本类型有字符串、数字、bool、列表、字典。字符串以单引号包裹,不支持双引号。
Meson大部分的可变参数都是扁平化的,可以把任意嵌套的列表展开为连续的参数。project('demo', 'c', 'cpp')
和project('demo', ['c', 'cpp'])
是等价的,为了提高可读性,本文都会使用后一种写法。
default_options
是对所有构建目录都生效的默认选项,之后还可以针对特定的构建目录执行meson configure
命令来改变这些选项。
这些选项有的会添加编译器参数,有的会添加链接参数,有的会改变安装时的行为。
c_std
选择C语言的标准版本,比如c17
就为编译器添加-std=c17
参数。如果编程语言为C++,选项名为cpp_std
。如果没有指定这个选项,则语言版本是由编译器决定的。warning_level
指定警告等级,不同等级对应的编译器参数可对照下表。因为有everything
这么个值的存在,所以选项的类型是字符串。需要注意的是,gcc 并没有-Weverything
这么个参数,所以不要使用everything
这个警告等级。警告等级3
包含了-Wpedantic
,这会对不符合语言标准的写法发出警告,如果设置了c_std
,warning_level
建议设置为3
。默认是1
。
Warning level | GCC/Clang | MSVC |
---|---|---|
0 | ||
1 | -Wall | /W2 |
2 | -Wall -Wextra | /W3 |
3 | -Wall -Wextra -Wpedantic | /W4 |
everything | -Weverything | /Wall |
werror
为编译器添加-Werror
参数。这会让所有的警告变成错误,从而让编译失败。我强烈建议开启这个选项,让开发者不再忽略警告,从而消灭隐患。如果是故意为之,也可以显式消除警告,提高代码可读性。默认是false
。optimization
改变编译器的优化等级,g
代表参数-Og
。Meson的默认构建类型是debug
,等价于参数-g -O0
,-Og
可以提升调试体验。如果meson setup
或meson configure
命令指定buildtype
为release
,等价于-O3
,optimization
会被覆盖掉,所以不必担心optimization
选项对meson setup
或meson configure
命令造成影响。strip
选项表示在执行meson install
命令时对二进制文件进行strip,这会减少二进制文件的体积。默认是false
.
接着我们指定此次构建的目标,以及它依赖哪些源文件。
python
executable('demo', sources: 'main.c', install: true)
executable
的第一个位置参数表明了可执行文件的名字,关键字参数sources
表明了编译哪些源文件,install
表明了在执行meson install
命令时是否安装这个可执行文件。
最后的meson.build文件长这样:
python
# Meson configuration for demo
project('demo', 'c',
version: '0.0.1',
default_options: {
'c_std': 'c17',
'warning_level': '3',
'werror': true,
'optimization': 'g',
'strip': true,
},
)
executable('demo', sources: 'main.c', install: true)
此时就可以完成构建了,点击扩展提示的Yes ,会创建一个名为builddir的目录。
如果这个提示消失了,也可以在项目根目录下手动运行以下命令生成构建目录。
sh
meosn setup builddir
目录名不能改,因为这是Meson扩展的默认目录名,后续很多操作都依赖于各个名字。
这时还会弹出一个提示,问你是否下载语言服务器,这个必须下载,不然扩展功能少一半。
选Yes 后,会在你的全局设置里加一条"mesonbuild.downloadLanguageServer": true
。
生成构建目录的同时也会生成一系列配置文件,比如build.ninja 就是Ninja的配置文件,.gitignore 和 .hgignore 分别是Git 和Mercurial 版本控制系统的忽略文件,builddir下的所有内容都不会添加到版本控制系统中。
除此之外,Meson扩展还能与C/C++扩展集成,通过compile_commands.json文件配置C/C++扩展的代码提示,体现为自动生成工作区配置。
这时我们执行meson compile -C builddir
命令,或者使用VS Code命令Meson: Build
就可以在builddir 下生成一个名为demo的可执行文件。
调试
Meson扩展提供了一系列任务,但没有提供调试配置。
如果我们编辑Meson: Build all targets
,可以看到里面的内容是这样的。
json
{
"version": "2.0.0",
"tasks": [
{
"type": "meson",
"mode": "build",
"problemMatcher": [
"$meson-gcc"
],
"group": "build",
"label": "Meson: Build all targets"
}
]
}
执行这个任务,在终端可以看到实际执行的命令。
接下来手写用于调试的launch.json 文件,对这个文件不了解的读者可以参考Linux下VS Code安装与C编程环境配置。
json
{
"version": "0.2.0",
"configurations": [
{
"name": "debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/${config:mesonbuild.buildFolder}/demo",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/rundir",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": false
}
],
"preLaunchTask": "Meson: Build all targets"
}
]
}
这里我把cwd
设置为一个单独的目录,避免扰乱代码树。preLaunchTask
可以在每次调试前都执行一次构建。
现在万事俱备,可以按F5
开始调试了。
多文件项目
当然我们不会为了一个源文件而使用构建工具,现在我们提升项目复杂度。创建两个子目录a 和 b ,每个子目录下都有对应的源文件和头文件。让main.c 依赖 a.h ,让a.c 依赖 b.h。
c
// main.c
#include <stdio.h>
#include "a/a.h"
int main(void) {
puts("hello, world");
fn_a();
return 0;
}
c
// a.h
extern void fn_a(void);
c
// a.c
#include <stdio.h>
#include "b/b.h"
void fn_a(void) {
puts("fn_a");
fn_b();
}
c
// b.h
extern void fn_b(void);
c
// b.c
#include <stdio.h>
void fn_b(void) {
puts("fn_b");
}
文件准备就绪,现在我们需要对meson.build进行一点修改。
python
# Meson configuration for demo
project('demo', 'c',
version: '0.0.1',
default_options: {
'c_std': 'c17',
'warning_level': '3',
'werror': true,
'optimization': 'g',
'strip': true,
},
)
sources = [
'main.c',
'a/a.c',
'b/b.c',
]
executable('demo', sources: sources, install: true)
改动点只有把原来的'main.c'
扩充成3个源文件,为了提高可读性,我们还把源文件列表独立成一个变量。
观察上面的几个文件,有两个关键信息:
- meson.build里只有源文件,没有头文件,也没有表明任何头文件依赖。这是因为Ninja可以借助编译器产生的信息自动分析头文件依赖,并且在必要的时候重新编译源文件。
- 源文件的
#include
指令并不是相对于自身的路径,而是相对于项目根目录的。这是因为meson.build 里的executable()
函数会自动添加包含路径,一个是当前meson.build 所在的目录,另一个是builddir中对应可执行文件所在的目录。
修改meson.build 后,不需要 重新执行meson setup
,可以直接进行构建,build.ninja 里有钩子,自动读取meson.build 并且更新builddir里的配置文件。
修改源文件或头文件的内容(注意不是添加源文件)后,也不需要 重新执行meson setup
,构建时Ninja会自动分析依赖并重新编译需要的文件。
执行结果:
hello, world
fn_a
fn_b
依赖管理
如果我们的项目依赖第三方提供的库,需要加一些编译参数或链接参数。比如libgcrypt ,熟悉gcc的朋友应该知道要加-lgcrypt
参数。不过直接加参数既不好管理,又不可移植。Meson有内置的依赖管理,可以借助pkg-config
查找依赖并自动添加编译和链接参数。
可以使用add_project_dependencies()
函数为整个项目的所有构建目标都添加依赖,也可以通过构建目标的dependencies
参数单独添加依赖。
python
# Meson configuration for demo
project('demo', 'c',
version: '0.0.1',
default_options: {
'c_std': 'c17',
'warning_level': '3',
'werror': true,
'optimization': 'g',
'strip': true,
},
)
libgcrypt = dependency('libgcrypt')
# add_project_dependencies(libgcrypt, language: 'c')
executable('demo', sources: 'main.c', dependencies: libgcrypt, install: true)
然后编写我们的源文件。
c
// main.c
#include <stdio.h>
#include <string.h>
#include <gcrypt.h>
int main(void) {
puts("hello, world");
enum gcry_md_algos algo = GCRY_MD_MD5;
unsigned int digest_len = gcry_md_get_algo_dlen(algo);
char source[] = "password";
void *result = malloc(digest_len);
gcry_md_hash_buffer(algo, result, source, strlen(source));
for (unsigned int iter = 0; iter < digest_len; iter++) {
printf("%02x", ((unsigned char*)result)[iter]);
}
putchar('\n');
return 0;
}
构建并运行:
hello, world
5f4dcc3b5aa765d61d8327deb882cf99
可以在终端的输出里看到查找依赖的过程。
yaml
The Meson build system
Version: 1.3.0
Source dir: /home/lonble/demo
Build dir: /home/lonble/demo/builddir
Build type: native build
Project name: demo
Project version: 0.0.1
C compiler for the host machine: cc (gcc 13.2.1 "cc (GCC) 13.2.1 20230801")
C linker for the host machine: cc ld.bfd 2.41.0
Host machine cpu family: x86_64
Host machine cpu: x86_64
Found pkg-config: YES (/usr/bin/pkg-config) 2.1.0
Run-time dependency libgcrypt found: YES 1.10.3-unknown
Build targets in project: 1
我们还可以指定依赖版本,以及是否使用静态库。
python
libgcrypt = dependency('libgcrypt', version: '>=1.5', static: false)
Meson还对线程库进行了封装。
python
threads = dependency('threads')
如果一些库既没有pkg-config
文件,也没有Meson封装,还可以通过编译器手动查找。这些库一般都是glibc从C标准库里拆分出来的,以数学库libm
为例:
python
cc = meson.get_compiler('c')
math = cc.find_library('m', required: false)
find_library()
的返回值类型和dependency()
相同,所以返回值的用法也是相同的。因为除Linux以外的其他平台都没有单独拆分的数学库,所以设置了required: false
,如果没找到这个库也不会报错。
自定义编译和链接参数
在project()
函数的default_options
参数里,就有名为c_args
和c_link_args
的键,对应的C++版本为cpp_args
和cpp_link_args
。这两个键分别设置编译和链接参数。
python
project('demo', 'c',
default_options: {
'c_args': ['-ansi', '-Wmain'],
'c_link_args': ['-s', '-static'],
},
)
我强烈不建议 在default_options
中添加编译和链接参数,原因如下:
default_options
在修改后不会自动同步到builddir ,如果要强行覆盖,必须执行meson setup --wipe builddir
,这会清空已经生成的目标文件- 在这里设置编译和链接参数会覆盖
CFLAGS
环境变量
为整个项目添加编译参数的推荐方法是add_project_arguments()
函数。
python
add_project_arguments(['-ansi', '-Wmain'], language: 'c')
这里的language
参数不能省略。
同样可以使用add_project_link_arguments()
函数为整个项目添加链接参数,用法和add_project_arguments()
是一样的。
还可以针对每个构建目标单独设置参数。
python
executable('demo',
sources: 'main.c',
install: true,
c_args: ['-ansi', '-Wmain'],
cpp_args: ['-Weffc++', '-Wnamespaces'],
link_args: ['-s', '-static']
)
需要注意,自定义参数不利于程序的可移植性,因为这些参数都是特定于编译器或平台的。我们可以针对不同平台使用不同的参数,尽可能保证程序的可移植性。
python
args = []
arg_syntax = meson.get_compiler('c').get_argument_syntax()
if arg_syntax == 'gcc'
args += ['-Wall', '-Wextra']
elif arg_syntax == 'msvc'
args += '/W3'
endif
add_project_arguments(args, language: 'c')
安装产物
Meson对产物安装也有一套标准流程。首先只有标记install: true
的构建目标才会被安装,构建目标有多个种类,每个种类都有对应的安装目录。
这是从官网扒下来的默认目录表。
Option | Default value | Description |
---|---|---|
prefix | see below | Installation prefix |
bindir | bin | Executable directory |
datadir | share | Data file directory |
includedir | include | Header file directory |
infodir | share/info | Info page directory |
libdir | see below | Library directory |
licensedir | Licenses directory | |
libexecdir | libexec | Library executable directory |
localedir | share/locale | Locale data directory |
localstatedir | var | Localstate data directory |
mandir | share/man | Manual page directory |
sbindir | sbin | System executable directory |
sharedstatedir | com | Architecture-independent data directory |
sysconfdir | etc | Sysconf data directory |
prefix
在Windows上是C:/
,其他平台是/usr/local
libdir
是根据平台自动推测的,不同发行版也有区别
当prefix
设置为某些特定值时,其他选项的默认值会改变。
- 当
prefix
为/usr
时,sysconfdir
默认为/etc
,localstatedir
默认为/var
,sharedstatedir
默认为/var/lib
- 当
prefix
为/usr/local
时,localstatedir
默认为/var/local
,sharedstatedir
默认为/var/local/lib
如果安装目录是相对路径,则是相对于prefix
的;如果是绝对路径,则忽略prefix
。
这些选项的默认值可以通过project()
函数的default_options
参数改变,也可以执行meson configure
命令手动修改。
每个构建目标也可以通过install_dir
参数改变自己的安装目录。
python
executable('demo',
sources: 'main.c',
install: true,
install_dir: 'exec'
)
以我们最简单的"hello, world"程序为例。此时我们没有改变任何安装目录,所以可执行文件demo 的安装目录是/usr/local/bin
。如果安装时缺少权限,Meson会向你索要权限。
python
# Meson configuration for demo
project('demo', 'c',
version: '0.0.1',
default_options: {
'c_std': 'c17',
'warning_level': '3',
'werror': true,
'optimization': 'g',
'strip': true,
},
)
executable('demo', sources: 'main.c', install: true)
Meson默认的buildtype
是debug
,我们当然不能安装debug版本。执行下面的命令生成release构建目录,默认开启-O3
优化。同时修改安装目录。接着就可以执行meson install
命令安装,安装前会检查是否需要构建,保证产物最新。
sh
meson setup --buildtype=release --prefix="$HOME/.local" build_release
meson install -C build_release
从终端输出可以看到demo 被安装到了$HOME/.local/bin
目录下,并且执行了strip
。
vbnet
ninja: Entering directory `/home/lonble/demo/build_release'
[2/2] Linking target demo
Installing demo to /home/lonble/.local/bin
Stripping target 'demo'