use std::collections::HashSet;
use std::path::Path;
use std::process::Command;
use crate::state::types::Source;
use super::command::run_command;
pub(super) fn collect_service_units_for_package(
package: &str,
source: &Source,
) -> Result<Vec<String>, String> {
match source {
Source::Official { .. } => {
let output = run_command(
"pacman",
&["-Fl", package],
&format!("pacman -Fl {package}"),
)?;
let units = extract_service_units_from_file_list(&output, package);
Ok(units)
}
Source::Aur => {
if let Ok(installed_files) = crate::logic::files::get_installed_file_list(package)
&& !installed_files.is_empty()
{
let file_list = installed_files
.iter()
.map(|f| format!("{package} {f}"))
.collect::<Vec<_>>()
.join("\n");
let units = extract_service_units_from_file_list(&file_list, package);
if !units.is_empty() {
tracing::debug!(
"Found {} service units from installed AUR package {}",
units.len(),
package
);
return Ok(units);
}
}
let has_paru = Command::new("paru").args(["--version"]).output().is_ok();
let has_yay = Command::new("yay").args(["--version"]).output().is_ok();
if has_paru {
tracing::debug!("Trying paru -Fl {} for AUR package service units", package);
if let Ok(output) = Command::new("paru")
.args(["-Fl", package])
.env("LC_ALL", "C")
.env("LANG", "C")
.output()
&& output.status.success()
{
let text = String::from_utf8_lossy(&output.stdout);
let units = extract_service_units_from_file_list(&text, package);
if !units.is_empty() {
tracing::debug!(
"Found {} service units from paru -Fl for {}",
units.len(),
package
);
return Ok(units);
}
}
}
if has_yay {
tracing::debug!("Trying yay -Fl {} for AUR package service units", package);
if let Ok(output) = Command::new("yay")
.args(["-Fl", package])
.env("LC_ALL", "C")
.env("LANG", "C")
.output()
&& output.status.success()
{
let text = String::from_utf8_lossy(&output.stdout);
let units = extract_service_units_from_file_list(&text, package);
if !units.is_empty() {
tracing::debug!(
"Found {} service units from yay -Fl for {}",
units.len(),
package
);
return Ok(units);
}
}
}
tracing::debug!(
"No file list available for AUR package {} (will use binary-based detection)",
package
);
Ok(Vec::new())
}
}
}
pub(super) fn extract_service_units_from_file_list(file_list: &str, package: &str) -> Vec<String> {
let mut seen = HashSet::new();
let mut units = Vec::new();
for line in file_list.lines() {
let Some((pkg, raw_path)) = line.split_once(' ') else {
continue;
};
if pkg != package {
continue;
}
let path = raw_path.strip_suffix('/').unwrap_or(raw_path);
if !is_service_path(path) {
continue;
}
if let Some(file_name) = Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.map(ToString::to_string)
.filter(|name| seen.insert(name.clone()))
{
units.push(file_name);
}
}
units
}
pub(super) fn is_service_path(path: &str) -> bool {
const PREFIXES: [&str; 2] = ["/usr/lib/systemd/system/", "/lib/systemd/system/"];
PREFIXES
.iter()
.any(|prefix| path.starts_with(prefix) && path.ends_with(".service"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_service_units_from_file_list_filters_correctly() {
let output = "\
mockpkg /usr/lib/systemd/system/example.service
mockpkg /usr/lib/systemd/system/example.service/
mockpkg /usr/lib/systemd/system/example.timer
mockpkg /lib/systemd/system/legacy.service
mockpkg /usr/bin/mock
otherpkg /usr/lib/systemd/system/other.service
";
let units = extract_service_units_from_file_list(output, "mockpkg");
assert_eq!(
units,
vec!["example.service".to_string(), "legacy.service".to_string()]
);
}
#[test]
fn extract_service_units_from_file_list_deduplicates_preserving_order() {
let output = "\
mockpkg /usr/lib/systemd/system/alpha.service
mockpkg /usr/lib/systemd/system/beta.service
mockpkg /usr/lib/systemd/system/alpha.service/
mockpkg /usr/lib/systemd/system/gamma.service
mockpkg /usr/lib/systemd/system/beta.service/
";
let units = extract_service_units_from_file_list(output, "mockpkg");
assert_eq!(
units,
vec![
"alpha.service".to_string(),
"beta.service".to_string(),
"gamma.service".to_string()
]
);
}
}