Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
372 changes: 364 additions & 8 deletions docs/concepts/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,355 @@ id: configurations
title: Configurations
---

For rule authors see also: [Configurations](../rule_authors/configurations.md)

When building a target, buck always builds it in a particular "configuration."
The configuration typically includes information like the target os, target
arch, sanitizers, opt level, etc. One way to understand the effect that a
configuration has is via the `cquery` and `uquery` commands. The cquery command
will compute the appropriate configuration for a target and display a version of
that target's attributes with the configuration applied. The `uquery` command
Build configurations are how Buck models building the same target in
different ways. This can include (but is not limited to):

- Target architecture
- Target OS
- Optimization level/build mode
- Compiler type/version
- Language version (e.g. C++ standard version)
- Sanitizers
- Passing arbitrary flags to build tools

When building a target, Buck always builds it in a particular
configuration. Build configurations are also sometimes called
"platforms". While technically separate, those two concepts are almost
identical.

Build configurations are [composed](../api/build/Configuration) of a set
of constraints and a set of values.

## Configuration constraints

Configuration constraints are enum-like constructs. Here is an example
definitions:

```python
# //config/BUCK

constraint(
name = "build_mode",
default = "debug",
values = [
"debug",
"release",
],
)
```

This will generate two configuration targets:
`//config:build_mode[debug]` and `//config:build_mode[release]`.

Note that extra contraint values can be added outside of the main
`constraint` definition:

```python
# //somewhere/else/BUCK

constraint_value(
name = "release_no_debug_info",
constraint_setting = "//config:build_mode",
)
```

Now, the three configuration targets that can be used to control the
build mode would be:

- `//config:build_mode[debug]`
- `//config:build_mode[release]`
- `//somewhere/else:release_no_debug_info`

Constraint values can also be grouped into larger logical pieces.
Assuming that we have also defined other constraints:

```python
config_setting(
name = "dev",
constraint_values = [
":build_mode[debug]",
":compiler[clang_21]",
":asan[enabled]",
],
)
```

Note that the prelude defines some generic constraints, e.g. under
`prelude//os:` and `prelude//cpu:`, which you might want to consider
using for interoperability.
Comment on lines +79 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A question I've had for a while is, if I have a new CPU type that's not in the prelude, can I extend that with a constraint_value() target outside the prelude? Seems like you might be able to with the old constraint_setting/constraint_value way, but I never tried it. The new unified API looks less amenable to that. Would be useful to document how to do this if it's possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I am aware, that's not possible.

I did consider documenting both APIs, but I don't want to confuse the readers (there's already a lot in here) and I assume that Meta does not want to encourage using the new API.

The question is valid though, and I suggest opening an issue about that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out it works great in both cases.

genrule(
    name = "printout",
    out = "out.txt",
    cmd = "echo 'cpu:" + select({
        "config//cpu:x86_64": "x86_64",
        "root//:napoleon": "napoleon",
        "DEFAULT": "other",
    }) + " abc:" + select({
        "root//:abc[abc]": "abc",
        "root//:abc[def]": "def",
        "root//:extra": "extra",
        "DEFAULT": "other",
    }) + "' > $OUT",
)

constraint_value(
    name = "napoleon",
    constraint_setting = "prelude//cpu/constraints:cpu",
)

constraint(name = "abc", values = ["abc", "def"], default = "abc")
constraint_value(name = "extra", constraint_setting = ":abc")
buck2 build :printout --out=- -m root//:napoleon -m root//:extra
BUILD SUCCEEDED
cpu:napoleon abc:extra

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh interesting! The inconsistency in syntax is... unfortunate, though.

Anyway, I've documented it as it's not obvious.


Once defined, this constraint can used in various ways, such as:

