use std::collections::HashSet;
use std::fs::remove_dir_all;
use std::path::PathBuf;
use std::{fs::File, io::Write};
use anyhow::{Context, Error, bail};
use clap::{Arg, ArgAction, Command, command};
use debian_control::relations::VersionConstraint;
use tracing::{debug, info, warn};
use crate::helpers::{
create_or_get_apt_root, determine_architecture, get_base_dir, get_packages_index_path,
get_sources_index_path,
};
use crate::package_indices::{
get_all_packages_for_crate, parse_packages_index, parse_sources_index,
};
mod helpers;
mod package_indices;
fn main() -> Result<(), Error> {
tracing_subscriber::fmt::init();
let matches = command!()
.propagate_version(true)
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("refresh")
.about("Refreshes the internal APT cache")
.arg(Arg::new("suite").long("suite").default_value("unstable")),
)
.subcommand(
Command::new("reverse-depends")
.about("Analyzes reverse-depends")
.arg(Arg::new("crate").required(true))
.arg(Arg::new("suite").long("suite").default_value("unstable"))
.arg(
Arg::new("sources-index")
.long("sources")
.conflicts_with("suite")
.requires("packages-index")
.action(ArgAction::Append),
)
.arg(
Arg::new("packages-index")
.long("packages")
.conflicts_with("suite")
.requires("sources-index")
.action(ArgAction::Append),
),
)
.subcommand(
Command::new("clean")
.about("clean cache directory")
.arg(Arg::new("suite").long("suite")),
)
.get_matches();
match matches.subcommand() {
Some(("refresh", sub_matches)) => handle_err(refresh_apt_cache(
sub_matches
.get_one::<String>("suite")
.map(|v| v.as_str())
.unwrap_or("unstable"),
)),
Some(("reverse-depends", sub_matches)) => {
let crate_name = sub_matches
.get_one::<String>("crate")
.expect("<crate> is required");
if sub_matches.contains_id("packages-index") {
let packages_indices = sub_matches
.get_many::<String>("packages-index")
.expect("\"packages-index\" should be present")
.map(PathBuf::from)
.collect::<Vec<_>>();
let sources_indices = sub_matches
.get_many::<String>("sources-index")
.expect("\"sources-index\" should be present")
.map(PathBuf::from)
.collect::<Vec<_>>();
handle_err(reverse_depends(
crate_name,
packages_indices,
sources_indices,
))
} else {
handle_err(reverse_depends_suite(
crate_name,
sub_matches
.get_one::<String>("suite")
.expect("suite must be available as fallback"),
))
}
}
Some(("clean", sub_matches)) => handle_err(clean(
sub_matches.get_one::<String>("suite").map(|v| v.as_str()),
)),
_ => unreachable!("Would have printed help.."),
}
Ok(())
}
fn handle_err(res: Result<(), Error>) {
if let Err(err) = res {
eprintln!("{err:#}");
}
}
#[tracing::instrument]
fn reverse_depends_suite(crate_name: &str, suite: &str) -> Result<(), Error> {
let apt_root = helpers::create_or_get_apt_root(suite)?;
let architecture = helpers::determine_architecture()?;
let suite_packages_index = apt_root.join(get_packages_index_path(suite, &architecture));
let suite_sources_index = apt_root.join(get_sources_index_path(suite));
reverse_depends(
crate_name,
vec![suite_packages_index],
vec![suite_sources_index],
)
}
#[tracing::instrument]
fn reverse_depends(
crate_name: &str,
packages: Vec<PathBuf>,
sources: Vec<PathBuf>,
) -> Result<(), Error> {
let package_index = parse_packages_index(&packages)?;
let source_index = parse_sources_index(&sources)?;
let found_packages =
get_all_packages_for_crate(crate_name, &package_index.packages, &source_index.sources)?;
if found_packages.source_packages.is_empty() && found_packages.binary_packages.is_empty() {
bail!("no packages matching '{crate_name}' found");
}
let source_package_count = found_packages.source_packages.len();
let _source_package_names = found_packages
.source_packages
.iter()
.map(|source| source.name.to_string())
.collect::<Vec<_>>();
let _bin_package_names = found_packages
.source_packages
.iter()
.map(|bin| bin.name.to_string())
.collect::<Vec<_>>();
let mut statically_linked_handled = HashSet::new();
let mut bin_packages_handled = HashSet::new();
for (index, source) in found_packages.source_packages.iter().enumerate() {
println!(
"Found source package {name}/{version}",
name = source.name,
version = source.version
);
let mut found_bin = false;
for binary in &found_packages.binary_packages {
let Some(binary_source) = &binary.source else {
continue;
};
let source_name = binary_source.name.as_str();
let source_ver = binary_source.version.as_ref().unwrap_or(&binary.version);
if source_name == source.name && source_ver != &source.version {
continue;
}
println!(
"- binary package '{name}/{version}'",
name = binary.name,
version = binary.version
);
bin_packages_handled.insert(binary.name.clone());
found_bin = true;
let virtual_pkgs = found_packages
.virtual_packages
.iter()
.filter(|virtual_pkg| &virtual_pkg.provider == binary)
.collect::<Vec<_>>();
if virtual_pkgs.is_empty() {
println!("-- no virtual packages provided");
} else {
let pkgs = virtual_pkgs
.iter()
.map(|virtual_pkg| {
bin_packages_handled.insert(virtual_pkg.name.clone());
if let Some(ref ver) = virtual_pkg.version {
format!("{pkg}/{ver}", pkg = virtual_pkg.name)
} else {
virtual_pkg.name.to_string()
}
})
.collect::<Vec<_>>();
println!("-- Provides: {}", pkgs.join(", "));
}
for (kind, relation) in &found_packages.reverse_depends {
for (reverse_dep, relation2) in relation {
let mut matching_deps = Vec::new();
for relation3 in relation2 {
let name = relation3.name.as_str();
let ver = relation3
.version
.as_ref()
.map(|(c, v)| format!(" ({c} {v})"))
.unwrap_or_default();
if name == binary.name {
matching_deps.push(format!("{name}{ver}"));
}
for virtual_pkg in &virtual_pkgs {
if name == virtual_pkg.name {
matching_deps.push(format!("{name}{ver}"));
}
}
}
if !matching_deps.is_empty() {
println!(
"-- {kind:?}: {pkg}/{pkg_ver} -> {deps}",
pkg = reverse_dep.name,
pkg_ver = reverse_dep.version,
deps = matching_deps.join(", ")
);
}
}
}
for (linking, linked) in &found_packages.statically_linked {
let linked_name = linked.name.as_str();
if linked_name == source_name
&& let Some((constraint, version)) = linked.version.as_ref()
&& constraint == &VersionConstraint::Equal
&& version == source_ver
{
let linked_ver = format!(" (= {version})");
let linking_name = linking.name.as_str();
let linking_ver = &linking.version;
println!(
"-- S-B-U: {linking_name} ({linking_ver}) -> {linked_name}{linked_ver}"
);
statically_linked_handled.insert(linked.clone());
}
}
}
if !found_bin {
println!("- no binaries packages found - old extra source package?");
}
if index + 1 < source_package_count {
println!();
}
}
let mut non_matching_sbu = Vec::new();
for (linking, linked) in &found_packages.statically_linked {
if !statically_linked_handled.contains(linked) {
let linked_name = linked.name.as_str();
if let Some((_constraint, version)) = linked.version.as_ref() {
let linked_ver = format!(" (= {version})");
let linking_name = linking.name.as_str();
let linking_ver = &linking.version;
non_matching_sbu.push(format!(
"{linking_name} ({linking_ver}) -> {linked_name}{linked_ver}"
));
}
}
}
let mut non_matching_rev_depends = Vec::new();
for (kind, relation) in &found_packages.reverse_depends {
for (reverse_dep, relation2) in relation {
let mut matching_deps = Vec::new();
for relation3 in relation2 {
let name = relation3.name.as_str();
let ver = relation3
.version
.as_ref()
.map(|(c, v)| format!(" ({c} {v})"))
.unwrap_or_default();
if !bin_packages_handled.contains(name) {
matching_deps.push(format!("{name}{ver}"));
}
}
if !matching_deps.is_empty() {
non_matching_rev_depends.push(format!(
"{kind:?}: {pkg}/{pkg_ver} -> {deps}",
pkg = reverse_dep.name,
pkg_ver = reverse_dep.version,
deps = matching_deps.join(", ")
));
}
}
}
if !non_matching_rev_depends.is_empty() {
println!();
println!("non-matching reverse dependecnies:");
for hit in non_matching_rev_depends {
println!("- {hit}");
}
}
if !non_matching_sbu.is_empty() {
println!();
println!("non-matching S-B-U:");
for hit in non_matching_sbu {
println!("- {hit}");
}
}
if found_packages.source_packages.is_empty() {
todo!()
}
Ok(())
}
#[tracing::instrument]
fn refresh_apt_cache(suite: &str) -> Result<(), Error> {
info!("Refreshing APT cache..");
let apt_root = create_or_get_apt_root(suite)?;
debug!("APT root dir: {apt_root:?}");
let architecture = determine_architecture()?;
debug!("Writing `apt.conf`..");
let apt_conf_path = apt_root.join("apt.conf");
let mut apt_conf = File::create(&apt_conf_path)
.with_context(|| format!("Failed to create {apt_conf_path:?}"))?;
writeln!(apt_conf, "Apt::Architecture \"{architecture}\";")?;
writeln!(apt_conf, "Apt::Architectures \"{architecture}\";")?;
writeln!(apt_conf, "Dir {apt_root:?};")?;
writeln!(apt_conf, "Acquire::Languages \"None\";")?;
drop(apt_conf);
debug!("`apt.conf` written to {apt_conf_path:?}");
debug!("Writing APT sources..");
let sources_d = apt_root.join("etc/apt/sources.list.d");
std::fs::create_dir_all(&sources_d).with_context(|| format!("Failed to {sources_d:?}"))?;
let debian_sources_path = sources_d.join("debian.sources");
let mut debian_sources = File::create(&debian_sources_path)
.with_context(|| format!("Failed to create {debian_sources_path:?}"))?;
writeln!(debian_sources, "Types: deb deb-src")?;
writeln!(debian_sources, "URIs: http://deb.debian.org/debian/")?;
writeln!(debian_sources, "Suites: {suite}")?;
writeln!(debian_sources, "Components: main")?;
writeln!(
debian_sources,
"Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg"
)?;
drop(debian_sources);
debug!("APT sources written to {debian_sources_path:?}");
debug!("Ensuring required directories exist..");
let conf_d = apt_root.join("etc/apt/apt.conf.d");
std::fs::create_dir_all(&conf_d).with_context(|| format!("Failed to create {conf_d:?}"))?;
let prefs_d = apt_root.join("etc/apt/preferences.d");
std::fs::create_dir_all(&prefs_d).with_context(|| format!("Failed to create {prefs_d:?}"))?;
let lists_d = apt_root.join("var/lib/apt/lists");
std::fs::create_dir_all(&lists_d).with_context(|| format!("Failed to create {lists_d:?}"))?;
info!("Running `apt update`..");
let apt_update = std::process::Command::new("apt")
.arg("update")
.env("APT_CONFIG", apt_conf_path)
.output()
.context("Failed to run `apt update`")?;
info!(
"`apt update` exited with {status}",
status = apt_update.status
);
if apt_update.status.success() {
println!("{}", String::from_utf8_lossy(&apt_update.stdout));
debug!("STDOUT: {}", String::from_utf8_lossy(&apt_update.stdout));
debug!("STDERR: {}", String::from_utf8_lossy(&apt_update.stderr));
} else {
warn!("STDOUT: {}", String::from_utf8_lossy(&apt_update.stdout));
warn!("STDERR: {}", String::from_utf8_lossy(&apt_update.stderr));
}
Ok(())
}
fn clean(suite: Option<&str>) -> Result<(), Error> {
info!("Cleaning APT cache..");
let dir = if let Some(suite) = suite {
create_or_get_apt_root(suite)?
} else {
get_base_dir()?
};
info!("Dir: {dir:?}");
if dir.exists() {
remove_dir_all(dir)?;
}
Ok(())
}