1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
|
# 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:
```python
# 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:
```python
# 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:
```python
# 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:
```python
# 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:
```python
# 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:
```python
# 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
```python
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.
|