Skip to content

Commit 4a0c075

Browse files
Scott Caofacebook-github-bot
Scott Cao
authored andcommitted
Implement exclusions for custom soft errors
Summary: Context: As part of "safer compatible with", we are rolling out enforcements of `dep_only_incompatible` errors to targets in the repo to prevent them from losing CI coverage due to a bad compatible_with definition change. Unfortunately, sometimes these rollout diffs break things and need to be reverted. I recently implemented custom soft errors as a way to more effectively source these issues before we roll things out. Each soft error has a set of target patterns attached and if any of those patterns violates `dep_only_incompatible`, then we fire the corresponding soft error that we can read in Scuba. This diff adds exclusions to custom soft errors to exclude a subset of target patterns specified from having soft errors. The exclusions help us get more targeted soft errors and match what we do for rollouts of `dep_only_incompatible` in having both inclusion and exclusion patterns. To support exclusions, I added a new provider `DepOnlyIncompatibleRollout` that supports a `target_patterns` field and an `exclusions` field. To make this backwards compatible with existing definitions, `DepOnlyIncompatibleInfo` now accepts either a list of strings or a `DepOnlyIncompatibleRollout` type in the `custom_soft_errors` field. Once version bump is out and the provider is migrated over, I will delete the existing support for list of target patterns. Reviewed By: Nero5023 Differential Revision: D71592839 fbshipit-source-id: 9c8e26cf4d92128f12b60f3a2253af03830b0ab2
1 parent affd2c1 commit 4a0c075

File tree

8 files changed

+252
-41
lines changed

8 files changed

+252
-41
lines changed

app/buck2_build_api/src/interpreter/rule_defs/provider/builtin.rs

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod constraint_setting_info;
1414
pub mod constraint_value_info;
1515
pub mod default_info;
1616
pub mod dep_only_incompatible_info;
17+
pub mod dep_only_incompatible_rollout;
1718
pub mod execution_platform_info;
1819
pub mod execution_platform_registration_info;
1920
pub mod external_runner_test_info;

app/buck2_build_api/src/interpreter/rule_defs/provider/builtin/dep_only_incompatible_info.rs

+108-33
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,34 @@ use buck2_core::cells::CellResolver;
1515
use buck2_core::error::validate_logview_category;
1616
use buck2_core::pattern::pattern::ParsedPattern;
1717
use buck2_core::pattern::pattern_type::TargetPatternExtra;
18+
use buck2_core::target::label::label::TargetLabel;
1819
use buck2_util::arc_str::ArcStr;
1920
use dupe::Dupe;
21+
use either::Either;
2022
use starlark::any::ProvidesStaticType;
2123
use starlark::collections::SmallMap;
2224
use starlark::environment::GlobalsBuilder;
25+
use starlark::eval::Evaluator;
2326
use starlark::values::dict::DictRef;
2427
use starlark::values::dict::DictType;
28+
use starlark::values::dict::UnpackDictEntries;
2529
use starlark::values::list::ListRef;
2630
use starlark::values::list::ListType;
2731
use starlark::values::Coerce;
2832
use starlark::values::Freeze;
2933
use starlark::values::FreezeResult;
3034
use starlark::values::Trace;
35+
use starlark::values::UnpackValue;
36+
use starlark::values::Value;
3137
use starlark::values::ValueLifetimeless;
3238
use starlark::values::ValueLike;
3339
use starlark::values::ValueOf;
40+
use starlark::values::ValueOfUnchecked;
3441
use starlark::values::ValueOfUncheckedGeneric;
3542

3643
use crate as buck2_build_api;
44+
use crate::interpreter::rule_defs::provider::builtin::dep_only_incompatible_rollout::DepOnlyIncompatibleRollout;
45+
use crate::interpreter::rule_defs::provider::builtin::dep_only_incompatible_rollout::FrozenDepOnlyIncompatibleRollout;
3746

