Skip to content

Commit daabe50

Browse files
philsccopybara-github
authored andcommitted
Deduplicate incompatible target_compatible_with labels
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
1 parent 7abfec5 commit daabe50

File tree

3 files changed

+72
-5
lines changed

3 files changed

+72
-5
lines changed

src/main/java/com/google/devtools/build/lib/analysis/IncompatiblePlatformProvider.java

+14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package com.google.devtools.build.lib.analysis;
1616

17+
import static com.google.common.collect.ImmutableList.toImmutableList;
18+
1719
import com.google.auto.value.AutoValue;
1820
import com.google.common.base.Preconditions;
1921
import com.google.common.collect.ImmutableList;
@@ -23,6 +25,7 @@
2325
import com.google.devtools.build.lib.packages.BuiltinProvider;
2426
import com.google.devtools.build.lib.packages.Info;
2527
import com.google.devtools.build.lib.starlarkbuildapi.platform.IncompatiblePlatformProviderApi;
28+
import java.util.Comparator;
2629
import javax.annotation.Nullable;
2730

2831
/**
@@ -69,6 +72,15 @@ public static IncompatiblePlatformProvider incompatibleDueToConstraints(
6972
@Nullable Label targetPlatform, ImmutableList<ConstraintValueInfo> constraints) {
7073
Preconditions.checkNotNull(constraints);
7174
Preconditions.checkArgument(!constraints.isEmpty());
75+
76+
// Deduplicate and sort the list of incompatible constraints. Doing it here means that everyone
77+
// inspecting this provider doesn't have to deal with it.
78+
constraints =
79+
constraints.stream()
80+
.sorted(Comparator.comparing(ConstraintValueInfo::label))
81+
.distinct()
82+
.collect(toImmutableList());
83+
7284
return new AutoValue_IncompatiblePlatformProvider(targetPlatform, null, constraints);
7385
}
7486

@@ -95,6 +107,8 @@ public boolean isImmutable() {
95107
*
96108
* <p>This may be null. If it is null, then {@code getTargetsResponsibleForIncompatibility()} is
97109
* guaranteed to be non-null. It will have at least one element in it if it is not null.
110+
*
111+
* <p>The list is sorted based on the stringified label of each constraint.
98112
*/
99113
@Nullable
100114
public abstract ImmutableList<ConstraintValueInfo> constraintsResponsibleForIncompatibility();

src/main/java/com/google/devtools/build/lib/analysis/constraints/TopLevelConstraintSemantics.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -325,12 +325,9 @@ private static String reportOnIncompatibility(ConfiguredTarget target) {
325325

326326
message += "s [";
327327

328-
// Print out a sorted list to make the output reproducible.
329328
boolean first = true;
330329
for (ConstraintValueInfo constraintValueInfo :
331-
ImmutableList.sortedCopyOf(
332-
(ConstraintValueInfo a, ConstraintValueInfo b) -> b.label().compareTo(a.label()),
333-
provider.constraintsResponsibleForIncompatibility())) {
330+
provider.constraintsResponsibleForIncompatibility()) {
334331
if (first) {
335332
first = false;
336333
} else {

src/test/shell/integration/target_compatible_with_test.sh

+57-1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@ platform(
154154
],
155155
)
156156
157+
platform(
158+
name = "foo3_bar2_platform",
159+
parents = ["${default_host_platform}"],
160+
constraint_values = [
161+
":foo3",
162+
":bar2",
163+
],
164+
)
165+
157166
sh_test(
158167
name = "pass_on_foo1",
159168
srcs = ["pass.sh"],
@@ -779,7 +788,7 @@ EOF
779788
expect_log '^Dependency chain:$'
780789
expect_log '^ //target_skipping:generated_test (.*)$'
781790
expect_log '^ //target_skipping:generate_with_tool (.*)$'
782-
expect_log "^ //target_skipping:generator_tool (.*) <-- target platform (//target_skipping:foo2_bar1_platform) didn't satisfy constraints \[//target_skipping:foo1, //target_skipping:bar2\]"
791+
expect_log "^ //target_skipping:generator_tool (.*) <-- target platform (//target_skipping:foo2_bar1_platform) didn't satisfy constraints \[//target_skipping:bar2, //target_skipping:foo1\]"
783792
expect_log 'FAILED: Build did NOT complete successfully'
784793
}
785794

@@ -880,6 +889,53 @@ EOF
880889
expect_log '^//target_skipping:pass_on_everything_but_foo1_and_foo2 * PASSED in'
881890
}
882891

892+
# Validates that we can reference the same incompatible constraint in several,
893+
# composed select() statements. This is useful for expressing compatibility for
894+
# orthogonal constraints. It's also useful when a macro author wants to express
895+
# incompatibility while also honouring the user-defined incompatibility.
896+
function test_composition() {
897+
# The first select() statement might come from a macro. The second might come
898+
# from the user who's calling that macro.
899+
cat >> target_skipping/BUILD <<EOF
900+
sh_test(
901+
name = "pass_on_foo3_and_bar2",
902+
srcs = [":pass.sh"],
903+
target_compatible_with = select({
904+
":foo3": [],
905+
"//conditions:default": [":not_compatible"],
906+
}) + select({
907+
":bar2": [],
908+
"//conditions:default": [":not_compatible"],
909+
}),
910+
)
911+
EOF
912+
913+
cd target_skipping || fail "couldn't cd into workspace"
914+
915+
bazel test \
916+
--show_result=10 \
917+
--host_platform=@//target_skipping:foo3_bar2_platform \
918+
--platforms=@//target_skipping:foo3_bar2_platform \
919+
--nocache_test_results \
920+
//target_skipping:pass_on_foo3_and_bar2 &> "${TEST_log}" \
921+
|| fail "Bazel failed unexpectedly."
922+
expect_log '^//target_skipping:pass_on_foo3_and_bar2 * PASSED in'
923+
924+
# Validate that we get an error about the target being incompatible. Make
925+
# sure that the ":not_compatible" constraint is only listed once even though
926+
# it appears twice in the configured "target_compatible_with" attribute.
927+
bazel test \
928+
--show_result=10 \
929+
--host_platform=@//target_skipping:foo1_bar1_platform \
930+
--platforms=@//target_skipping:foo1_bar1_platform \
931+
//target_skipping:pass_on_foo3_and_bar2 &> "${TEST_log}" \
932+
&& fail "Bazel passed unexpectedly."
933+
934+
expect_log 'ERROR: Target //target_skipping:pass_on_foo3_and_bar2 is incompatible and cannot be built, but was explicitly requested'
935+
expect_log "^ //target_skipping:pass_on_foo3_and_bar2 (.*) <-- target platform (//target_skipping:foo1_bar1_platform) didn't satisfy constraint //target_skipping:not_compatible$"
936+
expect_log 'FAILED: Build did NOT complete successfully'
937+
}
938+
883939
function test_incompatible_with_aliased_constraint() {
884940
cat >> target_skipping/BUILD <<'EOF'
885941
alias(

0 commit comments

Comments
 (0)