use std::{
path::{Path, PathBuf},
time::Duration,
};
use blue_build_process_management::{
drivers::{Driver, DriverArgs},
logging::CommandLogging,
};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::{
ARCHIVE_SUFFIX, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE,
};
use bon::Builder;
use clap::Args;
use colored::Colorize;
use comlexr::cmd;
use indicatif::ProgressBar;
use log::{debug, trace, warn};
use miette::{bail, IntoDiagnostic, Result};
use nix::unistd::Uid;
use tempfile::TempDir;
use crate::{commands::build::BuildCommand, rpm_ostree_status::RpmOstreeStatus};
use super::BlueBuildCommand;
#[derive(Default, Clone, Debug, Builder, Args)]
pub struct SwitchCommand {
#[arg()]
recipe: PathBuf,
#[arg(short, long)]
#[builder(default)]
reboot: bool,
#[arg(long)]
tempdir: Option<PathBuf>,
#[clap(flatten)]
#[builder(default)]
drivers: DriverArgs,
}
impl BlueBuildCommand for SwitchCommand {
fn try_run(&mut self) -> Result<()> {
trace!("SwitchCommand::try_run()");
Driver::init(self.drivers);
let status = RpmOstreeStatus::try_new()?;
trace!("{status:?}");
if status.transaction_in_progress() {
bail!("There is a transaction in progress. Please cancel it using `rpm-ostree cancel`");
}
let tempdir = if let Some(ref dir) = self.tempdir {
TempDir::new_in(dir).into_diagnostic()?
} else {
TempDir::new().into_diagnostic()?
};
trace!("{tempdir:?}");
#[cfg(feature = "multi-recipe")]
BuildCommand::builder()
.recipe([self.recipe.clone()])
.archive(tempdir.path())
.maybe_tempdir(self.tempdir.clone())
.build()
.try_run()?;
#[cfg(not(feature = "multi-recipe"))]
BuildCommand::builder()
.recipe(self.recipe.clone())
.archive(tempdir.path())
.maybe_tempdir(self.tempdir.clone())
.build()
.try_run()?;
let recipe = Recipe::parse(&self.recipe)?;
let image_file_name = format!(
"{}.{ARCHIVE_SUFFIX}",
recipe.name.to_lowercase().replace('/', "_")
);
let temp_file_path = tempdir.path().join(&image_file_name);
let archive_path = Path::new(LOCAL_BUILD).join(&image_file_name);
if !Uid::effective().is_root() {
warn!(
"{notice}: {} {sudo} {}",
"The next few steps will require".yellow(),
"You may have to supply your password".yellow(),
notice = "NOTICE".bright_red().bold(),
sudo = "`sudo`.".italic().bright_red().bold(),
);
}
Self::clean_local_build_dir()?;
Self::move_archive(&temp_file_path, &archive_path)?;
drop(tempdir);
self.switch(&archive_path, &status)
}
}
impl SwitchCommand {
fn switch(&self, archive_path: &Path, status: &RpmOstreeStatus<'_>) -> Result<()> {
trace!(
"SwitchCommand::switch({}, {status:#?})",
archive_path.display()
);
let status = if status.is_booted_on_archive(archive_path)
|| status.is_staged_on_archive(archive_path)
{
let command = cmd!("rpm-ostree", "upgrade", if self.reboot => "--reboot");
trace!("{command:?}");
command
} else {
let image_ref = format!(
"{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{path}",
path = archive_path.display()
);
let command = cmd!(
"rpm-ostree",
"rebase",
&image_ref,
if self.reboot => "--reboot",
);
trace!("{command:?}");
command
}
.build_status(
format!("{}", archive_path.display()),
"Switching to new image",
)
.into_diagnostic()?;
if !status.success() {
bail!("Failed to switch to new image!");
}
Ok(())
}
fn move_archive(from: &Path, to: &Path) -> Result<()> {
trace!(
"SwitchCommand::move_archive({}, {})",
from.display(),
to.display()
);
let progress = ProgressBar::new_spinner();
progress.enable_steady_tick(Duration::from_millis(100));
progress.set_message(format!("Moving image archive to {}...", to.display()));
let status = {
let c = if Uid::effective().is_root() {
cmd!("mv", from, to)
} else {
cmd!("sudo", "mv", from, to)
};
trace!("{c:?}");
c
}
.status()
.into_diagnostic()?;
progress.finish_and_clear();
if !status.success() {
bail!(
"Failed to move archive from {from} to {to}",
from = from.display(),
to = to.display()
);
}
Ok(())
}
fn clean_local_build_dir() -> Result<()> {
trace!("SwitchCommand::clean_local_build_dir()");
let local_build_path = Path::new(LOCAL_BUILD);
if local_build_path.exists() {
debug!("Cleaning out build dir {LOCAL_BUILD}");
trace!("sudo ls {LOCAL_BUILD}");
let mut command = {
let c = if Uid::effective().is_root() {
cmd!("ls", LOCAL_BUILD)
} else {
cmd!("sudo", "ls", LOCAL_BUILD)
};
trace!("{c:?}");
c
};
let output =
String::from_utf8(command.output().into_diagnostic()?.stdout).into_diagnostic()?;
trace!("{output}");
let files = output
.lines()
.filter(|line| line.ends_with(ARCHIVE_SUFFIX))
.map(|file| local_build_path.join(file).display().to_string())
.collect::<Vec<_>>();
if !files.is_empty() {
let files = files.join(" ");
let progress = ProgressBar::new_spinner();
progress.enable_steady_tick(Duration::from_millis(100));
progress.set_message("Removing old image archive files...");
trace!("sudo rm -f {files}");
let status = {
let c = if Uid::effective().is_root() {
cmd!("rm", "-f", files)
} else {
cmd!("sudo", "rm", "-f", files)
};
trace!("{c:?}");
c
}
.status()
.into_diagnostic()?;
progress.finish_and_clear();
if !status.success() {
bail!("Failed to clean out archives in {LOCAL_BUILD}");
}
}
} else {
debug!(
"Creating build output dir at {}",
local_build_path.display()
);
let status = {
let c = if Uid::effective().is_root() {
cmd!("mkdir", "-p", LOCAL_BUILD)
} else {
cmd!("sudo", "mkdir", "-p", LOCAL_BUILD)
};
trace!("{c:?}");
c
}
.status()
.into_diagnostic()?;
if !status.success() {
bail!("Failed to create directory {LOCAL_BUILD}");
}
}
Ok(())
}
}