Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lib/compatibility.bzl #381

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fa9f398
Create compatibility.bzl
trybka Mar 1, 2021
e86f542
Update compatlib and add some tests.
trybka Mar 23, 2021
b6a5584
Merge remote-tracking branch 'origin/main' into HEAD
philsc Aug 8, 2022
52b2e62
Tweak stuff a bit
philsc Aug 8, 2022
8190b2e
simplify some
philsc Aug 9, 2022
b3d4463
Consolidate tests
philsc Aug 9, 2022
977a070
Simplify some more
philsc Aug 9, 2022
63d9712
Clean up some more
philsc Aug 9, 2022
e202c2b
Get started on docs
philsc Aug 9, 2022
4b34b67
Expand docs a bit
philsc Aug 9, 2022
6abd9ad
Fix some syntax highlighting issues
philsc Aug 9, 2022
dc846a0
regenerate docs
philsc Aug 9, 2022
73b0ed8
Incorporate feedback
philsc Aug 10, 2022
5b4c97c
clean up some more
philsc Aug 10, 2022
b77d66e
Clean up some more
philsc Aug 10, 2022
7568466
Add composed incompatibility to test
philsc Aug 10, 2022
bf4a2cd
Run buildifier
philsc Aug 10, 2022
ee916b0
Fix test
philsc Aug 10, 2022
7939c78
Merge remote-tracking branch 'origin/main' into HEAD
philsc Aug 17, 2022
c333bc4
Remove `bazel shutdown` which is causing segfault on Windows
philsc Aug 17, 2022
d935d67
Switch to *settings
philsc Aug 25, 2022
ded484c
Merge remote-tracking branch 'origin/main' into HEAD
philsc Aug 30, 2022
8d07e16
Switch to predetermined `constraint_value` targets
philsc Aug 30, 2022
0ee8b41
update docs
philsc Aug 30, 2022
04caadf
Fix lint issues
philsc Aug 30, 2022
6765506
Merge remote-tracking branch 'origin/main' into HEAD
philsc Sep 14, 2022
76a22f4
Incorporate feedback
philsc Sep 14, 2022
0d87298
Rename to incompatible_in_*
philsc Sep 14, 2022
9eb896e
Switch everything to @platforms//:incompatible
philsc Sep 18, 2022
d27b58d
delete PATH manipulation
philsc Sep 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -43,6 +43,7 @@ s = shell.quote(p)
## List of modules (in lib/)

* [collections](docs/collections_doc.md)
* [compatibility](docs/compatibility_doc.md)
* [dicts](docs/dicts_doc.md)
* [partial](docs/partial_doc.md)
* [paths](docs/paths_doc.md)
Expand Down
5 changes: 5 additions & 0 deletions docs/BUILD
Expand Up @@ -22,6 +22,11 @@ stardoc_with_diff_test(
out_label = "//docs:common_settings_doc.md",
)

stardoc_with_diff_test(
bzl_library_target = "//lib:compatibility",
out_label = "//docs:compatibility_doc.md",
)

