Meson 常见问题解答

另请参阅 如何在 Meson 中执行 X

为什么它被称为 Meson?

最初选择这个名字时,有两个主要限制:不能存在相同名称的 Debian 软件包或 Sourceforge 项目。这排除了数十个潜在的项目名称。在某个时候,考虑过 Gluon 这个名字。胶子是将质子和中子结合在一起的基本粒子,就像构建系统的任务是将源代码片段和编译器绑定在一起形成一个完整的整体一样。

不幸的是,这个名字也被使用了。然后,我们检查了其他亚原子粒子,发现 Meson 可用。

使用线程(例如 pthreads)的正确方法是什么?

thread_dep = dependency('threads')

这将为你设置好一切。来自 Autotools 或 CMake 的用户希望通过手动查找 libpthread.so 来做到这一点。不要这样做,它有棘手的边缘情况,尤其是在交叉编译时。

如何在没有系统包的宿主主机上使用 Meson?

从 0.29.0 版本开始,Meson 可从 Python 包索引 获取,因此安装它只需运行以下命令

$ pip3 install <your options here> meson

如果你无法访问 PyPI,那也没关系。Meson 被设计为可以轻松地从解压缩的源代码压缩包或甚至 git 检出中运行。首先,你需要下载 Meson。然后,使用以下命令设置你的构建,而不是使用普通的 meson

$ /path/to/meson.py <options>

此后,你无需再关心调用 Meson。它会记住最初调用的位置,并相应地调用自身。作为用户,你只需要 cd 到你的构建目录并调用 meson compile

为什么不能使用通配符指定目标文件?

人们似乎不想显式地指定文件,而是想这样做

executable('myprog', sources : '*.cpp') # This does NOT work!

Meson 不支持这种语法,原因很简单。这无法同时做到可靠和快速。可靠是指,如果用户向子目录添加了一个新的源文件,Meson 应该检测到这一点,并将其自动包含到构建中。

Meson 的主要要求之一是它必须是快速的。这意味着,在 10 000 个源文件的树中进行无操作构建,花费的时间不应超过几分之一秒。只有当 Meson 知道要检查的确切文件列表时,这才是可能的。如果任何目标都以通配符 glob 指定,这就不再可能。Meson 需要在每次都重新评估 glob,并将生成的与前一个列表进行比较。这意味着要检查整个源代码树(因为 glob 模式可能是 src/\*/\*/\*/\*.cpp 或类似的模式)。这不可能有效地完成。

Meson 的主要后端是 Ninja,它也不支持通配符匹配,原因也是相同的。

因此,必须显式地指定所有源文件。

但是我真的想使用通配符!

如果你可以接受可靠性和便利性之间的权衡,那么 Meson 提供了你进行通配符 glob 所需的所有工具。你可以在配置期间运行任意命令。首先,你需要编写一个脚本,用于定位要编译的文件。以下是一个简单的 shell 脚本,它将当前目录中的所有 .c 文件逐行写入。

#!/bin/sh

for i in *.c; do
  echo $i
done

然后,你需要在 Meson 文件中运行此脚本,将输出转换为字符串数组,并将结果用于目标。

c = run_command('grabber.sh', check: true)
sources = c.stdout().strip().split('\n')
e = executable('prog', sources)

脚本可以是任何可执行文件,因此它可以用 shell、Python、Lua、Perl 或你想要的任何语言编写。

如上所述,权衡是,仅仅向源代码目录添加新文件并不会自动将它们添加到构建中。要添加它们,你需要告诉 Meson 重新初始化自身。最简单的方法是修改你源代码根目录中的 meson.build 文件。然后,下次运行构建命令时,Meson 会重新配置自身。高级用户甚至可以编写一个小的后台脚本,利用文件系统事件队列,例如 inotify,来自动执行此操作。

我应该使用 subdir 还是 subproject

答案几乎总是 subdir。Subproject 存在于一个非常具体的用例中:将外部依赖项嵌入到你的构建过程中。例如,假设我们正在编写一个游戏,并且想要使用 SDL。假设 SDL 带有一个 Meson 构建定义。假设我们不想使用预构建的二进制文件,而是想要自己编译 SDL。

