use std::env;
use std::process::ExitCode;
use flodl_cli::{
cluster, config, dispatch, gpus, overlay, prebuild, run, schema_cache,
};
use flodl_cli::cli_error;
use dispatch::{walk_commands, WalkOutcome};
use crate::print_usage;
pub(crate) fn load_project_config(
cwd: &std::path::Path,
env: Option<&str>,
) -> Option<(config::ProjectConfig, std::path::PathBuf)> {
let config_path = config::find_config(cwd)?;
let root = config_path.parent()?.to_path_buf();
match config::load_project_with_env(&config_path, env) {
Ok(project) => Some((project, root)),
Err(e) => {
cli_error!("{e}");
std::process::exit(2);
}
}
}
pub(crate) fn dispatch_config(
cmd: &str,
args: &[String],
env: Option<&str>,
no_append: bool,
no_prebuild: bool,
gpus_spec: Option<&gpus::GpusSpec>,
) -> ExitCode {
let cwd = env::current_dir().unwrap_or_default();
let (project, project_root) = match load_project_config(&cwd, env) {
Some(pair) => pair,
None => {
eprintln!("unknown command: {cmd}");
eprintln!();
print_usage();
return ExitCode::FAILURE;
}
};
if env.is_some()
&& let Some(cluster) = project.cluster.as_ref()
{
match cluster::prepare_test_cluster_env(cluster) {
Ok(hex) => {
unsafe {
env::set_var("FLODL_TESTING_CLUSTER_JSON", hex);
}
}
Err(e) => {
eprintln!(
"warning: cluster-test envelope export failed: {e}"
);
}
}
}
let tail: &[String] = args.get(2..).unwrap_or(&[]);
let outcome = walk_commands(cmd, tail, &project.commands, &project_root, env);
let cmd_cwd: std::path::PathBuf = match &outcome {
WalkOutcome::RunScript { cwd, .. } => cwd.clone(),
WalkOutcome::ExecCommand { cmd_dir, .. } => cmd_dir.clone(),
_ => project_root.clone(),
};
let cluster_chain: Option<&[Option<bool>]> = match &outcome {
WalkOutcome::RunScript { cluster_chain, .. } => Some(cluster_chain.as_slice()),
WalkOutcome::ExecCommand { cluster_chain, .. } => Some(cluster_chain.as_slice()),
_ => None,
};
let wants_cluster = cluster_chain
.map(config::resolve_cluster_dispatch)
.unwrap_or(false)
&& !cluster::is_recursive_invocation();
let cluster_to_dispatch: Option<config::ClusterConfig> = if wants_cluster {
match (project.cluster.as_ref(), gpus_spec) {
(Some(_), Some(_)) => {
cli_error!(
"--gpus cannot be combined with a `cluster:` block in \
fdl.yml; remove one or the other"
);
return ExitCode::FAILURE;
}
(Some(c), None) => Some(c.clone()),
(None, Some(spec)) => {
let devs = match spec.resolve() {
Ok(d) => d,
Err(e) => {
cli_error!("{e}");
return ExitCode::FAILURE;
}
};
if devs.len() < 2 {
unsafe { gpus::apply_cuda_visible_devices(&devs) };
None
} else {
match gpus::synthesize_local_cluster(&devs) {
Ok(c) => Some(c),
Err(e) => {
cli_error!("{e}");
return ExitCode::FAILURE;
}
}
}
}
(None, None) => {
if let Ok(n) = gpus::count_visible_gpus_via_nvidia_smi() {
if n >= 2 {
eprintln!(
"flodl: {n} GPUs visible but cluster mode is off; \
running single-device on GPU 0. Use --gpus all \
for multi-GPU."
);
}
}
None
}
}
} else {
if let Some(spec) = gpus_spec {
let devs = match spec.resolve() {
Ok(d) => d,
Err(e) => {
cli_error!("{e}");
return ExitCode::FAILURE;
}
};
unsafe { gpus::apply_cuda_visible_devices(&devs) };
}
None
};
if let Some(cluster) = cluster_to_dispatch {
if let Err(e) = cluster::validate_net_timeout_scale() {
cli_error!("{e}");
return ExitCode::FAILURE;
}
let controller = cluster::resolve_local_hostname();
if let Err(e) = prebuild::preflight_hosts(&cluster, &controller, !no_prebuild) {
cli_error!("{e}");
return ExitCode::FAILURE;
}
if !no_prebuild {
if let Err(e) = prebuild::prebuild_remotes(
&project_root, &cmd_cwd, &cluster, cmd, &controller,
) {
cli_error!("{e}");
return ExitCode::FAILURE;
}
}
match cluster::prepare_cluster_env(&cluster, env, cmd) {
Ok(warnings) => {
for w in warnings {
eprintln!("fdl: warning: {w}");
}
}
Err(e) => {
cli_error!("{e}");
return ExitCode::FAILURE;
}
}
}
match outcome {
WalkOutcome::RunScript {
command,
append,
user_args,
docker,
cwd,
cluster_chain: _,
} => {
let effective_append = if no_append { None } else { append.as_deref() };
run::exec_script(
&command,
effective_append,
&user_args,
docker.as_deref(),
&cwd,
)
}
WalkOutcome::ExecCommand {
config: cmd_config,
preset,
tail,
cmd_dir,
cluster_chain: _,
} => {
run::exec_command(&cmd_config, preset.as_deref(), &tail, &cmd_dir, &project_root)
}
WalkOutcome::RefreshSchema {
config,
cmd_dir,
cmd_name,
} => cmd_refresh_schema(&config, &cmd_dir, &cmd_name),
WalkOutcome::PrintCommandHelp { config, name } => {
run::print_command_help(&config, &name);
ExitCode::SUCCESS
}
WalkOutcome::PrintPresetHelp {
config,
parent_label,
preset_name,
} => {
run::print_preset_help(&config, &parent_label, &preset_name);
ExitCode::SUCCESS
}
WalkOutcome::PrintRunHelp {
name,
description,
run,
append,
docker,
} => {
run::print_run_help(
&name,
description.as_deref(),
&run,
append.as_deref(),
docker.as_deref(),
);
ExitCode::SUCCESS
}
WalkOutcome::UnknownCommand { name } => {
eprintln!("unknown command: {name}");
if let Some(base) = config::find_config(&cwd)
&& overlay::find_env_file(&base, &name).is_some()
{
eprintln!();
eprintln!(
"`{name}` is an env overlay (fdl.{name}.yml), not a command. \
Select it with the `@` sigil: `fdl @{name} <command>`."
);
}
eprintln!();
run::print_project_help(&project, &project_root, env);
ExitCode::FAILURE
}
WalkOutcome::PresetAtTopLevel { name } => {
eprintln!(
"error: preset command `{name}` has no enclosing \
fdl.yml (top-level commands must be `run:` or `path:`)"
);
ExitCode::FAILURE
}
WalkOutcome::Error(msg) => {
cli_error!("{msg}");
ExitCode::FAILURE
}
}
}
pub(crate) fn cmd_config_show(tail: &[String], active_env: Option<&str>) -> ExitCode {
let sub = tail.get(1).map(String::as_str).unwrap_or("--help");
match sub {
"show" => {}
"--help" | "-h" => {
print_config_usage();
return ExitCode::SUCCESS;
}
other => {
eprintln!("unknown config sub-command: {other}");
eprintln!();
print_config_usage();
return ExitCode::FAILURE;
}
}
let explicit_env = tail.get(2).map(String::as_str);
let target_env = explicit_env.or(active_env);
let cwd = env::current_dir().unwrap_or_default();
let base = match config::find_config(&cwd) {
Some(p) => p,
None => {
cli_error!("no fdl.yml found in {} or parent directories", cwd.display());
return ExitCode::FAILURE;
}
};
let layers = match config::resolve_config_layers(&base, target_env) {
Ok(ls) => ls,
Err(e) => {
cli_error!("{e}");
return ExitCode::FAILURE;
}
};
let labels: Vec<String> = layers
.iter()
.map(|(p, _)| {
p.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?")
.to_string()
})
.collect();
let values: Vec<serde_yaml_ng::Value> =
layers.iter().map(|(_, v)| v.clone()).collect();
let annotated = overlay::merge_layers_annotated(&values);
print!("{}", overlay::render_annotated_yaml(&annotated, &labels));
ExitCode::SUCCESS
}
fn print_config_usage() {
println!("fdl config -- inspect resolved project configuration");
println!();
println!("USAGE:");
println!(" fdl config show [<env>]");
println!();
println!("Without an env argument, prints the base fdl.yml. With an env argument");
println!("(e.g. `fdl config show ci`), prints the base deep-merged with");
println!("fdl.<env>.yml. When invoked through the `@` sigil form");
println!("(`fdl @ci config show`), the env is already active and no extra");
println!("argument is needed.");
}
pub(crate) fn cmd_refresh_schema(
cmd_config: &config::CommandConfig,
cmd_dir: &std::path::Path,
cmd_name: &str,
) -> ExitCode {
let entry = match &cmd_config.entry {
Some(e) => e.as_str(),
None => {
eprintln!(
"error: no entry point defined in {}/fdl.yml",
cmd_dir.display()
);
return ExitCode::FAILURE;
}
};
eprintln!("Probing `{entry} --fdl-schema`...");
let schema = match schema_cache::probe(entry, cmd_dir, cmd_config.docker.as_deref()) {
Ok(s) => s,
Err(e) => {
cli_error!("{e}");
if schema_cache::is_cargo_entry(entry) {
eprintln!();
eprintln!("Hint: cargo-based entries must be built first.");
eprintln!("Build with the right features, then rerun this command.");
}
return ExitCode::FAILURE;
}
};
let cache = schema_cache::cache_path(cmd_dir, cmd_name);
if let Err(e) = schema_cache::write_cache(&cache, &schema) {
cli_error!("{e}");
return ExitCode::FAILURE;
}
eprintln!("Cached schema for `{cmd_name}` at {}", cache.display());
if schema.commands.is_empty() {
eprintln!(
" {} options, {} positional args",
schema.options.len(),
schema.args.len()
);
} else {
eprintln!(" {} subcommands", schema.commands.len());
}
ExitCode::SUCCESS
}