mod change_detection;
mod release;
mod run;
pub mod schema;
mod split;
mod unify;
pub use change_detection::{ChangeDetectionConfig, ConfidenceProfile};
pub use release::{ChangelogConfig, ChangelogRelativeTo, CrateReleaseConfig, ReleaseConfig};
pub use run::{RunConfig, RunProfile, is_builtin_profile};
pub use split::{CratePath, CrateSplitConfig, CrateSyncConfig, SplitConfig, SplitMode, WorkspaceMode};
pub use unify::{ExactPinHandling, MajorVersionConflict, MsrvSource, TransitiveFeatureHost, UnifyConfig};
use crate::error::{ConfigError, RailError, RailResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RailConfig {
#[serde(default)]
pub targets: Vec<String>,
#[serde(default)]
pub unify: UnifyConfig,
#[serde(default)]
pub release: ReleaseConfig,
#[serde(default, rename = "change-detection")]
pub change_detection: ChangeDetectionConfig,
#[serde(default)]
pub run: RunConfig,
#[serde(default)]
pub crates: HashMap<String, CrateConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CrateConfig {
pub split: Option<CrateSplitConfig>,
pub release: Option<CrateReleaseConfig>,
pub changelog: Option<ChangelogConfig>,
pub sync: Option<CrateSyncConfig>,
}
pub enum ConfigLoadResult {
Loaded(Box<RailConfig>),
ParseError {
path: PathBuf,
message: String,
},
NotFound,
}
impl RailConfig {
pub fn find_config_path(path: &Path) -> Option<PathBuf> {
let candidates = [
path.join("rail.toml"),
path.join(".rail.toml"),
path.join(".cargo").join("rail.toml"),
path.join(".config").join("rail.toml"),
];
if let Some(found) = candidates.iter().find(|p| p.exists()) {
return Some(found.to_path_buf());
}
#[cfg(target_os = "windows")]
{
if let Ok(canonical) = path.canonicalize() {
let canonical_candidates = [
canonical.join("rail.toml"),
canonical.join(".rail.toml"),
canonical.join(".cargo").join("rail.toml"),
canonical.join(".config").join("rail.toml"),
];
if let Some(found) = canonical_candidates.iter().find(|p| p.exists()) {
return Some(found.to_path_buf());
}
}
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if file_name_str == "rail.toml" || file_name_str == ".rail.toml" {
return Some(entry.path());
}
}
}
for subdir in &[".cargo", ".config"] {
let subdir_path = path.join(subdir);
if let Ok(entries) = std::fs::read_dir(&subdir_path) {
for entry in entries.flatten() {
let file_name = entry.file_name();
if file_name.to_string_lossy() == "rail.toml" {
return Some(entry.path());
}
}
}
}
}
None
}
pub fn load(path: &Path) -> RailResult<Self> {
match Self::try_load(path) {
ConfigLoadResult::Loaded(config) => Ok(*config),
ConfigLoadResult::ParseError { path, message } => {
Err(RailError::Config(ConfigError::ParseError { path, message }))
}
ConfigLoadResult::NotFound => Err(RailError::Config(ConfigError::NotFound {
workspace_root: path.to_path_buf(),
})),
}
}
pub fn try_load(path: &Path) -> ConfigLoadResult {
let config_path = match Self::find_config_path(path) {
Some(p) => p,
None => return ConfigLoadResult::NotFound,
};
let content = match fs::read_to_string(&config_path) {
Ok(c) => c,
Err(e) => {
return ConfigLoadResult::ParseError {
path: config_path,
message: format!("failed to read file: {}", e),
};
}
};
match toml_edit::de::from_str(&content) {
Ok(config) => ConfigLoadResult::Loaded(Box::new(config)),
Err(e) => ConfigLoadResult::ParseError {
path: config_path,
message: e.to_string(),
},
}
}
pub fn get_split_crates(&self) -> Vec<(&str, &CrateSplitConfig)> {
self
.crates
.iter()
.filter_map(|(name, config)| config.split.as_ref().map(|split| (name.as_str(), split)))
.collect()
}
pub fn build_split_configs(&self) -> Vec<SplitConfig> {
self
.crates
.iter()
.filter_map(|(name, config)| {
config.split.as_ref().map(|split_cfg| {
split::build_split_config(
name.clone(),
split_cfg,
config.release.as_ref().map(|r| r.publish),
config.changelog.as_ref(),
)
})
})
.collect()
}
}