1use std::{
2 path::{Path, PathBuf},
3 time::Duration,
4};
5
6use blue_build_process_management::{
7 drivers::{Driver, DriverArgs},
8 logging::CommandLogging,
9};
10use blue_build_recipe::Recipe;
11use blue_build_utils::{
12 constants::{
13 ARCHIVE_SUFFIX, BB_SKIP_VALIDATION, LOCAL_BUILD, OCI_ARCHIVE, OSTREE_UNVERIFIED_IMAGE,
14 SUDO_ASKPASS,
15 },
16 has_env_var, running_as_root,
17};
18use bon::Builder;
19use clap::Args;
20use comlexr::cmd;
21use indicatif::ProgressBar;
22use log::{debug, trace};
23use miette::{IntoDiagnostic, Result, bail};
24use tempfile::TempDir;
25
26use crate::{commands::build::BuildCommand, rpm_ostree_status::RpmOstreeStatus};
27
28use super::BlueBuildCommand;
29
30#[derive(Default, Clone, Debug, Builder, Args)]
31pub struct SwitchCommand {
32 #[arg()]
34 recipe: PathBuf,
35
36 #[arg(short, long)]
39 #[builder(default)]
40 reboot: bool,
41
42 #[arg(long)]
45 tempdir: Option<PathBuf>,
46
47 #[arg(long, env = BB_SKIP_VALIDATION)]
49 #[builder(default)]
50 skip_validation: bool,
51
52 #[clap(flatten)]
53 #[builder(default)]
54 drivers: DriverArgs,
55}
56
57impl BlueBuildCommand for SwitchCommand {
58 fn try_run(&mut self) -> Result<()> {
59 trace!("SwitchCommand::try_run()");
60
61 Driver::init(self.drivers);
62
63 let status = RpmOstreeStatus::try_new()?;
64 trace!("{status:?}");
65
66 if status.transaction_in_progress() {
67 bail!("There is a transaction in progress. Please cancel it using `rpm-ostree cancel`");
68 }
69
70 let tempdir = if let Some(ref dir) = self.tempdir {
71 TempDir::new_in(dir).into_diagnostic()?
72 } else {
73 TempDir::new().into_diagnostic()?
74 };
75 trace!("{tempdir:?}");
76
77 BuildCommand::builder()
78 .recipe([self.recipe.clone()])
79 .archive(tempdir.path())
80 .maybe_tempdir(self.tempdir.clone())
81 .skip_validation(self.skip_validation)
82 .build()
83 .try_run()?;
84
85 let recipe = Recipe::parse(&self.recipe)?;
86 let image_file_name = format!(
87 "{}.{ARCHIVE_SUFFIX}",
88 recipe.name.to_lowercase().replace('/', "_")
89 );
90 let temp_file_path = tempdir.path().join(&image_file_name);
91 let archive_path = Path::new(LOCAL_BUILD).join(&image_file_name);
92
93 Self::clean_local_build_dir()?;
94 Self::move_archive(&temp_file_path, &archive_path)?;
95
96 drop(tempdir);
99
100 self.switch(&archive_path, &status)
101 }
102}
103
104impl SwitchCommand {
105 fn switch(&self, archive_path: &Path, status: &RpmOstreeStatus<'_>) -> Result<()> {
106 trace!(
107 "SwitchCommand::switch({}, {status:#?})",
108 archive_path.display()
109 );
110
111 let status = if status.is_booted_on_archive(archive_path)
112 || status.is_staged_on_archive(archive_path)
113 {
114 let command = cmd!("rpm-ostree", "upgrade", if self.reboot => "--reboot");
115
116 trace!("{command:?}");
117 command
118 } else {
119 let image_ref = format!(
120 "{OSTREE_UNVERIFIED_IMAGE}:{OCI_ARCHIVE}:{path}",
121 path = archive_path.display()
122 );
123
124 let command = cmd!(
125 "rpm-ostree",
126 "rebase",
127 &image_ref,
128 if self.reboot => "--reboot",
129 );
130
131 trace!("{command:?}");
132 command
133 }
134 .build_status(
135 format!("{}", archive_path.display()),
136 "Switching to new image",
137 )
138 .into_diagnostic()?;
139
140 if !status.success() {
141 bail!("Failed to switch to new image!");
142 }
143 Ok(())
144 }
145
146 fn move_archive(from: &Path, to: &Path) -> Result<()> {
147 trace!(
148 "SwitchCommand::move_archive({}, {})",
149 from.display(),
150 to.display()
151 );
152
153 let progress = ProgressBar::new_spinner();
154 progress.enable_steady_tick(Duration::from_millis(100));
155 progress.set_message(format!("Moving image archive to {}...", to.display()));
156
157 let status = {
158 let c = cmd!(
159 if running_as_root() {
160 "mv"
161 } else {
162 "sudo"
163 },
164 if !running_as_root() && has_env_var(SUDO_ASKPASS) => [
165 "-A",
166 "-p",
167 format!("Password needed to move {from:?} to {to:?}"),
168 ],
169 if !running_as_root() => "mv",
170 from,
171 to,
172 );
173 trace!("{c:?}");
174 c
175 }
176 .status()
177 .into_diagnostic()?;
178
179 progress.finish_and_clear();
180
181 if !status.success() {
182 bail!(
183 "Failed to move archive from {from} to {to}",
184 from = from.display(),
185 to = to.display()
186 );
187 }
188
189 Ok(())
190 }
191
192 fn clean_local_build_dir() -> Result<()> {
193 trace!("SwitchCommand::clean_local_build_dir()");
194
195 let local_build_path = Path::new(LOCAL_BUILD);
196
197 if local_build_path.exists() {
198 debug!("Cleaning out build dir {LOCAL_BUILD}");
199
200 let mut command = {
201 let c = cmd!(
202 if running_as_root() {
203 "ls"
204 } else {
205 "sudo"
206 },
207 if !running_as_root() && has_env_var(SUDO_ASKPASS) => [
208 "-A",
209 "-p",
210 format!("Password required to list files in {LOCAL_BUILD}"),
211 ],
212 if !running_as_root() => "ls",
213 LOCAL_BUILD
214 );
215 trace!("{c:?}");
216 c
217 };
218 let output =
219 String::from_utf8(command.output().into_diagnostic()?.stdout).into_diagnostic()?;
220
221 trace!("{output}");
222
223 let files = output
224 .lines()
225 .filter(|line| line.ends_with(ARCHIVE_SUFFIX))
226 .map(|file| local_build_path.join(file).display().to_string())
227 .collect::<Vec<_>>();
228
229 if !files.is_empty() {
230 let progress = ProgressBar::new_spinner();
231 progress.enable_steady_tick(Duration::from_millis(100));
232 progress.set_message("Removing old image archive files...");
233
234 let status = {
235 let c = cmd!(
236 if running_as_root() {
237 "rm"
238 } else {
239 "sudo"
240 },
241 if !running_as_root() && has_env_var(SUDO_ASKPASS) => [
242 "-A",
243 "-p",
244 format!("Password required to remove files: {files:?}"),
245 ],
246 if !running_as_root() => "rm",
247 "-f",
248 for files,
249 );
250 trace!("{c:?}");
251 c
252 }
253 .status()
254 .into_diagnostic()?;
255
256 progress.finish_and_clear();
257
258 if !status.success() {
259 bail!("Failed to clean out archives in {LOCAL_BUILD}");
260 }
261 }
262 } else {
263 debug!(
264 "Creating build output dir at {}",
265 local_build_path.display()
266 );
267
268 let status = {
269 let c = cmd!(
270 if running_as_root() {
271 "mkdir"
272 } else {
273 "sudo"
274 },
275 if !running_as_root() && has_env_var(SUDO_ASKPASS) => [
276 "-A",
277 "-p",
278 format!("Password needed to create directory {local_build_path:?}"),
279 ],
280 if !running_as_root() => "mkdir",
281 "-p",
282 local_build_path,
283 );
284 trace!("{c:?}");
285 c
286 }
287 .status()
288 .into_diagnostic()?;
289
290 if !status.success() {
291 bail!("Failed to create directory {LOCAL_BUILD}");
292 }
293 }
294
295 Ok(())
296 }
297}