#![deny(unsafe_code)]
use std::process;
#[cfg(feature = "completions")]
use clap::CommandFactory;
mod cli;
mod dispatch;
mod generate;
mod resolve;
mod startup;
use cli::*;
use generate::write_quadlet;
use resolve::*;
use startup::{init_tracing, internal_error_notice, is_mutating, parse_cli};
fn main() {
std::panic::set_hook(Box::new(|info| {
eprintln!("podup: internal error: {info}");
eprintln!("{}", internal_error_notice());
}));
std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.name("podup".into())
.spawn(run_to_exit)
.expect("spawn podup worker thread")
.join()
.expect("podup worker thread panicked");
}
fn run_to_exit() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("build Tokio runtime");
match runtime.block_on(run()) {
Ok(()) => {}
Err(podup::ComposeError::RunExited(code)) => process::exit(code as i32),
#[cfg(feature = "update")]
Err(e @ podup::ComposeError::Update(_)) => {
print_error(&e);
process::exit(podup::update::exit_code(&e));
}
Err(e) => {
print_error(&e);
process::exit(1);
}
}
}
fn print_error(e: &podup::ComposeError) {
use std::io::Write;
let style = podup::ui::error_style();
let mut err = anstream::stderr();
let _ = writeln!(
err,
"podup: {}error:{} {e}",
style.render(),
style.render_reset()
);
}
async fn run() -> podup::Result<()> {
let cli = parse_cli();
podup::ui::set_color_choice(cli.ansi.into());
let log_floor = if matches!(cli.command, Commands::Watch) {
"info"
} else {
"warn"
};
init_tracing(log_floor);
#[cfg(feature = "completions")]
if let Commands::Completions { shell } = cli.command {
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
let mut buf = Vec::new();
clap_complete::generate(shell, &mut cmd, name, &mut buf);
match std::io::Write::write_all(&mut std::io::stdout(), &buf) {
Ok(()) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => return Ok(()),
Err(e) => return Err(e.into()),
}
}
#[cfg(feature = "update")]
if let Commands::Update { check, force } = cli.command {
let opts = podup::update::UpdateOptions {
check_only: check,
force,
};
return tokio::task::spawn_blocking(move || podup::update::run(opts))
.await
.map_err(|e| podup::ComposeError::Update(format!("update task failed: {e}")))?;
}
if let Commands::Ls { all, quiet, format } = &cli.command {
let client = podup::podman::connect(cli.socket.as_deref())?;
return podup::list_projects(
&client,
podup::LsOptions {
all: *all,
quiet: *quiet,
json: *format == OutputFormat::Json,
},
)
.await;
}
let compose_files = resolve_compose_files(&cli.file);
let file = podup::parse_files_with_env_files(&compose_files, &cli.env_file)?;
if let Commands::Config {
format,
services,
quiet,
no_interpolate,
resolve_image_digests,
} = &cli.command
{
let parsed = if *no_interpolate {
podup::parse_files_with_env_files_interp(&compose_files, &cli.env_file, false)?
} else {
file
};
let mut resolved = if *resolve_image_digests {
let client = podup::podman::connect(cli.socket.as_deref())?;
podup::resolve_image_digests(&client, &parsed).await?
} else {
parsed
};
podup::retain_active_profiles(&mut resolved, &cli.profile);
return startup::render_config(&resolved, format, *services, *quiet);
}
let base_dir = resolve_base_dir(cli.project_directory.as_deref(), &compose_files[0]);
let project = resolve_project_name(cli.project, file.name.as_deref(), &base_dir);
if !podup::is_safe_project_name(&project) {
return Err(podup::ComposeError::Unsupported(format!(
"project name {project:?} is not a safe path component: use only ASCII \
letters, digits, '-', '_', '.', not starting with '.', max 128 chars"
)));
}
if let Commands::Generate {
kind: GenerateCommands::Quadlet { output },
} = &cli.command
{
return write_quadlet(&file, &project, output.as_deref());
}
let client = podup::podman::connect(cli.socket.as_deref())?;
let stop_timeout = match &cli.command {
Commands::Up { timeout, .. }
| Commands::Down { timeout, .. }
| Commands::Stop { timeout, .. }
| Commands::Restart { timeout, .. } => *timeout,
_ => None,
};
let scale_overrides: std::collections::HashMap<String, u32> = match &cli.command {
Commands::Up { scale, .. } => scale.iter().cloned().collect(),
Commands::Scale { pairs } => pairs.iter().cloned().collect(),
_ => std::collections::HashMap::new(),
};
let (pull_override, no_build, quiet_pull) = match &cli.command {
Commands::Up {
pull,
no_build,
quiet_pull,
..
} => (pull.clone(), *no_build, *quiet_pull),
Commands::Pull { quiet, policy, .. } => (policy.clone(), false, *quiet),
_ => (None, false, false),
};
let renew_anon_volumes = matches!(
&cli.command,
Commands::Up {
renew_anon_volumes: true,
..
}
);
let engine = podup::Engine::with_base_dir(client, project, base_dir)
.with_stop_timeout(stop_timeout)
.with_scale_overrides(scale_overrides)
.with_up_overrides(pull_override, no_build, quiet_pull)
.with_run_overrides(startup::run_overrides_for(&cli.command))
.with_renew_anon_volumes(renew_anon_volumes);
let _lock = if is_mutating(&cli.command) {
Some(engine.lock_project()?)
} else {
None
};
dispatch::dispatch(&engine, &file, cli.command, &cli.profile).await
}