cabin_workspace/graph.rs
1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::path::PathBuf;
3
4use cabin_core::standard_compatibility::{ConsumerStandards, dependency_attributes};
5use cabin_core::{
6 CStandard, CompilerWrapperRequest, Condition, CxxStandard, DependencyKind, Package,
7 PatchManifestSettings, ProfileDefinition, ProfileName, ToolchainSettings,
8 resolve_language_standards,
9};
10
11/// Root-manifest policy settings that apply workspace-wide even
12/// when the entry manifest is a pure `[workspace]` manifest.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct RootSettings {
15 pub profiles: BTreeMap<ProfileName, ProfileDefinition>,
16 pub toolchain: ToolchainSettings,
17 pub compiler_wrapper: Option<CompilerWrapperRequest>,
18 pub patches: PatchManifestSettings,
19}
20
21impl From<cabin_manifest::RootSettings> for RootSettings {
22 fn from(value: cabin_manifest::RootSettings) -> Self {
23 Self {
24 profiles: value.profiles,
25 toolchain: value.toolchain,
26 compiler_wrapper: value.compiler_wrapper,
27 patches: value.patches,
28 }
29 }
30}
31
32/// A loaded set of local Cabin packages with their dependency edges
33/// resolved against the local filesystem.
34///
35/// Packages appear in topological order: a package's local dependencies
36/// always appear before the package itself in [`PackageGraph::packages`].
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct PackageGraph {
39 /// Path to the manifest the user passed (canonicalized to absolute).
40 pub root_manifest_path: PathBuf,
41 /// Directory containing the root manifest.
42 pub root_dir: PathBuf,
43 /// Whether the root manifest declares a `[workspace]` table.
44 pub is_workspace_root: bool,
45 /// If the root manifest itself is a package (i.e. has a `[package]`
46 /// table), the index of that package in [`PackageGraph::packages`].
47 pub root_package: Option<usize>,
48 /// Root-manifest policy settings. For package roots this
49 /// mirrors the root package's root-owned fields; for pure
50 /// workspace roots this is the only place those settings are
51 /// exposed.
52 pub root_settings: RootSettings,
53 /// Indices of packages that count as "primary" - i.e. would be built
54 /// when no narrower package selection is given.
55 ///
56 /// For a single package this is the root. For a workspace root it
57 /// is every member declared by `[workspace.members]`. Path dependencies
58 /// pulled in transitively are *not* primary.
59 pub primary_packages: Vec<usize>,
60 /// Indices of packages listed under
61 /// `[workspace.default-members]`, validated to be members. Empty
62 /// when the workspace declares no defaults - callers fall back to
63 /// the documented "all members" behavior. Always a subset of
64 /// `primary_packages`.
65 pub default_members: Vec<usize>,
66 /// Relative paths under `root_dir` for any directories
67 /// dropped by `[workspace.exclude]`. Carried through purely for
68 /// metadata reporting; the loader has already removed them from
69 /// `primary_packages`.
70 pub excluded_members: Vec<PathBuf>,
71 /// All loaded packages, in topological order.
72 pub packages: Vec<WorkspacePackage>,
73}
74
75impl PackageGraph {
76 /// Find a package by name. Linear scan; package counts are small.
77 pub fn package_by_name(&self, name: &str) -> Option<&WorkspacePackage> {
78 self.packages
79 .iter()
80 .find(|p| p.package.name.as_str() == name)
81 }
82
83 /// Index of a package by name. Returned together with the reference
84 /// for callers that need to record edges by index.
85 pub fn index_of(&self, name: &str) -> Option<usize> {
86 self.packages
87 .iter()
88 .position(|p| p.package.name.as_str() == name)
89 }
90
91 /// The consumer standards for standard-aware version preference
92 /// (`docs/design/standard-compatibility/preference-mode.md`
93 /// section 1): per language, the minimum effective implementation
94 /// standard (spec D6 `impl_L`) across the targets of `members` that
95 /// implement it. `None` for a language none of them compiles - it
96 /// then imposes nothing on candidate ordering.
97 ///
98 /// `members` must be the package set the resolve is actually for -
99 /// the selected closure
100 /// ([`ResolvedSelection::closure`](crate::ResolvedSelection::closure)),
101 /// not the whole graph - so an unselected member never lowers the
102 /// consumer standard for a scoped resolve. Within each member the
103 /// targets this invocation can build count: default-buildable kinds
104 /// always, plus dev-only (`test` / `example`) targets for packages
105 /// named in `dev_for` (the set whose `[dev-dependencies]` this
106 /// invocation activates, e.g. `cabin test`), and in both cases only
107 /// when their `required-features` are satisfied by `enabled_features`
108 /// (keyed by package index). A target gated behind an unenabled
109 /// feature, or a `test` / `example` under a plain `cabin build`, does
110 /// not lower the consumer standard. Dev-only targets are counted
111 /// whenever `dev_for` activates them, without a per-target reachability
112 /// walk - the same conservative over-approximation applied to a path
113 /// dependency's libraries (below): it can only prefer an older, more
114 /// broadly compatible version, never lock one a built target (such as
115 /// an example a selected target references in `deps`) cannot consume.
116 ///
117 /// The set is deliberately every default-buildable (plus `dev_for`)
118 /// target of the selected packages, **not** the single target a
119 /// `--bin` / `--example` / test-name narrows a later build to.
120 /// `cabin.lock` is shared per project, so its versions must suit
121 /// every target `cabin build` compiles; scoping resolution to one
122 /// run/test target would under-constrain the shared lock for its
123 /// siblings. Which target is finally compiled is a build-time
124 /// decision, downstream of resolution.
125 ///
126 /// This is the Cargo-style workspace-level approximation used during
127 /// a partial solve: exactness is not required because the
128 /// post-resolution validation remains the correctness authority.
129 ///
130 /// `primary` is the originally selected package set
131 /// ([`ResolvedSelection::packages`](crate::ResolvedSelection::packages)),
132 /// a subset of `members`: `members` also holds the transitive
133 /// path-dependency packages the closure pulls in. A path dependency
134 /// is built only for the library targets its consumers link, never
135 /// for its own executables/tests, so a non-primary member counts
136 /// only its archive-producing (library) targets. Whether each such
137 /// library is in turn *reachable* (linked by a consumer target) is
138 /// deliberately not computed here: that per-target build-graph walk
139 /// is the planner's post-resolution job, and counting a path
140 /// dependency's archive targets is a conservative over-approximation
141 /// in the safe direction - it can only prefer an older, more broadly
142 /// compatible version, never cause a wrong build.
143 ///
144 /// This extends to a path dependency reached only through a
145 /// feature-disabled optional edge: the loader records optional path
146 /// edges unconditionally (only disabled optional *registry* deps are
147 /// pruned), and this walk does no package-level feature-reachability
148 /// pruning of `members`. That is deliberate and equally safe - each
149 /// added member contributes only to the per-language `min`, which
150 /// extra targets can lower but never raise, so an unbuilt optional
151 /// dependency can at most prefer an older, more broadly compatible
152 /// version. Pruning it would only ever raise the preferred version
153 /// and never changes solvability, so it is left to the planner.
154 #[must_use]
155 pub fn consumer_standards(
156 &self,
157 members: &BTreeSet<usize>,
158 primary: &[usize],
159 enabled_features: &HashMap<usize, BTreeSet<String>>,
160 dev_for: &BTreeSet<String>,
161 ) -> ConsumerStandards {
162 let empty = BTreeSet::new();
163 let mut c: Option<CStandard> = None;
164 let mut cxx: Option<CxxStandard> = None;
165 for &index in members {
166 let member = &self.packages[index];
167 let is_primary = primary.contains(&index);
168 let enabled = enabled_features.get(&index).unwrap_or(&empty);
169 let dev_active = dev_for.contains(member.package.name.as_str());
170 let resolved = resolve_language_standards(&member.package.language);
171 for target in &member.package.targets {
172 // A header-only target has no translation units, so as a
173 // consumer it compiles nothing (spec D7 `langs = empty`)
174 // and imposes no consumer level - even though
175 // `dependency_attributes` reports its header-only
176 // inference on the dependency side.
177 if target.kind.is_header_only() {
178 continue;
179 }
180 // Only targets this invocation can compile count. A
181 // primary package builds its default-buildable targets
182 // and, under `dev_for` (`cabin test`), its dev-only
183 // (`test` / `example`) targets; a path-dep member builds
184 // only the library targets it is linked for. Dev-only
185 // targets are counted whenever `dev_for` activates them,
186 // without walking which are actually reachable from a
187 // selected target - the same safe over-approximation
188 // applied to a path dependency's libraries below. A
189 // selected target may reference an `example` in its
190 // `deps` (the planner then compiles it), so excluding
191 // examples could raise the consumer above a built
192 // example's standard and lock a version it cannot
193 // consume; counting one that a run does not reach only
194 // lowers the consumer (an older, more broadly compatible
195 // pick), never raises it.
196 let built = if is_primary {
197 target.kind.is_default_buildable() || (dev_active && target.kind.is_dev_only())
198 } else {
199 target.kind.produces_archive()
200 };
201 if !built || !target.missing_required_features(enabled).is_empty() {
202 continue;
203 }
204 let attributes = dependency_attributes(target, &resolved, &member.package.language);
205 if let Some(level) = attributes.impl_c {
206 c = Some(c.map_or(level, |current| current.min(level)));
207 }
208 if let Some(level) = attributes.impl_cxx {
209 cxx = Some(cxx.map_or(level, |current| current.min(level)));
210 }
211 }
212 }
213 ConsumerStandards { c, cxx }
214 }
215}
216
217/// A single loaded package.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct WorkspacePackage {
220 pub package: Package,
221 /// Absolute path to this package's `cabin.toml`.
222 pub manifest_path: PathBuf,
223 /// Absolute path to the directory containing `manifest_path`.
224 pub manifest_dir: PathBuf,
225 /// Resolved package-dependency edges, in declaration order.
226 /// Each edge carries the index of the depended-on package
227 /// inside [`PackageGraph::packages`] together with the
228 /// [`DependencyKind`] under which it was declared.
229 ///
230 /// `Normal`-kind edges always appear here. `Dev`-kind edges
231 /// appear only when the loader was asked to activate this
232 /// package's `[dev-dependencies]` via
233 /// `WorkspaceLoadOptions::include_dev_for` - `cabin test` does
234 /// that for the selected packages; ordinary commands keep dev
235 /// deps declaration-only. The kind is preserved per-edge so
236 /// consumers can filter appropriately.
237 pub deps: Vec<DependencyEdge>,
238 /// Whether this package was loaded from a local source tree
239 /// or from an extracted registry archive.
240 pub kind: PackageKind,
241 /// Whether this package is a prepared foundation port (its
242 /// source tree was materialized from a `port.toml` recipe).
243 /// Ports are also [`PackageKind::Local`] - this flag is what
244 /// distinguishes them from ordinary `path` dependencies so
245 /// `cabin tree` / `explain` can tag them `[port]`.
246 pub is_port: bool,
247}
248
249impl WorkspacePackage {
250 /// Iterate dependency edges of a single kind. Used by the
251 /// build planner, which resolves ordinary targets through
252 /// `Normal`-kind edges only and additionally lets dev-only
253 /// targets (`test` / `example`) see activated `Dev`-kind edges.
254 pub fn deps_of_kind(&self, kind: DependencyKind) -> impl Iterator<Item = usize> + '_ {
255 self.deps
256 .iter()
257 .filter(move |edge| edge.kind == kind)
258 .map(|edge| edge.index)
259 }
260
261 /// Iterate all dependency edges as bare indices, in
262 /// declaration order. Used by closure walks (resolve / fetch /
263 /// metadata) that include every package-graph-resident kind.
264 pub fn all_dep_indices(&self) -> impl Iterator<Item = usize> + '_ {
265 self.deps.iter().map(|edge| edge.index)
266 }
267}
268
269/// A single resolved package-dependency edge in the package graph.
270///
271/// The graph only contains edges that *could* be active on the
272/// evaluation platform (the loader filters out non-matching
273/// `[target.'cfg(...)'.<kind>]` entries before constructing the
274/// graph), so consumers never need to re-check the condition
275/// against a different platform - the loader already did. The
276/// edge still records the originating condition for diagnostics
277/// and metadata.
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct DependencyEdge {
280 /// Index of the depended-on package in [`PackageGraph::packages`].
281 pub index: usize,
282 /// Which manifest section this edge was declared under.
283 pub kind: DependencyKind,
284 /// `Some` when this edge originated from a
285 /// `[target.'cfg(...)'.<kind>]` table that matched the
286 /// evaluation platform; `None` for unconditional edges.
287 pub condition: Option<Condition>,
288 /// Whether the declaration opted this edge out of the
289 /// standard-compatibility check with
290 /// `ignore-interface-standard = true`. Per-edge by design;
291 /// inert unless the check runs.
292 pub ignore_interface_standard: bool,
293}
294
295/// Where a [`WorkspacePackage`] came from.
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum PackageKind {
298 /// A local-filesystem package: the workspace root or a member, a
299 /// `path = "..."` dependency, a `[patch]`ed package, or a prepared
300 /// foundation port.
301 ///
302 /// `Local` is the trust boundary used when deciding whether to honor
303 /// a package's own raw `[profile]` compiler/linker flags: every
304 /// `Local` source is user-controlled. Root / members / path deps are
305 /// local working trees; patches are local override copies; and a
306 /// port's build flags come from its trusted overlay recipe (bundled
307 /// or user-pinned), not the downloaded source archive. The loader
308 /// guarantees a downloaded registry archive can never introduce a
309 /// `Local` package, because it rejects `path` / `port` dependencies
310 /// declared by a [`PackageKind::Registry`] package.
311 Local,
312 /// A registry package whose source archive was already fetched and
313 /// extracted into the artifact cache. Untrusted: its own `[profile]`
314 /// `cflags` / `cxxflags` / `ldflags` are dropped during build-flag
315 /// resolution.
316 Registry,
317}
318
319/// Synthesize a root identity for resolving over a pure-workspace
320/// root (no `[package]`). The name is a deterministic
321/// `__workspace_<dirname>` value the resolver uses for diagnostic
322/// output only; nothing else relies on it being canonical. Lives
323/// here because it is derived purely from a [`PackageGraph`]'s
324/// `root_dir`, keeping the synthetic-root naming rule out of the CLI.
325///
326/// # Panics
327/// Panics only if the constructed name were rejected by
328/// `PackageName::new`, which cannot happen: `sanitized` always begins
329/// with the literal `__workspace_` prefix (so it is non-empty) and
330/// every appended character is ASCII alphanumeric, `_`, or `-`.
331pub fn synthetic_root_identity(graph: &PackageGraph) -> (cabin_core::PackageName, semver::Version) {
332 let dirname = graph
333 .root_dir
334 .file_name()
335 .and_then(|s| s.to_str())
336 .unwrap_or("workspace");
337 let mut sanitized = String::with_capacity(dirname.len() + 12);
338 sanitized.push_str("__workspace_");
339 for c in dirname.chars() {
340 if c.is_ascii_alphanumeric() || matches!(c, '_' | '-') {
341 sanitized.push(c);
342 } else {
343 sanitized.push('_');
344 }
345 }
346 let name =
347 cabin_core::PackageName::new(sanitized).expect("synthesized name is non-empty and ASCII");
348 let version = semver::Version::new(0, 0, 0);
349 (name, version)
350}
351
352#[cfg(test)]
353mod consumer_standards_tests {
354 use super::*;
355 use cabin_core::{
356 CxxStandard, Features, LanguageStandardSettings, PackageConfigInput, PackageName,
357 StandardDeclaration, Target, TargetKind, TargetName,
358 };
359 use camino::Utf8PathBuf;
360
361 fn compiled_target(name: &str, ext: &str, language: LanguageStandardSettings) -> Target {
362 Target {
363 name: TargetName::new(name).unwrap(),
364 kind: TargetKind::Library,
365 sources: vec![Utf8PathBuf::from(format!("src/{name}.{ext}"))],
366 include_dirs: Vec::new(),
367 defines: Vec::new(),
368 deps: Vec::new(),
369 required_features: Vec::new(),
370 language,
371 }
372 }
373
374 fn gated_target(
375 name: &str,
376 ext: &str,
377 language: LanguageStandardSettings,
378 required_features: &[&str],
379 ) -> Target {
380 Target {
381 required_features: required_features.iter().map(|f| (*f).to_owned()).collect(),
382 ..compiled_target(name, ext, language)
383 }
384 }
385
386 fn header_only_target(name: &str, language: LanguageStandardSettings) -> Target {
387 Target {
388 kind: TargetKind::HeaderOnly,
389 sources: Vec::new(),
390 include_dirs: vec![Utf8PathBuf::from("include")],
391 ..compiled_target(name, "h", language)
392 }
393 }
394
395 fn executable_target(name: &str, ext: &str, language: LanguageStandardSettings) -> Target {
396 Target {
397 kind: TargetKind::Executable,
398 ..compiled_target(name, ext, language)
399 }
400 }
401
402 fn member(name: &str, targets: Vec<Target>) -> WorkspacePackage {
403 member_with_features(name, targets, Features::default())
404 }
405
406 fn member_with_features(
407 name: &str,
408 targets: Vec<Target>,
409 features: Features,
410 ) -> WorkspacePackage {
411 let package = Package::with_config(PackageConfigInput {
412 name: PackageName::new(name).unwrap(),
413 version: semver::Version::new(1, 0, 0),
414 targets,
415 dependencies: Vec::new(),
416 system_dependencies: Vec::new(),
417 features,
418 })
419 .unwrap();
420 WorkspacePackage {
421 package,
422 manifest_path: PathBuf::from(format!("/ws/{name}/cabin.toml")),
423 manifest_dir: PathBuf::from(format!("/ws/{name}")),
424 deps: Vec::new(),
425 kind: PackageKind::Local,
426 is_port: false,
427 }
428 }
429
430 fn graph(packages: Vec<WorkspacePackage>) -> PackageGraph {
431 PackageGraph {
432 root_manifest_path: PathBuf::from("/ws/cabin.toml"),
433 root_dir: PathBuf::from("/ws"),
434 is_workspace_root: true,
435 root_package: None,
436 root_settings: RootSettings::default(),
437 primary_packages: (0..packages.len()).collect(),
438 default_members: Vec::new(),
439 excluded_members: Vec::new(),
440 packages,
441 }
442 }
443
444 fn cxx(level: CxxStandard) -> LanguageStandardSettings {
445 LanguageStandardSettings {
446 cxx_standard: Some(StandardDeclaration::Declared(level)),
447 ..Default::default()
448 }
449 }
450
451 fn c(level: CStandard) -> LanguageStandardSettings {
452 LanguageStandardSettings {
453 c_standard: Some(StandardDeclaration::Declared(level)),
454 ..Default::default()
455 }
456 }
457
458 /// The consumer standard is the per-language minimum implementation
459 /// standard across every member target, and `None` for a language
460 /// no member compiles.
461 #[test]
462 fn consumer_standard_is_the_workspace_minimum_per_language() {
463 let workspace = graph(vec![
464 member(
465 "a",
466 vec![compiled_target("a", "cc", cxx(CxxStandard::Cxx20))],
467 ),
468 member(
469 "b",
470 vec![
471 compiled_target("b", "cc", cxx(CxxStandard::Cxx17)),
472 compiled_target("bc", "c", c(CStandard::C17)),
473 ],
474 ),
475 ]);
476 let all: BTreeSet<usize> = (0..workspace.packages.len()).collect();
477 let consumer =
478 workspace.consumer_standards(&all, &[0, 1], &HashMap::new(), &BTreeSet::new());
479 assert_eq!(consumer.cxx, Some(CxxStandard::Cxx17));
480 assert_eq!(consumer.c, Some(CStandard::C17));
481 }
482
483 /// Only the given members count: scoping to the C++20 member alone
484 /// keeps the consumer at C++20 even though a C++17 member exists in
485 /// the graph - a scoped resolve is not lowered by an unselected
486 /// member.
487 #[test]
488 fn consumer_standard_is_scoped_to_the_given_members() {
489 let workspace = graph(vec![
490 member(
491 "app20",
492 vec![compiled_target("app20", "cc", cxx(CxxStandard::Cxx20))],
493 ),
494 member(
495 "other17",
496 vec![compiled_target("other17", "cc", cxx(CxxStandard::Cxx17))],
497 ),
498 ]);
499 let only_app20: BTreeSet<usize> = [0].into_iter().collect();
500 let consumer =
501 workspace.consumer_standards(&only_app20, &[0], &HashMap::new(), &BTreeSet::new());
502 assert_eq!(consumer.cxx, Some(CxxStandard::Cxx20));
503 }
504
505 /// A target gated behind an unenabled feature does not lower the
506 /// consumer standard; enabling its feature counts it.
507 #[test]
508 fn feature_gated_target_does_not_lower_consumer_until_enabled() {
509 let features = Features::new(
510 Vec::new(),
511 [("legacy".to_owned(), Vec::new())].into_iter().collect(),
512 )
513 .unwrap();
514 let workspace = graph(vec![member_with_features(
515 "app",
516 vec![
517 compiled_target("app", "cc", cxx(CxxStandard::Cxx20)),
518 gated_target("legacy", "cc", cxx(CxxStandard::Cxx17), &["legacy"]),
519 ],
520 features,
521 )]);
522 let members: BTreeSet<usize> = [0].into_iter().collect();
523
524 // Feature off: the c++17 target is not built, so the consumer
525 // stays at c++20.
526 assert_eq!(
527 workspace
528 .consumer_standards(&members, &[0], &HashMap::new(), &BTreeSet::new())
529 .cxx,
530 Some(CxxStandard::Cxx20)
531 );
532
533 // Feature on: the c++17 target is built and lowers the consumer.
534 let enabled: HashMap<usize, BTreeSet<String>> =
535 [(0, ["legacy".to_owned()].into_iter().collect())]
536 .into_iter()
537 .collect();
538 assert_eq!(
539 workspace
540 .consumer_standards(&members, &[0], &enabled, &BTreeSet::new())
541 .cxx,
542 Some(CxxStandard::Cxx17)
543 );
544 }
545
546 /// A dev-only (`test`) target counts only when this invocation
547 /// activates the package's dev-dependencies (`dev_for`), matching
548 /// `cabin test`; a plain build does not let it lower the consumer.
549 #[test]
550 fn dev_only_target_counts_only_for_dev_for_packages() {
551 let test_target = Target {
552 kind: TargetKind::Test,
553 ..compiled_target("app_test", "cc", cxx(CxxStandard::Cxx17))
554 };
555 let workspace = graph(vec![member(
556 "app",
557 vec![
558 compiled_target("app", "cc", cxx(CxxStandard::Cxx20)),
559 test_target,
560 ],
561 )]);
562 let members: BTreeSet<usize> = [0].into_iter().collect();
563
564 // Plain build: the c++17 test target is not built.
565 assert_eq!(
566 workspace
567 .consumer_standards(&members, &[0], &HashMap::new(), &BTreeSet::new())
568 .cxx,
569 Some(CxxStandard::Cxx20)
570 );
571
572 // `cabin test` on this package: the test target is built and
573 // lowers the consumer.
574 let dev_for: BTreeSet<String> = ["app".to_owned()].into_iter().collect();
575 assert_eq!(
576 workspace
577 .consumer_standards(&members, &[0], &HashMap::new(), &dev_for)
578 .cxx,
579 Some(CxxStandard::Cxx17)
580 );
581 }
582
583 /// An `example` is dev-only: it counts under `dev_for` (`cabin test`)
584 /// exactly like a `test` target. A selected target can reference an
585 /// example in its `deps`, so the planner may compile it; counting
586 /// every activated example (the safe over-approximation) keeps the
587 /// consumer low enough that a built example never gets a version it
588 /// cannot consume. A plain build does not activate it.
589 #[test]
590 fn example_target_counts_under_dev_for() {
591 let example_target = Target {
592 kind: TargetKind::Example,
593 ..compiled_target("app_example", "cc", cxx(CxxStandard::Cxx17))
594 };
595 let workspace = graph(vec![member(
596 "app",
597 vec![
598 compiled_target("app", "cc", cxx(CxxStandard::Cxx20)),
599 example_target,
600 ],
601 )]);
602 let members: BTreeSet<usize> = [0].into_iter().collect();
603
604 // Plain build: the c++17 example is not built.
605 assert_eq!(
606 workspace
607 .consumer_standards(&members, &[0], &HashMap::new(), &BTreeSet::new())
608 .cxx,
609 Some(CxxStandard::Cxx20)
610 );
611
612 // `cabin test` activates dev-only targets: the c++17 example
613 // lowers the consumer standard.
614 let dev_for: BTreeSet<String> = ["app".to_owned()].into_iter().collect();
615 assert_eq!(
616 workspace
617 .consumer_standards(&members, &[0], &HashMap::new(), &dev_for)
618 .cxx,
619 Some(CxxStandard::Cxx17)
620 );
621 }
622
623 /// A header-only target has no translation units, so it imposes no
624 /// consumer standard - even though `dependency_attributes` reports
625 /// its header-only inference on the dependency side.
626 #[test]
627 fn header_only_target_imposes_no_consumer_standard() {
628 // A package whose only target is header-only compiles nothing.
629 let workspace = graph(vec![member(
630 "hdr",
631 vec![header_only_target("hdr", cxx(CxxStandard::Cxx20))],
632 )]);
633 let members: BTreeSet<usize> = [0].into_iter().collect();
634 let consumer =
635 workspace.consumer_standards(&members, &[0], &HashMap::new(), &BTreeSet::new());
636 assert_eq!(consumer.cxx, None);
637 assert_eq!(consumer.c, None);
638
639 // A header-only c++17 target beside a compiled c++20 library does
640 // not lower the consumer below c++20.
641 let workspace = graph(vec![member(
642 "app",
643 vec![
644 header_only_target("hdr", cxx(CxxStandard::Cxx17)),
645 compiled_target("app", "cc", cxx(CxxStandard::Cxx20)),
646 ],
647 )]);
648 assert_eq!(
649 workspace
650 .consumer_standards(&members, &[0], &HashMap::new(), &BTreeSet::new())
651 .cxx,
652 Some(CxxStandard::Cxx20)
653 );
654 }
655
656 /// A transitive path-dependency package is built only for the
657 /// library targets its consumers link; its own executable is never
658 /// built, so a non-primary member's executable does not lower the
659 /// consumer standard.
660 #[test]
661 fn path_dependency_executable_does_not_lower_consumer() {
662 let workspace = graph(vec![
663 member(
664 "app",
665 vec![compiled_target("app", "cc", cxx(CxxStandard::Cxx20))],
666 ),
667 member(
668 "dep",
669 vec![
670 compiled_target("dep", "cc", cxx(CxxStandard::Cxx20)),
671 executable_target("dep_bin", "cc", cxx(CxxStandard::Cxx17)),
672 ],
673 ),
674 ]);
675 // `app` is the selected primary; `dep` is a transitive path
676 // dependency (in the closure, not primary).
677 let members: BTreeSet<usize> = [0, 1].into_iter().collect();
678 let consumer =
679 workspace.consumer_standards(&members, &[0], &HashMap::new(), &BTreeSet::new());
680 assert_eq!(consumer.cxx, Some(CxxStandard::Cxx20));
681 }
682
683 /// No members imposes nothing.
684 #[test]
685 fn empty_member_set_has_no_consumer_standard() {
686 let workspace = graph(vec![member(
687 "a",
688 vec![compiled_target("a", "cc", cxx(CxxStandard::Cxx20))],
689 )]);
690 let consumer =
691 workspace.consumer_standards(&BTreeSet::new(), &[], &HashMap::new(), &BTreeSet::new());
692 assert_eq!(consumer.c, None);
693 assert_eq!(consumer.cxx, None);
694 }
695}