Skip to main content

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}