use std::collections::{BTreeMap, BTreeSet, 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 debversion::Version;
use serde::Serialize;
use serde_json::json;
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::{
DepKind, 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),
)
.arg(Arg::new("output-format").long("output-format").value_parser(["human", "json"]).default_value("human"))
)
.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");
let format = sub_matches.get_one::<String>("output-format").expect("--output-format 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).map(|res| {
if format == "human" {
println!("{res}");
} else {
println!("{}", json!(res));
}
}),
)
} else {
handle_err(
reverse_depends_suite(
crate_name,
sub_matches
.get_one::<String>("suite")
.expect("suite must be available as fallback"),
)
.map(|res| {
if format == "human" {
println!("{res}");
} else {
println!("{}", json!(res));
}
}),
)
}
}
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:#}");
}
}
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)]
struct PackageWithVersion {
package: String,
version: Version,
}
impl PackageWithVersion {
fn new(package: String, version: Version) -> Self {
Self { package, version }
}
}
impl std::fmt::Display for PackageWithVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{package}/{version}",
package = self.package,
version = self.version
)
}
}
impl Serialize for PackageWithVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer {
serializer.serialize_str(&self.to_string())
}
}
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize)]
struct PackageWithVersionConstraint {
package: String,
version_constraint: Option<(VersionConstraint, Version)>,
}
impl PackageWithVersionConstraint {
fn new(package: String, version_constraint: Option<(VersionConstraint, Version)>) -> Self {
Self {
package,
version_constraint,
}
}
}
impl std::fmt::Display for PackageWithVersionConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some((constraint, version)) = &self.version_constraint {
write!(
f,
"{package} ({constraint} {version})",
package = self.package
)
} else {
write!(f, "{package}", package = self.package)
}
}
}
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Serialize)]
struct PackageRelation {
target: PackageWithVersionConstraint,
#[serde(skip_serializing_if = "Option::is_none")]
origin: Option<PackageWithVersion>,
}
impl PackageRelation {
fn new(target: PackageWithVersionConstraint, origin: Option<PackageWithVersion>) -> Self {
Self { target, origin }
}
}
impl std::fmt::Display for PackageRelation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let target = &self.target;
if let Some(origin) = &self.origin {
write!(f, "{origin} -> ")?;
}
write!(f, "{target}")
}
}
#[derive(Default, Debug, Serialize)]
struct ReverseDepends {
packages: BTreeMap<
PackageWithVersion,
BTreeMap<PackageWithVersion, BTreeMap<DepKind, BTreeSet<PackageRelation>>>,
>,
orphaned: BTreeMap<DepKind, BTreeSet<PackageRelation>>,
}
impl std::fmt::Display for ReverseDepends {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.packages.is_empty() {
return writeln!(f, "No matching packages found");
}
for (source, packages) in &self.packages {
writeln!(f, "Source package {source}")?;
if packages.is_empty() {
writeln!(
f,
"- No matching binary packages, old extra source package?"
)?;
}
for (package, reverse_map) in packages {
writeln!(f, "- Binary package {package}")?;
for (kind, relations) in reverse_map {
if !relations.is_empty() {
writeln!(f, "-- {kind:?}")?;
}
for relation in relations {
writeln!(f, "--- {relation}")?;
}
}
}
writeln!(f)?;
}
if !self.orphaned.is_empty() {
writeln!(f, "Orphaned reverse dependencies:")?;
}
for (kind, relations) in &self.orphaned {
if !relations.is_empty() {
writeln!(f, "- {kind:?}")?;
}
for relation in relations {
writeln!(f, "-- {relation}")?;
}
}
Ok(())
}
}
#[tracing::instrument]
fn reverse_depends_suite(crate_name: &str, suite: &str) -> Result<ReverseDepends, 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<ReverseDepends, 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 res = ReverseDepends::default();
let mut bin_packages_handled = HashSet::new();
for source in &found_packages.source_packages {
let source_entry = res
.packages
.entry(PackageWithVersion::new(
source.name.clone(),
source.version.clone(),
))
.or_default();
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 {
continue;
}
if source_ver != &source.version {
continue;
}
let reverse_map = source_entry
.entry(PackageWithVersion::new(
binary.name.clone(),
binary.version.clone(),
))
.or_default();
bin_packages_handled.insert(binary.name.clone());
let provided = reverse_map.entry(DepKind::Provides).or_default();
let virtual_pkgs = found_packages
.virtual_packages
.iter()
.filter(|virtual_pkg| &virtual_pkg.provider == binary)
.collect::<Vec<_>>();
for virtual_pkg in &virtual_pkgs {
bin_packages_handled.insert(virtual_pkg.name.clone());
provided.insert(PackageRelation::new(
PackageWithVersionConstraint::new(
virtual_pkg.name.clone(),
virtual_pkg.version.clone(),
),
None,
));
}
for (kind, relation) in &found_packages.reverse_depends {
for (reverse_dep, relation2) in relation {
for relation3 in relation2 {
let name = relation3.name.as_str();
let map = reverse_map.entry(kind.clone()).or_default();
let tuple = PackageRelation::new(
PackageWithVersionConstraint::new(
name.to_owned(),
relation3.version.clone(),
),
Some(PackageWithVersion::new(
reverse_dep.name.clone(),
reverse_dep.version.clone(),
)),
);
if name == binary.name {
map.insert(tuple.clone());
}
for virtual_pkg in &virtual_pkgs {
if name == virtual_pkg.name {
map.insert(tuple.clone());
}
}
}
}
}
}
}
for (kind, relation) in &found_packages.reverse_depends {
for (reverse_dep, relation2) in relation {
for relation3 in relation2 {
let name = relation3.name.as_str();
if !bin_packages_handled.contains(name) {
let tuple = PackageRelation::new(
PackageWithVersionConstraint::new(
name.to_owned(),
relation3.version.clone(),
),
Some(PackageWithVersion::new(
reverse_dep.name.clone(),
reverse_dep.version.clone(),
)),
);
res.orphaned.entry(kind.clone()).or_default().insert(tuple);
}
}
}
}
if found_packages.source_packages.is_empty() {
todo!()
}
Ok(res)
}
#[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(())
}