mod change_detection;
mod release;
mod run;
pub(crate) mod schema;
mod split;
mod unify;
pub use change_detection::{ChangeDetectionConfig, ConfidenceProfile, UnknownFilePolicy};
pub use release::{
ChangelogConfig, ChangelogFilters, ChangelogRelativeTo, ChangelogShape, CommitPolicy, CrateReleaseConfig, GroupSpec,
Pre1BreakingBump, ReleaseConfig, ReleaseRemoteEffects, ReleaseSource, RequireChangeFiles, SemverCheckPolicy,
};
pub(crate) use run::{BUILTIN_ACTION_NAMES, MAX_ACTIONS, first_repository_output_overlap};
pub use run::{
CargoEnvironmentValue, RepositoryAction, RepositoryActionKind, RepositoryEnvironment, RepositoryEnvironmentEntry,
RepositoryPackageSelection, RunBaseline, RunConfig, RunProfile, is_builtin_profile,
};
pub use split::{CratePath, CrateSplitConfig, SplitConfig, SplitMode, WorkspaceMode};
pub use unify::{
ConsumerScope, ExactPinHandling, MajorVersionConflict, MsrvPolicy, MsrvSource, TransitiveFeatureHost,
TransitivePinning, UnifyConfig,
};
use crate::error::{ConfigError, RailError, RailResult};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
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: BTreeMap<String, CrateConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CrateConfig {
pub split: Option<CrateSplitConfig>,
pub release: Option<CrateReleaseConfig>,
pub changelog: Option<ChangelogConfig>,
}
pub enum ConfigLoadResult {
Loaded(Box<RailConfig>),
ParseError {
path: PathBuf,
message: String,
},
NotFound,
}
impl RailConfig {
fn parse_path(path: &Path) -> Result<(Self, Vec<u8>), String> {
let bytes = fs::read(path).map_err(|error| format!("failed to read file: {error}"))?;
let content = std::str::from_utf8(&bytes).map_err(|error| format!("file is not valid UTF-8: {error}"))?;
let doc: toml_edit::DocumentMut = content
.parse()
.map_err(|error: toml_edit::TomlError| error.to_string())?;
for deprecation in schema::present_deprecations(&doc) {
if let Some(message) = deprecation.spec.deprecation {
crate::warn!("{} in {}: {}", deprecation.path, path.display(), message);
}
}
let config = toml_edit::de::from_document(doc).map_err(|error| error.to_string())?;
Ok((config, bytes))
}
pub(crate) fn load_path_with_bytes(path: &Path) -> RailResult<(Self, Vec<u8>)> {
Self::parse_path(path).map_err(|message| {
RailError::Config(ConfigError::ParseError {
path: path.to_path_buf(),
message,
})
})
}
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,
};
match Self::parse_path(&config_path) {
Ok((config, _)) => ConfigLoadResult::Loaded(Box::new(config)),
Err(message) => ConfigLoadResult::ParseError {
path: config_path,
message,
},
}
}
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()
}
}