Introduction

This is the official documentation for Muon, a meson-compatible build system. Most of the topics discussed are muon-specific.

Features

In addition to trying maintain Meson compatibility, Muon also has a few features of its own.

muon analyze

muon has a full-featured static analyzer for meson build scripts. It can check for undefined variables, unused variables, variable reassignment to different types, undefined functions, undefined kwargs, missing required args, methods, operator typechecking, and more.

See muon analyze -h for more information.

muon fmt

A formatter for meson build scripts.

See muon fmt -h for more information.

Debugger (WIP)

Muon has an interactive gdb-like debugger. You can trigger it by setting breakpoints with the -b flag on either setup or internal eval subcommands. Inside the debugger you can look around, evaluate expressions, and step through code.

Built-In Ninja (samurai)

Muon has an embedded copy of samurai (at src/external/samurai) which has been ported to use muon's cross-platform layer and so works on all the platforms muon supports. This means you can just grab a single muon binary and that's all you need to build your project.

VM

Muon's language runtime is a fully featured stack-based VM. All scripts are compiled to bytecode before execution. This runtime is powerful enough to host modules as well as basic cross-platform scripting. See Scripting for more information

Fast

Muon is orders of magnitude faster than meson when your scripts are interpreter- bound. This means that as long as muon doesn't have to shell out to the compiler it is very fast.

Typically the most speedup can be seen during re-configs where most compiler checks have been cached.

Differences Between Muon and Meson

This document describes functional differences between muon and Meson. None of these is a set-in-stone design decision, just a reflection of the current state of affairs. This document is also not exhaustive, but is a best-effort list.

Some other small differences may be found by searching the tests/project for "# different" comments.

nested subproject promotion

Meson performs nested subproject promotion. This means that nested subprojects become top-level subprojects, and all subprojects share the same namespace.

For example, given the following project structure:

.
├── meson.build
└── subprojects
    ├── a
    │   ├── meson.build
    │   └── subprojects
    │       └── b
    │           └── meson.build
    └── b
        └── meson.build

The order of subproject calls determines which subprojects will be used:

project('main')

# This causes all subprojects under subprojects/a/subprojects/ to be "promoted"
subproject('a')

# This will now use subprojects/a/subprojects/b, instead of subprojects/b
subproject('b')

muon does not perform subproject promotion.

malformed escape sequences

Meson silently accepts malformed escape sequences and outputs them literally, removing the leading escape character. For example:

'\c'          # becomes 'c'
'\Uabcdefghi' # becomes 'Uabcdefghi'
'\xqr'        # becomes 'xqr'

In muon, malformed escape sequences are parse errors.

format strings

Format strings in various parts of Meson use @ as the delimiter. The behavior is inconsistent, configure_file() recognizes \ as an escape character, but format strings in string.format() and custom_target command arguments do not. Muon will accept \ as an escape character in string.format() and custom_target, but this is incompatible with meson. \ use in custom_target command arguments is already incompatible though (see next point).

configure_file() will also warn you about invalid substitutions, and will remove them in the output, string.format() will error on invalid substitutions, and custom_target command arguments will be silently treated as literals if they are invalid substitutions (e.g. @BAZ@).

Because some projects rely on the above custom target command argument behaviour, muon merely adds a warning for invalid substitutions. In all other cases muon will raise error.

custom_target replaces backslashes with slashes in the command arguments

In Meson, all backslashes in custom_target command line arguments are blindly replaced to forward slashes. This behavior is not present in muon.

Reference: https://github.com/mesonbuild/meson/issues/1564

build_target() functions with empty sources

Meson allows you to create build targets (executable(), shared_library(), static_library(), etc.) without specifying any sources. In muon this is an error.

global compiler cache

Meson maintains a global compiler cache, which means that all languages added by subprojects are available to the main project and vice-versa. This can hide bugs that will surface if the subproject is built by itself, or subproject calls are rearranged. muon also maintains a global cache, but requires subprojects to still call add_languages to access this cache.

backslash escaping in compiler defines

Meson replaces \ with \\ in compiler defines. muon does not. This is legacy behavior that prevents you from using things like C escapes (e.g. \n) in compiler defines, at the benefit of making it easier to use windows paths. See meson commit aca93df184a32ed7faf3636c0fbe90d05cb67857 for more information:

Jon Turney: Now that all command-line escaping for ninja is dealt with in the ninja backend, escape_extra_args() shouldn't need to do anything.

