Editing toolkit/moz.configure
Prerequisites
Some of the files that configure the build system are written in a restricted python dialect. It is probably easiest to think of them as “python-like DSLs”. They must be formatted using black
. Correct formatting is checked on CI.
To run black
on toolkit/moz.configure
:
./mach lint -l black toolkit/moz.configure
moz.configure
These files describe one of the first steps of the build. This step does not run tool chains or produce any other kind of artifacts. It only produces a few key/value dictionaries that later parts of the build will use.
Two important dictionaries declared in moz.configure
are configs and defines. The former is used in moz.build
files, the later is used to feed C and C++ compilers, as shown below.
This is typically the right place to add logic for:
Declaring options for the mozconfig file.
Deciding whether to enable/disable some build-time features based on the build configuration and environment.
Generating some
#define
identifiers for the C++ code based on the build configuration or environment.
It contains a lot of code that looks like:
# In toolkit/moz.configure:
# Adds a config key/value pair
set_config("FOO", foo)
# Adds a define key/value pair
set_define("BAR", bar)
We’ll see later how the lower case foo
symbol above is defined.
Configurations can be accessed in various parts of the build system, such as moz.build
files for example:
# In a moz.build file:
if CONFIG["FOO"]:
# For example let's add an exported header for our C++ code.
EXPORTS.mozilla += [
"foo.h"
]
# or
if CONFIG["FOO"] == "something":
# etc.
Defines map directly to C++ defines in the code as well as other files that use a C-like preprocessor, for example modules/libref/init/all.js
, or toolkit/content/license.html
.
The dependency graph
It is tempting to look at the code in moz.configure
and read its logic in with an imperative programming mindset, however a better mental model is to imagine this file as a script that declares a task graph which is evaluated later.
Let’s look at a simple example:
# In toolki/moz.configure
# Declare a build option that can be set via `ac_add_option` in the `mozconfig` file.
option("--enable-doodad", help="Enable a fancy feature")
@depends("--enable-doodad", target)
def doodad(enabled, target):
# Return True if --enable-doodad was set in mozconfig and
# if we are on Windows.
return enabled and target.os =!== "WINNT"
The code above declares a doodad
function that is decorated with @depends
.
We will never directly call this doodad
function ourselves. The @depends
decoration wraps it into a node of the dependency graph that will
be lazily evaluated later. Elsewhere in moz.configure
, when we write doodad
, it refers to the node that wraps the function.
The parameters in @depends
correspond to doodad
’s node dependency and map to the function parameters. So enabled
inside the function will only evaluate to True
if --enable-doodad
is set in mozconfig.
The body of the function is evaluated in the second stage when the graph is evaluated. It runs in a sand-boxed environment and has access to very few things other than what is provided as input to the node.
Only declaring a node has no effect, unless that node is used, so let’s use our doodad
node:
# Specify `doodad` as a dependency to resolving the "DOODAD" config key.
set_config("DOODAD", doodad)
# Specify a define. The syntax is the same as with `set_config`.
set_define("MOZ_DOODAD", 1, when=doodad)
Note the when=
syntax: the define will only be set if doodad evaluates to True
. This syntax can also be used with set_config
and @depends
.
Since set_config
is run when declaring the graph, and before evaluating it, we could not have expressed this condition using an if
statement:
# This does *not* work. `doodad` is not a value, it is a node.
if doodad:
set_define("MOZ_DOODAD", 1)
Another way to express this condition is via with only_when
blocks:
# This works!
with only_when(doodad):
set_define("MOZ_DOODAD", 1)
Now let’s add a slightly more complicated example. This time the node will not evaluate to
with only_when(compile_environment):
# Depend on the doodad node we defined earlier
@depends(doodad, target)
def advanced_doodad(basic_doodad, target):
# If the doodad is not enabled, don't enable the advanced
# version.
if not basic_doodad:
return Namespace(enabled=False)
header_name = "doodad_" + target.cpu + ".h"
return Namespace(
enabled=True,
header_name=header_name
)
with only_when(advanced_doodad.enabled):
set_config("DOODAD_ARCH_HEADER", advanced_doodad.header_name)
The advanced_doodad
node evaluates to a dictionary instead of just a boolean.
This is useful to write more expressive configurations and for, example, generate strings or path names based on earlier configuration.