use std::path::PathBuf;
use camino::{Utf8Path, Utf8PathBuf};
use cabin_driver::{BuildAction, CompileMode};
use crate::graph::BuildGraph;
fn check_stamp_path(object: &Utf8Path) -> Utf8PathBuf {
Utf8PathBuf::from(format!("{object}.check"))
}
pub fn into_check_graph(graph: BuildGraph, selected_pkg_dirs: &[PathBuf]) -> BuildGraph {
let mut actions = Vec::new();
let mut default_outputs = Vec::new();
for action in graph.actions {
let BuildAction::Compile(mut compile) = action else {
continue;
};
if !selected_pkg_dirs
.iter()
.any(|dir| compile.object.as_std_path().starts_with(dir))
{
continue;
}
let stamp = check_stamp_path(&compile.object);
compile.description = format!("CHECK {}", compile.object);
compile.mode = CompileMode::SyntaxOnly {
stamp: stamp.clone(),
};
actions.push(BuildAction::Compile(compile));
default_outputs.push(stamp);
}
let standard_violations = graph
.standard_violations
.into_iter()
.filter(|violation| {
selected_pkg_dirs
.iter()
.any(|dir| violation.object().as_std_path().starts_with(dir))
})
.collect();
BuildGraph {
actions,
dialect: graph.dialect,
default_outputs,
compile_commands: graph.compile_commands,
standard_violations,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::CompileCommand;
use cabin_core::{OptLevel, SourceLanguage};
use cabin_driver::{
ArchiveAction, CompileAction, CompileArguments, Dialect, LinkAction, LoweredActionKind,
lower,
};
fn compile(language: SourceLanguage, object: &str) -> BuildAction {
let depfile = format!("{object}.d");
let standard = match language {
SourceLanguage::C => cabin_core::LanguageStandard::C(cabin_core::DEFAULT_C_STANDARD),
SourceLanguage::Cxx => {
cabin_core::LanguageStandard::Cxx(cabin_core::DEFAULT_CXX_STANDARD)
}
};
BuildAction::Compile(CompileAction {
standard,
source: Utf8PathBuf::from("/src/a.cc"),
object: Utf8PathBuf::from(object),
mode: CompileMode::Object,
implicit_inputs: vec![],
depfile: Some(Utf8PathBuf::from(depfile)),
compiler: Utf8PathBuf::from("/usr/bin/c++"),
compiler_wrapper: None,
arguments: CompileArguments {
opt_level: OptLevel::O0,
debug_info: false,
define_ndebug: false,
include_dirs: vec![],
system_include_dirs: vec![],
defines: vec![],
extra_flags: vec![],
},
description: format!("CXX {object}"),
})
}
fn archive(object_input: &str, lib: &str) -> BuildAction {
BuildAction::Archive(ArchiveAction {
archiver: Utf8PathBuf::from("/usr/bin/ar"),
output: Utf8PathBuf::from(lib),
inputs: vec![Utf8PathBuf::from(object_input)],
description: format!("AR {lib}"),
})
}
fn link(object_input: &str, exe: &str) -> BuildAction {
BuildAction::Link(LinkAction {
linker: Utf8PathBuf::from("/usr/bin/c++"),
output: Utf8PathBuf::from(exe),
inputs: vec![Utf8PathBuf::from(object_input)],
implicit_inputs: vec![],
arguments: vec![],
link_libs: vec![],
description: format!("LINK {exe}"),
})
}
#[test]
fn rewrites_workspace_compile_to_syntax_only() {
let object = "/b/dev/packages/app/obj/app/src/a.cc.o";
let graph = BuildGraph {
dialect: Dialect::GnuLike,
actions: vec![compile(SourceLanguage::Cxx, object)],
default_outputs: vec![Utf8PathBuf::from("/b/dev/packages/app/app")],
compile_commands: Vec::<CompileCommand>::new(),
standard_violations: Vec::new(),
};
let out = into_check_graph(graph, &[PathBuf::from("/b/dev/packages/app")]);
assert_eq!(out.actions.len(), 1);
let stamp = Utf8PathBuf::from(format!("{object}.check"));
let BuildAction::Compile(c) = &out.actions[0] else {
panic!("expected a compile action");
};
assert_eq!(c.standard.language(), SourceLanguage::Cxx);
assert_eq!(
c.mode,
CompileMode::SyntaxOnly {
stamp: stamp.clone()
}
);
assert_eq!(c.depfile, Some(Utf8PathBuf::from(format!("{object}.d"))));
assert_eq!(c.description, format!("CHECK {object}"));
assert_eq!(out.default_outputs, vec![stamp.clone()]);
let lowered = lower(Dialect::GnuLike, &out.actions[0]);
assert_eq!(lowered.kind, LoweredActionKind::SyntaxCheckCpp);
assert!(lowered.command.contains(&"-fsyntax-only".to_string()));
assert!(
!lowered.command.contains(&"-c".to_string()),
"argv = {:?}",
lowered.command
);
assert!(
!lowered.command.contains(&"-o".to_string()),
"argv = {:?}",
lowered.command
);
assert!(lowered.command.contains(&"-MD".to_string()));
assert!(lowered.command.contains(&"-MF".to_string()));
let mt = lowered
.command
.iter()
.position(|t| t == "-MT")
.expect("-MT present");
assert_eq!(lowered.command[mt + 1], format!("{object}.check"));
assert_eq!(lowered.outputs, vec![stamp]);
assert_eq!(
lowered.depfile,
Some(Utf8PathBuf::from(format!("{object}.d")))
);
}
#[test]
fn rewrites_c_compile_to_c_syntax_check() {
let object = "/b/dev/packages/app/obj/app/src/a.c.o";
let graph = BuildGraph {
dialect: Dialect::GnuLike,
actions: vec![compile(SourceLanguage::C, object)],
default_outputs: vec![],
compile_commands: Vec::<CompileCommand>::new(),
standard_violations: Vec::new(),
};
let out = into_check_graph(graph, &[PathBuf::from("/b/dev/packages/app")]);
assert_eq!(
lower(Dialect::GnuLike, &out.actions[0]).kind,
LoweredActionKind::SyntaxCheckC
);
}
#[test]
fn drops_archive_and_link_actions() {
let object = "/b/dev/packages/app/obj/app/src/a.cc.o";
let graph = BuildGraph {
dialect: Dialect::GnuLike,
actions: vec![
compile(SourceLanguage::Cxx, object),
archive(object, "/b/dev/packages/app/libfoo.a"),
link(object, "/b/dev/packages/app/app"),
],
default_outputs: vec![],
compile_commands: Vec::<CompileCommand>::new(),
standard_violations: Vec::new(),
};
let out = into_check_graph(graph, &[PathBuf::from("/b/dev/packages/app")]);
assert_eq!(out.actions.len(), 1, "only the compile survives");
assert!(matches!(
&out.actions[0],
BuildAction::Compile(c) if matches!(c.mode, CompileMode::SyntaxOnly { .. })
));
}
#[test]
fn preserves_compiler_wrapper_into_check_command() {
let object = "/b/dev/packages/app/obj/app/src/a.cc.o";
let BuildAction::Compile(mut c) = compile(SourceLanguage::Cxx, object) else {
unreachable!("compile builds a compile action");
};
c.compiler_wrapper = Some(Utf8PathBuf::from("/usr/local/bin/ccache"));
let graph = BuildGraph {
dialect: Dialect::GnuLike,
actions: vec![BuildAction::Compile(c)],
default_outputs: vec![],
compile_commands: Vec::<CompileCommand>::new(),
standard_violations: Vec::new(),
};
let out = into_check_graph(graph, &[PathBuf::from("/b/dev/packages/app")]);
let lowered = lower(Dialect::GnuLike, &out.actions[0]);
assert_eq!(lowered.command[0], "/usr/local/bin/ccache");
assert_eq!(lowered.command[1], "/usr/bin/c++");
assert!(lowered.command.contains(&"-fsyntax-only".to_string()));
}
#[test]
fn drops_dependency_package_compiles() {
let app_obj = "/b/dev/packages/app/obj/app/src/a.cc.o";
let dep_obj = "/b/dev/packages/dep/obj/dep/src/d.cc.o";
let graph = BuildGraph {
dialect: Dialect::GnuLike,
actions: vec![
compile(SourceLanguage::Cxx, app_obj),
compile(SourceLanguage::Cxx, dep_obj),
],
default_outputs: vec![],
compile_commands: Vec::<CompileCommand>::new(),
standard_violations: Vec::new(),
};
let out = into_check_graph(graph, &[PathBuf::from("/b/dev/packages/app")]);
assert_eq!(out.actions.len(), 1);
let app_stamp = Utf8PathBuf::from(format!("{app_obj}.check"));
let BuildAction::Compile(c) = &out.actions[0] else {
panic!("expected a compile action");
};
assert_eq!(
c.mode,
CompileMode::SyntaxOnly {
stamp: app_stamp.clone()
}
);
assert_eq!(out.default_outputs, vec![app_stamp]);
}
#[test]
fn passes_compile_commands_through_unchanged() {
let cc = CompileCommand {
directory: Utf8PathBuf::from("/b"),
file: Utf8PathBuf::from("/src/a.cc"),
arguments: vec!["/usr/bin/c++".into(), "-c".into(), "/src/a.cc".into()],
output: Utf8PathBuf::from("/b/dev/packages/app/obj/app/src/a.cc.o"),
};
let graph = BuildGraph {
dialect: Dialect::GnuLike,
actions: vec![],
default_outputs: vec![],
compile_commands: vec![cc.clone()],
standard_violations: Vec::new(),
};
let out = into_check_graph(graph, &[]);
assert_eq!(out.compile_commands, vec![cc]);
}
#[test]
fn check_rewrite_prunes_dependency_standard_violations() {
use crate::graph::StandardViolation;
let violation = |object: &str| StandardViolation::MsvcSpelling {
target: "dep:lib".to_owned(),
language: "C++",
standard: "c++23",
object: Utf8PathBuf::from(object),
};
let graph = BuildGraph {
actions: vec![compile(
SourceLanguage::Cxx,
"/abs/build/dev/packages/app/app/main.o",
)],
dialect: Dialect::Msvc,
default_outputs: Vec::new(),
compile_commands: Vec::new(),
standard_violations: vec![
violation("/abs/build/dev/packages/dep/lib/dep.o"),
violation("/abs/build/dev/packages/app/app/exotic.o"),
],
};
let selected = vec![PathBuf::from("/abs/build/dev/packages/app")];
let checked = into_check_graph(graph, &selected);
assert_eq!(checked.standard_violations.len(), 1);
assert_eq!(
checked.standard_violations[0].object(),
&Utf8PathBuf::from("/abs/build/dev/packages/app/app/exotic.o")
);
assert!(crate::validate_planned_standards(&checked).is_err());
}
}