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

Conversation

philsc
Copy link

@philsc philsc commented Aug 9, 2022

This patch adds new helper macros for dealing with incompatible target
skipping. Specifically, the macros help make the target_compatible_with
more readable and easier to understand.

Without these helpers you are left to write verbose select() statements.

For example, you might write the following to express a target that is
incompatible with QNX and bare-bones systems.

cc_library(
    name = "lib",
    srcs = ["lib.cc"],
    target_compatible_with = select({
        "@platforms//os:qnx": ["@platforms//:incompatible"],
        "@platforms//os:none": ["@platforms//:incompatible"],
        "//conditions:default": [],
    }),
)

WIth one of the helpers from this patch you can instead rewrite this as:

cc_library(
    name = "lib",
    srcs = ["lib.cc"],
    target_compatible_with = compatibility.none_of(
        "@platforms//os:qnx",
        "@platforms//os:none",
    ),
)

That is now easier to understand. Especially for folks new to
incompatible target skipping.

This patch is based on @trybka's trybka#1. The
implementation is his. I cleaned up the tests and added documentation.

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.

lib/compatibility.bzl Outdated Show resolved Hide resolved
lib/compatibility.bzl Outdated Show resolved Hide resolved
@trybka
Copy link

trybka commented Aug 9, 2022

Oh hey -- thanks for picking this up!

@philsc philsc changed the title Add lib/compatibility bzl Add lib/compatibility.bzl Aug 10, 2022
@philsc
Copy link
Author

philsc commented Aug 10, 2022

If anyone has an idea why bazel is segfaulting on shutdown in one of the Windows tests, I'd appreciate it.

@philsc
Copy link
Author

philsc commented Aug 15, 2022

Any of the 5 code owners interested in taking a look at this PR?

docs/compatibility_doc.md Show resolved Hide resolved
lib/compatibility.bzl Outdated Show resolved Hide resolved
lib/compatibility.bzl Outdated Show resolved Hide resolved
lib/compatibility.bzl Outdated Show resolved Hide resolved
@comius
Copy link
Collaborator

comius commented Aug 24, 2022

@comius comius closed this Aug 24, 2022
@comius comius reopened this Aug 24, 2022
@comius
Copy link
Collaborator

comius commented Aug 24, 2022

Ups, wrong button.

any thoughts? I am not sure what to make of the complete lack of feedback here.

Too many reviewers - each one of them has time to review, but all of them together don't have it :)

@philsc philsc requested a review from comius August 30, 2022 06:06
@philsc
Copy link
Author

philsc commented Sep 9, 2022

@comius , any thoughts?

@comius
Copy link
Collaborator

comius commented Sep 9, 2022

@comius , any thoughts?

I did a quick pass. Looks nice and looks ok, thanks. I'll do another more thorough pass and let you know.

