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}