Skip to main content

cargo_rail/config/
mod.rs

1//! Configuration for cargo-rail
2//!
3//! This module provides all configuration types for cargo-rail:
4//! - `RailConfig` - Main configuration struct loaded from rail.toml
5//! - `UnifyConfig` - Dependency unification settings
6//! - `ReleaseConfig` - Release management settings
7//! - `SplitConfig` - Crate splitting/syncing settings
8//! - `ChangeDetectionConfig` - Change detection settings
9//! - `RunConfig` - `cargo rail run` profile settings
10//!
11//! Configuration is searched in order: rail.toml, .rail.toml, .cargo/rail.toml, .config/rail.toml
12
13mod change_detection;
14mod release;
15mod run;
16pub mod schema;
17mod split;
18mod unify;
19
20// Re-export all public types
21pub use change_detection::{ChangeDetectionConfig, ConfidenceProfile};
22pub use release::{ChangelogConfig, ChangelogRelativeTo, CrateReleaseConfig, ReleaseConfig};
23pub use run::{RunConfig, RunProfile, is_builtin_profile};
24pub use split::{CratePath, CrateSplitConfig, CrateSyncConfig, SplitConfig, SplitMode, WorkspaceMode};
25pub use unify::{ExactPinHandling, MajorVersionConflict, MsrvSource, TransitiveFeatureHost, UnifyConfig};
26
27use crate::error::{ConfigError, RailError, RailResult};
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30use std::fs;
31use std::path::{Path, PathBuf};
32
33/// Configuration for cargo-rail
34/// Searched in order: rail.toml, .rail.toml, .cargo/rail.toml, .config/rail.toml
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RailConfig {
37  /// Target triples for multi-platform validation (workspace-wide)
38  /// Detected via `cargo rail init`, used by multiple commands
39  #[serde(default)]
40  pub targets: Vec<String>,
41  /// Dependency unification settings
42  #[serde(default)]
43  pub unify: UnifyConfig,
44  /// Release management settings
45  #[serde(default)]
46  pub release: ReleaseConfig,
47  /// Change detection settings (for planner classification)
48  #[serde(default, rename = "change-detection")]
49  pub change_detection: ChangeDetectionConfig,
50  /// Run profile settings for `cargo rail run`.
51  #[serde(default)]
52  pub run: RunConfig,
53  /// Per-crate configuration (overrides workspace defaults)
54  #[serde(default)]
55  pub crates: HashMap<String, CrateConfig>,
56}
57
58/// Per-crate configuration
59#[derive(Debug, Clone, Serialize, Deserialize, Default)]
60pub struct CrateConfig {
61  /// Split/sync configuration for this crate
62  pub split: Option<CrateSplitConfig>,
63  /// Release configuration for this crate
64  pub release: Option<CrateReleaseConfig>,
65  /// Changelog configuration for this crate
66  pub changelog: Option<ChangelogConfig>,
67  /// Sync configuration for this crate (reserved for future use)
68  pub sync: Option<CrateSyncConfig>,
69}
70
71/// Result of attempting to load configuration
72pub enum ConfigLoadResult {
73  /// Config loaded successfully
74  Loaded(Box<RailConfig>),
75  /// Config file found but failed to parse
76  ParseError {
77    /// Path to the config file that failed to parse
78    path: PathBuf,
79    /// Error message describing the parse failure
80    message: String,
81  },
82  /// No config file found
83  NotFound,
84}
85
86impl RailConfig {
87  /// Find config file in search order: rail.toml, .rail.toml, .cargo/rail.toml, .config/rail.toml
88  ///
89  /// On Windows, this handles path canonicalization issues (UNC paths, 8.3 short names)
90  /// by checking both the original path and its parent's canonicalization.
91  pub fn find_config_path(path: &Path) -> Option<PathBuf> {
92    let candidates = [
93      path.join("rail.toml"),
94      path.join(".rail.toml"),
95      path.join(".cargo").join("rail.toml"),
96      path.join(".config").join("rail.toml"),
97    ];
98
99    // First, try the candidates as-is
100    if let Some(found) = candidates.iter().find(|p| p.exists()) {
101      return Some(found.to_path_buf());
102    }
103
104    // On Windows, if path is canonicalized (e.g., from cargo metadata),
105    // we may need to check using the original non-canonicalized path.
106    #[cfg(target_os = "windows")]
107    {
108      // 1. Try canonicalizing the path and searching there
109      // This handles 8.3 short paths vs long paths issues (RUNNER~1 vs runneradmin)
110      if let Ok(canonical) = path.canonicalize() {
111        let canonical_candidates = [
112          canonical.join("rail.toml"),
113          canonical.join(".rail.toml"),
114          canonical.join(".cargo").join("rail.toml"),
115          canonical.join(".config").join("rail.toml"),
116        ];
117        if let Some(found) = canonical_candidates.iter().find(|p| p.exists()) {
118          return Some(found.to_path_buf());
119        }
120      }
121
122      // 2. Try to find the config by reading the directory entries
123      if let Ok(entries) = std::fs::read_dir(path) {
124        for entry in entries.flatten() {
125          let file_name = entry.file_name();
126          let file_name_str = file_name.to_string_lossy();
127
128          if file_name_str == "rail.toml" || file_name_str == ".rail.toml" {
129            return Some(entry.path());
130          }
131        }
132      }
133
134      // Also check subdirectories .cargo and .config via read_dir
135      for subdir in &[".cargo", ".config"] {
136        let subdir_path = path.join(subdir);
137        if let Ok(entries) = std::fs::read_dir(&subdir_path) {
138          for entry in entries.flatten() {
139            let file_name = entry.file_name();
140            if file_name.to_string_lossy() == "rail.toml" {
141              return Some(entry.path());
142            }
143          }
144        }
145      }
146    }
147
148    None
149  }
150
151  /// Load config from rail.toml (searches multiple locations)
152  pub fn load(path: &Path) -> RailResult<Self> {
153    match Self::try_load(path) {
154      ConfigLoadResult::Loaded(config) => Ok(*config),
155      ConfigLoadResult::ParseError { path, message } => {
156        Err(RailError::Config(ConfigError::ParseError { path, message }))
157      }
158      ConfigLoadResult::NotFound => Err(RailError::Config(ConfigError::NotFound {
159        workspace_root: path.to_path_buf(),
160      })),
161    }
162  }
163
164  /// Try to load config, returning a result that distinguishes between
165  /// "not found" and "parse error". This is used by WorkspaceContext to
166  /// properly report parse errors instead of silently falling back to defaults.
167  pub fn try_load(path: &Path) -> ConfigLoadResult {
168    let config_path = match Self::find_config_path(path) {
169      Some(p) => p,
170      None => return ConfigLoadResult::NotFound,
171    };
172
173    let content = match fs::read_to_string(&config_path) {
174      Ok(c) => c,
175      Err(e) => {
176        return ConfigLoadResult::ParseError {
177          path: config_path,
178          message: format!("failed to read file: {}", e),
179        };
180      }
181    };
182
183    match toml_edit::de::from_str(&content) {
184      Ok(config) => ConfigLoadResult::Loaded(Box::new(config)),
185      Err(e) => ConfigLoadResult::ParseError {
186        path: config_path,
187        message: e.to_string(),
188      },
189    }
190  }
191
192  /// Get all crates that have split configuration
193  pub fn get_split_crates(&self) -> Vec<(&str, &CrateSplitConfig)> {
194    self
195      .crates
196      .iter()
197      .filter_map(|(name, config)| config.split.as_ref().map(|split| (name.as_str(), split)))
198      .collect()
199  }
200
201  /// Build all SplitConfigs from unified crate config
202  pub fn build_split_configs(&self) -> Vec<SplitConfig> {
203    self
204      .crates
205      .iter()
206      .filter_map(|(name, config)| {
207        config.split.as_ref().map(|split_cfg| {
208          split::build_split_config(
209            name.clone(),
210            split_cfg,
211            config.release.as_ref().map(|r| r.publish),
212            config.changelog.as_ref(),
213          )
214        })
215      })
216      .collect()
217  }
218}