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 /// Whether this package is a prepared foundation port (its
114 /// source tree was materialized from a `port.toml` recipe).
115 /// Ports are also [`PackageKind::Local`] — this flag is what
116 /// distinguishes them from ordinary `path` dependencies so
117 /// `cabin tree` / `explain` can tag them `[port]`.
118 pub is_port: bool,
119}
120
121impl WorkspacePackage {
122 /// Iterate dependency edges of a single kind. Used by the
123 /// build planner so cross-package target lookups stay limited
124 /// to `Normal`-kind edges.
125 pub fn deps_of_kind(&self, kind: DependencyKind) -> impl Iterator<Item = usize> + '_ {
126 self.deps
127 .iter()
128 .filter(move |edge| edge.kind == kind)
129 .map(|edge| edge.index)
130 }
131
132 /// Iterate all dependency edges as bare indices, in
133 /// declaration order. Used by closure walks (resolve / fetch /
134 /// metadata) that include every package-graph-resident kind.
135 pub fn all_dep_indices(&self) -> impl Iterator<Item = usize> + '_ {
136 self.deps.iter().map(|edge| edge.index)
137 }
138}
139
140/// A single resolved package-dependency edge in the package graph.
141///
142/// The graph only contains edges that *could* be active on the
143/// evaluation platform (the loader filters out non-matching
144/// `[target.'cfg(...)'.<kind>]` entries before constructing the
145/// graph), so consumers never need to re-check the condition
146/// against a different platform — the loader already did. The
147/// edge still records the originating condition for diagnostics
148/// and metadata.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct DependencyEdge {
151 /// Index of the depended-on package in [`PackageGraph::packages`].
152 pub index: usize,
153 /// Which manifest section this edge was declared under.
154 pub kind: DependencyKind,
155 /// `Some` when this edge originated from a
156 /// `[target.'cfg(...)'.<kind>]` table that matched the
157 /// evaluation platform; `None` for unconditional edges.
158 pub condition: Option<Condition>,
159}
160
161/// Where a [`WorkspacePackage`] came from.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum PackageKind {
164 /// A local-filesystem package: the workspace root or a member, a
165 /// `path = "..."` dependency, a `[patch]`ed package, or a prepared
166 /// foundation port.
167 ///
168 /// `Local` is the trust boundary used when deciding whether to honor
169 /// a package's own raw `[profile]` compiler/linker flags: every
170 /// `Local` source is user-controlled. Root / members / path deps are
171 /// local working trees; patches are local override copies; and a
172 /// port's build flags come from its trusted overlay recipe (bundled
173 /// or user-pinned), not the downloaded source archive. The loader
174 /// guarantees a downloaded registry archive can never introduce a
175 /// `Local` package, because it rejects `path` / `port` dependencies
176 /// declared by a [`PackageKind::Registry`] package.
177 Local,
178 /// A registry package whose source archive was already fetched and
179 /// extracted into the artifact cache. Untrusted: its own `[profile]`
180 /// `cflags` / `cxxflags` / `ldflags` are dropped during build-flag
181 /// resolution.
182 Registry,
183}
184
185/// Synthesize a root identity for resolving over a pure-workspace
186/// root (no `[package]`). The name is a deterministic
187/// `__workspace_<dirname>` value the resolver uses for diagnostic
188/// output only; nothing else relies on it being canonical. Lives
189/// here because it is derived purely from a [`PackageGraph`]'s
190/// `root_dir`, keeping the synthetic-root naming rule out of the CLI.
191///
192/// # Panics
193/// Panics only if the constructed name were rejected by
194/// `PackageName::new`, which cannot happen: `sanitized` always begins
195/// with the literal `__workspace_` prefix (so it is non-empty) and
196/// every appended character is ASCII alphanumeric, `_`, or `-`.
197pub fn synthetic_root_identity(graph: &PackageGraph) -> (cabin_core::PackageName, semver::Version) {
198 let dirname = graph
199 .root_dir
200 .file_name()
201 .and_then(|s| s.to_str())
202 .unwrap_or("workspace");
203 let mut sanitized = String::with_capacity(dirname.len() + 12);
204 sanitized.push_str("__workspace_");
205 for c in dirname.chars() {
206 if c.is_ascii_alphanumeric() || matches!(c, '_' | '-') {
207 sanitized.push(c);
208 } else {
209 sanitized.push('_');
210 }
211 }
212 let name =
213 cabin_core::PackageName::new(sanitized).expect("synthesized name is non-empty and ASCII");
214 let version = semver::Version::new(0, 0, 0);
215 (name, version)
216}