use crate::error::BuildError;
use crate::graph::{BuildGraph, CompileCommand, StandardViolation};
use cabin_core::{
InterfaceStandardSource, LanguageStandard, Package, 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_edge, 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,
pub enabled_features: Option<&'a HashMap<usize, BTreeSet<String>>>,
pub standard_compat: bool,
}
struct PreparedSource {
abs_source: Utf8PathBuf,
object: Utf8PathBuf,
language: SourceLanguage,
}
pub fn plan(req: &PlanRequest<'_>) -> Result<BuildGraph, BuildError> {
let empty_features = BTreeSet::new();
let enabled_of = |pkg_idx: usize| -> &BTreeSet<String> {
req.enabled_features
.and_then(|m| m.get(&pkg_idx))
.unwrap_or(&empty_features)
};
let selected = if let Some(sel) = &req.selected {
let selected = resolve_selection(sel, req.graph, req.selected_packages)?;
for tid in &selected {
let target = lookup_target(tid, req.graph)?;
let missing = target.missing_required_features(enabled_of(tid.0));
if !missing.is_empty() {
return Err(BuildError::TargetRequiresFeatures {
target: format_target_id(tid, req.graph),
package: req.graph.packages[tid.0].package.name.as_str().to_owned(),
missing,
});
}
}
selected
} else {
let (chosen, gated) =
default_selection(req.graph, req.selected_packages, req.enabled_features);
if chosen.is_empty() {
if gated.is_empty() {
return Err(BuildError::EmptySelectedPackages);
}
return Err(BuildError::AllDefaultTargetsRequireFeatures {
gated: gated
.into_iter()
.map(|(tid, missing)| (format_target_id(&tid, req.graph), missing))
.collect(),
});
}
chosen
};
let mut resolved_deps: HashMap<TargetId, Vec<TargetDepEdge>> = 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());
let dev_deps_visible = target.kind.is_dev_only();
for decl in &target.deps {
let edge = resolve_target_dep_edge(decl, tid.0, dev_deps_visible, req.graph)?;
let dep = edge.to.clone();
let dep_target = lookup_target(&dep, req.graph)?;
let missing = dep_target.missing_required_features(enabled_of(dep.0));
if !missing.is_empty() {
let gated_pkg_is_root = match req.selected_packages {
Some(s) => s.contains(&dep.0),
None => req.graph.primary_packages.contains(&dep.0),
};
let fix = if gated_pkg_is_root {
crate::error::FeatureGateFix::RootSelection
} else if dep.0 == tid.0 {
crate::error::FeatureGateFix::UpstreamEdge
} else if req.graph.packages[tid.0]
.deps_of_kind(cabin_core::DependencyKind::Normal)
.any(|di| di == dep.0)
{
crate::error::FeatureGateFix::DependencyEdge
} else {
crate::error::FeatureGateFix::DevDependencyEdge
};
return Err(BuildError::TargetDepRequiresFeatures {
consumer: format_target_id(&tid, req.graph),
dep_target: format_target_id(&dep, req.graph),
dep_package: req.graph.packages[dep.0].package.name.as_str().to_owned(),
missing,
fix,
});
}
to_visit.push(dep);
resolved.push(edge);
}
resolved_deps.insert(tid, resolved);
}
let topo = topo_sort_targets(&reachable, &resolved_deps, req.graph)?;
let standard_compat_violations = if req.standard_compat {
crate::standard_compat::edge_violations(&topo, &resolved_deps, req)?
} else {
Vec::new()
};
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();
let mut seen: HashSet<TargetId> = HashSet::new();
if let Some(deps) = resolved_deps.get(tid) {
for edge in deps {
let dep = &edge.to;
if seen.insert(dep.clone()) {
dep_closure.push(dep.clone());
}
if let Some(transitive) = transitive_deps.get(dep) {
for transitive_dep in transitive {
if seen.insert(transitive_dep.clone()) {
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 gnu_extensions = cabin_core::effective_gnu_extensions(
&req.graph.packages[tid.0].package.language,
target,
);
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 = req.compiler_wrapper.map(|wrapper| wrapper.path.clone());
let missing_standard = |field: &'static str| BuildError::MissingLanguageStandard {
target: format_target_id(tid, req.graph),
language: ps.language.human_label(),
field,
};
let standard = match ps.language {
SourceLanguage::C => LanguageStandard::C(
cabin_core::effective_c(&pkg_standards, target)
.ok_or_else(|| missing_standard("c-standard"))?
.standard,
),
SourceLanguage::Cxx => LanguageStandard::Cxx(
cabin_core::effective_cxx(&pkg_standards, target)
.ok_or_else(|| missing_standard("cxx-standard"))?
.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(),
});
}
let msvc_gnu_extensions = req.dialect == Dialect::Msvc && gnu_extensions;
if msvc_gnu_extensions {
standard_violations.push(StandardViolation::MsvcGnuExtensions {
target: format_target_id(tid, req.graph),
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,
gnu_extensions,
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 && !msvc_gnu_extensions {
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 edge in deps {
if let Some(dep_langs) = target_languages.get(&edge.to) {
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,
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 = objects;
inputs.extend(lib_paths);
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,
standard_compat_violations,
})
}
pub(crate) type TargetId = (usize, String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TargetDepEdge {
pub(crate) to: TargetId,
pub(crate) public: bool,
}
pub(crate) fn find_target<'a>(pkg: &'a Package, name: &str) -> Option<&'a Target> {
pkg.targets.iter().find(|t| t.name.as_str() == name)
}
pub(crate) fn lookup_target<'a>(
tid: &TargetId,
graph: &'a PackageGraph,
) -> Result<&'a Target, BuildError> {
let pkg = &graph.packages[tid.0];
find_target(&pkg.package, &tid.1).ok_or_else(|| BuildError::UnknownTargetInPackage {
package: pkg.package.name.as_str().to_owned(),
target: tid.1.clone(),
})
}
pub(crate) 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.is_library_like() {
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 Some(required) =
cabin_core::interface_c(&dep_standards, &dep_pkg.language, dep_target)
&& let Some(required_min) = required.requirement.min()
&& let Some(consumer) = cabin_core::effective_c(&pkg_standards, target)
&& consumer.standard < required_min
{
violations.push(interface_violation(
format_target_id(tid, req.graph),
format_target_id(dep_tid, req.graph),
SourceLanguage::C,
consumer.standard.as_str(),
required_min.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 Some(required) =
cabin_core::interface_cxx(&dep_standards, &dep_pkg.language, dep_target)
&& let Some(required_min) = required.requirement.min()
&& let Some(consumer) = cabin_core::effective_cxx(&pkg_standards, target)
&& consumer.standard < required_min
{
violations.push(interface_violation(
format_target_id(tid, req.graph),
format_target_id(dep_tid, req.graph),
SourceLanguage::Cxx,
consumer.standard.as_str(),
required_min.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 find_target(&pkg.package, &sel.name).is_none() {
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 find_target(&root.package, &sel.name).is_some() {
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 find_target(&pkg.package, &sel.name).is_some() {
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]>,
enabled_features: Option<&HashMap<usize, BTreeSet<String>>>,
) -> (Vec<TargetId>, Vec<(TargetId, Vec<String>)>) {
let empty = BTreeSet::new();
let mut out = Vec::new();
let mut gated = 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];
let enabled = enabled_features
.and_then(|m| m.get(&pkg_idx))
.unwrap_or(&empty);
for target in &pkg.package.targets {
if !target.kind.is_default_buildable() {
continue;
}
let tid = (pkg_idx, target.name.as_str().to_owned());
let missing = target.missing_required_features(enabled);
if missing.is_empty() {
out.push(tid);
} else {
gated.push((tid, missing));
}
}
}
(out, gated)
}
#[allow(clippy::implicit_hasher)]
pub fn selector_required_features_met(
sel: &ManifestTargetSelector,
graph: &PackageGraph,
enabled_features: &HashMap<usize, BTreeSet<String>>,
) -> bool {
let empty = BTreeSet::new();
let Some(pkg_name) = &sel.package else {
return true;
};
let Some(pkg_idx) = graph.index_of(pkg_name) else {
return true;
};
let Some(target) = graph.packages[pkg_idx]
.package
.targets
.iter()
.find(|t| t.name.as_str() == sel.name)
else {
return true;
};
let enabled = enabled_features.get(&pkg_idx).unwrap_or(&empty);
target.missing_required_features(enabled).is_empty()
}
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
}