在这种情况下,你将使用 subproject。具体方法是获取 SDL 的源代码,并将其放在你自己的源代码树中。然后,你将执行 sdl = subproject('sdl'),这将导致 Meson 作为你构建的一部分构建 SDL,然后允许你链接到它,或执行你可能喜欢的任何其他操作。

对于其他所有用途,你将使用 subdir。例如,如果你想在一个目录中构建一个共享库,并在另一个目录中链接测试,你将执行以下操作

project('simple', 'c')
subdir('src')   # library is built here
subdir('tests') # test binaries would link against the library here

为什么没有 Make 后端?

因为 Make 很慢。这不是实现问题,Make 本身无法变得很快。有关更多信息,我们建议你阅读由 Ninja 作者 Evan Martin 撰写的 这篇文章。Makefile 还有非常令人不快的编写语法,这使得它们成为一个巨大的维护负担。

使用 Make 而不是 Ninja 的唯一原因是在没有 Ninja 移植的平台上工作。即使在这种情况下,移植 Ninja 的工作量也要比为 Meson 编写 Make 后端要少一个数量级。

直接使用 Ninja,你会更高兴。我保证。

为什么 Meson 不是一个 Python 模块,这样我就可以用 Python 编写我的构建设置?

与这个问题相关的另一个问题是为什么 Meson 的配置语言不是图灵完备的?

有很多很好的理由,其中大部分都在此网页上总结:反对在配置文件中使用编程语言

除了这些原因之外,不公开 Python 或任何其他“真正的”编程语言,使 Meson 的实现可以移植到其他语言。如果 Python 成为性能瓶颈,这可能是必要的。这实际上是一个问题,它给 GNU Autotools 和 SCons 带来了一些麻烦。

如何执行 Libtools export-symbol 和 export-regex 的等效操作?

可以通过使用 GCC 符号可见性 或编写 链接器脚本 来实现。这样做的好处是,你的符号定义位于独立文件中,而不是埋在你的构建定义中。一个示例可以在这里找到 这里

对于 GCC,除非你另有指定,否则共享库上的所有符号都会自动导出。对于 MSVC,默认情况下不会导出任何符号。如果你的共享库没有导出任何符号,MSVC 会默默地不生成导入库文件,导致失败。解决方法是添加符号可见性定义,如 GCC wiki 中所述

我添加了一些编译器标志,现在构建失败,出现奇怪的错误。发生了什么事?

你可能做了等效于以下操作

executable('foobar', ...
           c_args : '-some_arg -other_arg')

Meson 是显式的。在这种特定情况下,它不会自动在你的字符串处分割空格,而是会原样地获取它,并努力地将它原封不动地传递给编译器,包括在 shell 调用上正确地引用它。这是必须的,才能使例如包含空格的文件完美无缺地工作。要传递多个命令行参数,你需要将它们显式地放在数组中,例如

executable('foobar', ...
           c_args : ['-some_arg', '-other_arg'])

为什么对默认项目选项的更改被忽略?

你可能有一个类似于这样的项目

project('foobar', 'cpp')

这在 GCC 编译器上默认为 c++11。假设你想要使用 c++14,因此你将定义更改为以下内容

project('foobar', 'cpp', default_options : ['cpp_std=c++14'])

但是,当你重新编译时,它仍然使用 c++11。原因是,只有在你第一次设置构建目录时才会查看默认选项。之后,该设置被认为具有值,因此默认值会被忽略。要将现有构建目录更改为 c++14,请使用 meson configure 重新配置你的构建目录,或删除构建目录并从头开始重新创建它。

我们没有在更改默认值时自动更改选项值的原因是,我们无法可靠地知道该怎么做。我们需要解决的实际问题是“如果选项的值是 foo,而默认值是 bar,我们是否也应该将选项值更改为 bar”。有很多选择

  • 如果用户自己从默认值更改了该值,那么我们绝不能将其更改回来

  • 如果用户没有更改该值,但更改了默认值,那么本节的前提似乎表明该值应该更改

  • 假设用户将该值从默认值更改为 foo,然后更改回 bar,然后将默认值更改为 bar,那么要采取的正确步骤本身是模棱两可的