3847
/// A provider for defining custom soft error categories for dep-only incompatible targets.
3948
/// This can be used to get finer-grained data on whether it is safe to enable
@@ -45,8 +54,14 @@ use crate as buck2_build_api;
4554
/// return [
4655
/// DefaultInfo(),
4756
/// DepOnlyIncompatibleInfo(custom_soft_errors = {
48-
/// "dep_only_incompatible_foo": ["root//foo/..."],
49-
/// "dep_only_incompatible_bar": ["root//bar/..."],
57+
/// "dep_only_incompatible_foo": DepOnlyIncompatibleRollout(
58+
/// target_patterns = ["root//foo/..."],
59+
/// exclusions = ["root//foo/excluded/..."],
60+
/// ),
61+
/// "dep_only_incompatible_bar": DepOnlyIncompatibleRollout(
62+
/// target_patterns = ["root//bar/..."],
63+
/// exclusions = [],
64+
/// ),
5065
/// })
5166
/// ]
5267
/// ```
@@ -59,38 +74,65 @@ use crate as buck2_build_api;
5974
#[derive(Clone, Debug, Trace, Coerce, Freeze, ProvidesStaticType, Allocative)]
6075
#[repr(C)]
6176
pub struct DepOnlyIncompatibleInfoGen<V: ValueLifetimeless> {
62-
pub custom_soft_errors: ValueOfUncheckedGeneric<V, DictType<String, ListType<String>>>,
77+
pub custom_soft_errors: ValueOfUncheckedGeneric<
78+
V,
79+
DictType<
80+
String,
81+
// We are in process of migrating from list[str] to DepOnlyIncompatibleRollout provider,
82+
// so for now accept both. Once migration is done, we can remove ListType<String> variant.
83+
Either<ListType<String>, FrozenDepOnlyIncompatibleRollout>,
84+
>,
85+
>,
6386
}
6487

6588
#[starlark_module]
6689
fn dep_only_incompatible_info_creator(globals: &mut GlobalsBuilder) {
6790
#[starlark(as_type = FrozenDepOnlyIncompatibleInfo)]
6891
fn DepOnlyIncompatibleInfo<'v>(
69-
#[starlark(require = named)] custom_soft_errors: ValueOf<
70-
'v,
71-
DictType<&'v str, ListType<&'v str>>,
92+
#[starlark(require = named)] custom_soft_errors: UnpackDictEntries<
93+
ValueOf<'v, &'v str>,
94+
ValueOf<'v, Either<ListType<&'v str>, &'v DepOnlyIncompatibleRollout<'v>>>,
7295
>,
96+
eval: &mut Evaluator<'v, '_, '_>,
7397
) -> starlark::Result<DepOnlyIncompatibleInfo<'v>> {
74-
let custom_soft_errors_dict = DictRef::from_value(custom_soft_errors.to_value())
75-
.ok_or_else(|| {
76-
starlark::Error::from(anyhow::anyhow!("Expected dict via type checking"))
77-
.into_internal_error()
78-
})?;
79-
for category in custom_soft_errors_dict.keys() {
80-
let category = category.to_value().unpack_str().ok_or_else(|| {
98+
let mut result = SmallMap::with_capacity(custom_soft_errors.entries.len());
99+
for (category, value) in custom_soft_errors.entries {
100+
let category_str = category.to_value().unpack_str().ok_or_else(|| {
81101
starlark::Error::from(anyhow::anyhow!("Expected string via type checking"))
82102
.into_internal_error()
83103
})?;
84-
validate_logview_category(category)?;
104+
validate_logview_category(category_str)?;
105+
result.insert(category_str, value.value);
85106
}
86107
Ok(DepOnlyIncompatibleInfo {
87-
custom_soft_errors: custom_soft_errors.as_unchecked().cast(),
108+
custom_soft_errors: ValueOfUnchecked::new(eval.heap().alloc(result)),
88109
})
89110
}
90111
}
91112

92-
pub type DepOnlyIncompatibleCustomSoftErrors =
93-
SmallMap<ArcStr, Box<[ParsedPattern<TargetPatternExtra>]>>;
113+
#[derive(Debug, Clone, PartialEq, Eq, Allocative)]
114+
pub struct DepOnlyIncompatibleRolloutPatterns {
115+
target_patterns: Box<[ParsedPattern<TargetPatternExtra>]>,
116+
exclusions: Box<[ParsedPattern<TargetPatternExtra>]>,
117+
}
118+
119+
impl DepOnlyIncompatibleRolloutPatterns {
120+
pub fn matches(&self, target: &TargetLabel) -> bool {
121+
for exclusion in &self.exclusions {
122+
if exclusion.matches(target) {
123+
return false;
124+
}
125+
}
126+
for target_pattern in &self.target_patterns {
127+
if target_pattern.matches(target) {
128+
return true;
129+
}
130+
}
131+
false
132+
}
133+
}
134+
135+
pub type DepOnlyIncompatibleCustomSoftErrors = SmallMap<ArcStr, DepOnlyIncompatibleRolloutPatterns>;
94136

95137
impl FrozenDepOnlyIncompatibleInfo {
96138
pub fn custom_soft_errors(
@@ -99,27 +141,60 @@ impl FrozenDepOnlyIncompatibleInfo {
99141
cell_resolver: &CellResolver,
100142
cell_alias_resolver: &CellAliasResolver,
101143
) -> buck2_error::Result<DepOnlyIncompatibleCustomSoftErrors> {
102-
let custom_soft_errors: SmallMap<_, Box<[_]>> =
144+
let value_to_target_pattern =
145+
|target_pattern: Value<'_>| -> buck2_error::Result<ParsedPattern<TargetPatternExtra>> {
146+
ParsedPattern::parse_precise(
147+
&target_pattern.to_value().to_str(),
148+
root_cell.dupe(),
149+
cell_resolver,
150+
cell_alias_resolver,
151+
)
152+
};
153+
154+
let custom_soft_errors: SmallMap<_, _> =
103155
DictRef::from_value(self.custom_soft_errors.get().to_value())
104156
.expect("Internal error: expected to be a dict")
105157
.iter()
106158
.map(|(k, v)| {
107-
let k = ArcStr::from(k.to_value().to_str());
108-
let mut target_patterns = Vec::new();
109-
for target_pattern in ListRef::from_value(v)
110-
.expect("Internal error: expected to be a list")
111-
.iter()
159+
let k = ArcStr::from(k.unpack_str().expect("Type checked to be string"));
160+
let (target_patterns, exclusions) = match Either::<
161+
&ListRef<'_>,
162+
&DepOnlyIncompatibleRollout<'_>,
163+
>::unpack_value(v)?
164+
.expect("Internal error: type checked to be list or provider")
112165
{
113-
let target_pattern = target_pattern.to_value().to_str();
114-
target_patterns.push(ParsedPattern::parse_precise(
115-
&target_pattern,
116-
root_cell.dupe(),
117-
&cell_resolver,
118-
&cell_alias_resolver,
119-
)?);
120-
}
121-
let v: Box<[_]> = target_patterns.into();
122-
Ok((k, v.into()))
166+
Either::Left(v) => {
167+
let target_patterns = v
168+
.iter()
169+
.map(value_to_target_pattern)
170+
.collect::<buck2_error::Result<Vec<_>>>()?;
171+
(
172+
target_patterns.into_boxed_slice(),
173+
vec![].into_boxed_slice(),
174+
)
175+
}
176+
Either::Right(v) => {
177+
let target_patterns = v
178+
.target_patterns()
179+
.iter()
180+
.map(value_to_target_pattern)
181+
.collect::<buck2_error::Result<Vec<_>>>()?;
182+
let exclusions = v
183+
.exclusions()
184+
.iter()
185+
.map(value_to_target_pattern)
186+
.collect::<buck2_error::Result<Vec<_>>>()?;
187+
(
188+
target_patterns.into_boxed_slice(),
189+
exclusions.into_boxed_slice(),
190+
)
191+
}
192+
};
193+
let v = DepOnlyIncompatibleRolloutPatterns {
194+
target_patterns,
195+
exclusions,
196+
};
197+
Ok((k, v))
123198
})
124199
.collect::<buck2_error::Result<_>>()?;
125200

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under both the MIT license found in the
5+
* LICENSE-MIT file in the root directory of this source tree and the Apache
6+
* License, Version 2.0 found in the LICENSE-APACHE file in the root directory
7+
* of this source tree.
8+
*/
9+
10+
use allocative::Allocative;
11+
use buck2_build_api_derive::internal_provider;
12+
use starlark::any::ProvidesStaticType;
13+
use starlark::environment::GlobalsBuilder;
14+
use starlark::values::list::ListRef;
15+
use starlark::values::list::ListType;
16+
use starlark::values::Coerce;
17+
use starlark::values::Freeze;
18+
use starlark::values::FreezeResult;
19+
use starlark::values::Trace;
20+
use starlark::values::ValueLifetimeless;
21+
use starlark::values::ValueOf;
22+
use starlark::values::ValueOfUncheckedGeneric;
23+
24+
use crate as buck2_build_api;
25+
26+
/// A provider that is used to signal the targets that we want to soft error on
27+
/// within the `DepOnlyIncompatibleInfo` provider.
28+
#[internal_provider(dep_only_incompatible_custom_soft_error_creator)]
29+
#[derive(Clone, Debug, Trace, Coerce, Freeze, ProvidesStaticType, Allocative)]
30+
#[repr(C)]
31+
pub struct DepOnlyIncompatibleRolloutGen<V: ValueLifetimeless> {
32+
/// A list of target patterns (minus exclusions) that we want to soft error on.
33+
pub target_patterns: ValueOfUncheckedGeneric<V, ListType<String>>,
34+
/// A list of target patterns that we want to exclude from the soft error.
35+
pub exclusions: ValueOfUncheckedGeneric<V, ListType<String>>,
36+
}
37+
38+
impl<'v> DepOnlyIncompatibleRollout<'v> {
39+
pub fn target_patterns(&'v self) -> &'v ListRef<'v> {
40+
ListRef::from_value(self.target_patterns.get())
41+
.expect("internal error: type checked as list")
42+
}
43+
44+
pub fn exclusions(&'v self) -> &'v ListRef<'v> {
45+
ListRef::from_value(self.exclusions.get()).expect("internal error: type checked as list")
46+
}
47+
}
48+
49+
#[starlark_module]
50+
fn dep_only_incompatible_custom_soft_error_creator(globals: &mut GlobalsBuilder) {
51+
#[starlark(as_type = FrozenDepOnlyIncompatibleRollout)]
52+
fn DepOnlyIncompatibleRollout<'v>(
53+
#[starlark(require = named)] target_patterns: ValueOf<'v, ListType<&'v str>>,
54+
#[starlark(require = named)] exclusions: ValueOf<'v, ListType<&'v str>>,
55+
) -> starlark::Result<DepOnlyIncompatibleRollout<'v>> {
56+
Ok(DepOnlyIncompatibleRollout {
57+
target_patterns: target_patterns.as_unchecked().cast(),
58+
exclusions: exclusions.as_unchecked().cast(),
59+
})
60+
}
61+
}

app/buck2_configured/src/nodes.rs

+9-7
Original file line numberDiff line numberDiff line change
@@ -1325,14 +1325,16 @@ async fn get_dep_only_incompatible_custom_soft_error(
13251325
let Some(custom_soft_errors) = ctx.compute(&GetDepOnlyIncompatibleInfo).await?? else {
13261326
return Ok(None);
13271327
};
1328-
let mut soft_error_categories = Vec::new();
1329-
for (soft_error_category, patterns) in custom_soft_errors.iter() {
1330-
for pattern in patterns.iter() {
1331-
if pattern.matches(target_label) {
1332-
soft_error_categories.push(soft_error_category.dupe());
1328+
let soft_error_categories: Vec<_> = custom_soft_errors
1329+
.iter()
1330+
.filter_map(|(soft_error_category, rollout_patterns)| {
1331+
if rollout_patterns.matches(target_label) {
1332+
Some(soft_error_category.dupe())
1333+
} else {
1334+
None
13331335
}
1334-
}
1335-
}
1336+
})
1337+
.collect();
13361338
Ok(Some(soft_error_categories))
13371339
}
13381340

tests/core/configurations/configurations/test_target_incompatible.py

+18
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,21 @@ async def test_dep_only_incompatible_custom_soft_errors(buck: Buck) -> None:
161161
await buck.cquery("//dep_incompatible:transitive_dep_incompatible", *args)
162162
result = await buck.log("show")
163163
assert "Soft Error: soft_error_two" in result.stdout
164+
165+
166+
@buck_test(allow_soft_errors=True)
167+
async def test_dep_only_incompatible_custom_soft_errors_with_exclusions(
168+
buck: Buck,
169+
) -> None:
170+
args = [
171+
"-c",
172+
"buck2.dep_only_incompatible_info=//dep_incompatible/dep_only_incompatible_info:dep_only_incompatible_info_with_exclusions",
173+
]
174+
await buck.cquery("//dep_incompatible:dep_incompatible", *args)
175+
result = await buck.log("show")
176+
assert "Soft Error: soft_error_one" in result.stdout
177+
assert "Soft Error: soft_error_three" in result.stdout
178+
179+
await buck.cquery("//dep_incompatible:transitive_dep_incompatible", *args)
180+
result = await buck.log("show")
181+
assert "Soft Error: soft_error_two" in result.stdout

tests/core/configurations/configurations/test_target_incompatible_data/dep_incompatible/dep_only_incompatible_info/TARGETS.fixture

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load(":defs.bzl", "dep_only_incompatible_info")
1+
load(":defs.bzl", "dep_only_incompatible_info", "dep_only_incompatible_info_with_exclusions")
22

33
dep_only_incompatible_info(
44
name = "dep_only_incompatible_info",
@@ -11,3 +11,29 @@ dep_only_incompatible_info(
1111
],
1212
},
1313
)
14+
15+
dep_only_incompatible_info_with_exclusions(
16+
name = "dep_only_incompatible_info_with_exclusions",
17+
custom_soft_errors = {
18+
"soft_error_one": {
19+
"exclusions": [],
20+
"target_patterns": [
21+
"root//dep_incompatible:dep_incompatible",
22+
],
23+
},
24+
"soft_error_three": {
25+
"exclusions": [
26+
"root//dep_incompatible:transitive_dep_incompatible",
27+
],
28+
"target_patterns": [
29+
"root//dep_incompatible:",
30+
],
31+
},
32+
"soft_error_two": {
33+
"exclusions": [],
34+
"target_patterns": [
35+
"root//dep_incompatible:transitive_dep_incompatible",
36+
],
37+
},
38+
},
39+
)

tests/core/configurations/configurations/test_target_incompatible_data/dep_incompatible/dep_only_incompatible_info/defs.bzl

+23
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,26 @@ dep_only_incompatible_info = rule(
1818
},
1919
is_configuration_rule = True,
2020
)
21+
22+
def _impl_with_exclusions(ctx: AnalysisContext) -> list[Provider]:
23+
custom_soft_errors = {
24+
category: DepOnlyIncompatibleRollout(
25+
target_patterns = v["target_patterns"],
26+
exclusions = v.get("exclusions", []),
27+
)
28+
for category, v in ctx.attrs.custom_soft_errors.items()
29+
}
30+
return [
31+
DefaultInfo(),
32+
DepOnlyIncompatibleInfo(
33+
custom_soft_errors = custom_soft_errors,
34+
),
35+
]
36+
37+
dep_only_incompatible_info_with_exclusions = rule(
38+
impl = _impl_with_exclusions,
39+
attrs = {
40+
"custom_soft_errors": attrs.dict(attrs.string(), attrs.dict(attrs.string(), attrs.list(attrs.string()))),
41+
},
42+
is_configuration_rule = True,
43+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# This file is @generated, regenerate by re-running test with `-- --env BUCK2_UPDATE_GOLDEN=1` appended to the test command
2+
3+
# DepOnlyIncompatibleRollout
4+
## DepOnlyIncompatibleRollout.exclusions
5+
## DepOnlyIncompatibleRollout.target\_patterns

0 commit comments

Comments
 (0)