podbox-cli 0.7.1

Declarative Podman-native container environment manager. Define an environment as a TOML file and let systemd own its lifecycle.
Documentation
//! Custom systemd unit writing, `podman quadlet install` invocation
//! (flat-file and `--application` variants), and leftover-unit removal.
//!
//! Extracted verbatim from `quadlet_install.rs`.

use std::path::Path;

use anyhow::Result;

use crate::systemd;

use super::paths::quadlet_dir;

/// Write custom systemd units (socket, host-service, optional dbus-proxy
/// and compositor) to sdir.
fn write_custom_units(
    name: &str,
    sdir: &Path,
    socket_content: &str,
    host_service_content: &str,
    dbus_proxy_content: Option<&str>,
    compositor_service_content: Option<&str>,
) -> Result<()> {
    std::fs::create_dir_all(sdir)?;
    std::fs::write(sdir.join(format!("{name}.socket")), socket_content)?;
    std::fs::write(
        sdir.join(format!("{name}-host.service")),
        host_service_content,
    )?;
    if let Some(proxy) = dbus_proxy_content {
        std::fs::write(sdir.join(format!("{name}-proxy.service")), proxy)?;
    }
    if let Some(comp) = compositor_service_content {
        std::fs::write(sdir.join(format!("{name}-compositor.service")), comp)?;
    }
    write_clean_stop_dropin(name, sdir)?;
    Ok(())
}

/// Container service units are generated by Quadlet, which cannot express
/// `SuccessExitStatus`. When the guest's idle timer fires, the host stops the
/// unit via `systemctl stop`; systemd SIGTERMs the container, the main process
/// exits 143, and the unit would otherwise land in `failed`. A drop-in marks
/// SIGTERM (and a graceful 0 exit) as a clean stop so idle shutdown settles in
/// `inactive` instead of `failed`.
fn write_clean_stop_dropin(name: &str, sdir: &Path) -> Result<()> {
    let dir = sdir.join(format!("{name}.service.d"));
    std::fs::create_dir_all(&dir)?;
    std::fs::write(
        dir.join("99-podbox-clean-stop.conf"),
        "[Service]\nSuccessExitStatus=0 143 SIGTERM SIGINT\n",
    )?;
    Ok(())
}

/// Activate custom units after Quadlet files are in place.
pub(crate) fn finalize_units(
    name: &str,
    sdir: &Path,
    socket_content: &str,
    host_service_content: &str,
    dbus_proxy_content: Option<&str>,
    compositor_service_content: Option<&str>,
    use_wayland_proxy: bool,
) -> Result<()> {
    write_custom_units(
        name,
        sdir,
        socket_content,
        host_service_content,
        dbus_proxy_content,
        compositor_service_content,
    )?;
    println!("Systemd units installed to {}", sdir.display());

    systemd::daemon_reload()?;
    systemd::reset_failed(name)?;
    systemd::stop_socket_and_host(name)?;
    if use_wayland_proxy {
        systemd::stop_compositor(name)?;
    }
    systemd::enable_now_socket(name)?;
    Ok(())
}

/// Install `.container` (+ optional `.build`) via `podman quadlet install` using
/// **file** arguments, not a directory.
///
/// Keeps the flat layout (`…/systemd/<name>.container`) for Podman 5.6–5.x.
pub(crate) fn podman_quadlet_install_files(
    name: &str,
    container_content: &str,
    build_content: Option<&str>,
) -> Result<()> {
    let tmp = std::env::temp_dir().join(format!("podbox-install-{name}"));
    let _ = std::fs::remove_dir_all(&tmp);
    std::fs::create_dir_all(&tmp)?;

    let container_path = tmp.join(format!("{name}.container"));
    std::fs::write(&container_path, container_content)?;

    let mut args: Vec<std::ffi::OsString> = vec![
        "quadlet".into(),
        "install".into(),
        "--replace".into(),
        container_path.into(),
    ];

    if let Some(bc) = build_content {
        let build_path = tmp.join(format!("{name}.build"));
        std::fs::write(&build_path, bc)?;
        args.push(build_path.into());
    }

    let output = crate::process::run_piped("podman", &args)?;
    let _ = std::fs::remove_dir_all(&tmp);
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("podman quadlet install failed: {stderr}");
    }
    println!("Quadlet files installed via podman quadlet install.");
    Ok(())
}

/// Install `.container` (+ optional `.build`) via `podman quadlet install` using
/// **directory** arguments with `--application` for Podman 6.x.
///
/// Podman 6 requires `--application` when the source is a directory. The units
/// end up at `…/systemd/<name>/<name>.container`.
pub(crate) fn podman_quadlet_install_application(
    name: &str,
    container_content: &str,
    build_content: Option<&str>,
) -> Result<()> {
    let tmp = std::env::temp_dir().join(format!("podbox-install-{name}"));
    let _ = std::fs::remove_dir_all(&tmp);
    std::fs::create_dir_all(&tmp)?;

    std::fs::write(tmp.join(format!("{name}.container")), container_content)?;
    if let Some(bc) = build_content {
        std::fs::write(tmp.join(format!("{name}.build")), bc)?;
    }

    let args: Vec<std::ffi::OsString> = vec![
        "quadlet".into(),
        "install".into(),
        "--replace".into(),
        "--application".into(),
        name.into(),
        tmp.clone().into(),
    ];

    let output = crate::process::run_piped("podman", &args)?;
    let _ = std::fs::remove_dir_all(&tmp);
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("podman quadlet install --application failed: {stderr}");
    }
    println!("Quadlet files installed via podman quadlet install --application {name}.");
    Ok(())
}

/// Best-effort removal of leftover flat unit files (`.container`, `.build`).
///
/// Called before a `--application` install to avoid dual installs from old
/// flat layouts.
pub(crate) fn remove_flat_units(name: &str) {
    let qdir = quadlet_dir();
    for ext in ["container", "build"] {
        let path = qdir.join(format!("{name}.{ext}"));
        if !path.exists() {
            continue;
        }
        // Best-effort podman quadlet rm first, then manual delete as fallback.
        let args: Vec<std::ffi::OsString> = vec![
            "quadlet".into(),
            "rm".into(),
            format!("{name}.{ext}").into(),
        ];
        let _ = crate::process::run_piped("podman", &args);
        // Manual fallback in case podman rm failed.
        let _ = std::fs::remove_file(&path);
    }
}

/// Best-effort removal of leftover application-scoped install dirs.
pub(crate) fn remove_application_dir(name: &str) {
    let app_dir = quadlet_dir().join(name);
    if app_dir.is_dir() {
        let _ = std::fs::remove_dir_all(&app_dir);
    }
}