为了解决后一个问题,我们需要记住的不仅是当前值和旧值,还要记住用户更改该值的次数,以及从哪个值更改到哪个值。由于人们不记得自己那么久以前的行动,因此根据漫长的历史来切换状态会令人困惑。

因此,我们做简单而易懂的事情:默认值只是默认值,并且永远不会影响设置后的选项的值。

wrap 会在背后下载源代码吗?

不会。为了使 Meson 在构建时从网络下载任何内容,必须满足两个条件。

首先,subprojects 目录中需要有一个包含下载 URL 的 .wrap 文件。如果没有,Meson 不会下载任何东西。

第二个要求是你的meson.build文件中需要有显式的子项目调用。要么是subproject('foobar'),要么是dependency('foobar', fallback : ['foobar', 'foo_dep'])。如果这些声明要么不在任何构建文件中,要么没有被调用(比如因为if/else),那么就不会下载任何东西。

如果这对你来说还不够,从0.40.0版本开始,Meson有一个叫做wrap-mode的选项,可以用来完全禁用wrap下载,使用--wrap-mode=nodownload。你也可以使用--wrap-mode=nofallback完全禁用依赖回退,这也意味着nodownload选项。

另一方面,如果你想让Meson始终使用依赖的回退,即使外部依赖存在并且可以满足版本要求,例如为了确保你的项目在使用回退时也能构建,你可以使用--wrap-mode=forcefallback(从0.46.0版本开始)。

为什么Meson是用Python而不是[编程语言X]实现的?

因为构建系统在某些方面与普通应用程序不同。

也许最大的限制是,因为Meson被用于构建操作系统最底层的软件,它是新系统核心引导的一部分。每当添加对新CPU架构的支持时,Meson必须在系统上运行,然后才能编译使用它的软件。

第一个限制是Meson必须具有最少的依赖关系,因为它们都必须在引导过程中构建,才能使Meson正常工作。

第二个限制是Meson必须支持所有CPU架构,包括现有和未来的架构。例如,许多新的编程语言只提供基于LLVM的编译器。与GCC相比,LLVM的CPU支持有限,因此在这些平台上引导Meson首先需要向LLVM添加新的处理器支持。在大多数情况下,这是不可行的。

另一个限制是我们希望尽可能多的平台上的开发者使用他们操作系统提供的默认工具向Meson开发提交代码。在实践中,这意味着Windows开发者应该能够只使用Visual Studio进行贡献。

在撰写本文时(2018年4月),只有三种语言可以满足这些要求。

  • C
  • C++
  • Python

在这三者中,我们选择了Python,因为它最符合我们的需求。

但我真的想要一个不使用python的Meson版本!

生态系统多样性是好事。我们鼓励感兴趣的用户自己编写这个竞争性的Meson实现。截至2021年9月,有3个项目正在尝试这样做。

我有一个专有的编译器工具链X,它与Meson不兼容,如何才能让它工作?

Meson需要了解每个编译器的一些细节,才能用它编译代码。这些细节包括为每个选项使用哪些编译器标志以及如何从其输出中检测编译器。这些信息不能通过配置文件输入,而是需要更改Meson的源代码,并将这些更改提交到Meson主仓库。理论上,你可以运行你自己的带有自定义补丁的分支版本,但这并不好用。请将代码提交到上游,以便每个人都能使用这个工具链。

为现有语言添加新编译器的步骤大致如下。为了简单起见,我们将假设这是一个C编译器。

  • mesonbuild/compilers/c.py中创建一个具有适当名称的新类。查看同一语言的其他编译器的方法,并复制它们的操作。

  • 如果编译器只能用于交叉编译,请确保将其标记为交叉编译(参见现有编译器类的示例)。

  • mesonbuild/environment.py中添加检测逻辑,寻找一个名为detect_c_compiler的方法。

  • 运行测试套件,并修复问题,直到测试通过。

  • 提交一个pull request,将测试套件的结果添加到你的MR中(链接现有页面即可)。

  • 如果编译器是免费提供的,请考虑将其添加到CI系统中。

