Skip to main content

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}