1mod change_detection;
14mod release;
15mod run;
16pub mod schema;
17mod split;
18mod unify;
19
20pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RailConfig {
37 #[serde(default)]
40 pub targets: Vec<String>,
41 #[serde(default)]
43 pub unify: UnifyConfig,
44 #[serde(default)]
46 pub release: ReleaseConfig,
47 #[serde(default, rename = "change-detection")]
49 pub change_detection: ChangeDetectionConfig,
50 #[serde(default)]
52 pub run: RunConfig,
53 #[serde(default)]
55 pub crates: HashMap<String, CrateConfig>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Default)]
60pub struct CrateConfig {
61 pub split: Option<CrateSplitConfig>,
63 pub release: Option<CrateReleaseConfig>,
65 pub changelog: Option<ChangelogConfig>,
67 pub sync: Option<CrateSyncConfig>,
69}
70
71pub enum ConfigLoadResult {
73 Loaded(Box<RailConfig>),
75 ParseError {
77 path: PathBuf,
79 message: String,
81 },
82 NotFound,
84}
85
86impl RailConfig {
87 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 if let Some(found) = candidates.iter().find(|p| p.exists()) {
101 return Some(found.to_path_buf());
102 }
103
104 #[cfg(target_os = "windows")]
107 {
108 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 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 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 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 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 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 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}