cargo-reclaim 0.2.2

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::path::{Path, PathBuf};

use super::generate::generate_scheduler_artifacts;
use super::model::{
    GeneratedArtifact, GeneratedArtifactKind, SchedulerError, SchedulerOperation,
    SchedulerOperationPlan, SchedulerPlanStep, SchedulerPlatform, SchedulerRequest,
    validate_scheduler_instance_name,
};
use super::paths::SchedulerPaths;

pub fn plan_scheduler_install(
    request: SchedulerRequest,
) -> Result<SchedulerOperationPlan, SchedulerError> {
    validate_scheduler_instance_name(&request.instance_name)?;
    let paths = SchedulerPaths::new(&request);
    let report = generate_scheduler_artifacts(request)?;
    let artifacts = operation_artifacts(report.platform, report.artifacts, &paths);
    let mut steps = ensure_artifact_dirs(&artifacts);

    steps.extend(
        artifacts
            .iter()
            .map(|artifact| SchedulerPlanStep::WriteFile {
                path: artifact.intended_install_path.clone(),
                artifact_kind: artifact.kind,
            }),
    );

    steps.extend(
        artifacts
            .iter()
            .filter(|artifact| {
                artifact.kind == GeneratedArtifactKind::RunnerScript
                    && matches!(
                        report.platform,
                        SchedulerPlatform::SystemdUser | SchedulerPlatform::Launchd
                    )
            })
            .map(|artifact| SchedulerPlanStep::SetExecutable {
                path: artifact.intended_install_path.clone(),
            }),
    );

    steps.extend(install_commands(report.platform, &artifacts, &paths));

    Ok(SchedulerOperationPlan {
        command: "scheduler-install",
        operation: SchedulerOperation::Install,
        dry_run: true,
        platform: report.platform,
        artifacts,
        steps,
    })
}

pub fn plan_scheduler_uninstall(
    request: SchedulerRequest,
) -> Result<SchedulerOperationPlan, SchedulerError> {
    validate_scheduler_instance_name(&request.instance_name)?;
    let paths = SchedulerPaths::new(&request);
    let platform = request.platform;
    let artifacts = uninstall_artifacts(platform, &paths);
    let mut steps = uninstall_commands(platform, &paths);

    steps.extend(
        artifacts
            .iter()
            .map(|artifact| SchedulerPlanStep::RemoveFile {
                path: artifact.intended_install_path.clone(),
            }),
    );
    if platform == SchedulerPlatform::SystemdUser {
        steps.push(run_command(["systemctl", "--user", "daemon-reload"]));
    }

    Ok(SchedulerOperationPlan {
        command: "scheduler-uninstall",
        operation: SchedulerOperation::Uninstall,
        dry_run: true,
        platform,
        artifacts,
        steps,
    })
}

fn uninstall_artifacts(
    platform: SchedulerPlatform,
    paths: &SchedulerPaths,
) -> Vec<GeneratedArtifact> {
    match platform {
        SchedulerPlatform::SystemdUser => vec![
            GeneratedArtifact {
                kind: GeneratedArtifactKind::RunnerScript,
                intended_install_path: paths.runner_path.clone(),
                contents: String::new(),
            },
            GeneratedArtifact {
                kind: GeneratedArtifactKind::SystemdService,
                intended_install_path: paths.systemd_service_path(),
                contents: String::new(),
            },
            GeneratedArtifact {
                kind: GeneratedArtifactKind::SystemdTimer,
                intended_install_path: paths.systemd_timer_path(),
                contents: String::new(),
            },
        ],
        SchedulerPlatform::Launchd => vec![
            GeneratedArtifact {
                kind: GeneratedArtifactKind::RunnerScript,
                intended_install_path: paths.runner_path.clone(),
                contents: String::new(),
            },
            GeneratedArtifact {
                kind: GeneratedArtifactKind::LaunchdPlist,
                intended_install_path: paths.launchd_plist_path(),
                contents: String::new(),
            },
        ],
        SchedulerPlatform::TaskScheduler => vec![
            GeneratedArtifact {
                kind: GeneratedArtifactKind::RunnerScript,
                intended_install_path: paths.runner_path.clone(),
                contents: String::new(),
            },
            GeneratedArtifact {
                kind: GeneratedArtifactKind::TaskSchedulerXml,
                intended_install_path: paths.task_scheduler_xml_path(),
                contents: String::new(),
            },
        ],
    }
}

