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}
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}