use crate::error::BuildError;
use crate::graph::{BuildGraph, CompileCommand, StandardViolation};
use cabin_core::{
InterfaceStandardSource, LanguageStandard, ResolvedCompilerWrapper, ResolvedLanguageStandards,
ResolvedProfile, ResolvedProfileFlags, ResolvedToolchain, SourceLanguage, StandardFlagConflict,
Target, TargetKind, classify_source, link_driver_language,
};
use cabin_driver::{
ArchiveAction, BuildAction, CompileAction, CompileArguments, CompileMode, Dialect, LinkAction,
compile_argv,
};
use cabin_workspace::PackageGraph;
use camino::Utf8PathBuf;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::PathBuf;
mod lowering;
#[cfg(test)]
mod tests;
use self::lowering::{
collect_include_dirs, collect_link_lib_names, collect_link_libs, compile_dispatch,
depfile_path, object_path, promote_dir, resolve_target_dep, topo_sort_targets,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManifestTargetSelector {
pub package: Option<String>,
pub name: String,
}
impl ManifestTargetSelector {
pub fn parse(s: &str) -> Self {
match s.split_once(':') {
Some((pkg, tgt)) => Self {
package: Some(pkg.to_owned()),
name: tgt.to_owned(),
},
None => Self {
package: None,
name: s.to_owned(),
},
}
}
}
#[derive(Debug)]
pub struct PlanRequest<'a> {
pub graph: &'a PackageGraph,
pub toolchain: &'a ResolvedToolchain,
pub build_flags: &'a HashMap<usize, ResolvedProfileFlags>,
pub language_standards: &'a HashMap<usize, ResolvedLanguageStandards>,
pub standard_flag_conflicts: &'a HashMap<usize, Vec<StandardFlagConflict>>,
pub build_dir: PathBuf,
pub profile: ResolvedProfile,
pub selected: Option<Vec<ManifestTargetSelector>>,
pub configuration: Option<&'a cabin_core::BuildConfiguration>,
pub selected_packages: Option<&'a [usize]>,
pub compiler_wrapper: Option<&'a ResolvedCompilerWrapper>,
pub dialect: Dialect,
pub msvc_external_includes: bool,
}
struct PreparedSource {
abs_source: Utf8PathBuf,
object: Utf8PathBuf,
language: SourceLanguage,
}
pub fn plan(req: &PlanRequest<'_>) -> Result<BuildGraph, BuildError> {
let selected = if let Some(sel) = &req.selected {
resolve_selection(sel, req.graph, req.selected_packages)?
} else {
let chosen = default_selection(req.graph, req.selected_packages);
if chosen.is_empty() {
return Err(BuildError::EmptySelectedPackages);
}
chosen
};
let mut resolved_deps: HashMap<TargetId, Vec<TargetId>> = HashMap::new();
let mut reachable: HashSet<TargetId> = HashSet::new();
let mut to_visit: Vec<TargetId> = selected.clone();
while let Some(tid) = to_visit.pop() {
if !reachable.insert(tid.clone()) {
continue;
}
let target = lookup_target(&tid, req.graph)?;
let mut resolved = Vec::with_capacity(target.deps.len());
for raw in &target.deps {
let dep = resolve_target_dep(raw.as_str(), tid.0, req.graph)?;
to_visit.push(dep.clone());
resolved.push(dep);
}
resolved_deps.insert(tid, resolved);
}
let topo = topo_sort_targets(&reachable, &resolved_deps, req.graph)?;
let build_dir = promote_dir(&req.build_dir)?;
let mut actions: Vec<BuildAction> = Vec::new();
let mut compile_commands: Vec<CompileCommand> = Vec::new();
let mut standard_violations: Vec<StandardViolation> = Vec::new();
let mut output_for_target: HashMap<TargetId, Utf8PathBuf> = HashMap::new();
let mut target_languages: HashMap<TargetId, BTreeSet<SourceLanguage>> = HashMap::new();
let mut transitive_deps: HashMap<TargetId, Vec<TargetId>> = HashMap::new();
for tid in &topo {
let target = lookup_target(tid, req.graph)?;
let mut dep_closure: Vec<TargetId> = Vec::new();
if let Some(deps) = resolved_deps.get(tid) {
for dep in deps {
if !dep_closure.contains(dep) {
dep_closure.push(dep.clone());
}
if let Some(transitive) = transitive_deps.get(dep) {
for transitive_dep in transitive {
if !dep_closure.contains(transitive_dep) {
dep_closure.push(transitive_dep.clone());
}
}
}
}
}
transitive_deps.insert(tid.clone(), dep_closure.clone());
let pkg = &req.graph.packages[tid.0];
let pkg_name = pkg.package.name.as_str();
let pkg_build_dir = build_dir
.join(req.profile.name.as_str())
.join("packages")
.join(pkg_name);
let manifest_dir = promote_dir(&pkg.manifest_dir)?;
if target.kind.is_header_only() {
target_languages.insert(tid.clone(), Default::default());
continue;
}
let mut prepared: Vec<PreparedSource> = Vec::with_capacity(target.sources.len());
for source in &target.sources {
let language =
classify_source(source).ok_or_else(|| BuildError::UnrecognizedSourceExtension {
target: format_target_id(tid, req.graph),
path: source.clone(),
})?;
let object = object_path(&pkg_build_dir, target.name.as_str(), source, req.dialect)
.map_err(|reason| BuildError::InvalidSourcePath {
target: format_target_id(tid, req.graph),
path: source.clone(),
reason,
})?;
prepared.push(PreparedSource {
abs_source: manifest_dir.join(source),
object,
language,
});
}
if prepared.is_empty() {
return Err(BuildError::EmptyTargetSources(format_target_id(
tid, req.graph,
)));
}
let pkg_standards = req
.language_standards
.get(&tid.0)
.copied()
.unwrap_or_default();
enforce_interface_standards(
tid,
target,
&prepared,
&dep_closure,
pkg_standards,
req,
&mut standard_violations,
)?;
let pkg_flags = req.build_flags.get(&tid.0);
let collected = collect_include_dirs(tid, target, &resolved_deps, req.graph)?;
let mut include_dirs = collected.user;
let mut system_include_dirs = collected.system;
if let Some(flags) = pkg_flags {
for inc in &flags.include_dirs {
let absolute = if inc.is_absolute() {
inc.clone()
} else {
manifest_dir.join(inc)
};
if !include_dirs.contains(&absolute) && !system_include_dirs.contains(&absolute) {
include_dirs.push(absolute);
}
}
for inc in &flags.system_include_dirs {
let absolute = if inc.is_absolute() {
inc.clone()
} else {
manifest_dir.join(inc)
};
if !include_dirs.contains(&absolute) && !system_include_dirs.contains(&absolute) {
system_include_dirs.push(absolute);
}
}
}
if req.dialect == Dialect::Msvc && !req.msvc_external_includes {
for dir in system_include_dirs.drain(..) {
if !include_dirs.contains(&dir) {
include_dirs.push(dir);
}
}
}
let mut defines: Vec<String> = target.defines.clone();
if let Some(flags) = pkg_flags {
for def in &flags.defines {
if !defines.contains(def) {
defines.push(def.clone());
}
}
}
let extra_compile_args: &[String] =
pkg_flags.map_or(&[], |f| f.extra_compile_args.as_slice());
let cflags: &[String] = pkg_flags.map_or(&[], |f| f.cflags.as_slice());
let cxxflags: &[String] = pkg_flags.map_or(&[], |f| f.cxxflags.as_slice());
let ldflags: &[String] = pkg_flags.map_or(&[], |f| f.ldflags.as_slice());
let mut objects: Vec<Utf8PathBuf> = Vec::with_capacity(prepared.len());
for ps in &prepared {
let depfile = depfile_path(&ps.object);
let dispatch = compile_dispatch(ps.language, req)
.map_err(|err| err.attach_target_path(tid, req.graph, &ps.abs_source))?;
let extra_language_compile_args = match ps.language {
SourceLanguage::C => cflags,
SourceLanguage::Cxx => cxxflags,
};
let mut extra_flags =
Vec::with_capacity(extra_compile_args.len() + extra_language_compile_args.len());
extra_flags.extend(extra_compile_args.iter().cloned());
extra_flags.extend(extra_language_compile_args.iter().cloned());
let compiler_wrapper = match (req.compiler_wrapper, ps.language) {
(Some(wrapper), SourceLanguage::Cxx) => Some(wrapper.path.clone()),
_ => None,
};
let standard = match ps.language {
SourceLanguage::C => {
LanguageStandard::C(cabin_core::effective_c(&pkg_standards, target).standard)
}
SourceLanguage::Cxx => LanguageStandard::Cxx(
cabin_core::effective_cxx(&pkg_standards, target).standard,
),
};
let msvc_spelling_missing =
req.dialect == Dialect::Msvc && standard.msvc_spelling().is_none();
if msvc_spelling_missing {
standard_violations.push(StandardViolation::MsvcSpelling {
target: format_target_id(tid, req.graph),
language: ps.language.human_label(),
standard: standard.as_str(),
object: ps.object.clone(),
});
}
if let Some(candidates) = req.standard_flag_conflicts.get(&tid.0) {
for candidate in candidates {
let covers = candidate.language == ps.language
&& candidate
.target
.as_deref()
.is_none_or(|t| t == tid.1.as_str());
if covers {
standard_violations.push(StandardViolation::FlagConflict {
conflict: candidate.clone(),
object: ps.object.clone(),
});
}
}
}
let compile = CompileAction {
standard,
source: ps.abs_source.clone(),
object: ps.object.clone(),
mode: CompileMode::Object,
implicit_inputs: Vec::new(),
depfile: Some(depfile),
compiler: dispatch.driver.to_path_buf(),
compiler_wrapper,
arguments: CompileArguments {
opt_level: req.profile.opt_level,
debug_info: req.profile.debug,
define_ndebug: !req.profile.assertions,
include_dirs: include_dirs.clone(),
system_include_dirs: system_include_dirs.clone(),
defines: defines.clone(),
extra_flags,
},
description: format!("{} {}", dispatch.description_tag, ps.object),
};
if !msvc_spelling_missing {
compile_commands.push(CompileCommand {
directory: build_dir.to_path_buf(),
file: ps.abs_source.clone(),
arguments: compile_argv(req.dialect, &compile),
output: ps.object.clone(),
});
}
objects.push(ps.object.clone());
actions.push(BuildAction::Compile(compile));
}
let mut languages_here: BTreeSet<SourceLanguage> =
prepared.iter().map(|p| p.language).collect();
if let Some(deps) = resolved_deps.get(tid) {
for dep in deps {
if let Some(dep_langs) = target_languages.get(dep) {
languages_here.extend(dep_langs.iter().copied());
}
}
}
match target.kind {
TargetKind::Library => {
let lib_path =
pkg_build_dir.join(req.dialect.static_library_name(target.name.as_str()));
actions.push(BuildAction::Archive(ArchiveAction {
archiver: req.toolchain.ar.path().to_path_buf(),
output: lib_path.clone(),
inputs: objects.clone(),
description: format!("AR {lib_path}"),
}));
output_for_target.insert(tid.clone(), lib_path);
}
TargetKind::Executable | TargetKind::Test | TargetKind::Example => {
let exe_path =
pkg_build_dir.join(req.dialect.executable_name(target.name.as_str()));
let lib_paths =
collect_link_libs(tid, &resolved_deps, req.graph, &output_for_target);
let mut inputs: Vec<Utf8PathBuf> = objects.clone();
inputs.extend(lib_paths.iter().cloned());
let link_arguments = ldflags.to_vec();
let link_libs = collect_link_lib_names(tid, &resolved_deps, req.build_flags);
let languages_slice: Vec<SourceLanguage> = languages_here.iter().copied().collect();
let driver_language = link_driver_language(&languages_slice);
let driver_path = match driver_language {
SourceLanguage::Cxx => req.toolchain.cxx.path(),
SourceLanguage::C => {
req.toolchain
.cc
.as_ref()
.map(cabin_core::ResolvedTool::path)
.ok_or_else(|| {
BuildError::MissingCCompiler {
target: format_target_id(tid, req.graph),
path: prepared
.iter()
.find(|p| p.language == SourceLanguage::C)
.map_or_else(|| exe_path.clone(), |p| p.abs_source.clone()),
}
})?
}
};
actions.push(BuildAction::Link(LinkAction {
linker: driver_path.to_path_buf(),
output: exe_path.clone(),
inputs,
implicit_inputs: Vec::new(),
arguments: link_arguments,
link_libs,
description: format!("LINK {exe_path}"),
}));
output_for_target.insert(tid.clone(), exe_path);
}
TargetKind::HeaderOnly => {
unreachable!("header-only targets are skipped before action generation")
}
}
target_languages.insert(tid.clone(), languages_here);
}
let default_outputs: Vec<Utf8PathBuf> = selected
.iter()
.filter_map(|tid| output_for_target.get(tid).cloned())
.collect();
Ok(BuildGraph {
actions,
dialect: req.dialect,
default_outputs,
compile_commands,
standard_violations,
})
}
pub(super) type TargetId = (usize, String);
fn lookup_target<'a>(tid: &TargetId, graph: &'a PackageGraph) -> Result<&'a Target, BuildError> {
let pkg = &graph.packages[tid.0];
pkg.package
.targets
.iter()
.find(|t| t.name.as_str() == tid.1)
.ok_or_else(|| BuildError::UnknownTargetInPackage {
package: pkg.package.name.as_str().to_owned(),
target: tid.1.clone(),
})
}
pub(super) fn format_target_id(tid: &TargetId, graph: &PackageGraph) -> String {
format!("{}:{}", graph.packages[tid.0].package.name.as_str(), tid.1)
}
fn enforce_interface_standards(
tid: &TargetId,
target: &Target,
prepared: &[PreparedSource],
dep_closure: &[TargetId],
pkg_standards: ResolvedLanguageStandards,
req: &PlanRequest<'_>,
violations: &mut Vec<StandardViolation>,
) -> Result<(), BuildError> {
let object_of = |language: SourceLanguage| {
prepared
.iter()
.find(|p| p.language == language)
.map(|p| p.object.clone())
};
for dep_tid in dep_closure {
let dep_target = lookup_target(dep_tid, req.graph)?;
if !(dep_target.kind.produces_archive() || dep_target.kind.is_header_only()) {
continue;
}
let dep_pkg = &req.graph.packages[dep_tid.0].package;
let dep_standards = req
.language_standards
.get(&dep_tid.0)
.copied()
.unwrap_or_default();
if let Some(object) = object_of(SourceLanguage::C)
&& cabin_core::imposes_requirement(dep_target, &dep_pkg.language, SourceLanguage::C)
{
let required = cabin_core::interface_c(&dep_standards, &dep_pkg.language, dep_target);
let consumer = cabin_core::effective_c(&pkg_standards, target);
if consumer.standard < required.standard {
violations.push(interface_violation(
format_target_id(tid, req.graph),
format_target_id(dep_tid, req.graph),
SourceLanguage::C,
consumer.standard.as_str(),
required.standard.as_str(),
required.source,
object,
));
}
}
if let Some(object) = object_of(SourceLanguage::Cxx)
&& cabin_core::imposes_requirement(dep_target, &dep_pkg.language, SourceLanguage::Cxx)
{
let required = cabin_core::interface_cxx(&dep_standards, &dep_pkg.language, dep_target);
let consumer = cabin_core::effective_cxx(&pkg_standards, target);
if consumer.standard < required.standard {
violations.push(interface_violation(
format_target_id(tid, req.graph),
format_target_id(dep_tid, req.graph),
SourceLanguage::Cxx,
consumer.standard.as_str(),
required.standard.as_str(),
required.source,
object,
));
}
}
}
Ok(())
}
fn interface_violation(
consumer: String,
dependency: String,
language: SourceLanguage,
consumer_standard: &'static str,
required: &'static str,
source: InterfaceStandardSource,
object: Utf8PathBuf,
) -> StandardViolation {
let requirement_source = match source {
InterfaceStandardSource::Target => "its target-level interface standard",
InterfaceStandardSource::Package => "its package-level interface standard",
InterfaceStandardSource::Workspace => "its workspace-inherited interface standard",
InterfaceStandardSource::CompileStandard => {
"its effective implementation standard (no interface standard declared)"
}
};
StandardViolation::InterfaceIncompatibility {
consumer,
dependency,
language: language.human_label(),
consumer_standard,
required,
requirement_source,
object,
}
}
fn resolve_selection(
selectors: &[ManifestTargetSelector],
graph: &PackageGraph,
selected_packages: Option<&[usize]>,
) -> Result<Vec<TargetId>, BuildError> {
let mut out: Vec<TargetId> = Vec::with_capacity(selectors.len());
for sel in selectors {
out.push(resolve_top_level_selector(sel, graph, selected_packages)?);
}
Ok(out)
}
fn resolve_top_level_selector(
sel: &ManifestTargetSelector,
graph: &PackageGraph,
selected_packages: Option<&[usize]>,
) -> Result<TargetId, BuildError> {
if let Some(pkg_name) = &sel.package {
let pkg_idx =
graph
.index_of(pkg_name)
.ok_or_else(|| BuildError::UnknownPackageInTargetSelector {
package: pkg_name.clone(),
selector: format!("{}:{}", pkg_name, sel.name),
})?;
let pkg = &graph.packages[pkg_idx];
if !pkg
.package
.targets
.iter()
.any(|t| t.name.as_str() == sel.name)
{
return Err(BuildError::UnknownTargetInPackage {
package: pkg_name.clone(),
target: sel.name.clone(),
});
}
return Ok((pkg_idx, sel.name.clone()));
}
let candidates: Vec<usize> = if let Some(s) = selected_packages {
s.to_vec()
} else {
let mut root_match: Option<TargetId> = None;
if let Some(root_idx) = graph.root_package {
let root = &graph.packages[root_idx];
if root
.package
.targets
.iter()
.any(|t| t.name.as_str() == sel.name)
{
root_match = Some((root_idx, sel.name.clone()));
}
}
if let Some(tid) = root_match {
return Ok(tid);
}
graph.primary_packages.clone()
};
let mut matches: Vec<TargetId> = Vec::new();
for idx in candidates {
let pkg = &graph.packages[idx];
if pkg
.package
.targets
.iter()
.any(|t| t.name.as_str() == sel.name)
{
matches.push((idx, sel.name.clone()));
}
}
match matches.len() {
0 => Err(BuildError::UnknownTargetReference(sel.name.clone())),
1 => Ok(matches.into_iter().next().unwrap()),
_ => Err(BuildError::AmbiguousTarget(
sel.name.clone(),
matches
.iter()
.map(|(i, t)| format!("{}:{}", graph.packages[*i].package.name.as_str(), t))
.collect(),
)),
}
}
fn default_selection(graph: &PackageGraph, selected_packages: Option<&[usize]>) -> Vec<TargetId> {
let mut out = Vec::new();
let pkg_indices: &[usize] = match selected_packages {
Some(s) => s,
None => graph.primary_packages.as_slice(),
};
for &pkg_idx in pkg_indices {
let pkg = &graph.packages[pkg_idx];
for target in &pkg.package.targets {
if target.kind.is_default_buildable() {
out.push((pkg_idx, target.name.as_str().to_owned()));
}
}
}
out
}
pub fn select_targets_of_kind(
graph: &PackageGraph,
selected_packages: Option<&[usize]>,
kind: TargetKind,
) -> Vec<ManifestTargetSelector> {
let pkg_indices: &[usize] = match selected_packages {
Some(s) => s,
None => graph.primary_packages.as_slice(),
};
let mut out = Vec::new();
for &pkg_idx in pkg_indices {
let pkg = &graph.packages[pkg_idx];
for target in &pkg.package.targets {
if target.kind == kind {
out.push(ManifestTargetSelector {
package: Some(pkg.package.name.as_str().to_owned()),
name: target.name.as_str().to_owned(),
});
}
}
}
out
}