But tests of existing behaviour rely on all backslashes in defines being C escaped: This means that Windows-style paths including backslashes can be safely used, but makes it impossible to have a define containing a C escape.

default option values

A number of builtin options have different default values:

  • default_library is 'static'

native: true

In muon native: true always means "for the build machine", while in meson sometimes it doesn't.

Meson distinguishes between cross and native builds. A native build is one where the build machine and the host machine are the same, while a cross build is one where they differ. Since this is a build system, "machine" in this context is mostly synonymous with "toolchain for a machine".

A nice cross compilation feature that meson supports is the ability to toggle which toolchain a particular target uses at configure time. This is accomplished by using the native keyword available for many functions. Passing native: false causes the target/dependency to be created/looked up for the host machine. Likewise, native: true causes the target/dependency to be created/looked up for the build machine. In most cases omitting the native keyword is the same as passing native: false, although some functions treat the omission of native specially, such as add_languages adding languages for both the host and build machine if the keyword is omitted.

The above paragraph is true for muon too, but it is actually subtly wrong for meson. You might be wondering, why meson uses native: true/false and not something more readable such as for_machine: 'build'/'host'. The reason is that during a native build, meson effectively ignores the native keyword. While this makes sense on some level since the machines you are selecting between are the same, it confuses the usage of the native keyword even more than just the odd mapping from boolean to machine, and makes it easier to create a build file that is broken for cross compilation.

