blob: 98a347f2e4e759c496fa52f7ee964fa56ee098f8 [file] [log] [blame]
// Copyright 2024 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::{collections::BTreeMap, path::PathBuf};
use anyhow::{bail, Result};
use itertools::Itertools;
use crate::proto::build_event_stream::{
BuildEvent, BuildEventId, BuildEventPayload, CommandLineSectionType, File, NamedSetOfFiles,
NamedSetOfFilesId,
};
/// Provides fast merge operations over [`NamedSetOfFiles`].
///
/// [`NamedSetOfFiles`] can be deeply nested with duplications (like diamond inheritance).
/// Therefore, on keeping track of files, it is important to also keep track of named set IDs that
/// are already merged to the set, in order to avoid repeatedly merging the same named sets.
#[derive(Clone, Default)]
struct FastFileSet {
// Invariant: named sets are closed over subsets, i.e. all subsets of the named sets are also in
// the named sets.
children: BTreeMap<NamedSetOfFilesId, NamedSetOfFiles>,
}
impl FastFileSet {
/// Creates a new empty set.
pub fn new() -> Self {
Default::default()
}
/// Returns an iterator of files included in this set.
pub fn files(&self) -> impl Iterator<Item = &File> {
self.children
.values()
.flat_map(|named_set| named_set.files.iter())
.sorted()
.dedup()
}
}
/// An index of [`BuildEvent`] that provides fast file set operations.
struct FastFileSetIndex {
index: BTreeMap<NamedSetOfFilesId, NamedSetOfFiles>,
}
impl<'a, T> From<T> for FastFileSetIndex
where
T: IntoIterator<Item = &'a BuildEvent>,
{
/// Constructs a new [`FastFileSetIndex`] from an iterator of [`BuildEvent`].
fn from(into_iter: T) -> Self {
let index = into_iter
.into_iter()
.filter_map(|event| {
let BuildEventId::NamedSet(id) = &event.id else {
return None;
};
let BuildEventPayload::NamedSetOfFiles(named_set) = &event.payload else {
return None;
};
Some((id.clone(), named_set.clone()))
})
.collect();
Self { index }
}
}
impl FastFileSetIndex {
/// Merges a named set of files to [`FastFileSet`].
pub fn merge(&self, mut fs: FastFileSet, id: &NamedSetOfFilesId) -> Result<FastFileSet> {
if fs.children.contains_key(id) {
return Ok(fs);
}
let Some(entry) = self.index.get(id) else {
bail!("NamedSetOfFiles {} not found", id.id);
};
fs.children.insert(id.clone(), entry.clone());
for subset in &entry.file_sets {
fs = self.merge(fs, subset)?;
}
Ok(fs)
}
}
pub struct BuildEventProcessor<'a> {
events: &'a [BuildEvent],
index: FastFileSetIndex,
}
impl<'a> From<&'a Vec<BuildEvent>> for BuildEventProcessor<'a> {
/// Constructs a new [`BuildEventProcessor`] from an iterator of [`BuildEvent`].
fn from(events: &'a Vec<BuildEvent>) -> Self {
Self {
events: events.as_slice(),
index: events.into(),
}
}
}
fn path_for_file(file: &File) -> PathBuf {
let mut path = PathBuf::new();
for prefix in &file.path_prefix {
path.push(prefix);
}
path.push(&file.name);
path
}
impl BuildEventProcessor<'_> {
/// Returns the workspace relative path to all files in the specified output group.
pub fn get_output_group_files(&self, output_group_name: &str) -> Result<Vec<PathBuf>> {
let mut fileset = FastFileSet::new();
for event in self.events {
let BuildEventId::TargetCompleted(_) = &event.id else {
continue;
};
let BuildEventPayload::Completed(complete) = &event.payload else {
// This can happen when the target was incomplete due to build errors.
continue;
};
for output_group in &complete.output_group {
if output_group.name != output_group_name {
continue;
}
for file_set_id in &output_group.file_sets {
fileset = self.index.merge(fileset, file_set_id)?;
}
}
}
Ok(fileset.files().map(path_for_file).collect())
}
/// Gets the value of the flag passed to the bazel command.
///
/// i.e., Given `bazel build --remote_instance_name=foo`, this would
/// search the flags passed after the `build` command.
pub fn get_command_flag(&self, flag: &str) -> Option<&str> {
for event in self.events {
let BuildEventId::StructuredCommandLine(_) = &event.id else {
continue;
};
let BuildEventPayload::StructuredCommandLine(command_line) = &event.payload else {
continue;
};
if command_line.command_line_label != "canonical" {
continue;
}
let Some(command_line_section) = command_line
.sections
.iter()
.find(|section| section.section_label == "command options")
else {
continue;
};
let CommandLineSectionType::OptionList(option_list) =
&command_line_section.section_type
else {
continue;
};
let Some(option_item) = option_list
.option
.iter()
.filter(|option| option.option_name == flag)
.last()
else {
continue;
};
return Some(option_item.option_value.as_str());
}
None
}
}
#[cfg(test)]
mod tests {
use crate::proto::build_event_stream::{
ChunkList, CommandLineSection, CommandLineSectionType, OptionItem, OptionList, OutputGroup,
StructuredCommandLine, StructuredCommandLineId, TargetComplete, TargetCompletedId,
};
use super::*;
#[test]
fn output_group() -> Result<()> {
let events = vec![
BuildEvent {
id: BuildEventId::NamedSet(NamedSetOfFilesId {
id: "a".to_string(),
}),
payload: BuildEventPayload::NamedSetOfFiles(NamedSetOfFiles {
files: vec![File {
name: "a.txt".to_string(),
path_prefix: vec![],
}],
file_sets: vec![],
}),
},
BuildEvent {
id: BuildEventId::NamedSet(NamedSetOfFilesId {
id: "b".to_string(),
}),
payload: BuildEventPayload::NamedSetOfFiles(NamedSetOfFiles {
files: vec![File {
name: "b.txt".to_string(),
path_prefix: vec![],
}],
file_sets: vec![],
}),
},
BuildEvent {
id: BuildEventId::TargetCompleted(TargetCompletedId {
label: "//foo".to_string(),
aspect: None,
}),
payload: BuildEventPayload::Completed(TargetComplete {
success: true,
output_group: vec![
OutputGroup {
name: "transitive_logs".to_string(),
file_sets: vec![NamedSetOfFilesId {
id: "a".to_string(),
}],
incomplete: false,
},
OutputGroup {
name: "other".to_string(),
file_sets: vec![NamedSetOfFilesId {
id: "b".to_string(),
}],
incomplete: false,
},
],
}),
},
];
let files = BuildEventProcessor::from(&events).get_output_group_files("transitive_logs")?;
assert_eq!(files, vec![PathBuf::from("a.txt")]);
Ok(())
}
#[test]
fn deeply_nested_named_sets() -> Result<()> {
// Create a fibonacci-like structure of named sets.
let mut events: Vec<BuildEvent> = vec![
BuildEvent {
id: BuildEventId::NamedSet(NamedSetOfFilesId {
id: "0".to_string(),
}),
payload: BuildEventPayload::NamedSetOfFiles(NamedSetOfFiles {
files: vec![File {
name: "0.txt".to_string(),
path_prefix: vec![],
}],
file_sets: vec![],
}),
},
BuildEvent {
id: BuildEventId::NamedSet(NamedSetOfFilesId {
id: "1".to_string(),
}),
payload: BuildEventPayload::NamedSetOfFiles(NamedSetOfFiles {
files: vec![File {
name: "1.txt".to_string(),
path_prefix: vec![],
}],
file_sets: vec![],
}),
},
];
for i in 2..=100 {
events.push(BuildEvent {
id: BuildEventId::NamedSet(NamedSetOfFilesId { id: i.to_string() }),
payload: BuildEventPayload::NamedSetOfFiles(NamedSetOfFiles {
files: vec![File {
name: format!("{i}.txt"),
path_prefix: vec![],
}],
file_sets: vec![
NamedSetOfFilesId {
id: (i - 2).to_string(),
},
NamedSetOfFilesId {
id: (i - 1).to_string(),
},
],
}),
})
}
events.push(BuildEvent {
id: BuildEventId::TargetCompleted(TargetCompletedId {
label: "//foo".to_string(),
aspect: None,
}),
payload: BuildEventPayload::Completed(TargetComplete {
output_group: vec![OutputGroup {
name: "transitive_logs".to_string(),
file_sets: vec![NamedSetOfFilesId {
id: "100".to_string(),
}],
incomplete: false,
}],
success: true,
}),
});
let files = BuildEventProcessor::from(&events).get_output_group_files("transitive_logs")?;
assert_eq!(
files.into_iter().sorted().collect_vec(),
(0..=100)
.map(|i| PathBuf::from(format!("{i}.txt")))
.sorted()
.collect_vec(),
);
Ok(())
}
#[test]
fn get_command_line_flag() -> Result<()> {
let events: Vec<BuildEvent> = vec![
BuildEvent {
id: BuildEventId::StructuredCommandLine(StructuredCommandLineId {
command_line_label: "canonical".to_string(),
}),
payload: BuildEventPayload::StructuredCommandLine(StructuredCommandLine {
command_line_label: "canonical".to_string(),
sections: vec![
CommandLineSection {
section_label: "executable".to_string(),
section_type: CommandLineSectionType::ChunkList(ChunkList {
chunk: vec!["bazel".to_string()]
})
},
CommandLineSection {
section_label: "startup options".to_string(),
section_type: CommandLineSectionType::OptionList(OptionList {
option: vec![
OptionItem {
combined_form: "--max_idle_secs=10800".to_string(),
option_name: "max_idle_secs".to_string(),
option_value: "10800".to_string()
}
]
})
},
CommandLineSection {
section_label: "command".to_string(),
section_type: CommandLineSectionType::ChunkList(ChunkList {
chunk: vec!["build".to_string()]
})
},
CommandLineSection {
section_label: "command options".to_string(),
section_type: CommandLineSectionType::OptionList(OptionList {
option: vec![
OptionItem {
combined_form: "--remote_instance_name=projects/chromeos-bot/instances/cros-rbe-nonrelease".to_string(),
option_name: "remote_instance_name".to_string(),
option_value: "projects/chromeos-bot/instances/cros-rbe-nonrelease".to_string()
}
]
})
},
]
})
}
];
let processor = BuildEventProcessor::from(&events);
let remote_instance_name = processor.get_command_flag("remote_instance_name");
assert_eq!(
remote_instance_name,
Some("projects/chromeos-bot/instances/cros-rbe-nonrelease"),
);
Ok(())
}
}