anodizer_core/config/accessors.rs
1use super::*;
2
3impl Config {
4 /// The full crate universe: top-level `crates` plus every
5 /// `workspaces[].crates` entry, deduplicated by name (first-seen wins,
6 /// so a top-level entry shadows a same-named workspace entry).
7 ///
8 /// Single source of the read-only "all crates that can carry per-crate
9 /// config" walk. Publisher registration, required/retain gate
10 /// collapsing, per-crate dispatch, requirement derivation,
11 /// `--crate`/`--all` selection, tool-need detection, artifact guards,
12 /// and default-naming decisions must all resolve through this walker so
13 /// a workspace-only crate carrying a publisher block is either visible
14 /// everywhere or nowhere — a consumer iterating `config.crates`
15 /// directly silently excludes workspace crates and hides their
16 /// publishes. Only two shapes may keep a raw chained walk: mutation
17 /// passes (`&mut` access — this walker hands out shared borrows) and
18 /// validation/diagnostics that must see every entry as written,
19 /// including the shadowed duplicates this walker dedups away.
20 pub fn crate_universe(&self) -> Vec<&CrateConfig> {
21 self.crate_universe_walk().0
22 }
23
24 /// Borrow a crate by name from [`Self::crate_universe`] (top-level wins
25 /// on a name collision). The single by-name lookup every consumer must
26 /// use — a `config.crates.iter().find(...)` cannot see workspace-only
27 /// crates.
28 pub fn find_crate(&self, name: &str) -> Option<&CrateConfig> {
29 self.crate_universe().into_iter().find(|c| c.name == name)
30 }
31
32 /// Operator-facing warnings for crate-name collisions in the universe
33 /// where the colliding entries disagree on `path` — almost certainly a
34 /// config mistake (two distinct crates sharing a name). The legitimate
35 /// duplicate (the same crate referenced from both top-level and a
36 /// workspace) dedups silently. Emitted by the publish stage at entry so
37 /// the warning appears once per run rather than once per universe walk.
38 pub fn crate_universe_collision_warnings(&self) -> Vec<String> {
39 self.crate_universe_walk().1
40 }
41
42 /// The one walk both [`Self::crate_universe`] and
43 /// [`Self::crate_universe_collision_warnings`] derive from, so the
44 /// merge/dedup policy and its diagnostics cannot diverge.
45 fn crate_universe_walk(&self) -> (Vec<&CrateConfig>, Vec<String>) {
46 let mut out: Vec<&CrateConfig> = self.crates.iter().collect();
47 let mut warnings = Vec::new();
48 for ws in self.workspaces.iter().flatten() {
49 for c in &ws.crates {
50 if let Some(existing) = out.iter().find(|e| e.name == c.name) {
51 if existing.path != c.path {
52 warnings.push(format!(
53 "workspace '{}' crate '{}' path '{}' shadowed by \
54 prior entry with path '{}'; workspace entry dropped (name \
55 collision with different paths — likely a config mistake)",
56 ws.name, c.name, c.path, existing.path
57 ));
58 }
59 continue;
60 }
61 out.push(c);
62 }
63 }
64 (out, warnings)
65 }
66
67 /// Return the monorepo tag prefix, if configured.
68 ///
69 /// Shorthand for `config.monorepo.as_ref().and_then(|m| m.tag_prefix.as_deref())`.
70 pub fn monorepo_tag_prefix(&self) -> Option<&str> {
71 self.monorepo.as_ref().and_then(|m| m.tag_prefix.as_deref())
72 }
73
74 /// Return the monorepo working directory, if configured.
75 ///
76 /// Shorthand for `config.monorepo.as_ref().and_then(|m| m.dir.as_deref())`.
77 pub fn monorepo_dir(&self) -> Option<&str> {
78 self.monorepo.as_ref().and_then(|m| m.dir.as_deref())
79 }
80
81 /// The build targets compiled when neither a per-build `targets` nor
82 /// `defaults.targets` is set: `defaults.targets` (when non-empty), else the
83 /// canonical `DEFAULT_TARGETS`. Single source of truth for the target-set
84 /// fallback — every target enumeration MUST resolve through this rather than
85 /// re-deriving the fallback, so they never diverge.
86 pub fn effective_default_targets(&self) -> Vec<String> {
87 self.defaults
88 .as_ref()
89 .and_then(|d| d.targets.clone())
90 .filter(|t| !t.is_empty())
91 .unwrap_or_else(|| {
92 crate::target::DEFAULT_TARGETS
93 .iter()
94 .map(|s| (*s).to_string())
95 .collect()
96 })
97 }
98
99 /// The cross-compilation strategy applied to a crate that does not set its
100 /// own `cross:` — `defaults.cross`, else `Auto`. SSOT for the per-crate
101 /// strategy fallback.
102 pub fn default_cross_strategy(&self) -> CrossStrategy {
103 self.defaults
104 .as_ref()
105 .and_then(|d| d.cross.clone())
106 .unwrap_or(CrossStrategy::Auto)
107 }
108
109 // --- Project metadata defaulting helpers ---
110 //
111 // Publishers that expose homepage/license/description/maintainer fields
112 // fall back to these when their own field is unset, so a project only
113 // needs to declare metadata once. Resolution precedence (highest first):
114 //
115 // 1. the per-publisher override (the publisher's own config field)
116 // 2. a hand-written top-level `metadata:` YAML field
117 // 3. the value derived from the crate's `Cargo.toml [package]` table
118 // (populated by `populate_derived_metadata`)
119 //
120 // Steps 1 is enforced by the publisher's `or_else(|| cfg.meta_*_for(..))`
121 // chain; steps 2-3 are enforced inside the `meta_*_for` accessors. A
122 // publisher that knows which crate it is publishing for should call the
123 // crate-aware `meta_*_for(crate_name)` variant so workspace/per-crate
124 // configs resolve each crate's OWN Cargo.toml metadata. The crate-agnostic
125 // `meta_*` variants resolve the top-level `metadata:` block only (no
126 // Cargo.toml fallback) and exist for truly project-level callers.
127
128 /// Per-crate derived metadata for `crate_name`, if `Cargo.toml` supplied any.
129 fn derived_for(&self, crate_name: &str) -> Option<&MetadataConfig> {
130 self.derived_metadata.get(crate_name)
131 }
132
133 /// Name of the primary crate (first declared `crates:` entry, else the
134 /// first workspace crate). Used as the metadata-derivation source and
135 /// crate-name fallback for project-level publishers (e.g. top-level
136 /// `homebrew_casks:`, `npms:`) that are not bound to a single crate.
137 pub fn primary_crate_name(&self) -> Option<&str> {
138 self.crate_universe().first().map(|c| c.name.as_str())
139 }
140
141 /// Project homepage: top-level `metadata.homepage` wins, else the primary
142 /// crate's `Cargo.toml`-derived homepage. For project-level publishers
143 /// (top-level casks) with no owning crate.
144 pub fn meta_homepage_project(&self) -> Option<&str> {
145 self.meta_homepage()
146 .or_else(|| self.meta_homepage_for(self.primary_crate_name()?))
147 }
148
149 /// Project description: top-level `metadata.description` wins, else the
150 /// primary crate's `Cargo.toml`-derived description.
151 pub fn meta_description_project(&self) -> Option<&str> {
152 self.meta_description()
153 .or_else(|| self.meta_description_for(self.primary_crate_name()?))
154 }
155
156 /// Project source-repository URL: top-level `metadata.repository` wins, else
157 /// the primary crate's `Cargo.toml`-derived repository. Backs the
158 /// `{{ Metadata.Repository }}` template var.
159 pub fn meta_repository_project(&self) -> Option<&str> {
160 self.meta_repository()
161 .or_else(|| self.meta_repository_for(self.primary_crate_name()?))
162 }
163
164 /// Project license: top-level `metadata.license` wins, else the primary
165 /// crate's `Cargo.toml`-derived license. For the `{{ Metadata.License }}`
166 /// template var and project-level publishers with no owning crate.
167 pub fn meta_license_project(&self) -> Option<&str> {
168 self.meta_license()
169 .or_else(|| self.meta_license_for(self.primary_crate_name()?))
170 }
171
172 /// Project documentation URL: top-level `metadata.documentation` wins, else
173 /// the primary crate's `Cargo.toml`-derived documentation URL.
174 pub fn meta_documentation_project(&self) -> Option<&str> {
175 self.meta_documentation()
176 .or_else(|| self.meta_documentation_for(self.primary_crate_name()?))
177 }
178
179 /// Project homepage from `metadata.homepage` (top-level YAML only).
180 pub fn meta_homepage(&self) -> Option<&str> {
181 self.metadata.as_ref().and_then(|m| m.homepage.as_deref())
182 }
183
184 /// Project license from `metadata.license` (top-level YAML only).
185 pub fn meta_license(&self) -> Option<&str> {
186 self.metadata.as_ref().and_then(|m| m.license.as_deref())
187 }
188
189 /// Project source-repository URL from `metadata.repository` (top-level YAML only).
190 pub fn meta_repository(&self) -> Option<&str> {
191 self.metadata.as_ref().and_then(|m| m.repository.as_deref())
192 }
193
194 /// Project description from `metadata.description` (top-level YAML only).
195 pub fn meta_description(&self) -> Option<&str> {
196 self.metadata
197 .as_ref()
198 .and_then(|m| m.description.as_deref())
199 }
200
201 /// Project documentation URL from `metadata.documentation` (top-level YAML only).
202 pub fn meta_documentation(&self) -> Option<&str> {
203 self.metadata
204 .as_ref()
205 .and_then(|m| m.documentation.as_deref())
206 }
207
208 /// Project maintainers from `metadata.maintainers` (top-level YAML only).
209 pub fn meta_maintainers(&self) -> &[String] {
210 self.metadata
211 .as_ref()
212 .and_then(|m| m.maintainers.as_deref())
213 .unwrap_or(&[])
214 }
215
216 /// First maintainer as `Name <email>` or just `Name` (publisher convention).
217 /// Returns None when no maintainers are configured.
218 pub fn meta_first_maintainer(&self) -> Option<&str> {
219 self.meta_maintainers().first().map(|s| s.as_str())
220 }
221
222 /// Homepage for `crate_name`: top-level `metadata.homepage` wins, else the
223 /// value derived from the crate's `Cargo.toml [package]`.
224 pub fn meta_homepage_for(&self, crate_name: &str) -> Option<&str> {
225 self.meta_homepage()
226 .or_else(|| self.derived_for(crate_name)?.homepage.as_deref())
227 }
228
229 /// License for `crate_name`: top-level `metadata.license` wins, else the
230 /// crate's `Cargo.toml [package].license` (never synthesised from
231 /// `license-file`).
232 pub fn meta_license_for(&self, crate_name: &str) -> Option<&str> {
233 self.meta_license()
234 .or_else(|| self.derived_for(crate_name)?.license.as_deref())
235 }
236
237 /// Source-repository URL for `crate_name`: top-level `metadata.repository`
238 /// wins, else the crate's `Cargo.toml [package].repository`. Feeds the npm
239 /// `package.json` `repository` field so npm provenance validation (which
240 /// matches it against the OIDC-claimed repository) passes without requiring
241 /// the operator to restate the URL in the publisher config.
242 pub fn meta_repository_for(&self, crate_name: &str) -> Option<&str> {
243 self.meta_repository()
244 .or_else(|| self.derived_for(crate_name)?.repository.as_deref())
245 }
246
247 /// Description for `crate_name`: top-level `metadata.description` wins, else
248 /// the crate's `Cargo.toml [package].description`.
249 pub fn meta_description_for(&self, crate_name: &str) -> Option<&str> {
250 self.meta_description()
251 .or_else(|| self.derived_for(crate_name)?.description.as_deref())
252 }
253
254 /// Documentation URL for `crate_name`: top-level `metadata.documentation`
255 /// wins, else the crate's `Cargo.toml [package].documentation`.
256 pub fn meta_documentation_for(&self, crate_name: &str) -> Option<&str> {
257 self.meta_documentation()
258 .or_else(|| self.derived_for(crate_name)?.documentation.as_deref())
259 }
260
261 /// Maintainers for `crate_name`: top-level `metadata.maintainers` wins
262 /// (when non-empty), else the crate's `Cargo.toml [package].authors`.
263 pub fn meta_maintainers_for(&self, crate_name: &str) -> &[String] {
264 let top = self.meta_maintainers();
265 if !top.is_empty() {
266 return top;
267 }
268 self.derived_for(crate_name)
269 .and_then(|m| m.maintainers.as_deref())
270 .unwrap_or(&[])
271 }
272
273 /// First maintainer for `crate_name` as `Name <email>` or just `Name`.
274 pub fn meta_first_maintainer_for(&self, crate_name: &str) -> Option<&str> {
275 self.meta_maintainers_for(crate_name)
276 .first()
277 .map(|s| s.as_str())
278 }
279
280 /// Vendor / distributing-entity name for `crate_name`: the first
281 /// maintainer with any `<email>` suffix stripped (e.g.
282 /// `"Ada Lovelace <ada@x>"` → `"Ada Lovelace"`). `None` when no maintainer
283 /// is derivable or the result is empty, so a Vendor field is never emitted
284 /// blank. Reused by the rpm/deb Vendor and the OCI image `vendor` label.
285 pub fn meta_vendor_for(&self, crate_name: &str) -> Option<String> {
286 self.meta_first_maintainer_for(crate_name)
287 .and_then(maintainer_name_only)
288 }
289
290 /// Populate [`Config::derived_metadata`] by reading each crate's
291 /// `Cargo.toml [package]` table (description / license / homepage /
292 /// authors), so publishers resolve a plain Rust project's metadata without
293 /// requiring a top-level `metadata:` YAML block.
294 ///
295 /// Covers every crate the config knows about: top-level `crates:` plus
296 /// every `workspaces[].crates[]`, so single-crate, workspace-lockstep, and
297 /// per-crate configs all populate. Each crate is read from
298 /// `<crate.path>/Cargo.toml` relative to `base_dir` (the directory the
299 /// config was loaded from / the monorepo working directory).
300 ///
301 /// Idempotent and non-destructive: only fills entries; existing
302 /// `derived_metadata` keys are overwritten with a fresh read. Crates whose
303 /// `Cargo.toml` is missing or supplies nothing contribute an all-`None`
304 /// entry (harmless — the accessors treat it as "no value").
305 pub fn populate_derived_metadata(&mut self, base_dir: &std::path::Path) {
306 let crate_paths: Vec<(String, String)> = self
307 .crate_universe()
308 .into_iter()
309 .map(|c| (c.name.clone(), c.path.clone()))
310 .collect();
311 for (name, path) in crate_paths {
312 let crate_dir = base_dir.join(&path);
313 let derived = derive_metadata_from_cargo_toml(&crate_dir);
314 self.derived_metadata.insert(name, derived);
315 }
316 }
317
318 /// Populate `depends_on` for every crate entry that OMITS it, by reading
319 /// the crate's `Cargo.toml` dependency tables (`[dependencies]`,
320 /// `[build-dependencies]`, and every `[target.'cfg(...)'.dependencies]`)
321 /// and matching against the real on-disk Cargo workspace's member names
322 /// ([`discover_cargo_workspace_member_names`]) — the same derivation
323 /// `anodizer init` performs at scaffold time
324 /// ([`derive_depends_on_from_cargo_toml`]), now re-run at every
325 /// config-load so a hand-maintained `crates:` list can never drift stale
326 /// behind the crate's real Cargo.toml dependencies.
327 /// An explicit `depends_on` (`Some(_)`) is a user override and is never
328 /// overwritten.
329 ///
330 /// Covers every crate the config knows about (top-level `crates:` plus
331 /// every `workspaces[].crates[]`), mirroring
332 /// [`Self::populate_derived_metadata`]. A single-crate project (no
333 /// `crates:` at all) has an empty crate universe, so this is a no-op.
334 /// A project with no on-disk Cargo workspace (no root `Cargo.toml`, or
335 /// a plain single-package `Cargo.toml`) has only itself as a "member",
336 /// so derivation naturally yields no deps.
337 pub fn populate_derived_depends_on(&mut self, base_dir: &std::path::Path) {
338 let member_names = discover_cargo_workspace_member_names(base_dir);
339 if member_names.is_empty() {
340 return;
341 }
342
343 let derived: HashMap<String, Vec<String>> = self
344 .crate_universe()
345 .into_iter()
346 .filter(|c| c.depends_on.is_none())
347 .map(|c| {
348 let crate_dir = base_dir.join(&c.path);
349 let deps = derive_depends_on_from_cargo_toml(&crate_dir, &member_names);
350 (c.name.clone(), deps)
351 })
352 .collect();
353
354 if derived.is_empty() {
355 return;
356 }
357
358 for c in self.crates.iter_mut().chain(
359 self.workspaces
360 .iter_mut()
361 .flatten()
362 .flat_map(|ws| ws.crates.iter_mut()),
363 ) {
364 if c.depends_on.is_none()
365 && let Some(deps) = derived.get(&c.name)
366 {
367 c.depends_on = Some(deps.clone());
368 }
369 }
370 }
371}