Skip to content

Commit 8a10c8e

Browse files
committed
Deduplicate target_compatible_with labels
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)
1 parent a06dca5 commit 8a10c8e

File tree

3 files changed

+59
-6
lines changed

3 files changed

+59
-6
lines changed

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.google.auto.value.AutoValue;
1818
import com.google.common.base.Preconditions;
1919
import com.google.common.collect.ImmutableList;
20+
import com.google.common.collect.ImmutableSet;
2021
import com.google.devtools.build.lib.analysis.platform.ConstraintValueInfo;
2122
import com.google.devtools.build.lib.cmdline.Label;
2223
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
@@ -69,7 +70,14 @@ public static IncompatiblePlatformProvider incompatibleDueToConstraints(
6970
@Nullable Label targetPlatform, ImmutableList<ConstraintValueInfo> constraints) {
7071
Preconditions.checkNotNull(constraints);
7172
Preconditions.checkArgument(!constraints.isEmpty());
72-
return new AutoValue_IncompatiblePlatformProvider(targetPlatform, null, constraints);
73+
74+
// Deduplicate and sort the list of constraints.
75+
ImmutableSet<ConstraintValueInfo> filteredConstraints = ImmutableSet.copyOf(constraints);
76+
ImmutableList<ConstraintValueInfo> filteredAndSortedConstraints = ImmutableList.sortedCopyOf(
77+
(ConstraintValueInfo a, ConstraintValueInfo b) -> b.label().compareTo(a.label()),
78+
filteredConstraints);
79+
80+
return new AutoValue_IncompatiblePlatformProvider(targetPlatform, null, filteredAndSortedConstraints);
7381
}
7482

7583
@Override
@@ -95,6 +103,8 @@ public boolean isImmutable() {
95103
*
96104
* <p>This may be null. If it is null, then {@code getTargetsResponsibleForIncompatibility()} is
97105
* guaranteed to be non-null. It will have at least one element in it if it is not null.
106+
*
107+
* <p>The list is sorted based on the toString() result of each constraint.
98108
*/
99109
@Nullable
100110
public abstract ImmutableList<ConstraintValueInfo> constraintsResponsibleForIncompatibility();

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

+1-5
Original file line numberDiff line numberDiff line change
@@ -325,12 +325,8 @@ 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;
330-
for (ConstraintValueInfo constraintValueInfo :
331-
ImmutableList.sortedCopyOf(
332-
(ConstraintValueInfo a, ConstraintValueInfo b) -> b.label().compareTo(a.label()),
333-
provider.constraintsResponsibleForIncompatibility())) {
329+
for (ConstraintValueInfo constraintValueInfo : provider.constraintsResponsibleForIncompatibility()) {
334330
if (first) {
335331
first = false;
336332
} else {

src/test/shell/integration/target_compatible_with_test.sh

+47
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"],
@@ -879,6 +888,44 @@ EOF
879888
expect_log '^//target_skipping:pass_on_everything_but_foo1_and_foo2 * PASSED in'
880889
}
881890
891+
function test_composition() {
892+
cat >> target_skipping/BUILD <<EOF
893+
sh_test(
894+
name = "pass_on_foo3_and_bar2",
895+
srcs = [":pass.sh"],
896+
target_compatible_with = select({
897+
":foo3": [],
898+
"//conditions:default": [":not_compatible"],
899+
}) + select({
900+
":bar2": [],
901+
"//conditions:default": [":not_compatible"],
902+
}),
903+
)
904+
EOF
905+
906+
cd target_skipping || fail "couldn't cd into workspace"
907+
908+
bazel test \
909+
--show_result=10 \
910+
--host_platform=@//target_skipping:foo3_bar2_platform \
911+
--platforms=@//target_skipping:foo3_bar2_platform \
912+
--nocache_test_results \
913+
//target_skipping:pass_on_foo3_and_bar2 &> "${TEST_log}" \
914+
|| fail "Bazel failed unexpectedly."
915+
expect_log '^//target_skipping:pass_on_foo3_and_bar2 * PASSED in'
916+
917+
bazel test \
918+
--show_result=10 \
919+
--host_platform=@//target_skipping:foo1_bar1_platform \
920+
--platforms=@//target_skipping:foo1_bar1_platform \
921+
//target_skipping:pass_on_foo3_and_bar2 &> "${TEST_log}" \
922+
&& fail "Bazel passed unexpectedly."
923+
924+
expect_log 'ERROR: Target //target_skipping:pass_on_foo3_and_bar2 is incompatible and cannot be built, but was explicitly requested'
925+
expect_log "^ //target_skipping:pass_on_foo3_and_bar2 (.*) <-- target platform (//target_skipping:foo1_bar1_platform) didn't satisfy constraint //target_skipping:not_compatible$"
926+
expect_log 'FAILED: Build did NOT complete successfully'
927+
}
928+
882929
function test_incompatible_with_aliased_constraint() {
883930
cat >> target_skipping/BUILD <<EOF
884931
alias(

0 commit comments

Comments
 (0)