为什么用MSVC构建我的项目会输出名为libfoo.a的静态库?

Windows上静态库的命名约定通常是foo.lib。不幸的是,导入库也被称为foo.lib

这会导致默认库类型的文件名冲突,因为我们同时构建了共享库和静态库,也会导致安装过程中的冲突,因为默认情况下所有库都被安装到同一个目录。

为了解决这个问题,我们决定在用MSVC构建时,默认创建形式为libfoo.a的静态库。这样做有以下优点:

  1. 文件名冲突完全避免了。
  2. MSVC静态库的格式是ar,它与GNU静态库格式相同,因此使用这种扩展名在语义上是正确的。
  3. 静态库文件名格式现在在所有平台和所有工具链上都相同。
  4. Clang和GNU编译器都可以在指定库为-lfoo时搜索libfoo.a。这对于静态库的替代命名方案(如libfoo.lib)是行不通的。
  5. 由于-lfoo可以开箱即用,因此pkgconfig文件将对用MSVC、GCC和Clang在Windows上构建的项目都能正常工作。
  6. MSVC没有用于搜索库文件名的参数,而且它不关心扩展名是什么,因此指定libfoo.a而不是foo.lib不会改变工作流程,而且是一个改进,因为它不那么模棱两可。
  7. 只要使用相同的CRT(例如MSYS2中的UCRT),用MinGW编译器构建的项目与MSVC完全兼容。这些项目也会将其静态库命名为libfoo.a

如果出于某种原因,你真的需要你的项目在用MSVC构建时输出形式为foo.lib的静态库,你可以将name_prefix:关键字参数设置为'',并将name_suffix:关键字参数设置为'lib'。要获得每个关键字参数的默认行为,你可以不指定关键字参数,也可以向其传递[](空数组)。

我需要像在Autotools中一样将我的头文件添加到源文件列表中吗?

Autotools要求你将私有和公共头文件添加到源文件列表中,以便它知道在make dist生成的tarball中包含哪些文件。Meson的dist命令只是收集你提交到git/hg仓库中的所有内容,并将它们添加到tarball中,因此将头文件添加到源文件列表中是毫无意义的。

Meson使用Ninja,Ninja使用编译器依赖信息自动确定C源文件和头文件之间的依赖关系,因此当头文件发生变化时,它将正确地重建东西。

唯一的例外是生成的头文件,对于这些头文件,你必须正确声明依赖关系

如果出于某种原因,你确实将非生成的头文件添加到目标的源文件列表中,Meson会简单地忽略它们。

如何告诉Meson我的源文件使用生成的头文件?

假设你使用custom_target()生成头文件,然后在你的C代码中#include它们。以下是确保Meson在尝试编译构建目标中的任何源文件之前生成头文件的方法。

libfoo_gen_headers = custom_target('gen-headers', ..., output: 'foo-gen.h')
libfoo_sources = files('foo-utils.c', 'foo-lib.c')
# Add generated headers to the list of sources for the build target
libfoo = library('foo', sources: [libfoo_sources + libfoo_gen_headers])

现在假设你有一个新的目标链接到libfoo

libbar_sources = files('bar-lib.c')
libbar = library('bar', sources: libbar_sources, link_with: libfoo)

这在两个目标之间添加了一个**链接时**依赖关系,但请注意,目标的源文件**没有编译时**依赖关系,可以按任何顺序构建;这提高了并行性并加快了构建速度。

如果libbar中的源文件也使用foo-gen.h,那么这是一个**编译时**依赖关系,你必须将libfoo_gen_headers也添加到libbarsources:

libbar_sources = files('bar-lib.c')
libbar = library('bar', sources: libbar_sources + libfoo_gen_headers, link_with: libfoo)

或者,如果你有多个库,它们的源文件链接到一个库并使用其生成的头文件,那么这段代码等同于上面的代码。

