cabin_workspace/graph.rs
1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use cabin_core::{
5 CompilerWrapperManifestSettings, Condition, DependencyKind, Package, PatchManifestSettings,
6 ProfileDefinition, ProfileName, ToolchainSettings,
7};
8
9/// Root-manifest policy settings that apply workspace-wide even
10/// when the entry manifest is a pure `[workspace]` manifest.
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct RootSettings {
13 pub profiles: BTreeMap<ProfileName, ProfileDefinition>,
14 pub toolchain: ToolchainSettings,
15 pub compiler_wrapper: CompilerWrapperManifestSettings,
16 pub patches: PatchManifestSettings,
17}
18
19impl From<cabin_manifest::RootSettings> for RootSettings {
20 fn from(value: cabin_manifest::RootSettings) -> Self {
21 Self {
22 profiles: value.profiles,
23 toolchain: value.toolchain,
24 compiler_wrapper: value.compiler_wrapper,
25 patches: value.patches,
26 }
27 }
28}
29
30/// A loaded set of local Cabin packages with their dependency edges
31/// resolved against the local filesystem.
32///
33/// Packages appear in topological order: a package's local dependencies
34/// always appear before the package itself in [`PackageGraph::packages`].
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct PackageGraph {
37 /// Path to the manifest the user passed (canonicalized to absolute).
38 pub root_manifest_path: PathBuf,
39 /// Directory containing the root manifest.
40 pub root_dir: PathBuf,
41 /// Whether the root manifest declares a `[workspace]` table.
42 pub is_workspace_root: bool,
43 /// If the root manifest itself is a package (i.e. has a `[package]`
44 /// Table), the index of that package in [`PackageGraph::packages`].
45 pub root_package: Option<usize>,
46 /// Root-manifest policy settings. For package roots this
47 /// mirrors the root package's root-owned fields; for pure
48 /// workspace roots this is the only place those settings are
49 /// exposed.
50 pub root_settings: RootSettings,
51 /// Indices of packages that count as "primary" — i.e. would be built
52 /// when no narrower package selection is given.
53 ///
54 /// For a single package this is just the root. For a workspace root it
55 /// is every member declared by `[workspace.members]`. Path dependencies
56 /// pulled in transitively are *not* primary.
57 pub primary_packages: Vec<usize>,
58 /// Indices of packages listed under
59 /// `[workspace.default-members]`, validated to be members. Empty
60 /// when the workspace declares no defaults — callers fall back to
61 /// the documented "all members" behavior. Always a subset of
62 /// `primary_packages`.
63 pub default_members: Vec<usize>,
64 /// Relative paths under `root_dir` for any directories
65 /// dropped by `[workspace.exclude]`. Carried through purely for
66 /// metadata reporting; the loader has already removed them from
67 /// `primary_packages`.
68 pub excluded_members: Vec<PathBuf>,
69 /// All loaded packages, in topological order.
70 pub packages: Vec<WorkspacePackage>,
71}
72
73impl PackageGraph {
74 /// Find a package by name. Linear scan; package counts are small.
75 pub fn package_by_name(&self, name: &str) -> Option<&WorkspacePackage> {
76 self.packages
77 .iter()
78 .find(|p| p.package.name.as_str() == name)
79 }
80
81 /// Index of a package by name. Returned together with the reference
82 /// for callers that need to record edges by index.
83 pub fn index_of(&self, name: &str) -> Option<usize> {
84 self.packages
85 .iter()
86 .position(|p| p.package.name.as_str() == name)
87 }
88}
89
90/// A single loaded package.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct WorkspacePackage {
93 pub package: Package,
94 /// Absolute path to this package's `cabin.toml`.
95 pub manifest_path: PathBuf,
96 /// Absolute path to the directory containing `manifest_path`.
97 pub manifest_dir: PathBuf,
98 /// Resolved package-dependency edges, in declaration order.
99 /// Each edge carries the index of the depended-on package
100 /// inside [`PackageGraph::packages`] together with the
101 /// [`DependencyKind`] under which it was declared.
102 ///
103 /// Only kinds that participate in ordinary resolution
104 /// (`Normal`) appear here today: dev path-deps are
105 /// declaration-only and therefore never enter the package
106 /// graph. The kind is preserved per-edge so the resolver /
107 /// fetch / closure-walk callers can iterate all edges
108 /// consistently with future kinds.
109 pub deps: Vec<DependencyEdge>,
110 /// Whether this package was loaded from a local source tree
111 /// or from an extracted registry archive.
112 pub kind: PackageKind,
113}
114
115impl WorkspacePackage {
116 /// Iterate dependency edges of a single kind. Used by the
117 /// build planner so cross-package target lookups stay limited
118 /// to `Normal`-kind edges.
119 pub fn deps_of_kind(&self, kind: DependencyKind) -> impl Iterator<Item = usize> + '_ {
120 self.deps
121 .iter()
122 .filter(move |edge| edge.kind == kind)
123 .map(|edge| edge.index)
124 }
125
126 /// Iterate all dependency edges as bare indices, in
127 /// declaration order. Used by closure walks (resolve / fetch /
128 /// metadata) that include every package-graph-resident kind.
129 pub fn all_dep_indices(&self) -> impl Iterator<Item = usize> + '_ {
130 self.deps.iter().map(|edge| edge.index)
131 }
132}
133
134/// A single resolved package-dependency edge in the package graph.
135///
136/// The graph only contains edges that *could* be active on the
137/// evaluation platform (the loader filters out non-matching
138/// `[target.'cfg(...)'.<kind>]` entries before constructing the
139/// graph), so consumers never need to re-check the condition
140/// against a different platform — the loader already did. The
141/// edge still records the originating condition for diagnostics
142/// and metadata.
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct DependencyEdge {
145 /// Index of the depended-on package in [`PackageGraph::packages`].
146 pub index: usize,
147 /// Which manifest section this edge was declared under.
148 pub kind: DependencyKind,
149 /// `Some` when this edge originated from a
150 /// `[target.'cfg(...)'.<kind>]` table that matched the
151 /// evaluation platform; `None` for unconditional edges.
152 pub condition: Option<Condition>,
153}
154
155/// Where a [`WorkspacePackage`] came from.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum PackageKind {
158 /// A local-filesystem package: the workspace root or a member, a
159 /// `path = "..."` dependency, a `[patch]`ed package, or a prepared
160 /// foundation port.
161 ///
162 /// `Local` is the trust boundary used when deciding whether to honor
163 /// a package's own raw `[profile]` compiler/linker flags: every
164 /// `Local` source is user-controlled. Root / members / path deps are
165 /// local working trees; patches are local override copies; and a
166 /// port's build flags come from its trusted overlay recipe (bundled
167 /// or user-pinned), not the downloaded source archive. The loader
168 /// guarantees a downloaded registry archive can never introduce a
169 /// `Local` package, because it rejects `path` / `port` dependencies
170 /// declared by a [`PackageKind::Registry`] package.
171 Local,
172 /// A registry package whose source archive was already fetched and
173 /// extracted into the artifact cache. Untrusted: its own `[profile]`
174 /// `cflags` / `cxxflags` / `ldflags` are dropped during build-flag
175 /// resolution.
176 Registry,
177}
178
179/// Synthesize a root identity for resolving over a pure-workspace
180/// root (no `[package]`). The name is a deterministic
181/// `__workspace_<dirname>` value the resolver uses for diagnostic
182/// output only; nothing else relies on it being canonical. Lives
183/// here because it is derived purely from a [`PackageGraph`]'s
184/// `root_dir`, keeping the synthetic-root naming rule out of the CLI.
185///
186/// # Panics
187/// Panics only if the constructed name were rejected by
188/// `PackageName::new`, which cannot happen: `sanitized` always begins
189/// with the literal `__workspace_` prefix (so it is non-empty) and
190/// every appended character is ASCII alphanumeric, `_`, or `-`.
191pub fn synthetic_root_identity(graph: &PackageGraph) -> (cabin_core::PackageName, semver::Version) {
192 let dirname = graph
193 .root_dir
194 .file_name()
195 .and_then(|s| s.to_str())
196 .unwrap_or("workspace");
197 let mut sanitized = String::with_capacity(dirname.len() + 12);
198 sanitized.push_str("__workspace_");
199 for c in dirname.chars() {
200 if c.is_ascii_alphanumeric() || matches!(c, '_' | '-') {
201 sanitized.push(c);
202 } else {
203 sanitized.push('_');
204 }
205 }
206 let name =
207 cabin_core::PackageName::new(sanitized).expect("synthesized name is non-empty and ASCII");
208 let version = semver::Version::new(0, 0, 0);
209 (name, version)
210}