A mature Autotools project does not merely turn C or C++ into a binary. Its configure.ac, Makefile.am files and Libtool rules collectively define which dependencies are optional, which preprocessor symbols appear in config.h, which tests may skip, which headers and libraries reach a staging root, what downstream pkg-config users see, how shared-library names are formed, and what a release tarball contains.
Meson can express that product with far less ceremony. That does not make the migration a syntax exercise. A new meson.build that reaches meson compile -C build has proved only that one machine can produce some targets. The migration is complete when the same feature choices produce the same public artifacts, tests, install layout, release archive and cross-built behavior—and when downstream packagers can explain every intentional difference.
This distinction is why speed belongs near the end of the case, not at the beginning. In an early Meson experiment on a Nexus 4, a partial GLib port configured in about 20 seconds versus roughly 220 seconds for Autotools, while a no-op check took about 0.25 seconds instead of 14 seconds. Meson's own report explicitly said the two builds were not fully equivalent.[12] The numbers made the opportunity visible; equivalence remained the work.
Image context: the cover frame shows Meson creator Jussi Pakkanen taking questions after a 2017 All Systems Go! talk about projects moving away from older build systems. The photograph belongs here because a build-system migration is ultimately a maintainer and downstream-coordination problem, not merely a faster local command.[13]
Begin with the product, not the build files
Before adding meson.build, produce a migration ledger from a clean Autotools release build. Save ./configure --help, the exact configure command used by each supported package, the generated config.h, the test inventory and outcomes, and the verbose compiler and linker commands. Stage the installation into an empty directory and record paths, file modes, symlinks, public headers, man pages, locale data, plugins, static archives, shared-library names and every .pc file.
Do this for more than the developer default. At minimum, capture a minimal build with optional integrations disabled, a full build in which every promised dependency is required, and each supported shared/static-library shape. A portable library should also retain one known cross build. If ./configure accepts 24 switches but CI exercises only three, the other 21 are not automatically requirements; they are unresolved promises. Classify them as preserve, rename, deprecate or remove before translation.
The staged installation is the most useful baseline because it describes what users receive rather than how maintainers produced it. It also exposes quiet contracts: a compatibility header, a versioned symlink, a plugin in libdir, or a Requires.private line that no unit test mentions. Meson's own porting guide spans configuration headers, dependency discovery, generated sources, libraries, installed headers, tests, introspection data, settings schemas and translation files for exactly this reason: the build graph is wider than compilation.[1]
Rollback should be equally concrete. Keep the last Autotools release recipe, its generated tarball and its packaging patches reproducible until the Meson package has passed downstream testing. “The old files are still in Git” is not a rollback if nobody can regenerate the release environment that made them work.
Translate decisions, not M4
The top-level Meson file starts by declaring a project and the minimum Meson version the maintainers actually test:
project(
'libsample', 'c',
version: '2.4.0',
meson_version: '>=1.4.0',
default_options: ['warning_level=2'],
)
From there, map each Autotools decision to a Meson concept rather than copying its shell mechanics. AC_CONFIG_HEADERS becomes a configuration_data() object and configure_file(). A PKG_CHECK_MODULES result becomes a dependency() object passed through dependencies:. Generated files become custom_target() outputs that targets consume explicitly. Installed headers use install_headers(). Library targets state version and soversion deliberately instead of inheriting Libtool arithmetic by accident.[1]
Optional dependencies deserve special care. A Meson feature option has three states: enabled, disabled and auto. Passed to dependency(..., required: get_option('feature_name')), those mean “fail if absent,” “do not probe,” and “use if found.” Packagers can also force all remaining automatic features on or off with auto_features.[2]
# meson.options
option('tls', type: 'feature', value: 'auto', description: 'TLS support')
# meson.build
tls_dep = dependency('openssl', required: get_option('tls'))
conf.set10('HAVE_TLS', tls_dep.found())
That is more than convenient syntax. It prevents a common packaging failure in which an optional library happens to exist on one builder, silently enlarging the product. CI should run -Dauto_features=enabled to prove the full dependency contract and -Dauto_features=disabled to prove the minimal one. If a feature is promised, its “enabled” lane must fail loudly when its dependency is missing.
Do not preserve hand-written compiler probes merely because they are old. Use compiler objects such as cc.has_header(), cc.has_function() and cc.links() when a capability truly must be tested; use Meson's dependency objects for established platform contracts. The project's FAQ specifically warns against finding libpthread manually and directs projects to dependency('threads'), because thread flags have cross-platform edge cases.[1]
Run two paths, but put an expiry date on the overlap
Keep Autotools authoritative while Meson is incomplete, then run both against the same commit and feature matrix. A typical Meson lane is intentionally plain:
meson setup build-meson \
--prefix=/usr \
--buildtype=debugoptimized \
-Dauto_features=enabled
meson compile -C build-meson
meson test -C build-meson --print-errorlogs
meson install -C build-meson --destdir "$PWD/stage-meson"
Match compiler, optimization, debug information, dependency versions and generated inputs before comparing time or output. Otherwise a “Meson improvement” may actually be a missing feature, a different compiler flag or fewer tests.
Dual maintenance is a verification phase, not a destination. GLib announced a bounded sequence in 2018: ship one release with Meson as the default while retaining Autotools, ask distributors and less-common toolchain users to test it, then remove Autotools in the following development cycle.[9] GStreamer's earlier experimental rollout likewise left Autotools primary while Linux, Windows, GCC, Clang and MSVC coverage matured; its maintainers reported a small example configuring, building and installing with Meson in under four seconds, compared with about 17 seconds of Autotools setup plus ten seconds of building on that developer's machine.[11]
The pattern matters more than those dated timings: declare what the overlap must discover and when the project will either cut over or abandon the attempt. LWN's account of Mesa's deliberations captured the same pressure from the other side. Developers wanted faster, simpler builds, but distributors and non-Linux paths were part of the decision; the proposed answer was one release with both systems, followed by a firm choice rather than indefinite duplication.[10]
Compare staged installations, not green compile logs
Install both builds into empty roots:
make DESTDIR="$PWD/stage-auto" install
meson install -C build-meson --destdir "$PWD/stage-meson"
diff -ruN stage-auto stage-meson
Byte-for-byte identity is not always sensible: embedded build paths, timestamps or debug sections may differ. Review the product-level differences instead. Compare the path and mode of every file, library symlinks and SONAMEs, exported symbols, public-header contents, plugin discovery, locale placement, man pages and downstream compile/link behavior. Meson installs nothing unless a target or file is marked for installation, while its DESTDIR support is explicitly designed for package staging.[3]
Use meson introspect build-meson --install-plan to inspect the proposed installation and --buildoptions, --dependencies, --targets and --tests to make the new graph reviewable by machines as well as people.[4] For a library, generate and then inspect the .pc file rather than assuming pkg.generate() inferred the intended public/private split. Its requires, requires_private, libraries and libraries_private fields affect dynamic and static consumers differently.[8]
Shared libraries need an ABI gate, not a filename glance. Compare SONAMEs and exported symbol sets, then compile a tiny downstream consumer against the staged headers and libraries. If the project uses symbol-version scripts, Darwin compatibility versions or Windows export definitions, test those on their native platforms. A Meson build that produces libsample.so while changing libsample.so.2, dropping an exported symbol or losing a private static dependency is not package parity.
Preserve the meaning of the test suite
Meson runs tests concurrently by default. A legacy suite that shares a fixed socket, database, temporary filename or D-Bus name can therefore become flaky even though every test worked under a serial Automake harness. Mark genuinely exclusive tests with is_parallel: false, then remove that restriction only after isolating their state.[5]
Also compare outcomes, not just the final exit status. Meson recognizes exit 77 as a skip and 99 as a hard setup error, but timeouts, expected failures, test wrappers, environment variables and suite membership still require explicit mapping.[5] Record the total tests, passes, skips and expected failures for each feature lane. A cross build that “passes” because all executable tests were skipped is weaker evidence than a native build with one failure.
Tests should consume the Meson-built artifacts rather than accidentally finding an installed system copy. The fastest way to discover a false-green port is to run it in a clean container or chroot whose only project files come from the current build and staged root.
Cross builds expose the wrong-machine mistakes
Autotools projects often hide years of build/host knowledge inside triplets, environment variables and custom macros. Meson moves that knowledge into a cross file: compilers and binutils under [binaries], sysroot and target pkg-config search paths, an optional exe_wrapper, and the [host_machine] system, CPU family and endianness. The file is selected at setup with meson setup --cross-file ....[7]
The high-risk seam is generated code. A program compiled to run during the build needs native: true; the library or executable being delivered uses the host toolchain. Confuse those roles and a native build may stay green while the first ARM, Windows or embedded build tries to execute a target binary on the build machine. Without a cross-file exe_wrapper, Meson skips tests that need to run host binaries, so skip counts belong in the acceptance record.[7]
For a Linux-only command-line utility maintained by two people, one native GCC lane and one Clang lane may be a proportionate gate. A portable library used by distributions needs more: at least one real cross file, native or emulated execution where feasible, downstream packaging, and explicit tests for build-time generators. Operational maturity should determine the matrix; popularity should not be used as a substitute for evidence.
Make the release archive earn the cutover
meson dist -C build-meson packages the latest revision-control commit, removes repository metadata, then performs a compile, test and install cycle on the resulting archive before creating a SHA-256 file. This differs from Autotools' source-tree-oriented dist: an uncommitted generated file will not quietly enter the Meson archive.[6]
Build the tarball in an offline clean environment. Check generated manuals, bindings, translation products and version files that Autotools may have shipped so end users did not need maintainer tools. If subprojects must travel with the release, decide whether --include-subprojects is part of the contract.[6] A build from the Git checkout and a build from the published tarball are two separate acceptance tests.
This is also where the toolchain floor becomes real. Meson requires Python, and the usual Ninja backend adds another bootstrap dependency. A project that must build on legacy Unix systems with no supportable Python/Meson path may rationally retain Autotools. So may a stable library whose downstream packagers cannot yet reproduce its platform probes or ABI. Migration is an operating decision, not a referendum on which syntax looks cleaner.
Six gates before deletion
Remove Autotools only after all six statements are true:
- Every retained configure feature has an explicit Meson option and both minimal and full matrices behave as documented.
- Native builds compile the same targets with reviewed compiler and linker differences.
- Test inventories, passes, skips and expected failures match—or every change has an accepted reason.
- Staged installations match in paths, modes, symlinks, metadata, ABI and downstream consumer behavior.
- Supported cross builds keep build-machine tools separate from host artifacts and do not hide behind unexpected skips.
- The release archive builds, tests and installs from a clean offline environment, and at least one downstream packager has reproduced it.
The falsifier is simple: if the Meson path repeatedly needs project-specific glue that is harder to explain than the Autotools logic it replaces, or if supported downstream environments cannot reproduce the same product, the promised maintenance reduction has not materialized. Keep the experiment, fix the gap or stop the migration. Do not delete the known path to force agreement.
When the gates pass, delete Autotools promptly. Two build descriptions double the places where a new source file, option, test or install artifact can drift. The rewarding moment is not the first sub-second no-op build. It is the first release in which maintainers, CI and packagers all use one legible graph—and the install tree gives them the same product they already knew how to trust.
Sources
- Meson documentation, “Porting from Autotools” — mappings for configuration headers, dependencies, generated sources, libraries, installed data, tests and GNOME-specific build products.
- Meson documentation, “Build options” —
meson.options, three-state feature options and the packager-facingauto_featuresoverride. - Meson documentation, “Installing” — install declarations, standard install directories, custom scripts and package-staging behavior with
DESTDIR. - Meson documentation, “Command-line commands” — setup, configure, compile, install and JSON introspection of options, dependencies, targets, tests and the install plan.
- Meson documentation, “Unit tests” — parallel execution, test environments, logs, skip and hard-error exit codes, suites and wrappers.
- Meson documentation, “Creating releases” — VCS-based
meson dist, archive compile/test/install verification, checksums and bundled-subproject behavior. - Meson documentation, “Cross compilation” — cross files, build/host/target terminology, executable wrappers, sysroots, target
pkg-configpaths and native build-time generators. - Meson documentation, “Pkgconfig module” — generated
.pcfiles, public/private requirements and libraries, install locations and uninstalled metadata. - Emmanuele Bassi, “News from GLib 2.58,” GTK Development Blog, July 11, 2018 — the project's one-release Meson-default overlap, distributor testing request and planned Autotools removal.
- Jonathan Corbet, “Moving Mesa to Meson,” LWN.net, March 29, 2017 — independent reporting on migration benefits, distribution concerns and the case for a time-bounded dual-build release.
- Tim Müller, “Experimental build using the Meson build system,” GStreamer-devel, September 2, 2016 — primary account of the staged rollout, platform/toolchain goals, known gaps and local timing comparison.
- Meson project, “Arm performance test” — the dated Nexus 4 GLib experiment, its measured configure/no-op results and its explicit non-equivalence caveat.
- media.ccc.de, “Meson and the changing Linux build landscape,” All Systems Go! 2017 — official archival recording used for the article's cover still and event provenance.