fn operation_artifacts(
    platform: SchedulerPlatform,
    artifacts: Vec<GeneratedArtifact>,
    paths: &SchedulerPaths,
) -> Vec<GeneratedArtifact> {
    artifacts
        .into_iter()
        .map(|mut artifact| {
            if platform == SchedulerPlatform::TaskScheduler
                && artifact.kind == GeneratedArtifactKind::TaskSchedulerXml
            {
                artifact.intended_install_path = paths.task_scheduler_xml_path();
            }
            artifact
        })
        .collect()
}

fn ensure_artifact_dirs(artifacts: &[GeneratedArtifact]) -> Vec<SchedulerPlanStep> {
    let mut dirs = Vec::new();
    for artifact in artifacts {
        let Some(parent) = artifact.intended_install_path.parent() else {
            continue;
        };
        if parent.as_os_str().is_empty() || contains_path(&dirs, parent) {
            continue;
        }
        dirs.push(parent.to_path_buf());
    }
    dirs.into_iter()
        .map(|path| SchedulerPlanStep::EnsureDir { path })
        .collect()
}

fn contains_path(paths: &[PathBuf], path: &Path) -> bool {
    paths.iter().any(|known| known == path)
}

fn install_commands(
    platform: SchedulerPlatform,
    artifacts: &[GeneratedArtifact],
    paths: &SchedulerPaths,
) -> Vec<SchedulerPlanStep> {
    match platform {
        SchedulerPlatform::SystemdUser => vec![
            run_command(["systemctl", "--user", "daemon-reload"]),
            run_command_vec(vec![
                "systemctl".to_string(),
                "--user".to_string(),
                "enable".to_string(),
                "--now".to_string(),
                paths.systemd_service_name.clone(),
                paths.systemd_timer_name.clone(),
            ]),
        ],
        SchedulerPlatform::Launchd => plist_path(artifacts)
            .map(|path| {
                vec![
                    run_command(["launchctl", "unload", path.as_str()]),
                    run_command(["launchctl", "load", "-w", path.as_str()]),
                ]
            })
            .unwrap_or_default(),
        SchedulerPlatform::TaskScheduler => task_xml_path(artifacts)
            .map(|path| {
                vec![run_command([
                    "schtasks",
                    "/Create",
                    "/TN",
                    paths.task_name.as_str(),
                    "/XML",
                    path.as_str(),
                    "/F",
                ])]
            })
            .unwrap_or_default(),
    }
}

fn uninstall_commands(
    platform: SchedulerPlatform,
    paths: &SchedulerPaths,
) -> Vec<SchedulerPlanStep> {
    match platform {
        SchedulerPlatform::SystemdUser => vec![run_command_vec(vec![
            "systemctl".to_string(),
            "--user".to_string(),
            "disable".to_string(),
            "--now".to_string(),
            paths.systemd_service_name.clone(),
            paths.systemd_timer_name.clone(),
        ])],
        SchedulerPlatform::Launchd => {
            vec![run_command([
                "launchctl",
                "remove",
                paths.launchd_label.as_str(),
            ])]
        }
        SchedulerPlatform::TaskScheduler => vec![run_command([
            "schtasks",
            "/Delete",
            "/TN",
            paths.task_name.as_str(),
            "/F",
        ])],
    }
}

fn plist_path(artifacts: &[GeneratedArtifact]) -> Option<String> {
    artifact_path(artifacts, GeneratedArtifactKind::LaunchdPlist)
}

fn task_xml_path(artifacts: &[GeneratedArtifact]) -> Option<String> {
    artifact_path(artifacts, GeneratedArtifactKind::TaskSchedulerXml)
}

fn artifact_path(artifacts: &[GeneratedArtifact], kind: GeneratedArtifactKind) -> Option<String> {
    artifacts
        .iter()
        .find(|artifact| artifact.kind == kind)
        .map(|artifact| artifact.intended_install_path.display().to_string())
}

fn run_command<const N: usize>(argv: [&str; N]) -> SchedulerPlanStep {
    SchedulerPlanStep::RunCommand {
        argv: argv.into_iter().map(ToOwned::to_owned).collect(),
    }
}

fn run_command_vec(argv: Vec<String>) -> SchedulerPlanStep {
    SchedulerPlanStep::RunCommand { argv }
}