use std::path::{Path, PathBuf};
use super::schema::ProjectConfig;
pub fn resolve_cluster_dispatch(chain: &[Option<bool>]) -> bool {
chain.iter().rev().find_map(|x| *x).unwrap_or(false)
}
pub fn cluster_dispatch_enabled(project: &ProjectConfig, chain: &[Option<bool>]) -> bool {
project.cluster.is_some() && resolve_cluster_dispatch(chain)
}
pub(super) const CONFIG_NAMES: &[&str] = &["fdl.yaml", "fdl.yml", "fdl.json"];
pub(super) const EXAMPLE_SUFFIXES: &[&str] = &[".example", ".dist"];
pub fn find_config(start: &Path) -> Option<PathBuf> {
let mut dir = start.to_path_buf();
loop {
for name in CONFIG_NAMES {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
for name in CONFIG_NAMES {
for suffix in EXAMPLE_SUFFIXES {
let example = dir.join(format!("{name}{suffix}"));
if example.is_file() {
let target = dir.join(name);
if try_copy_example(&example, &target) {
return Some(target);
}
return Some(example);
}
}
}
if !dir.pop() {
return None;
}
}
}
pub fn find_config_in(dir: &Path) -> Option<PathBuf> {
CONFIG_NAMES.iter().map(|n| dir.join(n)).find(|c| c.is_file())
}
pub(super) fn try_copy_example(example: &Path, target: &Path) -> bool {
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return false;
}
let example_name = example.file_name().unwrap_or_default().to_string_lossy();
let target_name = target.file_name().unwrap_or_default().to_string_lossy();
eprintln!(
"fdl: found {example_name} but no {target_name}. \
Copy it to create your local config? [Y/n] "
);
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_err() {
return false;
}
let answer = input.trim().to_lowercase();
if answer.is_empty() || answer == "y" || answer == "yes" {
match std::fs::copy(example, target) {
Ok(_) => {
eprintln!("fdl: created {target_name} (edit to customize)");
true
}
Err(e) => {
eprintln!("fdl: failed to copy: {e}");
false
}
}
} else {
false
}
}
pub fn load_project(path: &Path) -> Result<ProjectConfig, String> {
load_project_with_env(path, None)
}
pub fn load_project_with_env(
base_path: &Path,
env: Option<&str>,
) -> Result<ProjectConfig, String> {
let layers = resolve_config_layers(base_path, env)?;
let merged = crate::overlay::merge_layers(
layers.iter().map(|(_, v)| v.clone()).collect::<Vec<_>>(),
);
if merged.is_null() {
return Ok(ProjectConfig::default());
}
let merged_str = serde_yaml_ng::to_string(&merged).map_err(|e| {
format!(
"{}: failed to re-serialize merged YAML for diagnostics: {e}",
base_path.display()
)
})?;
let cfg = serde_yaml_ng::from_str::<ProjectConfig>(&merged_str).map_err(|e| {
let names: Vec<String> = layers
.iter()
.map(|(p, _)| {
p.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?")
.to_string()
})
.collect();
let env_hint = env
.map(|n| format!(" {n}"))
.unwrap_or_default();
let (loc_str, context) = match e.location() {
Some(loc) => (
format!(" at merged-view line {}, col {}", loc.line(), loc.column()),
extract_context(&merged_str, loc.line()),
),
None => (String::new(), String::new()),
};
format!(
"{} (layers: {}){}: {}{}\n inspect merged view: fdl{} config show",
base_path.display(),
names.join(" + "),
loc_str,
e,
context,
env_hint
)
})?;
reject_user_ranks(&cfg, base_path)?;
Ok(cfg)
}
fn reject_user_ranks(cfg: &ProjectConfig, base_path: &Path) -> Result<(), String> {
let Some(cluster) = &cfg.cluster else {
return Ok(());
};
for (i, w) in cluster.workers.iter().enumerate() {
if !w.ranks.is_empty() {
return Err(format!(
"{}: cluster.workers[{i}] ({:?}) declares `ranks:`, which is \
not user configuration; ranks are computed from probed device \
counts at launch. Remove the key.",
base_path.display(),
w.host,
));
}
}
Ok(())
}
fn extract_context(text: &str, line_no: usize) -> String {
if line_no == 0 {
return String::new();
}
let lines: Vec<&str> = text.lines().collect();
if line_no > lines.len() {
return String::new();
}
let start = line_no.saturating_sub(2).max(1);
let end = (line_no + 1).min(lines.len());
let mut out = String::from("\n");
for n in start..=end {
let marker = if n == line_no { ">>" } else { " " };
out.push_str(&format!(" {marker} {n:>4}: {}\n", lines[n - 1]));
}
out
}
pub fn load_merged_value(
base_path: &Path,
env: Option<&str>,
) -> Result<serde_yaml_ng::Value, String> {
let layers = resolve_config_layers(base_path, env)?;
Ok(crate::overlay::merge_layers(
layers.into_iter().map(|(_, v)| v).collect::<Vec<_>>(),
))
}
pub fn resolve_config_layers(
base_path: &Path,
env: Option<&str>,
) -> Result<Vec<(PathBuf, serde_yaml_ng::Value)>, String> {
let mut layers = crate::overlay::resolve_chain(base_path)?;
if let Some(name) = env {
match crate::overlay::find_env_file(base_path, name) {
Some(p) => {
let env_chain = crate::overlay::resolve_chain(&p)?;
layers.extend(env_chain);
}
None => {
return Err(format!(
"environment `{name}` not found (expected fdl.{name}.yml next to {})",
base_path.display()
));
}
}
}
let mut seen = std::collections::HashSet::new();
layers.retain(|(path, _)| seen.insert(path.clone()));
Ok(layers)
}
pub fn config_layer_sources(base_path: &Path, env: Option<&str>) -> Vec<PathBuf> {
resolve_config_layers(base_path, env)
.map(|ls| ls.into_iter().map(|(p, _)| p).collect())
.unwrap_or_else(|_| vec![base_path.to_path_buf()])
}