stardoc_with_diff_test(
bzl_library_target = "//rules:copy_directory",
out_label = "//docs:copy_directory_doc.md",
Expand Down
145 changes: 145 additions & 0 deletions docs/compatibility_doc.md
@@ -0,0 +1,145 @@
<!-- Generated with Stardoc: http://skydoc.bazel.build -->

Skylib module of convenience functions for `target_compatible_with`.

Load the macros as follows in your `BUILD` files:
```python
load("@bazel_skylib//lib:compatibility.bzl", "compatibility")
```

See the [Platform docs](https://bazel.build/docs/platforms#skipping-incompatible-targets) for
more information.


<a id="#compatibility.all_of"></a>

## compatibility.all_of
comius marked this conversation as resolved.
Show resolved Hide resolved

<pre>
compatibility.all_of(<a href="#compatibility.all_of-settings">settings</a>)
</pre>

Create a `select()` for `target_compatible_with` which matches all of the given settings.

All of the settings must be true to get an empty list. Failure to match will result
in an incompatible `constraint_value` for the purpose of target skipping.

In other words, use this function to make a target incompatible unless all of the settings are
true.

Example:

```python
config_setting(
name = "dbg",
values = {"compilation_mode": "dbg"},
)

cc_binary(
name = "bin",
srcs = ["bin.cc"],
# This target can only be built for Linux in debug mode.
target_compatible_with = compatibility.all_of([
":dbg",
"@platforms//os:linux",
]),
)
```

See also: `selects.config_setting_group(match_all)`


**PARAMETERS**


| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="compatibility.all_of-settings"></a>settings | A list of <code>config_setting</code> or <code>constraint_value</code> targets. | none |

**RETURNS**

A native `select()` which is "incompatible" unless all settings are true.


<a id="#compatibility.any_of"></a>

## compatibility.any_of

<pre>
compatibility.any_of(<a href="#compatibility.any_of-settings">settings</a>)
</pre>

Create a `select()` for `target_compatible_with` which matches any of the given settings.

Any of the settings will resolve to an empty list, while the default condition will map to
an incompatible `constraint_value` for the purpose of target skipping.

In other words, use this function to make target incompatible unless one or more of the
settings are true.

```python
cc_binary(
name = "bin",
srcs = ["bin.cc"],
# This target can only be built for Linux or Mac.
target_compatible_with = compatibility.any_of([
"@platforms//os:linux",
"@platforms//os:macos",
]),
)
```


**PARAMETERS**


| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="compatibility.any_of-settings"></a>settings | A list of <code>config_settings</code> or <code>constraint_value</code> targets. | none |

**RETURNS**

A native `select()` which maps any of the settings an empty list.


<a id="#compatibility.none_of"></a>

## compatibility.none_of

<pre>
compatibility.none_of(<a href="#compatibility.none_of-settings">settings</a>)
</pre>

Create a `select()` for `target_compatible_with` which matches none of the given settings.

Any of the settings will resolve to an incompatible `constraint_value` for the
purpose of target skipping.

In other words, use this function to make target incompatible if any of the settings are true.

```python
cc_binary(
name = "bin",
srcs = ["bin.cc"],
# This target cannot be built for Linux or Mac, but can be built for
# everything else.
target_compatible_with = compatibility.none_of([
"@platforms//os:linux",
"@platforms//os:macos",
]),
)
```


**PARAMETERS**


| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="compatibility.none_of-settings"></a>settings | A list of <code>config_setting</code> or <code>constraint_value</code> targets. | none |

**RETURNS**

A native `select()` which maps any of the settings to the incompatible target.


8 changes: 8 additions & 0 deletions lib/BUILD
Expand Up @@ -15,6 +15,14 @@ bzl_library(
srcs = ["collections.bzl"],
)

bzl_library(
name = "compatibility",
srcs = ["compatibility.bzl"],
deps = [
":selects",
],
)

bzl_library(
name = "dicts",
srcs = ["dicts.bzl"],
Expand Down
185 changes: 185 additions & 0 deletions lib/compatibility.bzl
@@ -0,0 +1,185 @@
"""Skylib module of convenience functions for `target_compatible_with`.

Load the macros as follows in your `BUILD` files:
```python
load("@bazel_skylib//lib:compatibility.bzl", "compatibility")
```

See the [Platform docs](https://bazel.build/docs/platforms#skipping-incompatible-targets) for
more information.
"""

load(":selects.bzl", "selects")

def _get_name_from_target_list(targets, joiner = " or "):
"""Join a list of strings into a string which is suitable as a target name.

The return value has a hash appended so that it is different between multiple "targets" values
that have the same name portion. For readability, we keep only the name portion of the
specified targets. But for uniqueness we need the hash. For example, the following two calls
may return strings as follows. The hashes are arbitrary in this example.

>>> _get_name_from_target_list(["@platforms//os:linux", "@platforms//cpu:x86_64"])
"linux or x86_64 (3ef2349)"
>>> _get_name_from_target_list(["//some/custom:linux", "//some/custom:x86_64"])
"linux or x86_64 (98ab64)"

Args:
targets: A list of target names.
joiner: An optional string to use for joining the list.

Returns:
A string which is a valid target name.
"""

# Compute absolute labels from the user-specified ones. They can be
# relative or absolute. We do this to make the final name as unique as
# possible via a hash.
package_label = Label("//" + native.package_name())
absolute_targets = sorted([package_label.relative(target) for target in targets])
joined_names = joiner.join([target.name for target in absolute_targets])
name = "%s (%x)" % (joined_names, hash(str(absolute_targets)))
return name
philsc marked this conversation as resolved.
Show resolved Hide resolved

def _maybe_make_unique_incompatible_value(name):
"""Creates a `native.constraint_value` which is "incompatible."

When composing selects which could all resolve to "incompatible" we need distinct labels.
Copy link
Contributor

Choose a reason for hiding this comment

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

Just out of curiosity: Is there a particular reason for why target_compatible_with can't deduplicate it's value before processing it? That won't help simplify this for existing Bazel versions though.

Copy link
Author

Choose a reason for hiding this comment

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

I suspect that the reason is that there's some fairly generic logic somewhere that complains about duplicate labels in label_list attributes. E.g. I remember that adding duplicate deps to py_library also throws a bazel error. In other words, I suspect that this behaviour cannot be changed without affecting the same logic for all other attributes. Unless we hard-code something for target_compatible_with.

Copy link
Contributor

@fmeum fmeum Aug 9, 2022

Choose a reason for hiding this comment

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

True. It's pretty easy to hit this error when programmatically adding dependencies from a macro.

@gregestren Can you perhaps shed some light on why Bazel errors out rather than stably deduping the label list? AFAIK buildifier already handles non-macro duplications.

Copy link
Contributor

Choose a reason for hiding this comment

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

@philsc and I followed up on this on another issue/PR (which was that?). I assume the historic logic is to encourage tidy BUILD curation. I ran this by the devs most concerned about API consistency and didn't hear strong opinion on keeping it this way.

This will create a `constraint_value` with the given name, if it does not already exist.

Args:
name: A target name to check and use.
"""
if not native.existing_rule(name):
native.constraint_value(
name = name,
constraint_setting = "@platforms//:incompatible_setting",
)
philsc marked this conversation as resolved.
Show resolved Hide resolved

def _none_of(settings):
"""Create a `select()` for `target_compatible_with` which matches none of the given settings.

Any of the settings will resolve to an incompatible `constraint_value` for the
purpose of target skipping.

In other words, use this function to make target incompatible if any of the settings are true.

```python
cc_binary(
name = "bin",
srcs = ["bin.cc"],
# This target cannot be built for Linux or Mac, but can be built for
# everything else.
target_compatible_with = compatibility.none_of([
"@platforms//os:linux",
"@platforms//os:macos",
]),
)
```

Args:
settings: A list of `config_setting` or `constraint_value` targets.

Returns:
A native `select()` which maps any of the settings to the incompatible target.
"""
compat_name = " incompatible with " + _get_name_from_target_list(settings)
_maybe_make_unique_incompatible_value(compat_name)

return selects.with_or({
"//conditions:default": [],
tuple(settings): [":" + compat_name],
})

def _any_of(settings):
"""Create a `select()` for `target_compatible_with` which matches any of the given settings.

Any of the settings will resolve to an empty list, while the default condition will map to
an incompatible `constraint_value` for the purpose of target skipping.

In other words, use this function to make target incompatible unless one or more of the
settings are true.

```python
cc_binary(
name = "bin",
srcs = ["bin.cc"],
# This target can only be built for Linux or Mac.
target_compatible_with = compatibility.any_of([
"@platforms//os:linux",
"@platforms//os:macos",
]),
)
```

Args:
settings: A list of `config_settings` or `constraint_value` targets.

Returns:
A native `select()` which maps any of the settings an empty list.
"""
compat_name = " compatible with any of " + _get_name_from_target_list(settings)
_maybe_make_unique_incompatible_value(compat_name)

return selects.with_or({
tuple(settings): [],
"//conditions:default": [":" + compat_name],
})

def _all_of(settings):
philsc marked this conversation as resolved.
Show resolved Hide resolved
"""Create a `select()` for `target_compatible_with` which matches all of the given settings.

All of the settings must be true to get an empty list. Failure to match will result
in an incompatible `constraint_value` for the purpose of target skipping.

In other words, use this function to make a target incompatible unless all of the settings are
true.

Example:

```python
config_setting(
name = "dbg",
values = {"compilation_mode": "dbg"},
)

cc_binary(
name = "bin",
srcs = ["bin.cc"],
# This target can only be built for Linux in debug mode.
target_compatible_with = compatibility.all_of([
":dbg",
"@platforms//os:linux",
]),
)
```

See also: `selects.config_setting_group(match_all)`

Args:
settings: A list of `config_setting` or `constraint_value` targets.

Returns:
A native `select()` which is "incompatible" unless all settings are true.
"""
group_name = _get_name_from_target_list(settings, joiner = " and ")
compat_name = " compatible with all of " + group_name
_maybe_make_unique_incompatible_value(compat_name)

# all_of can only be accomplished with a config_setting_group.match_all.
if not native.existing_rule(group_name):
comius marked this conversation as resolved.
Show resolved Hide resolved
selects.config_setting_group(
name = group_name,
match_all = settings,
)

return select({
":" + group_name: [],
"//conditions:default": [":" + compat_name],
})

compatibility = struct(
all_of = _all_of,
any_of = _any_of,
none_of = _none_of,
)
13 changes: 13 additions & 0 deletions tests/BUILD
Expand Up @@ -92,6 +92,19 @@ sh_test(
tags = ["local"],
)

sh_test(
name = "compatibility_test",
srcs = ["compatibility_test.sh"],
data = [
":unittest.bash",
"//lib:compatibility",
],
tags = ["local"],
deps = [
"@bazel_tools//tools/bash/runfiles",
],
)

shell_args_test_gen(
name = "shell_spawn_e2e_test_src",
)
Expand Down