use std::process::Command;
use crate as cindy;
use crate::Context;
#[derive(Clone, Debug, PartialEq, Eq)]
#[crate::wire]
pub enum Presence {
Absent,
Purged,
Present,
Version(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[crate::wire]
pub struct State {
pub name: String,
pub presence: Option<Presence>,
pub hold: Option<bool>,
}
impl crate::Diff for State {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum InstallStatus {
Installed,
ConfigFiles,
NotInstalled,
}
struct CurrentState {
install_status: InstallStatus,
version: Option<String>,
held: bool,
}
fn run_check(cmd: &mut Command) -> crate::Result<()> {
let out = cmd
.output()
.context(format!("Failed to spawn {:?}", cmd.get_program()))?;
if !out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
crate::bail!(
"{:?} failed with {:?}:\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}",
cmd.get_program(),
out.status,
);
}
Ok(())
}
fn apt_get() -> Command {
let mut c = Command::new("apt-get");
c.env("DEBIAN_FRONTEND", "noninteractive");
c.arg("-y");
c
}
fn read_current_state(pkg: &str) -> crate::Result<CurrentState> {
let dpkg = Command::new("dpkg-query")
.args(["-W", "-f", "${db:Status-Abbrev}|${Version}", "--", pkg])
.output()
.context("Failed to spawn `dpkg-query`")?;
let (install_status, version) = if dpkg.status.success() {
let raw = String::from_utf8_lossy(&dpkg.stdout);
let (abbrev, ver) = raw.split_once('|').unwrap_or((raw.as_ref(), ""));
let current = abbrev.chars().nth(1).unwrap_or(' ');
let status = match current {
'i' => InstallStatus::Installed,
'c' => InstallStatus::ConfigFiles,
_ => InstallStatus::NotInstalled,
};
let version = matches!(status, InstallStatus::Installed).then(|| ver.trim().to_owned());
(status, version)
} else {
(InstallStatus::NotInstalled, None)
};
let mark = Command::new("apt-mark")
.arg("showhold")
.output()
.context("Failed to spawn `apt-mark showhold`")?;
if !mark.status.success() {
crate::bail!(
"`apt-mark showhold` failed:\n{}",
String::from_utf8_lossy(&mark.stderr)
);
}
let held = String::from_utf8_lossy(&mark.stdout)
.lines()
.any(|line| line.trim() == pkg);
Ok(CurrentState {
install_status,
version,
held,
})
}
fn current_to_presence(cur: &CurrentState) -> Presence {
match cur.install_status {
InstallStatus::Installed => match &cur.version {
Some(v) => Presence::Version(v.clone()),
None => Presence::Present,
},
InstallStatus::ConfigFiles => Presence::Absent,
InstallStatus::NotInstalled => Presence::Purged,
}
}
fn presence_satisfied(cur: &CurrentState, desired: &Presence) -> bool {
match desired {
Presence::Absent => matches!(
cur.install_status,
InstallStatus::ConfigFiles | InstallStatus::NotInstalled
),
Presence::Purged => matches!(cur.install_status, InstallStatus::NotInstalled),
Presence::Present => matches!(cur.install_status, InstallStatus::Installed),
Presence::Version(v) => {
cur.install_status == InstallStatus::Installed
&& cur.version.as_deref() == Some(v.as_str())
}
}
}
#[crate::remote]
pub fn apt(state: State) -> crate::Result<super::Return> {
let cur = read_current_state(&state.name)?;
let old_view = State {
name: state.name.clone(),
presence: Some(current_to_presence(&cur)),
hold: Some(cur.held),
};
let new_view = State {
name: state.name.clone(),
presence: Some(
state
.presence
.clone()
.unwrap_or_else(|| current_to_presence(&cur)),
),
hold: Some(state.hold.unwrap_or(cur.held)),
};
if old_view != new_view {
let _ = <State as crate::Diff>::diff(&old_view, &new_view, &mut std::io::stderr().lock());
}
let mut changed = false;
let mut effective_held = cur.held;
let presence_needs_change = state
.presence
.as_ref()
.is_some_and(|p| !presence_satisfied(&cur, p));
let final_held = state.hold.unwrap_or(effective_held);
if effective_held && (state.hold == Some(false) || presence_needs_change) {
run_check(Command::new("apt-mark").args(["unhold", "--", &state.name]))?;
effective_held = false;
changed = true;
}
if let Some(presence) = state.presence.as_ref()
&& !presence_satisfied(&cur, presence)
{
match presence {
Presence::Absent => {
run_check(apt_get().args(["remove", "--", &state.name]))?;
}
Presence::Purged => {
run_check(apt_get().args(["purge", "--", &state.name]))?;
}
Presence::Present => {
run_check(apt_get().args(["install", "--", &state.name]))?;
}
Presence::Version(v) => {
let spec = format!("{}={}", state.name, v);
run_check(apt_get().args(["install", "--", &spec]))?;
}
}
changed = true;
}
if effective_held != final_held {
let subcmd = if final_held { "hold" } else { "unhold" };
run_check(Command::new("apt-mark").args([subcmd, "--", &state.name]))?;
changed = true;
}
Ok(if changed {
super::Return::Changed
} else {
super::Return::Unchanged
})
}