For example (taken from meson's own tests), the following code is perfectly valid during a native build:

meson.override_dependency('expat', declare_dependency())
dependency('expat')
dependency('expat', native : true)
dependency('expat', native : false)

But will break with a dependency not found error during a cross build.

largefile source

Meson always adds -D_FILE_OFFSET_BITS=64 to the list of compile arguments for all targets on certain compiler/platform combinations. muon does not pass this by default, but can match meson's behavior if the b_largefile option is set to auto. The muon meson wrapper automatically passes -Db_largefile=auto.

Contributing

Thanks for considering contributing to muon. Please send patches and questions to ~lattis/muon@lists.sr.ht. Alternatively you may make a pull request at https://github.com/muon-build/muon. Before making any big changes, please send a proposal to the mailing list or open an issue so I can give you some pointers, and make sure you don't waste your time.

Contribution areas

There are two main ways to contribute to Muon:

  • Add a script module. There are several unimplemented modules that could likely be easily written as a script module. See src/script/modules for examples.
  • Work on the runtime. Please read the documentation on Muon Internals.

C Style

Muon uses a style similar to the linux kernel. A few differences are:

  • the return type of a function goes on the line above its declaration. e.g.

    int
    main(void)
    {
            return 0;
    }
    
  • it still goes on the same line in a function prototype e.g.

    int main(void);
    
  • never omit braces for single statement if, else, for, while, etc.

  • avoid function-like macros except in exceptional cases

  • it is OK (but not great) if your line is more than 80 characters

  • please use fixed-width integer types (e.g. uint32_t, uint64_t, uint8_t) whenever possible

As usual, just try to follow the style of surrounding code.

Scripting

Muon supports modules and small scripts written in meson. In order to facilitate this, various language features are enabled during module and script evaluation.

Evaluation Modes

Muon's implementation of the meson language includes three evaluation modes:

  • external: Regular meson.build files external to muon's implementation are evaluated in this mode.
  • internal: Any meson file evaluated using muon internal eval ... is evaluated in the internal mode.
  • extended: A layering of internal on top of external, allows features from both modes to be used simultaneously. This is the mode that script modules execute in.

Both internal and extended mode enable the same additional language features collectively referred to as scripting mode. The primary difference between the two modes lies in which functions may be called. In particular, most functions that require a project() or manipulate the build state in any way are not available in internal mode.

Scripting Mode

Muon's scripting mode consists of several langauge features as well as additional functions and modules.

This guide focuses on the language features. The additional functions and modules are documented in the Muon Reference. All functions, modules, and arguments are tagged with their availability in muon's different evaluation modes.

Mutability

In scripting mode mutability rules are relaxed slightly. list and dict types may be mutated by reference.

# Mutating a list with + and +=:
list = [0, 2, 3]
list[0] = 1
assert(list == [1, 2, 3])
list[0] += 1
assert(list == [2, 2, 3])
# Mutating a dict with + and +=:
dict = {'a': 1}
dict['a'] = 2
assert(dict == {'a': 2})
dict['a'] += 1
assert(dict == {'a': 3})
dict['b'] = 4
assert(dict == {'a': 3, 'b': 4})

Dictionaries may also be indexed using the . operator in scripting mode so the above example could be rewritten as:

# some asserts removed
dict = {'a': 1}
dict.a = 2
dict.a += 1
dict.b = 4
assert(dict == {'a': 3, 'b': 4})

Re-assigning creates a copy1, mutations do not affect the copied variables.

list = [0, 1]
list2 = list
list[0] += 1

assert(list == [1, 1])
assert(list2 == [0, 1])
1

Muon implements this using copy-on-write but the effect should be the same.

Function declaration

Function definition is started with the func keyword optionally followed by an identifier, and a list of arguments enclosed in parenthesis. If the function returns a value it must specify the type of that value by adding -> <type> after the argument list.

  • The identifier is required if the function definition is a statement.
  • The identifier is not allowed if the function definition is in an expression.

Positional and keyword arguments are supported in the argument list. Positional arguments must come first. Keyword arguments are specified by adding a colon (:) to the end of the argument followed by an optional default value. All arguments must specify their type by following the argument name with a type specifier.

Function definition is ended with the endfunc keyword.

Examples:

func a() -> int
    return 1
endfunc

func b(arg1 int) -> int
    return arg1
endfunc

func c(arg1 int, kw str:, kw_with_default str: 'default')
    message(arg1)
    message(kw)
    message(kw_with_default)
endfunc

d = func()
    message('hello from an anonymous function!')
endfunc

Scope

Modules have their own scope and cannot access any external variables (save builtins such as meson, build_machine, etc.).

Functions also get their own scope, but can capture variables that have been declared in parent scopes prior to the function definition.

Variable declaration implicitly occurs the first time it is assigned. To force a local declaration (e.g. to shadow a variable) you may use the := syntax.

For examples see tests/lang/scope.meson.

Type Specifiers

Type specifiers consist of primitive types with optional subtypes.

Supported primitive types are listed in the Muon Reference under the Objects header.

A value that can take one of many types can be specified by writing multiple types separated with a | such as int|str.

There are a few preset combinations of types:

  • any - all types except null
  • exe - str|file|external_program|python_installation|build_tgt|custom_tgt

dict and list must be followed by a sub-type enclosed in square brackets, e.g. dict[str|int] or list[any].

You can also wrap the entire type in glob[<type>] or listify[<type>] to get the special argument handling detailed in Muon Internals (search for TYPE_TAG_{GLOB,LISTIFY}).

Applying the glob tag to the type of a kwarg causes any unmatched keywords to be stored in that kwarg as a dict.

Function behavior changes

This document lists all modifications to function behavior when a function is called during scripting mode vs external mode.

  • In regular meson functions which create targets, the output parameter is not allowed to contain file separators. This has the effect that targets must only produce outputs which live in the current build directory. In extended meson file separators are permitted in these cases and so extended meson functions can create targets whose outputs may exist in other build directories.

Module Structure

A script module consists of functions that perform the various module operations. The final return at module scope is what "exports" the functions to module consumers. The returned object must be of type dict[func].

func func1()
endfunc

func func2()
endfunc

return {
    'func1': func1,
    'func2': func2,
}

Using some of the new syntax for dict mutation we can define modules like this too:

M = {}

M.func1 = func()
endfunc

M.func2 = func()
endfunc

return M

Adding a new builtin script module

After you have written your script module you must register it so it can be imported.

All script modules live at src/script/modules/<module_name>.meson. After adding the module, you must modify src/script/meson.build to register it. muon will embed the module source text in the built executable, and when the module is import()ed that source will be interpreted.

Rationale

Meson doesn't support user defined functions as one of its core principles. Why would muon relax this limitation? Partially because I believe that careful implementation of user defined functionality would not be harmful. We have seen meson scripts can get plenty complex anyways. The main reason however, is that requiring every last bit of functionality to be implemented in the build system itself is fundamentally untenable, especially if the build system will support multiple implementations.

  • Many meson modules are effectively reusable chunks of meson code (calls to dependency, custom_target, etc.), so it is not necessary to access things that a native module has access to like the interpreter state.
  • Lowering the bar for module contributions (this is important because they often involve specific tools I have no experience with/interest in).
  • Potential to some day share module code with other meson implementations.

That last point is a bit pie in the sky, but at least this makes it technically possible from muon's point of view and proves that it has value.

Muon Internals

Error handling

All errors that can be checked, should be checked. If an error is detected, an error message should be printed using vm_error, or if there is no source code associated with the error, LOG_E. The error should be immediately returned. Most functions returning a bool return false on error. The most common other type of error returning function has the return type enum iteration_result. These functions should return ir_err on error.

Meson functions

All meson functions are defined in functions with a separate file for each object type on which the function is defined. Functions not defined on an object are in kernel.c. If the function implementation is sufficiently large, it may be broken up into a separate file under functions/<object type>/<function name>.c.

When declaring a new function, you need to add it to the "impl_tbl" at the bottom of the file. All functions should call pop_args() before they do anything, even if they don't take any arguments. pop_args takes 3 arrays, conventionally named an (args, normal), ao (args, optional), and akw (args, keyword). Any of these may be NULL. In particular, if they are all NULL then the function takes no arguments, and pop_args will ensure this is the case.

Arguments should specify what types they accept by bitwise or-ing tc_-prefixed types together. an and ao argument arrays must be terminated by an argument of type ARG_TYPE_NULL.

TYPE_TAG_GLOB can be or-d with the type of the last element of an, and will store the remaining arguments in an obj_array. This is similar to e.g. def func(a, b, *c): in python, where c is the "glob" argument.

You may also bitwise or any type with TYPE_TAG_LISTIFY. This "type" will cause pop_args to do the following things:

  1. coerce single elements to arrays
    • 'hello' #=> ['hello']
  2. flatten arrays
    • ['hello', [], [['world']]] #=> ['hello', 'world']
  3. typecheck all elements of the array
    • given the "type" TYPE_TAG_LISTIFY | obj_string, the above examples would pass, but ['a', false] would not.

Workspace

The workspace is a structure that contains all the data for an entire build setup, including all subprojects, AST, options, objects, strings. Most interpreter related functions take a workspace as one of their arguments.

Objects

Meson objects are created with the make_obj function. See object.h for more information. Conventionally, all objects are referred to by id of type obj rather than a pointer.

Memory

You may be wondering, what about Garbage collection? Muon's current approach is to cleanup only once at the very end. Meson is not a Turing-complete language, so we don't need to worry about long running programs.

Release Notes

0.8.0

  • improve support for building and testing muon using meson
  • support b_lto_threads

✓ 0.7.0

released on 2026.10.04

  • more cross file support
  • add a way to disable docs
  • set more muon meson defaults to improve drop in behavior
  • fully implement meson.add_devenv
  • improvements to rpath fixer
  • improve haiku support
  • bump meson support to 1.11
    • 1.8.0
      • New argument android_exe_type for executables stub
      • Changes to the b_sanitize option
      • i18n module xgettext stub
      • version_compare now accept multiple compare strings
      • Improvements to Objective-C and Objective-C++
      • Per project subproject options rewrite*
        • Note: internals are done, but some questionable edge cases such as supporting -U and -A, or setting project options without yielding them using -D:foo=bar syntax are not implemented.
    • 1.9.0
      • Array .flatten() method
      • Support response files for custom targets (postponing for now, also unclear why this would be needed since custom_targets already have rsp-like behavior using muon internal exe -a)
      • Added license keyword to pkgconfig.generate
      • pkgconfig.generate supports internal dependencies in requires
    • 1.10.0
      • Support for the counted_by attribute
      • Added a values() method for dictionaries
      • Add cmd_array method to ExternalProgram
      • Added OS/2 support (shortname kwarg / os2_emxomf builtin opt) stub
      • Array .slice() method
      • -Db_vscrt on clang
      • Added build_subdir arg to various targets
      • Methods from compiler object now accept strings for include_directories (already supported)
      • Using meson.get_compiler() to get a language from another project is marked (already an error)
      • Add a configure log in meson-logs (already supported)
      • Added new namingscheme option
      • New method to handle GNU and Windows symbol visibility for C/C++/ObjC/ObjC++
    • 1.11.0
      • BuildTarget(install_dir) length > 1 replaced with keywords
      • Deprecate should_fail and rename it to expected_fail, also introduce expected_exitcode
      • install_man and install_headers: add support for install_tag kwarg
      • Added link_early_args to targets performing linking
      • Machine files now expand ~ as the user's home directory
      • windows.compile_resources now detects header changes with rc.exe
      • Added implicit_include_directories argument to windows.compile_resources
      • Console kwarg on run_command
  • 10 contributors:
    • Daksh Mamodiya (3)
    • Jan200101 (1)
    • Juri Sacchetta (2)
    • Ross Burton (1)
    • Stone Tickle (137)
    • VaiTon (1)
    • kzc (11)
    • moi15moi (7)
    • tox (1)
    • uni-dos (1)

✓ 0.6.0

released on 2026.06.21

  • internals / language features
    • in script mode: refactor how scope works, all variable resolution happens at compile time, globals are disallowed. Improves performance and clarifies scoping rules. (github pr)[https://github.com/muon-build/muon/pull/241]
    • all memory is tracked an managed with an arena allocator. This should improve memory usage, performance, and developer ergonomics.
  • toolchains
    • toolchains are now defined in scripts: src/script/runtime/toolchains.meson
    • tcc
    • clang-cl
    • ccache and sccache
  • documentation
  • tooling
    • DAP debugger protocol support & dcmake integration github issue
  • wayland module
  • support cross/native files
  • 12 contributors:
    • Christopher Wellons (1)
    • Daniel Wagner (1)
    • Fredrik Foss-Indrehus (1)
    • kzc (20)
    • Michael Forney (3)
    • Sertonix (1)
    • Stone Tickle (544)
    • Teselka (9)
    • VaiTon (14)
    • Vincent Torri (2)
    • vtorri (1)
    • Zephyr Lykos (1)

✓ 0.5.0

released on 2025.07.28

  • muon analyze now has an LSP mode
  • pkg-config-exec backend added. muon now supports shelling out to pkg-config (or pkgconf).
  • bump meson support to 1.7
  • more builtin dependency handlers
  • lots of bug fixes
  • 9 contributors:
    • Jonathan Schleifer (3)
    • LoneFox78 (1)
    • Michael Forney (1)
    • NRK (6)
    • Stone Tickle (416)
    • VaiTon (3)
    • Vincent Torri (2)
    • kzc (4)
    • m-hugo (1)

✓ 0.4.0

released on 2025.02.12

  • More windows improvements
    • only ~30 tests failing with msvc
    • vsenv ported from meson
    • .exe deployed by CI
  • Coverage targets are now supported thanks to Andrew McNulty
  • Various default compiler options were brought in line with meson thanks to Michal Sieron
  • XCode backend
  • Improvements/bug fixes for script modules
  • gnome module improvements thanks to sewn.
  • dependency() overhaul, now more closely matches meson's implementation
  • docs/*.md ported to mdbook and hosted at https://docs.muon.build
  • A reference manual containing all the functions, modules, objects, and methods that muon knows about is auto-generated on every build.
    • It also generates documentation for script modules using doc comments!
  • An experimental UI
  • As always, lots of bugs fixed!
  • 12 contributors:
    • Andrew McNulty (1)
    • Arsen Arsenović (1)
    • Eli Schwartz (1)
    • Jonathan Schleifer (1)
    • Jürg Billeter (1)
    • Michael Forney (2)
    • Michal Sieron (4)
    • Stone Tickle (303)
    • Theo Paris (1)
    • Vincent Torri (3)
    • kzc (1)
    • sewn (4)

✓ 0.3.0

released on 2024.09.24

  • Brand-new interpreter: https://mochiro.moe/posts/10-muon-internals/
  • Lots of windows improvements
    • bootstrap.bat
    • Simple tests passing
  • Lots of macOS improvements
    • All tests passing
    • Universal binary deployed by CI
  • Script modules introduced: a way of writing muon modules with mostly normal meson code.
    • i18n module
    • gnome module (wip)
  • Embedded cross-platform samurai implementation
  • Lots of bugs fixed!
  • 17 contributors:
    • Andrea Pappacoda (1)
    • Andrew McNulty (11)
    • Eli Schwartz (1)
    • Filipe Laíns (4)
    • JCWasmx86 (1)
    • Michael Forney (2)
    • Michal Sieron (2)
    • Seedo Paul (27)
    • Sertonix (3)
    • Stone Tickle (497)
    • Thomas Adam (1)
    • Tokunori Ikegami (1)
    • Vincent Torri (12)
    • kzc (2)
    • rofl0r (1)
    • sewn (1)
    • torque (1)

✓ 0.2.0

released on 2023.04.07

✓ 0.1.0

released on 2022.10.11