# Add generated headers to the list of sources for the build target
libfoo = library('foo', sources: libfoo_sources + libfoo_gen_headers)

# Declare a dependency that will add the generated headers to sources
libfoo_dep = declare_dependency(link_with: libfoo, sources: libfoo_gen_headers)

...

libbar = library('bar', sources: libbar_sources, dependencies: libfoo_dep)

注意:在声明依赖关系时,你应该只将头文件添加到sources:中。如果你的自定义目标既输出源文件又输出头文件,你可以使用下标表示法只获取头文件。

libfoo_gen_sources = custom_target('gen-headers', ..., output: ['foo-gen.h', 'foo-gen.c'])
libfoo_gen_headers = libfoo_gen_sources[0]

# Add static and generated sources to the target
libfoo = library('foo', sources: libfoo_sources + libfoo_gen_sources)

# Declare a dependency that will add the generated *headers* to sources
libfoo_dep = declare_dependency(link_with: libfoo, sources: libfoo_gen_headers)
...
libbar = library('bar', sources: libbar_sources, dependencies: libfoo_dep)

gnome.mkenums()是一个很好的例子,它既输出源文件又输出头文件。

如何在C++项目中禁用异常和RTTI?

使用cpp_ehcpp_rtti选项。典型的调用方式如下:

meson -Dcpp_eh=none -Dcpp_rtti=false <other options>

RTTI选项仅在Meson 0.53.0版本及以后可用。

我应该在构建文件中检查buildtype还是像debug这样的单个选项?

这在很大程度上取决于你真正需要发生的事情。`buildtype`选项旨在描述当前构建的意图。也就是说,它将用于什么。单个选项用于确定确切的状态。通过一些示例,这会变得更加清晰。

假设你有一个源文件,已知在使用-O3时会编译错误,并且需要解决方法。然后,你可以这样写:

if get_option('optimization') == '3'
    add_project_arguments('-DOPTIMIZATION_WORKAROUND', ...)
endif

另一方面,如果你的项目有额外的日志记录和健全性检查,你希望在日常开发工作中(使用debug构建类型)启用这些检查,那么你应该这样做:

if get_option('buildtype') == 'debug'
    add_project_arguments('-DENABLE_EXTRA_CHECKS', ...)
endif

这样,额外的选项在开发过程中会自动使用,但在发布构建中不会被编译。请注意(从Meson 0.57.0版本开始),你可以在调试构建中将优化设置为2,例如,如果你想这样做。如果你试图根据优化级别设置这个标志,那么在这种情况下它会失败。

如何在声明库之前使用它?

这是有效的(而且好的)代码:

libA = library('libA', 'fileA.cpp', link_with : [])
libB = library('libB', 'fileB.cpp', link_with : [libA])

但目前没有办法让类似这样的代码工作:

libB = library('libB', 'fileB.cpp', link_with : [libA])
libA = library('libA', 'fileA.cpp', link_with : [])

这意味着你必须按依赖关系流动的顺序编写你的library(...)调用。虽然有一些想法可以让任意顺序成为可能,但它们被拒绝了,因为将library(...)调用重新排序被认为是“正确”的方法。有关讨论,请参见这里

为什么meson没有用户定义的函数/宏?

对此的简短答案是,meson的设计重点是解决特定问题,而不是提供一种通用语言来在构建文件中编写复杂的代码解决方案。构建系统应该快速编写和理解,函数会使这种简单性变得混乱。

长答案有两个方面:

首先,Meson 的目标是提供一套丰富的工具,这些工具可以轻松地解决特定问题。这类似于 Python 的“包含电池”理念。通过提供以最简单的方式解决常见问题的工具,我们为每个人解决了这个问题,而不是强迫每个人一遍又一遍地解决这个问题,而且往往效果不佳。一个例子是 Meson 对各种配置工具可执行文件(sdl-config、llvm-config 等)的依赖项包装器。在其他构建系统中,每个使用该依赖项的用户都会编写一个包装器并处理特殊情况(或者不处理,就像经常发生的那样),在 Meson 中,我们会在内部处理它们,每个人都会得到修复,特殊情况会为每个人解决。提供用户定义的函数或宏会直接违背这个设计目标。

