Both GN and Meson emerged as attempts to fix the CMake ergonomics problem without giving up the C/C++ focus. Both generate build.ninja files (plus Visual Studio/Xcode projects). Both have gained significant production use. But their security properties are meaningfully different, and I have deployed both in supply-chain-sensitive contexts over the past two years. This post is the comparison I wish I had before those projects started.
I am going to keep this concrete: specific versions, specific config files, specific trade-offs. The goal is to help someone choose between them for a new project, or understand the risk profile of an existing one.
GN: Chromium's Build Language
GN ("Generate Ninja") is the build system Google wrote for Chromium when GYP started to scale poorly. It is Python-inspired, declarative, and tightly coupled to Ninja. GN itself is a small C++ binary that parses .gn files and emits .ninja files. It is not a general-purpose build tool in the way CMake is; it is optimized for large C++ codebases that want strong structure.
A minimal BUILD.gn:
import("//build/config/chrome_build.gni")
executable("payments_daemon") {
sources = [
"src/main.cc",
"src/ledger.cc",
]
deps = [
"//base",
"//net",
"//third_party/boringssl",
]
configs += [ "//build/config/compiler:wexit_time_destructors" ]
}
Three security-relevant properties:
- No arbitrary script execution at evaluation. GN's language is restricted. You cannot
exec()a subprocess or read a file from disk during evaluation. This is stronger than CMake, which allowsexecute_processliberally. - Explicit toolchain definitions. Every target declares a toolchain. Chromium's build uses pinned toolchain versions fetched via
depot_tools, so the compiler is a project artifact, not an environment assumption. - Strict visibility. GN's
visibilityandpublic_depsrules enforce that a target cannot silently depend on a non-exported internal.
For supply chain, the toolchain property matters most. A Chromium build on a random developer laptop uses the same clang version as CI, because the clang binary is fetched into src/third_party/llvm-build/ and referenced by path. There is no $PATH lookup, no "whichever gcc happens to be installed." This closes the class of attacks where a malicious compiler wrapper influences the build.
GN's Weaknesses
GN is not a package manager. Third-party dependencies are vendored into third_party/ and built in place. The update process is manual: a human runs roll-dep, checks in the new source, and the build picks up the changes. There is no lockfile because there is nothing being locked -- the dependency is the source checked into your repository.
This shifts the supply chain boundary from "what does the package manager give me" to "what does my third_party/ directory contain." It is a different problem, not necessarily a worse one. Chromium handles it through aggressive code review of third_party/ changes and automated scanning. For smaller projects, the same discipline is harder to maintain.
GN is also very Chromium-shaped. Using it outside Chromium requires porting build configs, which means maintaining a fork of //build. This is doable -- Fuchsia uses GN, as does the Dawn graphics library -- but it is work. For a new project, the GN ecosystem is narrow.
Meson: Smaller and More Approachable
Meson is a Python 3 build system that, like GN, emits Ninja files. It is used by GNOME, GStreamer, systemd, and the Mesa graphics stack. Meson 1.3.0 (released December 2023) is what I ran for a recent audit.
A minimal meson.build:
project('payments-daemon', 'cpp',
version : '1.4.2',
default_options : ['warning_level=3', 'cpp_std=c++20'])
boringssl_dep = dependency('boringssl', version : '>=0.0', required : true)
spdlog_dep = dependency('spdlog', version : '>=1.12')
executable('payments_daemon',
sources : ['src/main.cc', 'src/ledger.cc'],
dependencies : [boringssl_dep, spdlog_dep],
install : true)
Meson's supply chain story is richer than GN's in one key area: the wrap file system. A subprojects/spdlog.wrap file looks like:
[wrap-file]
directory = spdlog-1.12.0
source_url = https://github.com/gabime/spdlog/archive/v1.12.0.tar.gz
source_filename = spdlog-1.12.0.tar.gz
source_hash = 4dccf2d10f410c1e2feaff89966bfc49a1abb29ef6f08246335b110e001e09a9
patch_directory = spdlog
[provide]
spdlog = spdlog_dep
The source_hash is enforced. Meson refuses to build if the downloaded file does not match, and it caches the file in subprojects/packagecache/ keyed by hash. This is roughly equivalent to Bazel Central Registry's integrity enforcement, done per-project.
Meson's WrapDB (https://wrapdb.mesonbuild.com/) is a small central index of approved wrap files for common dependencies. Use wraps from WrapDB by running meson wrap install spdlog, which fetches the wrap file, hash-verifies it, and drops it into subprojects/.
Meson's Weaknesses
Meson's evaluation is stricter than CMake but looser than GN. You cannot execute arbitrary shell, but you can call run_command() to invoke external tools, which means a malicious meson.build has a local code execution vector at configuration time. Treat meson.build files the same way you treat setup.py or Cargo.toml build scripts: inspect before running.
The Python implementation is a mixed blessing. It makes Meson easy to extend and debug, but it also means every Meson build ships with an implicit dependency on a working Python 3 interpreter. If your threat model includes supply chain attacks on Python packages, Meson is technically downstream of pypi in a way GN is not.
Third-party wraps are community-maintained. The quality varies. A wrap in WrapDB has been through some review, but wraps published elsewhere can contain anything. Pin to WrapDB, and be suspicious of wraps that live outside it.
Toolchain Handling: GN Wins
GN's explicit, path-based toolchains are genuinely more secure than Meson's cc detection logic. Meson finds the compiler by asking the environment ($CC, then $PATH). You can override with --native-file and --cross-file, which pin to absolute paths:
# cross-x86_64-linux.ini
[binaries]
c = '/opt/clang-17.0.6/bin/clang'
cpp = '/opt/clang-17.0.6/bin/clang++'
ar = '/opt/clang-17.0.6/bin/llvm-ar'
strip = '/opt/clang-17.0.6/bin/llvm-strip'
[properties]
sys_root = '/opt/sysroot-linux-x86_64'
Do this in CI and audit developer setups for consistency. Without --cross-file, Meson will silently use whatever compiler is on the build machine.
Dependency Resolution
GN has no dependency resolver. You either vendor a dependency or you don't.
Meson has three paths:
- System dependencies via
pkg-config. Meson askspkg-configfor a library; the supply chain boundary is the system package manager. - Subprojects via wrap files. The supply chain boundary is the hash in the wrap file.
- CMake subprojects via
import('cmake'). This lets Meson wrap a CMake-built dependency, inheriting all of CMake's supply chain properties (usually not great).
For supply chain posture, prefer wrap files with hashes over system dependencies. Wrap files make the build self-contained and auditable; system dependencies push the trust boundary outward to an OS package manager whose integrity you may not control.
Reproducibility
Both GN and Meson produce reproducible builds with effort. The usual fixes apply: SOURCE_DATE_EPOCH, -ffile-prefix-map, LC_ALL=C, deterministic linker options.
Meson 1.2+ has built-in support for reproducible builds via b_reproducible=true (experimental as of 1.3). GN inherits reproducibility from Chromium's extensive work on the topic; is_official_build=true in Chromium gives bit-reproducible output on Linux when the toolchain is pinned.
For a greenfield project targeting reproducibility, both are workable. GN gives you stronger defaults; Meson gives you a faster path to get there without Chromium-scale infrastructure.
Choosing Between Them
Pick GN if:
- You are building a Chromium fork or using Chromium-derived libraries.
- Your project is large (millions of LOC) and you want declarative discipline.
- You have infrastructure to maintain a pinned toolchain and vendored dependencies.
Pick Meson if:
- You want a build system that integrates cleanly with existing Linux distribution packaging.
- You have external dependencies you want to pull via wrap files rather than vendor.
- Python 3 is already in your trusted base.
For most new C++ projects I advise on, Meson is the pragmatic choice. For projects that are serious about supply chain at Chromium's threat level, GN's model is more defensible.
How Safeguard Helps
Safeguard ingests both GN build graphs (via gn desc output) and Meson introspection data (meson introspect --all) to produce CycloneDX 1.5 SBOMs that capture every vendored subproject, wrap file, and pkg-config dependency with integrity context. Policy gates can enforce wrap file hash presence, block wraps sourced outside WrapDB, or require absolute toolchain paths in Meson cross-files. For GN-based projects, Safeguard correlates third_party/ directory changes with commit provenance to detect unexpected dependency rolls. The combination layers centralized governance on top of the per-project supply chain controls that GN and Meson provide natively, giving security teams visibility across projects that use either generator.