- Being passed on the command line to run a build in debug or release
mode.
- Being "selected on" so that building in debug vs release mode has
different effects.
- Being used for constraining compatibility (e.g. "this target can only
be built in release mode" for a benchmark).
- Being used to "transition" part of the build (e.g. "this target and
its dependencies are always built in release mode, regardless of the
dependent")

## Configuration values

`config_setting` can also include values taken from the buckconfig.
These can ease a migration from a legacy buckconfig setting to a build
constraint by allowing you to `select()` (more on that later) on known
buckconfig values:

```python
config_setting(
name = "fastmode_enabled",
values = {
"build.fastmode": "true",
},
)
```

This setting will be satisfied if the associated buckconfig matches,
i.e. if the user passes `build.fastmode=true` via the `-c`/`--config`
CLI flag, or if the following is set in the cell's `.buckconfig` file:

```ini
[build]
fastmode = true
```

This feature only allows reading buckconfig values, not reading them.

They are also incompatible with [configuration modifiers](./modifiers.md):
`--modifier :fastmode_enabled` does nothing.

## Using configuration: `select()`

Configurations can be used to change the build behavior based on which
value is currently active:

```cpp
cxx_binary(
name = "bin",
srcs = ["main.cpp"],
compiler_flags = select({
"//config:build_mode[debug]": ["-O0", "-g"],
"//config:build_mode[release]": ["-O3"],
}),
)
```

The above example is simplistic, and build mode compiler flags would
typically be set at the toolchain level, rather than per-target, but it
shows how build constraints can be used to change a build's behavior.

`select()` can appear in almost all attributes, and it can be composed
with other collection types. For example, the following is valid:

```python
cxx_library(
name = "lib",
exported_deps = [
"//common:lib",
] + select({
"//config:os[linux]": ["//linux:lib"],
"//config:os[mac]": ["//mac:lib"],
# `DEFAULT` is a special value that is always available.
# In this case, we do not link against any extra libraries.
"DEFAULT": [],
}),
)
```

If only one condition matches, the `select()` resolves to that
condition.

If multiple conditions match, then the select will be resolved to the
"most refined" of the conditions that match. A set of constraints (as in
a `config_setting`) is said to "refine" another if it is a superset of
that other's constraints. The "most refined" of a set is then the
condition that refines all the others.

Note that `select()` is resolved during configuration. This happens
after the evaluation of the BUCK file is completed, and so Starlark code
run during BUCK file evaluation does not have access to the resolved
value. This can make it difficult to have macros that do extensive
modification or inspection of attributes (which should be done in rules
instead). However, some functions
([`select_map`](../api/build/#select_map) and
[`select_test`](../api/build/#select_test)) allow performing
limited operations on these objects.

## Using configuration: compatibility

Constraints can also be used to limit target compatibility. For example,
assuming that our repo supports C++20, C++23 and C++26:

```python
# Reflection is only available starting with C++26, so we require it.
cxx_library(
name = "uses_reflection",
exported_headers = ["foo.h"],
target_compatible_with = ["//:cxx_standard[26]"]
)

# Deducing this is not available in C++20, so we make it incompatible.
cxx_library(
name = "uses_deducing_this",
exported_headers = ["foo.h"],
target_compatible_with = select({
"//:cxx_standard[20]": ["prelude//:none"],
"DEFAULT": [],
})
)
```

Target compatibility requires all transitive dependencies to be
compatible as well. In other words, a node is compatible if and only if
the node itself and all of its transitive dependencies are compatible.
In the usual cases of a dependency via `attrs.dep()`, a target's
dependency will be configured and then checked for compatibility with
the same configuration as the dependent target.

When trying to build a target with the wrong configuration (we will see
how shortly), the build will just fail (unless
`--skip-incompatible-targets` is passed).

When trying to build a set of targets using a
[pattern](./target_pattern)) (e.g. `//some/package:` or
`//some/package/...`), Buck will simply ignore incompatible targets.

See the [reference
documentation](../api/build/Select/#target_compatible_with) for more
information.

## Changing the build configuration

The build configuration is determined as follows:

1. A base platform is resolved:
1. If the user passed `--target-platforms` via the CLI, use that.
2. Else, if the target being built has a `default_target_platform`
attribute, use that. Note that since it is used to determine the
configuration, it is one of the few attributes that are not
`select`able.
3. Else, use the default (`parser.target_platform_detector_spec` in
the `.buckconfig` file).
2. [Configuration modifiers](./modifiers.md) are applied. Those are a
lightweight way to add ad-hoc constraints to an existing
configuration (e.g. "build with the default configuration/platform,
except with a different compiler").
3. [Configuration transitions](./transitions.md) are applied. Those
allow changing the configuration of parts of the build graph based on
arbitrary logic (e.g. "this part of the build graph should always be
built in release mode").

The target platform resolution is not applied to all nodes in the graph.
Once the top-level nodes have been configured via the target platform
resolution, the configuration is propagated to dependencies (possibly
altered by transitions).

For example:

```sh
# Build this target with the default configuration.
buck2 build :my_target
# Build it with an entirely different configuration.
buck2 build :my_target --target-platforms //my/other:platform
# Build it with the default configuration, plus release mode.
buck2 build :my_target?release
# Equivalent to the above, but applies to all targets if multiple were built.
buck2 build :my_target -m release
```

See the [configurations for author](../rule_authors/configurations.md)
page for information on how to define a platform.

Other example:

```python
java_binary(
name = "cats",
default_target_platform = "//platforms:windows-arm64-dev",
deps = ["//libs:foo"],
)

java_binary(
name = "dogs",
default_target_platform = "//platforms:mac-x86-dev",
deps = ["//libs:foo"],
)

java_library(
name = "foo",
deps = [
"//libs:common",
] + select({
"//constraints:x86": ["//libs:x86"],
"//constraints:mac-arm64": ["//libs:mac-arm64"],
"//constraints:windows-arm64": ["//libs:win-arm64"],
"DEFAULT": ["//libs:generic"],
})
)
```

When running `buck2 build //binaries:cats //binaries:dogs`, the
`//binaries:cats` binary will be built in the `//platforms:windows-arm64-dev`
configuration and the `//binaries:dogs` binary will be built in the
`//platforms:mac-x86-dev` configuration.

Each of those binaries depend on `//libs:foo`, but they will get
different versions of it as the binaries' configurations will each be
passed down to their dependencies. For `//binaries:cats`, its resolved
dependencies will include `//libs:win-arm64` and for `//binaries:dogs`,
it would contain `//libs:x86`.

Note that `//libs:common` will be built twice, once for each
configuration.

When running `buck2 build //binaries:cats //binaries:dogs --target-platforms
//platforms:mac-x86-opt`, both `//binaries:cats` and `//binaries:dogs` will
be built in the `//platforms:mac-x86-opt` configuration, use the same
dependencies, which would only be built once.

## Configurations and output paths

Since a target may appear within a build in multiple different configurations,
output paths cannot be derived based on just targets (as multiple actions would
map to the same outputs). For this reason, the target and the configuration are
encoded into output paths. The configuration is currently represented as a hash
of its values (a "hashed buck-out").

## Target platform vs execution platform

Buck distinguishes two kinds of platforms: "regular" ones (where your
code will run), and the ones used to run compilers and other tools.
Those are distinct because it is typical to want build tools to use a
different build configuration. For example, you may want a compiler to
be built/run in release mode, even when building debug
targets.

For this reason, Buck requires both _target_ platforms and _execution_
platforms to be defined. The execution platforms are specified via the
`build.execution_platforms` value in `.buckconfig`.

## Queries

### Getting configuration constraints from its hash

Build configurations are uniquely identified by their hash, which is not
human friendly.

To determine what constraints are part of a configuration, run `buck2
cquery //...` sot that Buck will discover all existing configurations,
then run `buck2 audit configurations`.

This will list all available configurations and print their composing
contraints.

### `cquery` and `uquery`

One way to understand the effect that a configuration has is via the
`cquery` and `uquery` commands. The `cquery` command will compute the
appropriate configuration for a target and display a version of that
target's attributes with the configuration applied. The `uquery` command
will not apply a configuration.

Here is a heavily trimmed version of the outputs of invoking `uquery` and
Expand Down Expand Up @@ -89,3 +430,18 @@ while the `nix` dependency is needed only for Linux. In `cquery` that
distinction has been resolved; because the target has been configured for Linux,
the `nix` dependency is present and indistinguishable from any other, while the
`common-path` dependency is gone.

## Execution groups

Execution groups are a future feature that will allow a rule to perform
execution platform resolution multiple times and then specify in which of the
resolved platforms each action runs in.

Traditionally, each target resolves a single execution platform.

## See also

- [Configuration modifiers](./modifiers.md)
- [Configuration transitions](./transitions.md)
- [Configurations for rule authors](../rule_authors/configurations.md)
- [Configuration transitions for rule authors](../rule_authors/configuration_transitions.md)
8 changes: 6 additions & 2 deletions docs/concepts/modifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ title: Configuration modifiers

Modifiers (also referred to as configuration modifiers) are a feature that lets
users add [constraints](../../rule_authors/configurations) to individual
directories, target definitions and individual `buck2` invocations. They are the
recommended to customize build configuration.
directories, target definitions and individual `buck2` invocations.

They are the recommended to customize build configurations when building
targets directly. If you need to customize parts of your build graph
(e.g. always build some specific dependencies in release mode),
[configuration transitions](transitions.md) are more appropriate.

## (Open-Source Only) Getting started with modifiers

Expand Down
Loading
Loading