其次,函数和宏使构建系统更难推理。当您遇到某个函数调用时,您可以参考参考手册以查看该函数及其签名。与其花几个小时尝试解释一段 m4 代码或遵循长长的包含路径来弄清楚 function1(它调用 function2,它调用 function3,以此类推)是什么,不如了解构建系统正在做什么。除非您正在积极开发 Meson 本身,否则它只是一个用于协调构建您真正关心的东西的工具。我们希望您尽可能少地担心构建系统,这样您就可以花更多的时间在您的代码上。

很多时候,用户定义的函数之所以被使用,是因为缺少循环,或者因为在语言中使用循环很乏味。Meson 本身就包含数组/列表和哈希/字典。比较以下伪代码

func(name, sources, extra_args)
  executable(
    name,
    sources,
    c_args : extra_args
  )
endfunc

func(exe1, ['1.c', 'common.c'], [])
func(exe2, ['2.c', 'common.c'], [])
func(exe2_a, ['2.c', 'common.c'], ['-arg'])
foreach e : [['1', '1.c', []],
             ['2', '2.c', []],
             ['2', '2.c', ['-arg']]]
  executable(
    'exe' + e[0],
    e[1],
    c_args : e[2],
  )
endforeach

与函数版本相比,循环代码更少,更容易理解,尤其是当函数位于单独的文件中时,这在其他流行的构建系统中很常见。

构建系统 DSL 往往也被设计成通用的编程语言,Meson 试图简化使用外部脚本或程序来处理复杂问题。虽然不能总是将构建逻辑转换为脚本语言(或编译语言),但如果可以这样做,这通常是更好的解决方案。外部语言往往经过深思熟虑并经过测试,通常不会发生退化,用户更有可能拥有关于它们的领域知识。它们也往往具有更好的工具(如自动完成、代码检查、测试解决方案),这使得它们在时间上的维护负担更小。

给定如下代码

add_project_link_arguments(['-Wl,-foo'], language : ['c'])
executable(
  'main',
  'main.c',
  'helper.cpp',
)

您可能会惊讶地发现 -Wl,-foo 没有应用于 main 可执行文件的链接。在这种情况下,Meson 按预期工作,因为 Meson 会尝试自动确定要使用的正确链接器。这避免了在 autotools 中,必须向某些编译目标添加虚拟 C++ 源代码以获得正确链接的情况。因此,在上述情况下,C++ 链接器被使用,而不是 C 链接器,因为 helper.cpp 可能无法使用 C 链接器链接。

通常,解决此问题的最佳方法是将 cpp 语言添加到 add_project_link_arguments 调用中。

add_project_link_arguments(['-Wl,-foo'], language : ['c', 'cpp'])
executable(
  'main',
  'main.c',
  'helper.cpp',
)

要强制使用 C 链接器,可以使用 link_language 关键字参数。请注意,如果存在 C 链接器无法解析的符号,这会导致编译失败。

add_project_link_arguments(['-Wl,-foo'], language : ['c'])
executable(
  'main',
  'main.c',
  'helper.cpp',
  link_language : 'c',
)

如何在 VCS 中忽略构建目录?

您不需要这样做,假设您使用 git 或 mercurial!Meson >=0.57.0 会为您创建 .gitignore.hgignore 文件,位于每个构建目录中。它使用通配符忽略 "*",因为所有生成的文件都不应该检入 git。

使用旧版本 Meson 的用户可能需要自行设置忽略文件。

如何向目标添加预处理器定义?

只需将 -DFOO 添加到 c_argscpp_args 中。这适用于所有已知的编译器。

mylib = library('mylib', 'mysource.c', c_args: ['-DFOO'])

即使 MSVC 文档 使用 /D 用于预处理器定义,但其 命令行语法 接受 - 而不是 /。在 Meson 中,无需对预处理器定义进行特殊处理 (GH-6269).

搜索结果为