Skip to main content

cabin_build/planner/
mod.rs

1use crate::error::BuildError;
2use crate::graph::{BuildGraph, CompileCommand, StandardViolation};
3use cabin_core::{
4    InterfaceStandardSource, LanguageStandard, Package, ResolvedCompilerWrapper,
5    ResolvedLanguageStandards, ResolvedProfile, ResolvedProfileFlags, ResolvedToolchain,
6    SourceLanguage, StandardFlagConflict, Target, TargetKind, classify_source,
7    link_driver_language,
8};
9use cabin_driver::{
10    ArchiveAction, BuildAction, CompileAction, CompileArguments, CompileMode, Dialect, LinkAction,
11    compile_argv,
12};
13use cabin_workspace::PackageGraph;
14use camino::Utf8PathBuf;
15use std::collections::{BTreeSet, HashMap, HashSet};
16use std::path::PathBuf;
17
18mod lowering;
19#[cfg(test)]
20mod tests;
21
22use self::lowering::{
23    collect_include_dirs, collect_link_lib_names, collect_link_libs, compile_dispatch,
24    depfile_path, object_path, promote_dir, resolve_target_dep_edge, topo_sort_targets,
25};
26
27/// Reference to a manifest target - one of the `[target.<name>]`
28/// declarations in a package's `cabin.toml`.  May be qualified
29/// `package:target` or unqualified `target`.  Resolution against a
30/// [`PackageGraph`] happens in the planner.
31///
32/// This is the *manifest-target* selector.  It has nothing to do
33/// with a platform / toolchain target (e.g. an
34/// `x86_64-unknown-linux-gnu` triple); Cabin does not yet model
35/// the latter.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ManifestTargetSelector {
38    pub package: Option<String>,
39    pub name: String,
40}
41
42impl ManifestTargetSelector {
43    /// Parse a `package:target` or bare `target` string.  Unknown formats
44    /// (multiple `:`s) are accepted and surfaced later by resolution
45    /// errors.
46    pub fn parse(s: &str) -> Self {
47        match s.split_once(':') {
48            Some((pkg, tgt)) => Self {
49                package: Some(pkg.to_owned()),
50                name: tgt.to_owned(),
51            },
52            None => Self {
53                package: None,
54                name: s.to_owned(),
55            },
56        }
57    }
58}
59
60/// Inputs to the build planner.
61#[derive(Debug)]
62pub struct PlanRequest<'a> {
63    pub graph: &'a PackageGraph,
64    /// Resolved C/C++ toolchain.  The planner picks the compile
65    /// driver per source language (`toolchain.cc.path()` for `.c`,
66    /// `toolchain.cxx.path()` for `.cc` / `.cpp` / `.cxx` /
67    /// `.c++` / `.C`) and the link driver per target (C++ if any
68    /// linked object came from a C++ source, otherwise C).
69    /// `toolchain.ar.path()` drives archive commands.
70    pub toolchain: &'a ResolvedToolchain,
71    /// Per-package resolved build flags.  Missing entries fall
72    /// back to an empty [`ResolvedProfileFlags`]; the planner does
73    /// not require every package to be present so consumers can
74    /// resolve flags lazily for the selected closure only.
75    pub build_flags: &'a HashMap<usize, ResolvedProfileFlags>,
76    /// Per-package effective language standards.  Missing entries
77    /// resolve to no package-level standards, mirroring
78    /// `build_flags`; a compile of a language with no effective
79    /// standard is then rejected at its compile site.
80    pub language_standards: &'a HashMap<usize, ResolvedLanguageStandards>,
81    /// Per-package standard-flag conflict candidates, detected by
82    /// the CLI on the manifest-derived flags *before* env /
83    /// pkg-config augmentation (so `CFLAGS` / `CXXFLAGS` stay
84    /// exempt).  The planner records a violation for each planned
85    /// compile a candidate's scope covers; candidates whose scope
86    /// is never planned (an unbuilt sibling target) gate nothing.
87    pub standard_flag_conflicts: &'a HashMap<usize, Vec<StandardFlagConflict>>,
88    /// Absolute path under which all build outputs are placed.
89    pub build_dir: PathBuf,
90    /// Resolved build profile.  Drives compile flags and the per-
91    /// profile output directory.
92    pub profile: ResolvedProfile,
93    /// Specific manifest targets to build, plus their transitive
94    /// deps.  `None` means "every C/C++ target in every primary
95    /// package".
96    pub selected: Option<Vec<ManifestTargetSelector>>,
97    /// Resolved root-package configuration.  Carried through
98    /// the planner so future cache logic and any planner-level
99    /// fingerprint comparisons see the same selection the build
100    /// script and metadata observed.  The planner does not yet
101    /// change C++ flags based on this value.
102    pub configuration: Option<&'a cabin_core::BuildConfiguration>,
103    /// Indices of `graph.packages` that the user picked
104    /// through workspace package-selection flags.  `None` means
105    /// "use the graph's primary set" (the documented default).
106    /// When `Some`, default-target enumeration narrows to the
107    /// supplied indices and any manifest-target selectors in
108    /// `selected` are validated against them so an unrelated
109    /// package never sneaks into the build.
110    pub selected_packages: Option<&'a [usize]>,
111    /// Optional compiler wrapper applied to every C and C++
112    /// compile command.  The Ninja `command` field is prefixed with
113    /// the wrapper executable; the matching `compile_commands.json`
114    /// `arguments` array stays *unwrapped* so clangd / IDE tooling
115    /// keeps seeing the underlying compiler.  Link and archive
116    /// commands are never wrapped.
117    pub compiler_wrapper: Option<&'a ResolvedCompilerWrapper>,
118    /// Compiler command-line dialect for this build.  Selected from
119    /// the detected C++ compiler (MSVC drives the `cl.exe` dialect).
120    /// Governs artifact naming (`.o` vs `.obj`, `lib<x>.a` vs
121    /// `<x>.lib`, `<x>` vs `<x>.exe`) and the spelling of every
122    /// compile / archive / link command the lowering emits.
123    pub dialect: Dialect,
124    /// Whether the MSVC-dialect compilers accept the `/external:I`
125    /// block ([`crate::msvc_external_includes_supported`]).  When
126    /// `false` on an MSVC build, the planner collapses the system
127    /// include bucket into the plain `/I` list instead of emitting a
128    /// switch an old `cl` would reject.  Ignored by the GCC/Clang
129    /// dialect, where `-isystem` is part of the base command line.
130    pub msvc_external_includes: bool,
131    /// Per-package enabled feature names from the cross-package
132    /// feature resolver, keyed by `graph.packages` index.  Gates
133    /// targets that declare `required-features`: default
134    /// enumeration skips a target whose required features are not
135    /// all enabled, while naming one explicitly (a selector or a
136    /// `deps` entry) is a hard error.  `None` (and missing
137    /// entries) mean "no features enabled" for gating purposes.
138    pub enabled_features: Option<&'a HashMap<usize, BTreeSet<String>>>,
139    /// Whether the post-resolution standard-compatibility check runs
140    /// over the resolved target graph ([`crate::standard_compat`]).
141    /// The build / check / run / test commands set it; `cabin tidy`
142    /// leaves it `false`, and then the planner records no violations
143    /// and its output is unchanged.
144    pub standard_compat: bool,
145}
146
147/// One manifest-declared source resolved to its absolute path and the
148/// per-target object path it compiles to.
149struct PreparedSource {
150    abs_source: Utf8PathBuf,
151    object: Utf8PathBuf,
152    language: SourceLanguage,
153}
154
155/// Plan a build for the requested package graph.
156///
157/// # Errors
158/// Returns a [`BuildError`] when the request cannot be turned into
159/// a valid graph: [`BuildError::NonUtf8Path`] when the build directory
160/// or a package's manifest directory is not valid UTF-8 and so cannot
161/// anchor the UTF-8 build model; [`BuildError::EmptySelectedPackages`]
162/// when the default selection yields no C/C++ targets; selection
163/// and dependency-resolution errors ([`BuildError::UnknownTargetReference`],
164/// [`BuildError::AmbiguousTarget`], [`BuildError::UnknownPackageInTargetSelector`],
165/// [`BuildError::UnknownTargetInPackage`],
166/// [`BuildError::NoSameNameTargetInDependency`],
167/// [`BuildError::DevDependencyNotActive`]); required-feature gating
168/// errors ([`BuildError::TargetRequiresFeatures`],
169/// [`BuildError::TargetDepRequiresFeatures`],
170/// [`BuildError::AllDefaultTargetsRequireFeatures`]);
171/// [`BuildError::DependencyCycle`]
172/// when the target dependency graph contains a cycle; and per-target
173/// source errors ([`BuildError::UnrecognizedSourceExtension`],
174/// [`BuildError::InvalidSourcePath`], [`BuildError::EmptyTargetSources`],
175/// [`BuildError::MissingCCompiler`]).
176pub fn plan(req: &PlanRequest<'_>) -> Result<BuildGraph, BuildError> {
177    let empty_features = BTreeSet::new();
178    let enabled_of = |pkg_idx: usize| -> &BTreeSet<String> {
179        req.enabled_features
180            .and_then(|m| m.get(&pkg_idx))
181            .unwrap_or(&empty_features)
182    };
183
184    let selected = if let Some(sel) = &req.selected {
185        let selected = resolve_selection(sel, req.graph, req.selected_packages)?;
186        // Explicitly named targets hard-error when their
187        // `required-features` are not enabled - a silent skip here
188        // would turn a typo'd feature selection into "built
189        // nothing".
190        for tid in &selected {
191            let target = lookup_target(tid, req.graph)?;
192            let missing = target.missing_required_features(enabled_of(tid.0));
193            if !missing.is_empty() {
194                return Err(BuildError::TargetRequiresFeatures {
195                    target: format_target_id(tid, req.graph),
196                    package: req.graph.packages[tid.0].package.name.as_str().to_owned(),
197                    missing,
198                });
199            }
200        }
201        selected
202    } else {
203        let (chosen, gated) =
204            default_selection(req.graph, req.selected_packages, req.enabled_features);
205        if chosen.is_empty() {
206            if gated.is_empty() {
207                return Err(BuildError::EmptySelectedPackages);
208            }
209            return Err(BuildError::AllDefaultTargetsRequireFeatures {
210                gated: gated
211                    .into_iter()
212                    .map(|(tid, missing)| (format_target_id(&tid, req.graph), missing))
213                    .collect(),
214            });
215        }
216        chosen
217    };
218
219    // Walk the target dep graph, resolving each raw `deps` entry to a
220    // concrete (package, target) ID and recording the edges together
221    // with their declared visibility.
222    let mut resolved_deps: HashMap<TargetId, Vec<TargetDepEdge>> = HashMap::new();
223    let mut reachable: HashSet<TargetId> = HashSet::new();
224    let mut to_visit: Vec<TargetId> = selected.clone();
225
226    while let Some(tid) = to_visit.pop() {
227        if !reachable.insert(tid.clone()) {
228            continue;
229        }
230        let target = lookup_target(&tid, req.graph)?;
231        let mut resolved = Vec::with_capacity(target.deps.len());
232        // Dev-only target kinds (`test` / `example`) may reference
233        // the owning package's activated `[dev-dependencies]`
234        // edges; every other kind resolves through Normal edges
235        // only.
236        let dev_deps_visible = target.kind.is_dev_only();
237        for decl in &target.deps {
238            let edge = resolve_target_dep_edge(decl, tid.0, dev_deps_visible, req.graph)?;
239            let dep = edge.to.clone();
240            // A dep edge is an explicit request: a feature-gated
241            // dep target whose required features are not enabled
242            // on its package is a hard error, not a skip.
243            let dep_target = lookup_target(&dep, req.graph)?;
244            let missing = dep_target.missing_required_features(enabled_of(dep.0));
245            if !missing.is_empty() {
246                // CLI feature selection applies to the selected
247                // roots only, so the actionable fix depends on how
248                // the gated package entered the graph.  For a
249                // cross-package reference, mirror
250                // `resolve_target_dep`'s edge preference: a Normal
251                // edge wins, so the help only points at
252                // `[dev-dependencies]` when the dep is reachable
253                // through an activated dev edge alone.
254                let gated_pkg_is_root = match req.selected_packages {
255                    Some(s) => s.contains(&dep.0),
256                    None => req.graph.primary_packages.contains(&dep.0),
257                };
258                let fix = if gated_pkg_is_root {
259                    crate::error::FeatureGateFix::RootSelection
260                } else if dep.0 == tid.0 {
261                    crate::error::FeatureGateFix::UpstreamEdge
262                } else if req.graph.packages[tid.0]
263                    .deps_of_kind(cabin_core::DependencyKind::Normal)
264                    .any(|di| di == dep.0)
265                {
266                    crate::error::FeatureGateFix::DependencyEdge
267                } else {
268                    crate::error::FeatureGateFix::DevDependencyEdge
269                };
270                return Err(BuildError::TargetDepRequiresFeatures {
271                    consumer: format_target_id(&tid, req.graph),
272                    dep_target: format_target_id(&dep, req.graph),
273                    dep_package: req.graph.packages[dep.0].package.name.as_str().to_owned(),
274                    missing,
275                    fix,
276                });
277            }
278            to_visit.push(dep);
279            resolved.push(edge);
280        }
281        resolved_deps.insert(tid, resolved);
282    }
283
284    let topo = topo_sort_targets(&reachable, &resolved_deps, req.graph)?;
285
286    // Post-resolution standard-compatibility check (spec D13 over
287    // every resolved edge).  When the caller opts out (`cabin
288    // tidy`), the planner's output is byte-for-byte what it was
289    // before the pass existed.
290    let standard_compat_violations = if req.standard_compat {
291        crate::standard_compat::edge_violations(&topo, &resolved_deps, req)?
292    } else {
293        Vec::new()
294    };
295
296    // Promote the OS-supplied build directory to UTF-8 once: it
297    // prefixes every object, archive, and executable path in the
298    // semantic IR, so it must be valid UTF-8 to be embedded in build
299    // commands.  A non-UTF-8 build directory is rejected here rather
300    // than silently lossily converted downstream.
301    let build_dir = promote_dir(&req.build_dir)?;
302
303    let mut actions: Vec<BuildAction> = Vec::new();
304    let mut compile_commands: Vec<CompileCommand> = Vec::new();
305    let mut standard_violations: Vec<StandardViolation> = Vec::new();
306    let mut output_for_target: HashMap<TargetId, Utf8PathBuf> = HashMap::new();
307    // Per-target source-language manifest, including transitive
308    // contributions through `target.deps`.  Used to pick the
309    // link-driver language deterministically: a target with any
310    // direct or transitive C++ object link-drives through the C++
311    // compiler, every other target link-drives through the C
312    // compiler.  Populated in topo order so dependents inherit
313    // their dependencies' contributions.
314    let mut target_languages: HashMap<TargetId, BTreeSet<SourceLanguage>> = HashMap::new();
315    // Transitively reachable dependency targets per target, in
316    // first-occurrence order, populated in topo order (direct deps
317    // plus their reachable sets).  Drives the interface-standard
318    // compatibility check.
319    let mut transitive_deps: HashMap<TargetId, Vec<TargetId>> = HashMap::new();
320
321    for tid in &topo {
322        let target = lookup_target(tid, req.graph)?;
323        // Vec keeps first-occurrence order; the HashSet is only a
324        // seen-filter so membership checks stay O(1).
325        let mut dep_closure: Vec<TargetId> = Vec::new();
326        let mut seen: HashSet<TargetId> = HashSet::new();
327        if let Some(deps) = resolved_deps.get(tid) {
328            for edge in deps {
329                let dep = &edge.to;
330                if seen.insert(dep.clone()) {
331                    dep_closure.push(dep.clone());
332                }
333                if let Some(transitive) = transitive_deps.get(dep) {
334                    for transitive_dep in transitive {
335                        if seen.insert(transitive_dep.clone()) {
336                            dep_closure.push(transitive_dep.clone());
337                        }
338                    }
339                }
340            }
341        }
342        transitive_deps.insert(tid.clone(), dep_closure.clone());
343
344        let pkg = &req.graph.packages[tid.0];
345        let pkg_name = pkg.package.name.as_str();
346        // Per-profile output root keeps `dev` and `release`
347        // builds from overwriting each other and gives custom
348        // profiles a deterministic, non-colliding output tree.
349        let pkg_build_dir = build_dir
350            .join(req.profile.name.as_str())
351            .join("packages")
352            .join(pkg_name);
353        // The manifest directory is an OS-canonicalized path; promote
354        // it to UTF-8 (rejecting non-UTF-8) so the source and include
355        // paths it anchors enter the IR as `Utf8PathBuf`.
356        let manifest_dir = promote_dir(&pkg.manifest_dir)?;
357
358        // Header-only libraries declare include dirs but no
359        // translation units.  Skip every action - `collect_link_libs`
360        // and `collect_include_dirs` already walk dep targets by
361        // their declared `include_dirs`, so consumers still pick up
362        // the headers; no `.a` is available to link against.
363        if target.kind.is_header_only() {
364            target_languages.insert(tid.clone(), Default::default());
365            continue;
366        }
367
368        // Build the per-source list.  Each manifest-declared source
369        // resolves to an absolute path under the manifest directory
370        // and a per-target object path.
371        let mut prepared: Vec<PreparedSource> = Vec::with_capacity(target.sources.len());
372        for source in &target.sources {
373            let language =
374                classify_source(source).ok_or_else(|| BuildError::UnrecognizedSourceExtension {
375                    target: format_target_id(tid, req.graph),
376                    path: source.clone(),
377                })?;
378            let object = object_path(&pkg_build_dir, target.name.as_str(), source, req.dialect)
379                .map_err(|reason| BuildError::InvalidSourcePath {
380                    target: format_target_id(tid, req.graph),
381                    path: source.clone(),
382                    reason,
383                })?;
384            prepared.push(PreparedSource {
385                abs_source: manifest_dir.join(source),
386                object,
387                language,
388            });
389        }
390        if prepared.is_empty() {
391            return Err(BuildError::EmptyTargetSources(format_target_id(
392                tid, req.graph,
393            )));
394        }
395
396        let pkg_standards = req
397            .language_standards
398            .get(&tid.0)
399            .copied()
400            .unwrap_or_default();
401        enforce_interface_standards(
402            tid,
403            target,
404            &prepared,
405            &dep_closure,
406            pkg_standards,
407            req,
408            &mut standard_violations,
409        )?;
410
411        // Per-package resolved build flags from the manifest's
412        // `[profile]`, `[target.'cfg(...)'.profile]`, and the active
413        // `[profile.<name>]` table.  Layered on top of per-target
414        // defines / include dirs.
415        let pkg_flags = req.build_flags.get(&tid.0);
416
417        // Compose include_dirs and defines: existing target +
418        // per-package build flags, partitioned into the user (`-I`)
419        // and system (`-isystem` / `/external:I`) buckets.
420        let collected = collect_include_dirs(tid, target, &resolved_deps, req.graph)?;
421        let mut include_dirs = collected.user;
422        let mut system_include_dirs = collected.system;
423        if let Some(flags) = pkg_flags {
424            for inc in &flags.include_dirs {
425                let absolute = if inc.is_absolute() {
426                    inc.clone()
427                } else {
428                    manifest_dir.join(inc)
429                };
430                if !include_dirs.contains(&absolute) && !system_include_dirs.contains(&absolute) {
431                    include_dirs.push(absolute);
432                }
433            }
434            for inc in &flags.system_include_dirs {
435                let absolute = if inc.is_absolute() {
436                    inc.clone()
437                } else {
438                    manifest_dir.join(inc)
439                };
440                if !include_dirs.contains(&absolute) && !system_include_dirs.contains(&absolute) {
441                    system_include_dirs.push(absolute);
442                }
443            }
444        }
445        // An MSVC toolchain that predates `/external:I` cannot spell
446        // the system bucket; fall back to plain `/I` for those dirs -
447        // exactly the pre-system-include command shape.
448        if req.dialect == Dialect::Msvc && !req.msvc_external_includes {
449            for dir in system_include_dirs.drain(..) {
450                if !include_dirs.contains(&dir) {
451                    include_dirs.push(dir);
452                }
453            }
454        }
455        let mut defines: Vec<String> = target.defines.clone();
456        if let Some(flags) = pkg_flags {
457            for def in &flags.defines {
458                if !defines.contains(def) {
459                    defines.push(def.clone());
460                }
461            }
462        }
463        let extra_compile_args: &[String] =
464            pkg_flags.map_or(&[], |f| f.extra_compile_args.as_slice());
465        let cflags: &[String] = pkg_flags.map_or(&[], |f| f.cflags.as_slice());
466        let cxxflags: &[String] = pkg_flags.map_or(&[], |f| f.cxxflags.as_slice());
467        let ldflags: &[String] = pkg_flags.map_or(&[], |f| f.ldflags.as_slice());
468
469        // Strictly per-target: two targets in one build may differ in
470        // both level and `gnu-extensions`.
471        let gnu_extensions = cabin_core::effective_gnu_extensions(
472            &req.graph.packages[tid.0].package.language,
473            target,
474        );
475        let mut objects: Vec<Utf8PathBuf> = Vec::with_capacity(prepared.len());
476        for ps in &prepared {
477            let depfile = depfile_path(&ps.object);
478            // Pick the language-appropriate compiler driver, the
479            // language-appropriate standard / profile flags, the
480            // matching escape-hatch arg list, and the human-readable
481            // tag.  Naming the components here is the single point that
482            // enforces "C/C++ compile lines never share argv space".
483            let dispatch = compile_dispatch(ps.language, req)
484                .map_err(|err| err.attach_target_path(tid, req.graph, &ps.abs_source))?;
485            // Escape-hatch flags: the language-neutral list first, then
486            // the language-specific one - so a per-language override
487            // always appears later in argv, where the compiler treats
488            // it as the final word.
489            let extra_language_compile_args = match ps.language {
490                SourceLanguage::C => cflags,
491                SourceLanguage::Cxx => cxxflags,
492            };
493            let mut extra_flags =
494                Vec::with_capacity(extra_compile_args.len() + extra_language_compile_args.len());
495            extra_flags.extend(extra_compile_args.iter().cloned());
496            extra_flags.extend(extra_language_compile_args.iter().cloned());
497            // Ninja runs the wrapped command for C and C++ compiles.
498            // The wrapper lives on the semantic action and is applied
499            // only when lowering the *run* command;
500            // `compile_commands.json` below is derived from the
501            // unwrapped lowering so clangd / IDE tooling still sees
502            // the underlying compiler. Link and archive commands are
503            // deliberately never wrapped.
504            let compiler_wrapper = req.compiler_wrapper.map(|wrapper| wrapper.path.clone());
505            // Manifest loading rejects targets that compile a
506            // language without an effective standard, so a `None`
507            // here is a package that bypassed the parser - reject it
508            // with the same actionable message.
509            let missing_standard = |field: &'static str| BuildError::MissingLanguageStandard {
510                target: format_target_id(tid, req.graph),
511                language: ps.language.human_label(),
512                field,
513            };
514            let standard = match ps.language {
515                SourceLanguage::C => LanguageStandard::C(
516                    cabin_core::effective_c(&pkg_standards, target)
517                        .ok_or_else(|| missing_standard("c-standard"))?
518                        .standard,
519                ),
520                SourceLanguage::Cxx => LanguageStandard::Cxx(
521                    cabin_core::effective_cxx(&pkg_standards, target)
522                        .ok_or_else(|| missing_standard("cxx-standard"))?
523                        .standard,
524                ),
525            };
526            // An MSVC-dialect compile whose standard has no stable
527            // `/std:` flag cannot be lowered.  Record the violation
528            // instead of failing eagerly: the `cabin check` rewrite
529            // prunes dependency compiles after planning, and a
530            // pruned compile must not gate the command.  The CLI
531            // surfaces surviving violations through
532            // `validate_planned_standards` before anything is
533            // lowered or written.
534            let msvc_spelling_missing =
535                req.dialect == Dialect::Msvc && standard.msvc_spelling().is_none();
536            if msvc_spelling_missing {
537                standard_violations.push(StandardViolation::MsvcSpelling {
538                    target: format_target_id(tid, req.graph),
539                    language: ps.language.human_label(),
540                    standard: standard.as_str(),
541                    object: ps.object.clone(),
542                });
543            }
544            // `cl.exe` has no GNU dialect mode, so `gnu-extensions`
545            // cannot be honored - and must never be silently
546            // ignored.  Recorded (not failed eagerly) for the same
547            // check-rewrite reason as the missing-spelling case.
548            let msvc_gnu_extensions = req.dialect == Dialect::Msvc && gnu_extensions;
549            if msvc_gnu_extensions {
550                standard_violations.push(StandardViolation::MsvcGnuExtensions {
551                    target: format_target_id(tid, req.graph),
552                    object: ps.object.clone(),
553                });
554            }
555            // Escape-hatch conflicts: a candidate covers this
556            // compile when its language matches and its scope is the
557            // whole package or this specific target.
558            if let Some(candidates) = req.standard_flag_conflicts.get(&tid.0) {
559                for candidate in candidates {
560                    let covers = candidate.language == ps.language
561                        && candidate
562                            .target
563                            .as_deref()
564                            .is_none_or(|t| t == tid.1.as_str());
565                    if covers {
566                        standard_violations.push(StandardViolation::FlagConflict {
567                            conflict: candidate.clone(),
568                            object: ps.object.clone(),
569                        });
570                    }
571                }
572            }
573            let compile = CompileAction {
574                standard,
575                gnu_extensions,
576                source: ps.abs_source.clone(),
577                object: ps.object.clone(),
578                mode: CompileMode::Object,
579                implicit_inputs: Vec::new(),
580                depfile: Some(depfile),
581                compiler: dispatch.driver.to_path_buf(),
582                compiler_wrapper,
583                arguments: CompileArguments {
584                    opt_level: req.profile.opt_level,
585                    debug_info: req.profile.debug,
586                    define_ndebug: !req.profile.assertions,
587                    include_dirs: include_dirs.clone(),
588                    system_include_dirs: system_include_dirs.clone(),
589                    defines: defines.clone(),
590                    extra_flags,
591                },
592                description: format!("{} {}", dispatch.description_tag, ps.object),
593            };
594            // `compile_commands.json` records the unwrapped, object-mode
595            // argv.  Deriving it from the same lowering the Ninja writer
596            // uses (minus the wrapper) keeps the two in lockstep.  A
597            // violating compile has no lowerable argv, so its entry is
598            // omitted - the violation above makes that loud, never
599            // silent.
600            if !msvc_spelling_missing && !msvc_gnu_extensions {
601                compile_commands.push(CompileCommand {
602                    directory: build_dir.to_path_buf(),
603                    file: ps.abs_source.clone(),
604                    arguments: compile_argv(req.dialect, &compile),
605                    output: ps.object.clone(),
606                });
607            }
608            objects.push(ps.object.clone());
609            actions.push(BuildAction::Compile(compile));
610        }
611
612        // Per-target language manifest: own sources' languages
613        // unioned with every direct target dep's manifest.  The
614        // topo iteration guarantees dependencies are populated
615        // before we visit the dependent.
616        let mut languages_here: BTreeSet<SourceLanguage> =
617            prepared.iter().map(|p| p.language).collect();
618        if let Some(deps) = resolved_deps.get(tid) {
619            for edge in deps {
620                if let Some(dep_langs) = target_languages.get(&edge.to) {
621                    languages_here.extend(dep_langs.iter().copied());
622                }
623            }
624        }
625
626        match target.kind {
627            TargetKind::Library => {
628                let lib_path =
629                    pkg_build_dir.join(req.dialect.static_library_name(target.name.as_str()));
630                actions.push(BuildAction::Archive(ArchiveAction {
631                    archiver: req.toolchain.ar.path().to_path_buf(),
632                    output: lib_path.clone(),
633                    inputs: objects,
634                    description: format!("AR {lib_path}"),
635                }));
636                output_for_target.insert(tid.clone(), lib_path);
637            }
638            // Every executable kind takes the same link path:
639            // `executable` (production binaries), `test`
640            // (run by `cabin test`), and `example`.  The build
641            // planner does not distinguish between them here because
642            // the link/compile semantics are identical; the kind
643            // difference is only consulted when deciding *which*
644            // targets to select (default-buildable vs. dev-only) and
645            // which targets `cabin test` runs.  Compiler-driver
646            // selection is per-source via `link_driver_language`, so
647            // an `executable` that declares only `.c` sources
648            // drives the link with the C compiler, while one that
649            // mixes in any `.cc` / `.cpp` source - directly or
650            // transitively - drives the link with the C++ compiler.
651            TargetKind::Executable | TargetKind::Test | TargetKind::Example => {
652                let exe_path =
653                    pkg_build_dir.join(req.dialect.executable_name(target.name.as_str()));
654                let lib_paths =
655                    collect_link_libs(tid, &resolved_deps, req.graph, &output_for_target);
656
657                let mut inputs = objects;
658                inputs.extend(lib_paths);
659
660                // System libraries required by this executable's
661                // dependency closure (e.g. a static library port's
662                // `link-libs`).  Carried as bare names on the LinkAction
663                // so the dialect lowering spells them (`-l<name>` for
664                // GNU, `<name>.lib` for MSVC) and places them after the
665                // archives for left-to-right resolution. `arguments`
666                // stays the package's own raw `ldflags`.
667                let link_arguments = ldflags.to_vec();
668                let link_libs = collect_link_lib_names(tid, &resolved_deps, req.build_flags);
669
670                // Link-driver pick: C++ if any of this target's
671                // own objects came from a C++ source, or if any
672                // transitively reachable object did.  Otherwise
673                // the C compiler drives the link, which keeps
674                // pure-C executables off the C++ runtime.
675                let languages_slice: Vec<SourceLanguage> = languages_here.iter().copied().collect();
676                let driver_language = link_driver_language(&languages_slice);
677                let driver_path = match driver_language {
678                    SourceLanguage::Cxx => req.toolchain.cxx.path(),
679                    SourceLanguage::C => {
680                        req.toolchain
681                            .cc
682                            .as_ref()
683                            .map(cabin_core::ResolvedTool::path)
684                            .ok_or_else(|| {
685                                BuildError::MissingCCompiler {
686                                    target: format_target_id(tid, req.graph),
687                                    // Pick a representative source for the
688                                    // diagnostic; pure-C link errors
689                                    // always have at least one C source on
690                                    // this target.
691                                    path: prepared
692                                        .iter()
693                                        .find(|p| p.language == SourceLanguage::C)
694                                        .map_or_else(|| exe_path.clone(), |p| p.abs_source.clone()),
695                                }
696                            })?
697                    }
698                };
699                actions.push(BuildAction::Link(LinkAction {
700                    linker: driver_path.to_path_buf(),
701                    output: exe_path.clone(),
702                    inputs,
703                    implicit_inputs: Vec::new(),
704                    arguments: link_arguments,
705                    link_libs,
706                    description: format!("LINK {exe_path}"),
707                }));
708                output_for_target.insert(tid.clone(), exe_path);
709            }
710            TargetKind::HeaderOnly => {
711                unreachable!("header-only targets are skipped before action generation")
712            }
713        }
714        target_languages.insert(tid.clone(), languages_here);
715    }
716
717    let default_outputs: Vec<Utf8PathBuf> = selected
718        .iter()
719        .filter_map(|tid| output_for_target.get(tid).cloned())
720        .collect();
721
722    Ok(BuildGraph {
723        actions,
724        dialect: req.dialect,
725        default_outputs,
726        compile_commands,
727        standard_violations,
728        standard_compat_violations,
729    })
730}
731
732// ---------------------------------------------------------------------------
733// internal: target IDs and lookups
734// ---------------------------------------------------------------------------
735
736/// Stable identifier for a target within a [`PackageGraph`]: the index of
737/// its package in `graph.packages` and its target name.
738pub(crate) type TargetId = (usize, String);
739
740/// One resolved edge of the target dependency graph.  Alias
741/// resolution has already happened - `to` is a concrete
742/// (package, target), never a pre-alias bare name - and the edge
743/// carries the visibility declared on the manifest entry.  The
744/// `public` classification feeds the standard-compatibility
745/// pass, which propagates interface requirements along public edges
746/// (`docs/design/standard-compatibility/spec.md`, D5).
747#[derive(Debug, Clone, PartialEq, Eq)]
748pub(crate) struct TargetDepEdge {
749    pub(crate) to: TargetId,
750    pub(crate) public: bool,
751}
752
753/// Find a target by name within a package's manifest.  Target
754/// names are unique within a package, so this returns the sole
755/// match (or `None`).
756pub(crate) fn find_target<'a>(pkg: &'a Package, name: &str) -> Option<&'a Target> {
757    pkg.targets.iter().find(|t| t.name.as_str() == name)
758}
759
760pub(crate) fn lookup_target<'a>(
761    tid: &TargetId,
762    graph: &'a PackageGraph,
763) -> Result<&'a Target, BuildError> {
764    let pkg = &graph.packages[tid.0];
765    find_target(&pkg.package, &tid.1).ok_or_else(|| BuildError::UnknownTargetInPackage {
766        package: pkg.package.name.as_str().to_owned(),
767        target: tid.1.clone(),
768    })
769}
770
771pub(crate) fn format_target_id(tid: &TargetId, graph: &PackageGraph) -> String {
772    format!("{}:{}", graph.packages[tid.0].package.name.as_str(), tid.1)
773}
774
775// ---------------------------------------------------------------------------
776// internal: interface-standard compatibility
777// ---------------------------------------------------------------------------
778
779/// Pre-build interface-standard compatibility: a consuming target's
780/// effective implementation standard must be at least every reachable
781/// library-like dependency's interface requirement, per language the
782/// consumer compiles.  The chronological `>=` comparison is a
783/// compatibility policy, not a proof of header validity - see
784/// `docs/language-standards.md`.
785///
786/// Incompatibilities are *recorded* on the consumer's first compile
787/// of the offending language rather than failing the plan: the
788/// `cabin check` rewrite prunes dependency compiles after planning,
789/// and an incompatibility between two dependencies whose compiles
790/// are all pruned must not gate the command.
791/// `validate_planned_standards` surfaces the survivors.
792fn enforce_interface_standards(
793    tid: &TargetId,
794    target: &Target,
795    prepared: &[PreparedSource],
796    dep_closure: &[TargetId],
797    pkg_standards: ResolvedLanguageStandards,
798    req: &PlanRequest<'_>,
799    violations: &mut Vec<StandardViolation>,
800) -> Result<(), BuildError> {
801    let object_of = |language: SourceLanguage| {
802        prepared
803            .iter()
804            .find(|p| p.language == language)
805            .map(|p| p.object.clone())
806    };
807    for dep_tid in dep_closure {
808        let dep_target = lookup_target(dep_tid, req.graph)?;
809        if !dep_target.kind.is_library_like() {
810            continue;
811        }
812        let dep_pkg = &req.graph.packages[dep_tid.0].package;
813        let dep_standards = req
814            .language_standards
815            .get(&dep_tid.0)
816            .copied()
817            .unwrap_or_default();
818        // A `None` requirement or consumer standard cannot happen
819        // for manifest-derived packages: `imposes_requirement` is
820        // true only when some declaration (or a source file, whose
821        // standard manifest loading guarantees) backs the language,
822        // and the consumer compiles the language, so its own
823        // standard was already demanded at its compile site.
824        // An explicit `none` requirement carries no minimum to
825        // compare against; rejecting consumers of not-consumable
826        // headers is deferred alongside the rest of the range work.
827        if let Some(object) = object_of(SourceLanguage::C)
828            && cabin_core::imposes_requirement(dep_target, &dep_pkg.language, SourceLanguage::C)
829            && let Some(required) =
830                cabin_core::interface_c(&dep_standards, &dep_pkg.language, dep_target)
831            && let Some(required_min) = required.requirement.min()
832            && let Some(consumer) = cabin_core::effective_c(&pkg_standards, target)
833            && consumer.standard < required_min
834        {
835            violations.push(interface_violation(
836                format_target_id(tid, req.graph),
837                format_target_id(dep_tid, req.graph),
838                SourceLanguage::C,
839                consumer.standard.as_str(),
840                required_min.as_str(),
841                required.source,
842                object,
843            ));
844        }
845        if let Some(object) = object_of(SourceLanguage::Cxx)
846            && cabin_core::imposes_requirement(dep_target, &dep_pkg.language, SourceLanguage::Cxx)
847            && let Some(required) =
848                cabin_core::interface_cxx(&dep_standards, &dep_pkg.language, dep_target)
849            && let Some(required_min) = required.requirement.min()
850            && let Some(consumer) = cabin_core::effective_cxx(&pkg_standards, target)
851            && consumer.standard < required_min
852        {
853            violations.push(interface_violation(
854                format_target_id(tid, req.graph),
855                format_target_id(dep_tid, req.graph),
856                SourceLanguage::Cxx,
857                consumer.standard.as_str(),
858                required_min.as_str(),
859                required.source,
860                object,
861            ));
862        }
863    }
864    Ok(())
865}
866
867fn interface_violation(
868    consumer: String,
869    dependency: String,
870    language: SourceLanguage,
871    consumer_standard: &'static str,
872    required: &'static str,
873    source: InterfaceStandardSource,
874    object: Utf8PathBuf,
875) -> StandardViolation {
876    let requirement_source = match source {
877        InterfaceStandardSource::Target => "its target-level interface standard",
878        InterfaceStandardSource::Package => "its package-level interface standard",
879        InterfaceStandardSource::Workspace => "its workspace-inherited interface standard",
880        InterfaceStandardSource::CompileStandard => {
881            "its effective implementation standard (no interface standard declared)"
882        }
883    };
884    StandardViolation::InterfaceIncompatibility {
885        consumer,
886        dependency,
887        language: language.human_label(),
888        consumer_standard,
889        required,
890        requirement_source,
891        object,
892    }
893}
894
895// ---------------------------------------------------------------------------
896// internal: manifest-target selector resolution
897// ---------------------------------------------------------------------------
898
899fn resolve_selection(
900    selectors: &[ManifestTargetSelector],
901    graph: &PackageGraph,
902    selected_packages: Option<&[usize]>,
903) -> Result<Vec<TargetId>, BuildError> {
904    let mut out: Vec<TargetId> = Vec::with_capacity(selectors.len());
905    for sel in selectors {
906        out.push(resolve_top_level_selector(sel, graph, selected_packages)?);
907    }
908    Ok(out)
909}
910
911fn resolve_top_level_selector(
912    sel: &ManifestTargetSelector,
913    graph: &PackageGraph,
914    selected_packages: Option<&[usize]>,
915) -> Result<TargetId, BuildError> {
916    if let Some(pkg_name) = &sel.package {
917        let pkg_idx =
918            graph
919                .index_of(pkg_name)
920                .ok_or_else(|| BuildError::UnknownPackageInTargetSelector {
921                    package: pkg_name.clone(),
922                    selector: format!("{}:{}", pkg_name, sel.name),
923                })?;
924        let pkg = &graph.packages[pkg_idx];
925        if find_target(&pkg.package, &sel.name).is_none() {
926            return Err(BuildError::UnknownTargetInPackage {
927                package: pkg_name.clone(),
928                target: sel.name.clone(),
929            });
930        }
931        return Ok((pkg_idx, sel.name.clone()));
932    }
933
934    // unqualified selectors search the selected
935    // package set (or the primary set when no selection is
936    // active).  We no longer fall back to the root package when it
937    // is outside the selected set - that would silently build
938    // something the user did not ask for.
939    let candidates: Vec<usize> = if let Some(s) = selected_packages {
940        s.to_vec()
941    } else {
942        // Unqualified selector with no workspace selection
943        // active: walk the root first, then every primary.
944        let mut root_match: Option<TargetId> = None;
945        if let Some(root_idx) = graph.root_package {
946            let root = &graph.packages[root_idx];
947            if find_target(&root.package, &sel.name).is_some() {
948                root_match = Some((root_idx, sel.name.clone()));
949            }
950        }
951        if let Some(tid) = root_match {
952            return Ok(tid);
953        }
954        graph.primary_packages.clone()
955    };
956
957    let mut matches: Vec<TargetId> = Vec::new();
958    for idx in candidates {
959        let pkg = &graph.packages[idx];
960        if find_target(&pkg.package, &sel.name).is_some() {
961            matches.push((idx, sel.name.clone()));
962        }
963    }
964    match matches.len() {
965        0 => Err(BuildError::UnknownTargetReference(sel.name.clone())),
966        1 => Ok(matches.into_iter().next().unwrap()),
967        _ => Err(BuildError::AmbiguousTarget(
968            sel.name.clone(),
969            matches
970                .iter()
971                .map(|(i, t)| format!("{}:{}", graph.packages[*i].package.name.as_str(), t))
972                .collect(),
973        )),
974    }
975}
976
977/// Default-buildable targets of the selected packages, split into
978/// the buildable set and the targets skipped because their
979/// `required-features` are not enabled (with the missing names, for
980/// the all-gated diagnostic).
981fn default_selection(
982    graph: &PackageGraph,
983    selected_packages: Option<&[usize]>,
984    enabled_features: Option<&HashMap<usize, BTreeSet<String>>>,
985) -> (Vec<TargetId>, Vec<(TargetId, Vec<String>)>) {
986    let empty = BTreeSet::new();
987    let mut out = Vec::new();
988    let mut gated = Vec::new();
989    let pkg_indices: &[usize] = match selected_packages {
990        Some(s) => s,
991        None => graph.primary_packages.as_slice(),
992    };
993    for &pkg_idx in pkg_indices {
994        let pkg = &graph.packages[pkg_idx];
995        let enabled = enabled_features
996            .and_then(|m| m.get(&pkg_idx))
997            .unwrap_or(&empty);
998        for target in &pkg.package.targets {
999            if !target.kind.is_default_buildable() {
1000                continue;
1001            }
1002            let tid = (pkg_idx, target.name.as_str().to_owned());
1003            let missing = target.missing_required_features(enabled);
1004            if missing.is_empty() {
1005                out.push(tid);
1006            } else {
1007                gated.push((tid, missing));
1008            }
1009        }
1010    }
1011    (out, gated)
1012}
1013
1014/// Whether the selector's target has all of its
1015/// `required-features` enabled under `enabled_features`.  Used by
1016/// enumeration-style callers (`cabin test` without `--test`,
1017/// `cabin tidy`) to *skip* feature-gated targets; explicitly named
1018/// targets go through [`plan`], which hard-errors instead.
1019/// Selectors that do not resolve to a target return `true` so
1020/// resolution diagnostics stay owned by [`plan`].
1021// The map is always the std-hasher one the CLI builds from the
1022// feature resolver; a `BuildHasher` parameter would be dead
1023// flexibility.
1024#[allow(clippy::implicit_hasher)]
1025pub fn selector_required_features_met(
1026    sel: &ManifestTargetSelector,
1027    graph: &PackageGraph,
1028    enabled_features: &HashMap<usize, BTreeSet<String>>,
1029) -> bool {
1030    let empty = BTreeSet::new();
1031    let Some(pkg_name) = &sel.package else {
1032        return true;
1033    };
1034    let Some(pkg_idx) = graph.index_of(pkg_name) else {
1035        return true;
1036    };
1037    let Some(target) = graph.packages[pkg_idx]
1038        .package
1039        .targets
1040        .iter()
1041        .find(|t| t.name.as_str() == sel.name)
1042    else {
1043        return true;
1044    };
1045    let enabled = enabled_features.get(&pkg_idx).unwrap_or(&empty);
1046    target.missing_required_features(enabled).is_empty()
1047}
1048
1049/// Build-time selector for `cabin test`: expand a package
1050/// selection into the set of targets of a specific
1051/// development-only kind (`test` today).  Returns
1052/// deterministic `(package_index, target_name)` tuples in the same
1053/// order as the planner consumes selectors.  Useful for callers that
1054/// want every dev-only target of a given kind without naming each
1055/// one explicitly.
1056pub fn select_targets_of_kind(
1057    graph: &PackageGraph,
1058    selected_packages: Option<&[usize]>,
1059    kind: TargetKind,
1060) -> Vec<ManifestTargetSelector> {
1061    let pkg_indices: &[usize] = match selected_packages {
1062        Some(s) => s,
1063        None => graph.primary_packages.as_slice(),
1064    };
1065    let mut out = Vec::new();
1066    for &pkg_idx in pkg_indices {
1067        let pkg = &graph.packages[pkg_idx];
1068        for target in &pkg.package.targets {
1069            if target.kind == kind {
1070                out.push(ManifestTargetSelector {
1071                    package: Some(pkg.package.name.as_str().to_owned()),
1072                    name: target.name.as_str().to_owned(),
1073                });
1074            }
1075        }
1076    }
1077    out
1078}