mod journal;
use crate::aur::{AurClient, AurPackage};
use crate::cli::Options;
use crate::download::build_path;
use crate::localdb::LocalDb;
use crate::output;
use crate::pacman::{PackageBackend, Toolchain};
use crate::resolver::{self, Plan};
use crate::transaction::{self, AurItemStatus, AurTransactionItem, Transaction, TransactionPhase};
use crate::upgrade;
use crate::version::vercmp;
use anyhow::{bail, Result};
use journal::Journal;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::io;
use std::path::{Path, PathBuf};
pub async fn sync(
options: &Options,
tools: &Toolchain,
aur: &AurClient,
targets: &[String],
refresh: bool,
sysupgrade: bool,
tx: Option<&mut Transaction>,
) -> Result<i32> {
if options.dry_run {
output::line("dry-run: transaction plan");
}
let mut journal = match tx {
Some(tx) => Journal::Durable(tx),
None if options.dry_run => Journal::Memory(transaction::new_unsaved(
Vec::new(),
refresh,
sysupgrade,
options,
)),
None => Journal::None,
};
let code = sync_with_journal(
options,
tools,
aur,
targets,
refresh,
sysupgrade,
&mut journal,
)
.await;
match code {
Ok(0) => {
journal.mark_completed()?;
Ok(0)
}
Ok(code) => Ok(code),
Err(err) => {
journal.mark_failed(&err)?;
Err(err)
}
}
}
pub async fn resume_transaction<B>(
options: &Options,
tools: &B,
aur: Option<&AurClient>,
tx: &mut Transaction,
) -> Result<i32>
where
B: PackageBackend,
{
if tx.phase.blocks_aur_resume() || failed_during_repo_upgrade(tx) {
print_repo_interrupted(tx);
return Ok(1);
}
let mut resumed_options = tx.options.to_options();
resumed_options.no_confirm |= options.no_confirm;
resumed_options.no_progress |= options.no_progress;
let mut journal = Journal::Durable(tx);
let code = install_saved_plan(&resumed_options, tools, aur, &mut journal).await;
match code {
Ok(0) => {
journal.mark_completed()?;
Ok(0)
}
Ok(code) => Ok(code),
Err(err) => {
journal.mark_failed(&err)?;
Err(err)
}
}
}
pub async fn repair_transaction<B>(
options: &Options,
tools: &B,
aur: Option<&AurClient>,
tx: &mut Transaction,
) -> Result<i32>
where
B: PackageBackend,
{
let mut resumed_options = tx.options.to_options();
resumed_options.no_confirm |= options.no_confirm;
resumed_options.no_progress |= options.no_progress;
warn_pacman_lock();
if tx.phase == TransactionPhase::RepoUpgradeStarted || failed_during_repo_upgrade(tx) {
output::warning(
"repo upgrade did not complete; AUR work will not continue until pacman succeeds",
);
let code = tools
.system_upgrade(
true,
true,
resumed_options.no_confirm,
resumed_options.dry_run,
)
.await?;
if code != 0 {
transaction::mark_failed(
tx,
"pacman repo upgrade failed or was interrupted; run `sudo pacman -Syu` or `knott --repair` before continuing AUR work",
)?;
return Ok(code);
}
tx.phase = TransactionPhase::RepoUpgradeDone;
transaction::save_current(tx)?;
}
let check = tools.pacman_database_check(resumed_options.dry_run).await?;
if check != 0 {
transaction::mark_failed(
tx,
"pacman database check failed; repair pacman before resuming",
)?;
return Ok(check);
}
if tx.aur_items.is_empty() && tx.repo_deps.is_empty() {
return Ok(0);
}
resume_transaction(&resumed_options, tools, aur, tx).await
}
pub fn print_repo_interrupted(tx: &Transaction) {
output::error("knott: repo upgrade was interrupted or did not complete");
output::line(format!("id: {}", output::inline(&tx.id)));
output::line("finish pacman first:");
output::line(" sudo pacman -Syu");
output::line("then resume:");
output::line(" knott --resume");
}
async fn sync_with_journal(
options: &Options,
tools: &Toolchain,
aur: &AurClient,
targets: &[String],
refresh: bool,
sysupgrade: bool,
journal: &mut Journal<'_>,
) -> Result<i32> {
if (refresh || sysupgrade) && options.mode.repo() && !repo_upgrade_already_done(journal) {
journal.set_phase(TransactionPhase::RepoUpgradeStarted)?;
let code = tools
.system_upgrade(refresh, sysupgrade, options.no_confirm, options.dry_run)
.await?;
if code != 0 {
journal.mark_failed(
"pacman repo upgrade failed or was interrupted; run `sudo pacman -Syu` or `knott --repair` before continuing AUR work",
)?;
return Ok(code);
}
journal.set_phase(TransactionPhase::RepoUpgradeDone)?;
}
if targets.is_empty() {
if sysupgrade && options.mode.aur() {
if options.dry_run {
output::line("dry-run: resolve AUR updates");
}
let updates = upgrade::aur_update_targets(options, tools, aur).await?;
if updates.is_empty() {
return Ok(0);
}
output::status(
"AUR packages to upgrade",
output::join_inline(&updates, " "),
);
if !options.no_confirm && !confirm("Proceed with AUR upgrade? [Y/n] ")? {
return Ok(0);
}
return install_targets(options, tools, aur, &updates, journal, true).await;
}
return Ok(0);
}
install_targets(options, tools, aur, targets, journal, true).await
}
async fn install_targets(
options: &Options,
tools: &Toolchain,
aur: &AurClient,
targets: &[String],
journal: &mut Journal<'_>,
allow_prefer_bin: bool,
) -> Result<i32> {
let mut repo_targets = Vec::new();
let mut aur_targets = Vec::new();
for target in targets {
if options.mode.repo()
&& !matches!(options.mode, crate::cli::TargetMode::Aur)
&& tools.repo_has_package(target).await
{
repo_targets.push(target.clone());
continue;
}
if options.mode.aur() {
aur_targets.push(target.clone());
} else {
repo_targets.push(target.clone());
}
}
if !repo_targets.is_empty() {
journal.set_repo_deps(repo_targets.clone(), false)?;
journal.set_phase(TransactionPhase::RepoDepsInstallStarted)?;
let code = tools
.install_repo_packages(
&repo_targets,
options.needed,
options.no_confirm,
false,
options.dry_run,
)
.await?;
if code != 0 {
journal.mark_failed("repository package install failed or was interrupted")?;
return Ok(code);
}
journal.set_phase(TransactionPhase::RepoDepsInstalled)?;
}
if aur_targets.is_empty() {
return Ok(0);
}
let local = LocalDb::load_default().unwrap_or_else(|_| LocalDb::empty());
if options.needed {
aur_targets = filter_current_aur_targets(aur, &local, &aur_targets).await?;
if aur_targets.is_empty() {
return Ok(0);
}
}
if allow_prefer_bin && options.prefer_bin {
aur_targets = prefer_bin_targets(aur, &aur_targets).await?;
}
if options.dry_run {
output::line(format!(
"dry-run: resolve AUR targets: {}",
output::join_inline(&aur_targets, " ")
));
}
let plan = resolver::resolve(options, tools, aur, &local, &aur_targets).await?;
print_plan(&plan);
let items = plan_items(tools, &plan, &aur_targets);
journal.write_plan(plan.repo_deps.clone(), items, plan.unresolved.clone())?;
if !options.no_confirm && !confirm("Proceed with installation? [Y/n] ")? {
return Ok(0);
}
install_saved_plan(options, tools, Some(aur), journal).await
}
async fn install_saved_plan<B>(
options: &Options,
tools: &B,
aur: Option<&AurClient>,
journal: &mut Journal<'_>,
) -> Result<i32>
where
B: PackageBackend,
{
let Some(tx) = journal.tx() else {
return Ok(0);
};
if tx.aur_items.is_empty() && tx.repo_deps.is_empty() {
return Ok(0);
}
if should_install_repo_deps(tx) {
let deps = tx.repo_deps.clone();
let as_deps = tx.options.repo_deps_as_deps;
if options.dry_run {
output::line(format!(
"dry-run: install repo deps: {}",
output::join_inline(&deps, " ")
));
}
journal.set_phase(TransactionPhase::RepoDepsInstallStarted)?;
let code = tools
.install_repo_packages(&deps, true, options.no_confirm, as_deps, options.dry_run)
.await?;
if code != 0 {
journal.mark_failed("repository dependency install failed or was interrupted")?;
return Ok(code);
}
journal.set_phase(TransactionPhase::RepoDepsInstalled)?;
}
let bases = journal
.tx()
.map(|tx| unique_bases(&tx.aur_items))
.unwrap_or_default();
for base in bases {
let code = process_aur_base(options, tools, aur, journal, &base).await?;
if code != 0 {
return Ok(code);
}
}
Ok(0)
}
async fn process_aur_base<B>(
options: &Options,
tools: &B,
aur: Option<&AurClient>,
journal: &mut Journal<'_>,
base: &str,
) -> Result<i32>
where
B: PackageBackend,
{
let Some(status) = journal.base_status(base) else {
return Ok(0);
};
if status == AurItemStatus::Installed {
return Ok(0);
}
if !options.dry_run && base_already_current(tools, journal, base).await? {
journal.set_phase(TransactionPhase::AurInstalled)?;
journal.set_base_status(base, AurItemStatus::Installed)?;
return Ok(0);
}
if status == AurItemStatus::InstallStarted && base_already_current(tools, journal, base).await?
{
journal.set_phase(TransactionPhase::AurInstalled)?;
journal.set_base_status(base, AurItemStatus::Installed)?;
return Ok(0);
}
let mut status = status;
if matches!(status, AurItemStatus::Pending | AurItemStatus::Failed) {
let Some(aur) = aur else {
bail!("cannot sync AUR source for '{base}' without an AUR client");
};
let build_dir = journal
.base_build_dir(base)
.unwrap_or_else(|| tools.build_dir().join(base));
if options.dry_run {
output::line(format!("dry-run: sync AUR base: {base}"));
}
journal.set_phase(TransactionPhase::AurSourceSyncStarted)?;
let code = tools
.sync_git_repo(&aur.git_url(base), &build_dir, options.dry_run)
.await?;
if code != 0 {
journal.set_base_status(base, AurItemStatus::Failed)?;
journal.mark_failed(format!("AUR source sync failed for {base}"))?;
return Ok(code);
}
journal.set_phase(TransactionPhase::AurSourceSynced)?;
journal.set_base_status(base, AurItemStatus::SourceSynced)?;
status = AurItemStatus::SourceSynced;
}
if matches!(
status,
AurItemStatus::SourceSynced | AurItemStatus::BuildStarted
) {
let code = build_base(options, tools, journal, base).await?;
if code != 0 {
return Ok(code);
}
status = AurItemStatus::Built;
}
if status == AurItemStatus::Built {
let artifacts = journal.base_artifacts(base);
if !artifacts_exist(&artifacts) && !options.dry_run {
output::warning(format!(
"saved artifacts for '{base}' are missing; rebuilding before install"
));
journal.set_base_status(base, AurItemStatus::SourceSynced)?;
let code = build_base(options, tools, journal, base).await?;
if code != 0 {
return Ok(code);
}
}
return install_base(options, tools, journal, base).await;
}
if status == AurItemStatus::InstallStarted {
return install_base(options, tools, journal, base).await;
}
Ok(0)
}
async fn build_base<B>(
options: &Options,
tools: &B,
journal: &mut Journal<'_>,
base: &str,
) -> Result<i32>
where
B: PackageBackend,
{
let build_dir = journal
.base_build_dir(base)
.unwrap_or_else(|| tools.build_dir().join(base));
journal.set_phase(TransactionPhase::AurBuildStarted)?;
journal.set_base_status(base, AurItemStatus::BuildStarted)?;
let code = tools
.build_aur_package(
&build_dir,
options.no_confirm,
options.no_check,
options.dry_run,
)
.await?;
if code != 0 {
journal.set_base_status(base, AurItemStatus::Failed)?;
journal.mark_failed(format!("AUR build failed for {base}"))?;
return Ok(code);
}
let artifacts = if options.dry_run {
output::line(format!(
"dry-run: cd {} && makepkg --packagelist",
build_dir.display()
));
vec![build_dir.join(format!("{base}.pkg.tar.zst"))]
} else {
tools.package_list(&build_dir).await?
};
journal.set_base_artifacts(base, artifacts)?;
journal.set_phase(TransactionPhase::AurBuilt)?;
journal.set_base_status(base, AurItemStatus::Built)?;
Ok(0)
}
async fn install_base<B>(
options: &Options,
tools: &B,
journal: &mut Journal<'_>,
base: &str,
) -> Result<i32>
where
B: PackageBackend,
{
let mut artifacts = journal.base_artifacts(base);
if artifacts.is_empty() {
journal.set_base_status(base, AurItemStatus::SourceSynced)?;
let code = build_base(options, tools, journal, base).await?;
if code != 0 {
return Ok(code);
}
artifacts = journal.base_artifacts(base);
}
journal.set_phase(TransactionPhase::AurInstallStarted)?;
journal.set_base_status(base, AurItemStatus::InstallStarted)?;
let code = tools
.install_local_packages(
&artifacts,
options.needed,
options.no_confirm,
options.dry_run,
)
.await?;
if code != 0 {
journal.mark_failed(format!("AUR package install failed for {base}"))?;
return Ok(code);
}
journal.set_phase(TransactionPhase::AurInstalled)?;
journal.set_base_status(base, AurItemStatus::Installed)?;
Ok(0)
}
async fn base_already_current<B>(tools: &B, journal: &Journal<'_>, base: &str) -> Result<bool>
where
B: PackageBackend,
{
let Some(tx) = journal.tx() else {
return Ok(false);
};
let items = tx
.aur_items
.iter()
.filter(|item| item.base == base)
.collect::<Vec<_>>();
if items.is_empty() {
return Ok(false);
}
for item in items {
let Some(installed) = tools.query_installed_version(&item.name).await? else {
return Ok(false);
};
if let Some(planned) = item.version.as_deref() {
if vercmp(&installed, planned) == Ordering::Less {
return Ok(false);
}
}
}
Ok(true)
}
async fn filter_current_aur_targets(
aur: &AurClient,
local: &LocalDb,
targets: &[String],
) -> Result<Vec<String>> {
if targets.is_empty() {
return Ok(Vec::new());
}
let packages = aur.info_map(targets).await?;
let mut kept = Vec::new();
let mut skipped = Vec::new();
for target in targets {
let Some(pkg) = packages.get(target) else {
kept.push(target.clone());
continue;
};
if local
.get(target)
.is_some_and(|installed| vercmp(&installed.version, &pkg.version) != Ordering::Less)
{
skipped.push(target.clone());
} else {
kept.push(target.clone());
}
}
if !skipped.is_empty() {
output::status(
"AUR packages already up to date",
output::join_inline(&skipped, " "),
);
}
Ok(kept)
}
async fn prefer_bin_targets(aur: &AurClient, targets: &[String]) -> Result<Vec<String>> {
let mut candidate_names = targets
.iter()
.filter_map(|target| bin_candidate_name(target))
.collect::<Vec<_>>();
candidate_names.sort();
candidate_names.dedup();
if candidate_names.is_empty() {
return Ok(targets.to_vec());
}
let candidates = aur.info_map(&candidate_names).await?;
let (targets, replacements) = apply_preferred_bin_targets(targets, &candidates);
if !replacements.is_empty() {
output::status(
"AUR binary variants",
output::join_inline(
replacements
.iter()
.map(|(from, to)| format!("{from} -> {to}")),
", ",
),
);
}
Ok(targets)
}
fn apply_preferred_bin_targets(
targets: &[String],
candidates: &HashMap<String, AurPackage>,
) -> (Vec<String>, Vec<(String, String)>) {
let mut replacements = Vec::new();
let targets = targets
.iter()
.map(|target| {
let Some(candidate) = bin_candidate_name(target) else {
return target.clone();
};
if candidates.contains_key(&candidate) {
replacements.push((target.clone(), candidate.clone()));
candidate
} else {
target.clone()
}
})
.collect();
(targets, replacements)
}
fn bin_candidate_name(target: &str) -> Option<String> {
if target.ends_with("-bin")
|| target
.bytes()
.any(|byte| matches!(byte, b'<' | b'>' | b'='))
{
return None;
}
Some(format!("{target}-bin"))
}
fn print_plan(plan: &Plan) {
if !plan.repo_deps.is_empty() {
output::status(
"Repository dependencies",
output::join_inline(&plan.repo_deps, " "),
);
}
if !plan.aur.is_empty() {
output::status(
"AUR build order",
output::join_inline(plan.aur.iter().map(|pkg| pkg.name.as_str()), " "),
);
}
for dep in &plan.unresolved {
output::warning(format!(
"dependency '{dep}' was not resolved up front; makepkg may still resolve it"
));
}
}
fn plan_items(tools: &Toolchain, plan: &Plan, targets: &[String]) -> Vec<AurTransactionItem> {
let direct = targets.iter().collect::<HashSet<_>>();
plan.aur
.iter()
.map(|pkg| AurTransactionItem {
name: pkg.name.clone(),
base: pkg.base().to_string(),
version: Some(pkg.version.clone()),
status: AurItemStatus::Pending,
build_dir: build_path(tools, pkg.base()),
artifacts: Vec::new(),
installed_as_dependency: !direct.contains(&pkg.name),
})
.collect()
}
fn should_install_repo_deps(tx: &Transaction) -> bool {
if tx.repo_deps.is_empty() {
return false;
}
matches!(
tx.phase,
TransactionPhase::AurPlanWritten
| TransactionPhase::RepoDepsInstallStarted
| TransactionPhase::Failed
)
}
fn repo_upgrade_already_done(journal: &Journal<'_>) -> bool {
journal.tx().is_some_and(|tx| {
phase_rank(tx.phase) >= phase_rank(TransactionPhase::RepoUpgradeDone)
&& tx.phase != TransactionPhase::Failed
})
}
fn failed_during_repo_upgrade(tx: &Transaction) -> bool {
tx.phase == TransactionPhase::Failed
&& tx
.last_error
.as_deref()
.is_some_and(|err| err.contains("pacman repo upgrade"))
}
fn phase_rank(phase: TransactionPhase) -> u8 {
match phase {
TransactionPhase::Planned => 0,
TransactionPhase::RepoUpgradeStarted => 1,
TransactionPhase::RepoUpgradeDone => 2,
TransactionPhase::AurPlanWritten => 3,
TransactionPhase::RepoDepsInstallStarted => 4,
TransactionPhase::RepoDepsInstalled => 5,
TransactionPhase::AurSourceSyncStarted => 6,
TransactionPhase::AurSourceSynced => 7,
TransactionPhase::AurBuildStarted => 8,
TransactionPhase::AurBuilt => 9,
TransactionPhase::AurInstallStarted => 10,
TransactionPhase::AurInstalled => 11,
TransactionPhase::Completed => 12,
TransactionPhase::Failed => 13,
TransactionPhase::Aborted => 14,
}
}
fn unique_bases(items: &[AurTransactionItem]) -> Vec<String> {
let mut seen = HashSet::new();
let mut bases = Vec::new();
for item in items {
if seen.insert(item.base.clone()) {
bases.push(item.base.clone());
}
}
bases
}
fn artifacts_exist(artifacts: &[PathBuf]) -> bool {
!artifacts.is_empty() && artifacts.iter().all(|path| path.exists())
}
fn warn_pacman_lock() {
let lock = Path::new("/var/lib/pacman/db.lck");
if lock.exists() {
output::warning(format!(
"pacman database lock exists at {}; remove it only after confirming pacman is not running",
lock.display()
));
}
}
fn confirm(prompt: &str) -> Result<bool> {
output::prompt(prompt)?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
Ok(input.is_empty() || input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes"))
}
#[cfg(test)]
#[allow(clippy::await_holding_lock)]
mod tests;