mod aur;
mod cli;
mod dep;
mod download;
mod info;
mod install;
mod localdb;
mod menu;
mod output;
mod pacman;
mod resolver;
mod search;
mod transaction;
mod upgrade;
mod version;
use anyhow::{bail, Result};
use std::io;
pub async fn run(args: Vec<String>) -> Result<i32> {
let original_args = args.clone();
let parsed = cli::parse(args)?;
let recovery_flags =
parsed.options.resume as u8 + parsed.options.abort as u8 + parsed.options.repair as u8;
if recovery_flags > 1 {
bail!("choose only one of --resume, --abort, or --repair");
}
match &parsed.command {
cli::Command::Help => {
cli::print_help();
return Ok(0);
}
cli::Command::Version => {
println!("knott {}", env!("CARGO_PKG_VERSION"));
return Ok(0);
}
cli::Command::Diagnostics => {
print_diagnostics();
return Ok(0);
}
_ => {}
}
let tools = pacman::Toolchain::from_env();
let aur = aur::AurClient::from_env_with_progress(
!parsed.options.quiet && !parsed.options.no_progress,
)?;
if parsed.options.abort {
let _lock = transaction::acquire_lock()?;
return abort_current();
}
if parsed.options.repair {
let _lock = transaction::acquire_lock()?;
return repair_current(&parsed.options, &tools, &aur).await;
}
if parsed.options.resume {
let _lock = transaction::acquire_lock()?;
return resume_current(&parsed.options, &tools, &aur).await;
}
match parsed.command {
cli::Command::DefaultUpgrade => {
run_mutating(
original_args,
&parsed.options,
&tools,
&aur,
&[],
true,
true,
)
.await
}
cli::Command::Search { terms } => {
search::print_search(&parsed.options, &tools, &aur, &terms).await
}
cli::Command::Info { targets } => {
info::print_info(&parsed.options, &tools, &aur, &targets).await
}
cli::Command::Install {
targets,
refresh,
sysupgrade,
} => {
run_mutating(
original_args,
&parsed.options,
&tools,
&aur,
&targets,
refresh,
sysupgrade,
)
.await
}
cli::Command::Get { targets, print } => {
download::get_pkgbuilds(&parsed.options, &tools, &aur, &targets, print).await
}
cli::Command::QueryForeign => upgrade::print_foreign(&parsed.options, &tools).await,
cli::Command::QueryAurUpdates => {
upgrade::print_aur_updates(&parsed.options, &tools, &aur).await
}
cli::Command::Forward(args) => tools.forward(&args).await,
cli::Command::Interactive { terms } => {
let selected =
search::interactive_select(&parsed.options, &tools, &aur, &terms).await?;
if selected.is_empty() {
return Ok(0);
}
let mut command = vec!["-S".to_string()];
command.extend(selected.iter().cloned());
run_mutating(
command,
&parsed.options,
&tools,
&aur,
&selected,
false,
false,
)
.await
}
cli::Command::Help | cli::Command::Version | cli::Command::Diagnostics => unreachable!(),
}
}
async fn run_mutating(
command: Vec<String>,
options: &cli::Options,
tools: &pacman::Toolchain,
aur: &aur::AurClient,
targets: &[String],
refresh: bool,
sysupgrade: bool,
) -> Result<i32> {
if options.dry_run {
return install::sync(options, tools, aur, targets, refresh, sysupgrade, None).await;
}
let _lock = transaction::acquire_lock()?;
if let Some(mut tx) = transaction::load_current()? {
print_interrupted(&tx);
if options.no_confirm {
output::error("--noconfirm will not choose a recovery path; run knott --resume, --abort, or --repair");
return Ok(1);
}
match prompt_recovery()? {
RecoveryChoice::Resume => return resume_loaded(options, tools, aur, &mut tx).await,
RecoveryChoice::Abort => {
transaction::mark_aborted(&mut tx)?;
output::warning("interrupted transaction marked aborted; rerun the requested command to start over");
return Ok(1);
}
RecoveryChoice::IgnoreOnce => {
output::warning("not starting a second mutating transaction while one is active");
return Ok(1);
}
}
}
let mut tx = transaction::create_current(command, refresh, sysupgrade, options)?;
install::sync(
options,
tools,
aur,
targets,
refresh,
sysupgrade,
Some(&mut tx),
)
.await
}
fn abort_current() -> Result<i32> {
let Some(mut tx) = transaction::load_current()? else {
output::line("knott: no active Knott transaction");
return Ok(0);
};
transaction::mark_aborted(&mut tx)?;
output::line(format!(
"knott: transaction {} marked aborted; installed packages were not rolled back",
output::inline(&tx.id)
));
Ok(0)
}
async fn resume_current(
options: &cli::Options,
tools: &pacman::Toolchain,
aur: &aur::AurClient,
) -> Result<i32> {
let Some(mut tx) = transaction::load_current()? else {
output::line("knott: no active Knott transaction");
return Ok(0);
};
resume_loaded(options, tools, aur, &mut tx).await
}
async fn repair_current(
options: &cli::Options,
tools: &pacman::Toolchain,
aur: &aur::AurClient,
) -> Result<i32> {
let Some(mut tx) = transaction::load_current()? else {
let code = tools.pacman_database_check(options.dry_run).await?;
if code == 0 {
output::line("knott: no active Knott transaction");
}
return Ok(code);
};
let code = install::repair_transaction(options, tools, Some(aur), &mut tx).await?;
if code != 0 || transaction::load_current()?.is_none() {
return Ok(code);
}
resume_loaded(options, tools, aur, &mut tx).await
}
async fn resume_loaded(
options: &cli::Options,
tools: &pacman::Toolchain,
aur: &aur::AurClient,
tx: &mut transaction::Transaction,
) -> Result<i32> {
if tx.phase.blocks_aur_resume() || failed_during_repo_upgrade(tx) {
install::print_repo_interrupted(tx);
return Ok(1);
}
if tx.aur_items.is_empty()
&& tx.repo_deps.is_empty()
&& matches!(
tx.phase,
transaction::TransactionPhase::Planned
| transaction::TransactionPhase::RepoUpgradeDone
| transaction::TransactionPhase::AurPlanWritten
)
{
return resume_saved_command(options, tools, aur, tx).await;
}
install::resume_transaction(options, tools, Some(aur), tx).await
}
async fn resume_saved_command(
options: &cli::Options,
tools: &pacman::Toolchain,
aur: &aur::AurClient,
tx: &mut transaction::Transaction,
) -> Result<i32> {
let parsed = cli::parse(tx.command.clone())?;
let mut tx_options = tx.options.to_options();
tx_options.no_confirm |= options.no_confirm;
tx_options.no_progress |= options.no_progress;
match parsed.command {
cli::Command::DefaultUpgrade => {
install::sync(
&tx_options,
tools,
aur,
&[],
tx.refresh,
tx.sysupgrade,
Some(tx),
)
.await
}
cli::Command::Install {
targets,
refresh,
sysupgrade,
} => {
install::sync(
&tx_options,
tools,
aur,
&targets,
refresh || tx.refresh,
sysupgrade || tx.sysupgrade,
Some(tx),
)
.await
}
_ => {
output::error("saved transaction command cannot be resumed automatically");
Ok(1)
}
}
}
fn print_interrupted(tx: &transaction::Transaction) {
if tx.phase == transaction::TransactionPhase::RepoUpgradeStarted
|| failed_during_repo_upgrade(tx)
{
install::print_repo_interrupted(tx);
return;
}
output::error("knott: interrupted transaction detected");
output::line(format!("id: {}", output::inline(&tx.id)));
output::line(format!("phase: {}", tx.phase));
if let Some(item) = tx
.aur_items
.iter()
.find(|item| item.status != transaction::AurItemStatus::Installed)
{
output::line(format!("package: {}", output::inline(&item.name)));
}
output::line("resume: knott --resume");
output::line("abort: knott --abort");
output::line("repair: knott --repair");
}
fn failed_during_repo_upgrade(tx: &transaction::Transaction) -> bool {
tx.phase == transaction::TransactionPhase::Failed
&& tx
.last_error
.as_deref()
.is_some_and(|err| err.contains("pacman repo upgrade"))
}
enum RecoveryChoice {
Resume,
Abort,
IgnoreOnce,
}
fn prompt_recovery() -> Result<RecoveryChoice> {
output::prompt("Interrupted transaction: [R]esume, [A]bort, [I]gnore once? ")?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() || input.eq_ignore_ascii_case("r") || input.eq_ignore_ascii_case("resume") {
return Ok(RecoveryChoice::Resume);
}
if input.eq_ignore_ascii_case("a") || input.eq_ignore_ascii_case("abort") {
return Ok(RecoveryChoice::Abort);
}
if input.eq_ignore_ascii_case("i") || input.eq_ignore_ascii_case("ignore") {
return Ok(RecoveryChoice::IgnoreOnce);
}
Ok(RecoveryChoice::IgnoreOnce)
}
pub fn sanitize_for_terminal(input: &str) -> String {
output::text(input)
}
fn print_diagnostics() {
println!("knott diagnostics");
println!("version: {}", env!("CARGO_PKG_VERSION"));
println!("aur_info_batch_size: {}", aur::INFO_CHUNK);
println!("aur_info_cache_limit: {}", aur::INFO_CACHE_LIMIT);
println!("aur_search_cache_limit: {}", aur::SEARCH_CACHE_LIMIT);
println!(
"aur_search_cache_result_limit: {}",
aur::SEARCH_CACHE_RESULT_LIMIT
);
println!("pkgbuild_byte_limit: {}", aur::MAX_PKG_BUILD_BYTES);
println!("resolver_node_limit: {}", resolver::MAX_RESOLVE_NODES);
println!("local_desc_byte_limit: {}", localdb::MAX_DESC_BYTES);
println!("spawned_tokio_tasks: 0");
println!("channels: 0");
println!("state_root: {}", transaction::state_root_path().display());
println!(
"transaction_dir: {}",
transaction::transactions_dir_path().display()
);
println!(
"transaction_current: {}",
transaction::current_path().display()
);
println!("transaction_lock: {}", transaction::lock_path().display());
println!("output_sanitization: ansi-and-control-stripped");
println!("progress_loader: aisling:tqdm");
match current_rss_kib() {
Some(rss) => println!("rss_kib: {rss}"),
None => println!("rss_kib: unavailable"),
}
}
fn current_rss_kib() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
status.lines().find_map(|line| {
line.strip_prefix("VmRSS:")?
.split_whitespace()
.next()?
.parse()
.ok()
})
}