use crate::aur::AurClient;
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 anyhow::{bail, Result};
use std::collections::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).await;
}
return Ok(0);
}
install_targets(options, tools, aur, targets, journal).await
}
async fn install_targets(
options: &Options,
tools: &Toolchain,
aur: &AurClient,
targets: &[String],
journal: &mut Journal<'_>,
) -> 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);
}
if options.dry_run {
output::line(format!(
"dry-run: resolve AUR targets: {}",
output::join_inline(&aur_targets, " ")
));
}
let local = LocalDb::load_default().unwrap_or_else(|_| LocalDb::empty());
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 status == AurItemStatus::InstallStarted
&& base_already_installed(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_installed<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 {
if tools.query_installed_version(&item.name).await?.is_none() {
return Ok(false);
}
}
Ok(true)
}
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"))
}
enum Journal<'a> {
Durable(&'a mut Transaction),
Memory(Transaction),
None,
}
impl Journal<'_> {
fn tx(&self) -> Option<&Transaction> {
match self {
Self::Durable(tx) => Some(tx),
Self::Memory(tx) => Some(tx),
Self::None => None,
}
}
fn tx_mut(&mut self) -> Option<&mut Transaction> {
match self {
Self::Durable(tx) => Some(tx),
Self::Memory(tx) => Some(tx),
Self::None => None,
}
}
fn set_phase(&mut self, phase: TransactionPhase) -> Result<()> {
if let Some(tx) = self.tx_mut() {
tx.phase = phase;
}
self.save()
}
fn set_repo_deps(&mut self, deps: Vec<String>, as_deps: bool) -> Result<()> {
if let Some(tx) = self.tx_mut() {
tx.repo_deps = deps;
tx.options.repo_deps_as_deps = as_deps;
}
self.save()
}
fn write_plan(
&mut self,
repo_deps: Vec<String>,
aur_items: Vec<AurTransactionItem>,
unresolved: Vec<String>,
) -> Result<()> {
if let Some(tx) = self.tx_mut() {
tx.repo_deps = repo_deps;
tx.options.repo_deps_as_deps = true;
tx.aur_items = aur_items;
tx.unresolved = unresolved;
tx.phase = TransactionPhase::AurPlanWritten;
}
self.save()
}
fn base_status(&self, base: &str) -> Option<AurItemStatus> {
self.tx()?
.aur_items
.iter()
.find(|item| item.base == base)
.map(|item| item.status)
}
fn base_build_dir(&self, base: &str) -> Option<PathBuf> {
self.tx()?
.aur_items
.iter()
.find(|item| item.base == base)
.map(|item| item.build_dir.clone())
}
fn base_artifacts(&self, base: &str) -> Vec<PathBuf> {
self.tx()
.and_then(|tx| {
tx.aur_items
.iter()
.find(|item| item.base == base && !item.artifacts.is_empty())
})
.map(|item| item.artifacts.clone())
.unwrap_or_default()
}
fn set_base_status(&mut self, base: &str, status: AurItemStatus) -> Result<()> {
if let Some(tx) = self.tx_mut() {
for item in &mut tx.aur_items {
if item.base == base {
item.status = status;
}
}
}
self.save()
}
fn set_base_artifacts(&mut self, base: &str, artifacts: Vec<PathBuf>) -> Result<()> {
if let Some(tx) = self.tx_mut() {
for item in &mut tx.aur_items {
if item.base == base {
item.artifacts = artifacts.clone();
}
}
}
self.save()
}
fn mark_failed(&mut self, err: impl std::fmt::Display) -> Result<()> {
match self {
Self::Durable(tx) => transaction::mark_failed(tx, err),
Self::Memory(tx) => {
tx.phase = TransactionPhase::Failed;
tx.last_error = Some(err.to_string());
Ok(())
}
Self::None => Ok(()),
}
}
fn mark_completed(&mut self) -> Result<()> {
match self {
Self::Durable(tx) => transaction::mark_completed(tx),
Self::Memory(tx) => {
tx.phase = TransactionPhase::Completed;
tx.last_error = None;
Ok(())
}
Self::None => Ok(()),
}
}
fn save(&self) -> Result<()> {
match self {
Self::Durable(tx) => transaction::save_current(tx),
Self::Memory(_) | Self::None => Ok(()),
}
}
}
#[cfg(test)]
#[allow(clippy::await_holding_lock)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Default)]
struct FakeBackend {
build_dir: PathBuf,
calls: Mutex<Vec<String>>,
artifacts: Mutex<Vec<PathBuf>>,
installed: Mutex<HashMap<String, String>>,
}
impl FakeBackend {
fn new(build_dir: PathBuf) -> Self {
Self {
build_dir,
calls: Mutex::new(Vec::new()),
artifacts: Mutex::new(Vec::new()),
installed: Mutex::new(HashMap::new()),
}
}
fn calls(&self) -> Vec<String> {
self.calls.lock().unwrap().clone()
}
}
impl PackageBackend for FakeBackend {
fn build_dir(&self) -> &Path {
&self.build_dir
}
async fn system_upgrade(
&self,
_refresh: bool,
_sysupgrade: bool,
_no_confirm: bool,
_dry_run: bool,
) -> Result<i32> {
self.calls.lock().unwrap().push("system_upgrade".into());
Ok(0)
}
async fn install_repo_packages(
&self,
packages: &[String],
needed: bool,
_no_confirm: bool,
as_deps: bool,
_dry_run: bool,
) -> Result<i32> {
self.calls.lock().unwrap().push(format!(
"repo needed={needed} as_deps={as_deps} {}",
packages.join(" ")
));
Ok(0)
}
async fn sync_git_repo(&self, _url: &str, dest: &Path, _dry_run: bool) -> Result<i32> {
self.calls
.lock()
.unwrap()
.push(format!("sync {}", dest.display()));
Ok(0)
}
async fn build_aur_package(
&self,
dir: &Path,
_no_confirm: bool,
_no_check: bool,
_dry_run: bool,
) -> Result<i32> {
self.calls
.lock()
.unwrap()
.push(format!("build {}", dir.display()));
Ok(0)
}
async fn package_list(&self, _dir: &Path) -> Result<Vec<PathBuf>> {
Ok(self.artifacts.lock().unwrap().clone())
}
async fn install_local_packages(
&self,
packages: &[PathBuf],
needed: bool,
_no_confirm: bool,
_dry_run: bool,
) -> Result<i32> {
self.calls.lock().unwrap().push(format!(
"local needed={needed} {}",
packages
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(" ")
));
Ok(0)
}
async fn pacman_database_check(&self, _dry_run: bool) -> Result<i32> {
self.calls.lock().unwrap().push("dbcheck".into());
Ok(0)
}
async fn query_installed_version(&self, package: &str) -> Result<Option<String>> {
Ok(self.installed.lock().unwrap().get(package).cloned())
}
}
fn temp_dir(name: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"knott-install-{name}-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&path).unwrap();
path
}
fn options() -> Options {
Options {
needed: true,
no_confirm: true,
..Options::default()
}
}
fn tx_with_phase(
phase: TransactionPhase,
build_dir: PathBuf,
artifact: PathBuf,
) -> Transaction {
let mut tx =
transaction::new_unsaved(vec!["-S".into(), "foo".into()], false, false, &options());
tx.phase = phase;
tx.repo_deps = vec!["repo-dep".into()];
tx.aur_items = vec![AurTransactionItem {
name: "foo".into(),
base: "foo".into(),
version: Some("1-1".into()),
status: match phase {
TransactionPhase::AurBuildStarted => AurItemStatus::BuildStarted,
TransactionPhase::AurBuilt => AurItemStatus::Built,
TransactionPhase::AurInstallStarted => AurItemStatus::InstallStarted,
_ => AurItemStatus::Pending,
},
build_dir,
artifacts: if matches!(
phase,
TransactionPhase::AurBuilt | TransactionPhase::AurInstallStarted
) {
vec![artifact]
} else {
Vec::new()
},
installed_as_dependency: false,
}];
tx
}
async fn save_for_resume(tx: &Transaction) {
transaction::save_current(tx).unwrap();
}
#[tokio::test]
async fn dry_run_does_not_create_durable_transaction() {
let _guard = transaction::test_env_lock();
let state = temp_dir("dry-run-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let mut dry = options();
dry.dry_run = true;
let tools = FakeBackend::new(temp_dir("dry-run-build"));
let mut journal = Journal::Memory(transaction::new_unsaved(Vec::new(), false, false, &dry));
let code = install_saved_plan(&dry, &tools, None, &mut journal)
.await
.unwrap();
assert_eq!(code, 0);
assert!(transaction::load_current().unwrap().is_none());
}
#[tokio::test]
async fn resume_from_repo_upgrade_started_refuses_aur() {
let _guard = transaction::test_env_lock();
let state = temp_dir("repo-started-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let artifact = temp_dir("repo-started-build").join("foo.pkg.tar.zst");
let mut tx = tx_with_phase(
TransactionPhase::RepoUpgradeStarted,
temp_dir("repo-started-build"),
artifact,
);
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("repo-started-tools"));
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 1);
assert!(tools.calls().is_empty());
}
#[tokio::test]
async fn resume_from_repo_deps_started_reruns_with_needed() {
let _guard = transaction::test_env_lock();
let state = temp_dir("repo-deps-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let artifact = temp_dir("repo-deps-build").join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::RepoDepsInstallStarted,
temp_dir("repo-deps-build"),
artifact.clone(),
);
tx.aur_items[0].status = AurItemStatus::Installed;
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("repo-deps-tools"));
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == "repo needed=true as_deps=true repo-dep"));
}
#[tokio::test]
async fn resume_from_aur_build_started_rebuilds_current_base() {
let _guard = transaction::test_env_lock();
let state = temp_dir("build-started-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("build-started-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::AurBuildStarted,
build_dir.clone(),
artifact.clone(),
);
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(build_dir.clone());
*tools.artifacts.lock().unwrap() = vec![artifact];
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == &format!("build {}", build_dir.display())));
}
#[tokio::test]
async fn resume_from_aur_built_installs_existing_artifacts() {
let _guard = transaction::test_env_lock();
let state = temp_dir("aur-built-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("aur-built-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(TransactionPhase::AurBuilt, build_dir, artifact.clone());
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("aur-built-tools"));
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == &format!("local needed=true {}", artifact.display())));
}
#[tokio::test]
async fn resume_from_install_started_checks_installed_before_reinstall() {
let _guard = transaction::test_env_lock();
let state = temp_dir("install-started-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("install-started-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::AurInstallStarted,
build_dir,
artifact.clone(),
);
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("install-started-tools"));
tools
.installed
.lock()
.unwrap()
.insert("foo".into(), "1-1".into());
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(!tools
.calls()
.iter()
.any(|call| call.starts_with("local needed=true")));
}
#[tokio::test]
async fn resume_from_install_started_reruns_pacman_u_when_not_installed() {
let _guard = transaction::test_env_lock();
let state = temp_dir("install-rerun-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("install-rerun-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::AurInstallStarted,
build_dir,
artifact.clone(),
);
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("install-rerun-tools"));
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == &format!("local needed=true {}", artifact.display())));
}
}