lib/compatibility/BUILD Outdated Show resolved Hide resolved
lib/compatibility.bzl Outdated Show resolved Hide resolved
for i, setting in enumerate(settings):
result += select({
setting: [],
"//conditions:default": ["@bazel_skylib//lib/compatibility:all_of_{}".format(i)],
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens if you replace "@bazel_skylib//lib/compatibility:all_of_{}".format(i) with "@platforms//:incompatible" in this line?

It if works, it's much simpler.

Copy link
Author

@philsc philsc Sep 13, 2022

Choose a reason for hiding this comment

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

Same issue as #381 (comment)

EDIT: Fixed link.

Copy link
Collaborator

Choose a reason for hiding this comment

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

There's one alternative, which I considered worse until now. How about:

result = []
for setting in enumerate(settings):
   result += select({
      setting: result,
       "//conditions:default": ["@bazel_skylib//lib/compatibility:incompatible_in_all_of"]

I think this should evaluate to a single label when a condition is not met.

First line could also be result = settings. That might work better in cquery-es.

Copy link
Author

Choose a reason for hiding this comment

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

I have to admit that I am struggling to follow your code snippet. Using setting: result, inside the dictionary means that we're nesting select() statements. Which is, as far as I can tell, not allowed. bazelbuild/bazel#1623 (comment)

Bazel also gets unhappy because the types of each select() path no longer match:

Error: '+' operator applied to incompatible types (select of list, select of select)

Comment on lines +289 to +294
target_compatible_with = compatibility.any_of(
":foo1",
":foo2",
) + compatibility.none_of(
":bar1",
),
Copy link
Collaborator

Choose a reason for hiding this comment

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

So any_of(foo1)+none_of(foo1) is always false, right?
What about all+none, any+all, any+any, all+all, none+none, they all work right?

Copy link
Author

@philsc philsc Sep 13, 2022

Choose a reason for hiding this comment

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

So any_of(foo1)+none_of(foo1) is always false, right?

I have not added a test to explicitly test this, but I don't see any way that it would ever evaluate to true.

What about all+none, any+all, any+any, all+all, none+none, they all work right?

Theoretically yes. The challenge at the moment is that because of bazel's dislike of duplicated labels in label lists, we can't do, say, any+any, all+all, or none+none. I think the only way to make that work is to either:

  1. Figure out how to make bazel not complain about duplicate labels in a label list, or
  2. Accept a name-like parameter for these helpers that will create new constraint_value targets for that invocation.

EDIT: I'm very interested to hear what you think on this topic.

Copy link
Collaborator

@comius comius Sep 13, 2022

Choose a reason for hiding this comment

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

I've got a feeling there is something fishy going on, and I'm not able to put my finger on it. There is negation without universal set and + seems to mean the same as conjunction. The feeling is you're not using the right set of primitives. But I'm failing to figure out what those should be.

For example using + for conjunction, not for single negation and any for disjunction. This way you should be able to write any boolean expression as something like = any(a,b,not(c)) + any(c,d,not(e)). But then a+b only works for constraints. Should you also have as_constraints, that wraps a config value?

I can't really tell what's more usable. So far I've seen the most places need just conjunction of constraints and some more rare places a disjunction of constraints.

Copy link
Collaborator

@comius comius Sep 13, 2022

Choose a reason for hiding this comment

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

Thinking out loud: maybe opposite approach is the solution: a single function match([a,b],c,[d,e], noneof=[f,g]) which says (a or b) and c and (d or e) and (not f) and (not g).

With current situation about composition - I would be more happy to make no guarantees about it, because they seem to be quite "flaky". Maybe even better, breaking composition on purpose, by using that single incompatible constraint value.

Copy link
Author

Choose a reason for hiding this comment

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

The single function approach is a cool idea. At first glance, however, I think it's harder for someone to understand who hasn't read the documentation. The intent of the current functions is to make them easy to understand from a newcomer point of view:

This is compatible with any of foo1 or foo2, but not compatible with bar1

The single function approach also cannot work if we restrict ourselves to using the single incompatible value. I.e. you cannot express the and operations without risking creating duplicate labels in the label list for target_compatible_with.

That being said... it appears that with my last refactor (bazelbuild/bazel#14096) I unintentionally made it so you can duplicate labels in target_compatible_with and bazel will deal with it "as you expect". I.e. duplicated labels are effectively NOPs.

So... we could take that as a feature and I think that feature means we can use @platforms//:incompatible for everything 🤔

None of this will work until bazel 6, however, so whatever API you end up being okay with will only work with bazel 6 onwards.

I still prefer the existing API over your single-function unless we expand it to something like the following. I.e. to make it really obvious what's going on when reading a BUILD file.

target_compatible_with = compatibility.match(all_of = [
    compatibility.or(a, b),
    c,
    compatibility.or(d, e),
], none_of = [
    f,
    g,
]),

Copy link
Author

Choose a reason for hiding this comment

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

@comius , I spent some time implementing your match idea, but I'm not convinced that it'll be ergonomic. Starting with this:

target_compatible_with = compatibility.match([":a", ":b"]),  # This means a _or_ b.
target_compatible_with = compatibility.match(":a", ":b"),  # This means a _and_ b.

makes me think the difference is too subtle. Making it more verbose:

target_compatible_with = compatibility.match_all(  # This means a _or_ b.
    compatibility.or_group(":a", ":b"),
),
target_compatible_with = compatibility.match_all(":a", ":b"),  # This means a _and_ b.

makes it easier to understand But then we're not really saving the user much over doing:

target_compatible_with = compatibility.any_of(":a", ":b"),  # This means a _or_ b.
target_compatible_with = compatibility.all_of(":a", ":b"),  # This means a _and_ b.

With bazel HEAD, it's actually pretty easy now to compose the match([a,b],c,[d,e], noneof=[f,g]) example from earlier.

target_compatible_with = compatibility.any_of(a, b) + \
                         compatibility.all_of(c) + \
                         compatibility.any_of(d, e) + \
                         compatibility.none_of(f, g),

WDYT about the current state of things?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also think different behavior for list of conditions vs. a single set is far too subtle. People will get that wrong.
any_of and all_of are excellent names.

Copy link
Author

Choose a reason for hiding this comment

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

@comius , any thoughts?

I can disable the tests for pre-6.0 versions of bazel. But that would mean not running tests at all for now.

Copy link
Author

Choose a reason for hiding this comment

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

@comius , WDYT?

Copy link
Author

Choose a reason for hiding this comment

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

@comius , ping

philsc added a commit to philsc/bazel that referenced this pull request Sep 14, 2022
The latest refactor unintentionally made it so you can list the same
constraint multiple times in the `target_compatible_with` attribute.

It was unintentional, but actually greatly simplifies a discussion
point on bazelbuild/bazel-skylib#381. That skylib patch aims to make
it easier to write non-trivial `target_compatible_with` expressions.
For example, to express that something is compatible with everything
except for Windows, one can use the following:

    foo_binary(
        name = "bin",
        target_compatible_with = select({
            "@platforms//os:windows": ["@platforms//:incomaptible"],
            "//conditions:default: [],
        }),
    )

The skylib patch aims to reduce that to:

    foo_binary(
        name = "bin",
        target_compatible_with = compatibility.none_of("@platforms//os:windows"),
    )

This works fine on its own, but a problem arises when these
expressions are composed. For example, a macro author might write the
following:

    def foo_wrapped_binary(name, target_compatible_with = [], **kwargs):
        foo_binary(
            name = name,
            target_compatible_with = target_compatible_with + select({
                "@platforms//os:none": ["@platforms//:incompatible"],
                "//conditions:default": [],
            }),
        )

A macro author should be able to express their own incompatibility
while also honouring user-defined incompatibility.

It turns out that my latest refactor (bazelbuild#14096) unintentionally made
this work. This happened because we now check for incompatibility
before we perform a lot of sanity checks. That's also one of the
reasons that visibility restrictions are not honoured for incomaptible
targets at the moment (bazelbuild#16044).

Without the deduplicating behaviour, macro authors are stuck with
either not allowing composition, or having to create unique
incompatible constraints for every piece in a composed
`target_compatible_with` expression.

This patch adds a test to validate the deduplicating behaviour to
cement it as a feature.

Unfortunately, this means that `target_compatible_with` behaves
differently from other label list attributes. For other label list
attributes, bazel complains when labels are duplicated. For example,
the following BUILD file:

    py_library(
        name = "lib",
    )

    py_binary(
        name = "bin",
        srcs = ["bin.py"],
        deps = [
        |   ":lib",
        |   ":lib",
        ],
    )

will result in the following error:

    $ bazel build :bin
    INFO: Invocation ID: 3d8345a4-2e76-493e-8343-c96a36185b52
    ERROR: /home/jenkins/repos/test_repo/BUILD:41:10: Label '//:lib' is duplicated in the 'deps' attribute of rule 'bin'
    ERROR: error loading package '': Package '' contains errors
    INFO: Elapsed time: 2.514s
    INFO: 0 processes.
    FAILED: Build did NOT complete successfully (1 packages loaded)
copybara-service bot pushed a commit to bazelbuild/bazel that referenced this pull request Sep 16, 2022
The latest refactor unintentionally made it so you can list the same
incompatible constraint multiple times in the `target_compatible_with`
attribute.

It was unintentional, but actually greatly simplifies a discussion
point on bazelbuild/bazel-skylib#381. That skylib patch aims to make
it easier to write non-trivial `target_compatible_with` expressions.
For example, to express that something is compatible with everything
except for Windows, one can use the following:

    foo_binary(
        name = "bin",
        target_compatible_with = select({
            "@platforms//os:windows": ["@platforms//:incomaptible"],
            "//conditions:default: [],
        }),
    )

The skylib patch aims to reduce that to:

    foo_binary(
        name = "bin",
        target_compatible_with = compatibility.none_of("@platforms//os:windows"),
    )

This works fine on its own, but a problem arises when these
expressions are composed. For example, a macro author might write the
following:

    def foo_wrapped_binary(name, target_compatible_with = [], **kwargs):
        foo_binary(
            name = name,
            target_compatible_with = target_compatible_with + select({
                "@platforms//os:none": ["@platforms//:incompatible"],
                "//conditions:default": [],
            }),
        )

A macro author should be able to express their own incompatibility
while also honouring user-defined incompatibility.

It turns out that my latest refactor (#14096) unintentionally made
this work. This happened because we now check for incompatibility
before we perform a lot of sanity checks. That's also one of the
reasons that visibility restrictions are not honoured for incomaptible
targets at the moment (#16044).

Without the deduplicating behaviour, macro authors are stuck with
either not allowing composition, or having to create unique
incompatible constraints for every piece in a composed
`target_compatible_with` expression.

This patch adds a test to validate the deduplicating behaviour to
cement it as a feature.

Unfortunately, this means that `target_compatible_with` behaves
differently from other label list attributes. For other label list
attributes, bazel complains when labels are duplicated. For example,
the following BUILD file:

    py_library(
        name = "lib",
    )

    py_binary(
        name = "bin",
        srcs = ["bin.py"],
        deps = [
        |   ":lib",
        |   ":lib",
        ],
    )

will result in the following error:

    $ bazel build :bin
    INFO: Invocation ID: 3d8345a4-2e76-493e-8343-c96a36185b52
    ERROR: /home/jenkins/repos/test_repo/BUILD:41:10: Label '//:lib' is duplicated in the 'deps' attribute of rule 'bin'
    ERROR: error loading package '': Package '' contains errors
    INFO: Elapsed time: 2.514s
    INFO: 0 processes.
    FAILED: Build did NOT complete successfully (1 packages loaded)

Closes #16274.

PiperOrigin-RevId: 474747867
Change-Id: Iacc4de12ce90176d803e0bc9e695d4ec14603449
@philsc philsc requested review from comius and removed request for gregestren September 20, 2022 04:40
aiuto pushed a commit to aiuto/bazel that referenced this pull request Oct 12, 2022
The latest refactor unintentionally made it so you can list the same
incompatible constraint multiple times in the `target_compatible_with`
attribute.

It was unintentional, but actually greatly simplifies a discussion
point on bazelbuild/bazel-skylib#381. That skylib patch aims to make
it easier to write non-trivial `target_compatible_with` expressions.
For example, to express that something is compatible with everything
except for Windows, one can use the following:

    foo_binary(
        name = "bin",
        target_compatible_with = select({
            "@platforms//os:windows": ["@platforms//:incomaptible"],
            "//conditions:default: [],
        }),
    )

The skylib patch aims to reduce that to:

    foo_binary(
        name = "bin",
        target_compatible_with = compatibility.none_of("@platforms//os:windows"),
    )

This works fine on its own, but a problem arises when these
expressions are composed. For example, a macro author might write the
following:

    def foo_wrapped_binary(name, target_compatible_with = [], **kwargs):
        foo_binary(
            name = name,
            target_compatible_with = target_compatible_with + select({
                "@platforms//os:none": ["@platforms//:incompatible"],
                "//conditions:default": [],
            }),
        )

A macro author should be able to express their own incompatibility
while also honouring user-defined incompatibility.

It turns out that my latest refactor (bazelbuild#14096) unintentionally made
this work. This happened because we now check for incompatibility
before we perform a lot of sanity checks. That's also one of the
reasons that visibility restrictions are not honoured for incomaptible
targets at the moment (bazelbuild#16044).

Without the deduplicating behaviour, macro authors are stuck with
either not allowing composition, or having to create unique
incompatible constraints for every piece in a composed
`target_compatible_with` expression.

This patch adds a test to validate the deduplicating behaviour to
cement it as a feature.

Unfortunately, this means that `target_compatible_with` behaves
differently from other label list attributes. For other label list
attributes, bazel complains when labels are duplicated. For example,
the following BUILD file:

    py_library(
        name = "lib",
    )

    py_binary(
        name = "bin",
        srcs = ["bin.py"],
        deps = [
        |   ":lib",
        |   ":lib",
        ],
    )

will result in the following error:

    $ bazel build :bin
    INFO: Invocation ID: 3d8345a4-2e76-493e-8343-c96a36185b52
    ERROR: /home/jenkins/repos/test_repo/BUILD:41:10: Label '//:lib' is duplicated in the 'deps' attribute of rule 'bin'
    ERROR: error loading package '': Package '' contains errors
    INFO: Elapsed time: 2.514s
    INFO: 0 processes.
    FAILED: Build did NOT complete successfully (1 packages loaded)

Closes bazelbuild#16274.

PiperOrigin-RevId: 474747867
Change-Id: Iacc4de12ce90176d803e0bc9e695d4ec14603449
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants