blob: 2d19b865942b2b6729aa360bb8ddbfbcabe219f8 [file] [log] [blame]
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;
use serde::ser::{SerializeMap, Serializer};
use serde::Serialize;
use serde_starlark::{FunctionCall, MULTILINE};
use crate::select::{Select, SelectableOrderedValue, SelectableValue};
use crate::utils::starlark::{
looks_like_bazel_configuration_label, NoMatchingPlatformTriples, WithOriginalConfigurations,
};
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SelectDict<U, T>
where
U: SelectableOrderedValue,
T: SelectableValue,
{
// Invariant: keys in this map are not in any of the inner maps of `selects`.
common: BTreeMap<U, T>,
// Invariant: none of the inner maps are empty.
selects: BTreeMap<String, BTreeMap<U, WithOriginalConfigurations<T>>>,
// Elements from the `Select` whose configuration did not get mapped to any
// new configuration. They could be ignored, but are preserved here to
// generate comments that help the user understand what happened.
unmapped: BTreeMap<String, BTreeMap<U, T>>,
}
impl<U, T> SelectDict<U, T>
where
U: SelectableOrderedValue,
T: SelectableValue,
{
/// Re-keys the provided Select by the given configuration mapping.
/// This mapping maps from configurations in the input Select to sets
/// of configurations in the output SelectDict.
pub(crate) fn new(
select: Select<BTreeMap<U, T>>,
platforms: &BTreeMap<String, BTreeSet<String>>,
) -> Self {
let (common, selects) = select.into_parts();
// Map new configuration -> WithOriginalConfigurations(value, old configurations).
let mut remapped: BTreeMap<String, BTreeMap<U, WithOriginalConfigurations<T>>> =
BTreeMap::new();
// Map unknown configuration -> value.
let mut unmapped: BTreeMap<String, BTreeMap<U, T>> = BTreeMap::new();
for (original_configuration, entries) in selects {
match platforms.get(&original_configuration) {
Some(configurations) => {
for configuration in configurations {
for (key, value) in &entries {
remapped
.entry(configuration.clone())
.or_default()
.entry(key.clone())
.or_insert_with(|| WithOriginalConfigurations {
value: value.clone(),
original_configurations: BTreeSet::new(),
})
.original_configurations
.insert(original_configuration.clone());
}
}
}
None => {
for (key, value) in entries {
if looks_like_bazel_configuration_label(&original_configuration) {
remapped
.entry(original_configuration.clone())
.or_default()
.entry(key)
.or_insert_with(|| WithOriginalConfigurations {
value: value.clone(),
original_configurations: BTreeSet::new(),
})
.original_configurations
.insert(original_configuration.clone());
} else {
unmapped
.entry(original_configuration.clone())
.or_default()
.insert(key, value);
};
}
}
}
}
Self {
common,
selects: remapped,
unmapped,
}
}
pub(crate) fn is_empty(&self) -> bool {
self.common.is_empty() && self.selects.is_empty() && self.unmapped.is_empty()
}
}
impl<U, T> Serialize for SelectDict<U, T>
where
U: SelectableOrderedValue,
T: SelectableValue,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// If there are no platform-specific entries, we output just an ordinary
// dict.
//
// If there are platform-specific ones, we use the following. Ideally it
// could be done as `dicts.add({...}, select({...}))` but bazel_skylib's
// dicts.add does not support selects.
//
// select({
// "configuration": {
// "common-key": "common-value",
// "plat-key": "plat-value", # cfg(whatever)
// },
// "//conditions:default": {
// "common-key": "common-value",
// },
// })
//
// If there are unmapped entries, we include them like this:
//
// selects.with_unmapped({
// "configuration": {
// "common-key": "common-value",
// "plat-key": "plat-value", # cfg(whatever)
// },
// "//conditions:default": {
// "common-key": "common-value",
// },
// selects.NO_MATCHING_PLATFORM_TRIPLES: {
// "cfg(obscure): {
// "unmapped-key": "unmapped-value",
// },
// },
// })
if self.selects.is_empty() && self.unmapped.is_empty() {
return self.common.serialize(serializer);
}
struct SelectInner<'a, U, T>(&'a SelectDict<U, T>)
where
U: SelectableOrderedValue,
T: SelectableValue;
impl<'a, U, T> Serialize for SelectInner<'a, U, T>
where
U: SelectableOrderedValue,
T: SelectableValue,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(MULTILINE))?;
for (configuration, dict) in &self.0.selects {
#[derive(Serialize)]
#[serde(untagged)]
enum Either<'a, T> {
Common(&'a T),
Selects(&'a WithOriginalConfigurations<T>),
}
let mut combined = BTreeMap::new();
combined.extend(
self.0
.common
.iter()
.map(|(key, value)| (key, Either::Common(value))),
);
combined.extend(
dict.iter()
.map(|(key, value)| (key, Either::Selects(value))),
);
map.serialize_entry(configuration, &combined)?;
}
map.serialize_entry("//conditions:default", &self.0.common)?;
if !self.0.unmapped.is_empty() {
struct SelectUnmapped<'a, U, T>(&'a BTreeMap<String, BTreeMap<U, T>>)
where
U: SelectableOrderedValue,
T: SelectableValue;
impl<'a, U, T> Serialize for SelectUnmapped<'a, U, T>
where
U: SelectableOrderedValue,
T: SelectableValue,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(MULTILINE))?;
for (cfg, dict) in self.0.iter() {
map.serialize_entry(cfg, dict)?;
}
map.end()
}
}
map.serialize_entry(
&NoMatchingPlatformTriples,
&SelectUnmapped(&self.0.unmapped),
)?;
}
map.end()
}
}
let function = if self.unmapped.is_empty() {
"select"
} else {
"selects.with_unmapped"
};
FunctionCall::new(function, [SelectInner(self)]).serialize(serializer)
}
}
#[cfg(test)]
mod test {
use super::*;
use indoc::indoc;
#[test]
fn empty_select_dict() {
let select_dict: SelectDict<String, String> =
SelectDict::new(Default::default(), &Default::default());
let expected_starlark = indoc! {r#"
{}
"#};
assert_eq!(
select_dict.serialize(serde_starlark::Serializer).unwrap(),
expected_starlark,
);
}
#[test]
fn no_platform_specific_select_dict() {
let mut select: Select<BTreeMap<String, String>> = Select::default();
select.insert(("Greeting".to_owned(), "Hello".to_owned()), None);
let select_dict = SelectDict::new(select, &Default::default());
let expected_starlark = indoc! {r#"
{
"Greeting": "Hello",
}
"#};
assert_eq!(
select_dict.serialize(serde_starlark::Serializer).unwrap(),
expected_starlark,
);
}
#[test]
fn only_platform_specific_select_dict() {
let mut select: Select<BTreeMap<String, String>> = Select::default();
select.insert(
("Greeting".to_owned(), "Hello".to_owned()),
Some("platform".to_owned()),
);
let platforms = BTreeMap::from([(
"platform".to_owned(),
BTreeSet::from(["platform".to_owned()]),
)]);
let select_dict = SelectDict::new(select, &platforms);
let expected_starlark = indoc! {r#"
select({
"platform": {
"Greeting": "Hello", # platform
},
"//conditions:default": {},
})
"#};
assert_eq!(
select_dict.serialize(serde_starlark::Serializer).unwrap(),
expected_starlark,
);
}
#[test]
fn mixed_select_dict() {
let mut select: Select<BTreeMap<String, String>> = Select::default();
select.insert(
("Greeting".to_owned(), "Hello".to_owned()),
Some("platform".to_owned()),
);
select.insert(("Message".to_owned(), "Goodbye".to_owned()), None);
let platforms = BTreeMap::from([(
"platform".to_owned(),
BTreeSet::from(["platform".to_owned()]),
)]);
let select_dict = SelectDict::new(select, &platforms);
let expected_starlark = indoc! {r#"
select({
"platform": {
"Greeting": "Hello", # platform
"Message": "Goodbye",
},
"//conditions:default": {
"Message": "Goodbye",
},
})
"#};
assert_eq!(
select_dict.serialize(serde_starlark::Serializer).unwrap(),
expected_starlark,
);
}
#[test]
fn remap_select_dict_configurations() {
let mut select: Select<BTreeMap<String, String>> = Select::default();
select.insert(
("dep-a".to_owned(), "a".to_owned()),
Some("cfg(macos)".to_owned()),
);
select.insert(
("dep-b".to_owned(), "b".to_owned()),
Some("cfg(macos)".to_owned()),
);
select.insert(
("dep-d".to_owned(), "d".to_owned()),
Some("cfg(macos)".to_owned()),
);
select.insert(
("dep-a".to_owned(), "a".to_owned()),
Some("cfg(x86_64)".to_owned()),
);
select.insert(
("dep-c".to_owned(), "c".to_owned()),
Some("cfg(x86_64)".to_owned()),
);
select.insert(
("dep-e".to_owned(), "e".to_owned()),
Some("cfg(pdp11)".to_owned()),
);
select.insert(("dep-d".to_owned(), "d".to_owned()), None);
select.insert(
("dep-f".to_owned(), "f".to_owned()),
Some("@platforms//os:magic".to_owned()),
);
select.insert(
("dep-g".to_owned(), "g".to_owned()),
Some("//another:platform".to_owned()),
);
let platforms = BTreeMap::from([
(
"cfg(macos)".to_owned(),
BTreeSet::from(["x86_64-macos".to_owned(), "aarch64-macos".to_owned()]),
),
(
"cfg(x86_64)".to_owned(),
BTreeSet::from(["x86_64-linux".to_owned(), "x86_64-macos".to_owned()]),
),
]);
let select_dict = SelectDict::new(select, &platforms);
let expected = SelectDict {
common: BTreeMap::from([("dep-d".to_string(), "d".to_owned())]),
selects: BTreeMap::from([
(
"x86_64-macos".to_owned(),
BTreeMap::from([
(
"dep-a".to_string(),
WithOriginalConfigurations {
value: "a".to_owned(),
original_configurations: BTreeSet::from([
"cfg(macos)".to_owned(),
"cfg(x86_64)".to_owned(),
]),
},
),
(
"dep-b".to_string(),
WithOriginalConfigurations {
value: "b".to_owned(),
original_configurations: BTreeSet::from(["cfg(macos)".to_owned()]),
},
),
(
"dep-c".to_string(),
WithOriginalConfigurations {
value: "c".to_owned(),
original_configurations: BTreeSet::from(["cfg(x86_64)".to_owned()]),
},
),
]),
),
(
"aarch64-macos".to_owned(),
BTreeMap::from([
(
"dep-a".to_string(),
WithOriginalConfigurations {
value: "a".to_owned(),
original_configurations: BTreeSet::from(["cfg(macos)".to_owned()]),
},
),
(
"dep-b".to_string(),
WithOriginalConfigurations {
value: "b".to_owned(),
original_configurations: BTreeSet::from(["cfg(macos)".to_owned()]),
},
),
]),
),
(
"x86_64-linux".to_owned(),
BTreeMap::from([
(
"dep-a".to_string(),
WithOriginalConfigurations {
value: "a".to_owned(),
original_configurations: BTreeSet::from(["cfg(x86_64)".to_owned()]),
},
),
(
"dep-c".to_string(),
WithOriginalConfigurations {
value: "c".to_owned(),
original_configurations: BTreeSet::from(["cfg(x86_64)".to_owned()]),
},
),
]),
),
(
"@platforms//os:magic".to_owned(),
BTreeMap::from([(
"dep-f".to_string(),
WithOriginalConfigurations {
value: "f".to_owned(),
original_configurations: BTreeSet::from([
"@platforms//os:magic".to_owned()
]),
},
)]),
),
(
"//another:platform".to_owned(),
BTreeMap::from([(
"dep-g".to_string(),
WithOriginalConfigurations {
value: "g".to_owned(),
original_configurations: BTreeSet::from([
"//another:platform".to_owned()
]),
},
)]),
),
]),
unmapped: BTreeMap::from([(
"cfg(pdp11)".to_owned(),
BTreeMap::from([("dep-e".to_string(), "e".to_owned())]),
)]),
};
assert_eq!(select_dict, expected);
let expected_starlark = indoc! {r#"
selects.with_unmapped({
"//another:platform": {
"dep-d": "d",
"dep-g": "g", # //another:platform
},
"@platforms//os:magic": {
"dep-d": "d",
"dep-f": "f", # @platforms//os:magic
},
"aarch64-macos": {
"dep-a": "a", # cfg(macos)
"dep-b": "b", # cfg(macos)
"dep-d": "d",
},
"x86_64-linux": {
"dep-a": "a", # cfg(x86_64)
"dep-c": "c", # cfg(x86_64)
"dep-d": "d",
},
"x86_64-macos": {
"dep-a": "a", # cfg(macos), cfg(x86_64)
"dep-b": "b", # cfg(macos)
"dep-c": "c", # cfg(x86_64)
"dep-d": "d",
},
"//conditions:default": {
"dep-d": "d",
},
selects.NO_MATCHING_PLATFORM_TRIPLES: {
"cfg(pdp11)": {
"dep-e": "e",
},
},
})
"#};
assert_eq!(
select_dict.serialize(serde_starlark::Serializer).unwrap(),
expected_starlark,
);
}
}