From 21ec0dbc51cd6be0a5cab708ba6f8f443525f941 Mon Sep 17 00:00:00 2001 From: AlexKaravaev Date: Tue, 24 Feb 2026 21:48:28 +0100 Subject: [PATCH 1/5] feat: make rts less verbose, support 2 different rts for nfsboot --- hil/src/commands/nfsboot.rs | 64 ++++++++++++++++++++++++++++++++----- hil/src/nfsboot.rs | 57 ++++++++++++++++++++++++--------- hil/src/rts.rs | 2 +- 3 files changed, 99 insertions(+), 24 deletions(-) diff --git a/hil/src/commands/nfsboot.rs b/hil/src/commands/nfsboot.rs index 8db27caad..556e83893 100644 --- a/hil/src/commands/nfsboot.rs +++ b/hil/src/commands/nfsboot.rs @@ -13,7 +13,7 @@ use crate::nfsboot::{error_detection_for_host_state, request_sudo, MountSpec}; /// Boot the orb using NFS #[derive(Debug, Parser)] pub struct Nfsboot { - /// The s3 URI of the RTS to use. + /// The s3 URI of the RTS to use for NFS boot. #[arg( long, conflicts_with = "rts_path", @@ -23,7 +23,8 @@ pub struct Nfsboot { /// The directory to save the s3 artifact we download. #[arg(long)] download_dir: Option, - /// Path to a downloaded RTS (zipped .tar or an already-extracted directory). + /// Path to a downloaded RTS (zipped .tar or an already-extracted directory) + /// used for NFS boot. #[arg( long, conflicts_with = "s3_url", @@ -31,6 +32,15 @@ pub struct Nfsboot { required_unless_present = "s3_url" )] rts_path: Option, + /// S3 URI of a separate RTS whose `rts/` directory is used for `--mount` + /// content instead of the boot RTS. Use this when NFS-booting with a dev + /// build but flashing a stage or prod build. + #[arg(long, conflicts_with = "mount_rts_path")] + mount_s3_url: Option, + /// Local path to a separate RTS tarball whose `rts/` directory is used for + /// `--mount` content instead of the boot RTS. + #[arg(long, conflicts_with = "mount_s3_url")] + mount_rts_path: Option, /// If this flag is given, overwites any existing files when downloading the rts. #[arg(long)] overwrite_existing: bool, @@ -51,12 +61,18 @@ impl Nfsboot { error_detection_for_host_state() .await .wrap_err("failed to assert host's state")?; - let rts_path = self.maybe_download_rts().await?; - debug!("resolved RTS path: {rts_path}"); + let rts_path = self.resolve_rts_path().await?; + debug!("resolved boot RTS path: {rts_path}"); + + let mount_rts_path = self.resolve_mount_rts_path().await?; + if let Some(ref p) = mount_rts_path { + debug!("resolved mount RTS path: {p}"); + } let rng = rand::rngs::StdRng::from_rng(rand::thread_rng()).unwrap(); let _mount_guard = crate::nfsboot::nfsboot( rts_path, + mount_rts_path, self.mounts, self.persistent_img_path.as_deref().map(|p| p.as_std_path()), rng, @@ -69,13 +85,16 @@ impl Nfsboot { unreachable!() } - async fn maybe_download_rts(&self) -> Result { - let existing_file_behavior = if self.overwrite_existing { + fn existing_file_behavior(&self) -> ExistingFileBehavior { + if self.overwrite_existing { ExistingFileBehavior::Overwrite } else { ExistingFileBehavior::Abort - }; - // Determine RTS tarball path: download from S3 or use provided path + } + } + + async fn resolve_rts_path(&self) -> Result { + let existing_file_behavior = self.existing_file_behavior(); let rts_path = if let Some(ref s3_url) = self.s3_url { assert!( self.rts_path.is_none(), @@ -114,4 +133,33 @@ impl Nfsboot { Ok(rts_path) } + + async fn resolve_mount_rts_path(&self) -> Result> { + let path = if let Some(ref s3_url) = self.mount_s3_url { + assert!( + self.mount_rts_path.is_none(), + "sanity: mutual exclusion guaranteed by clap" + ); + let download_dir = + self.download_dir.clone().unwrap_or_else(crate::current_dir); + let download_path = download_dir.join( + crate::download_s3::parse_filename(s3_url) + .wrap_err("failed to parse mount rts filename")?, + ); + + crate::download_s3::download_url( + s3_url, + &download_path, + self.existing_file_behavior(), + ) + .await + .wrap_err("error while downloading mount rts from s3")?; + + Some(download_path) + } else { + self.mount_rts_path.clone() + }; + + Ok(path) + } } diff --git a/hil/src/nfsboot.rs b/hil/src/nfsboot.rs index b46a21468..64f937288 100644 --- a/hil/src/nfsboot.rs +++ b/hil/src/nfsboot.rs @@ -41,8 +41,14 @@ impl From for MountGuard { /// nfsboot.sh from the rts. /// /// The filesystems will remain mounted until `cancel` is cancelled. +/// * `path_to_rts` - RTS tarball used for NFS boot (rootfs, nfsbootcmd). +/// * `mount_rts_path` - Optional separate RTS tarball whose `rts/` directory +/// is used for `--mount` content. When a stage or prod build is provided +/// here, the dev build in `path_to_rts` handles NFS boot while this one +/// supplies the content that gets flashed onto the orb. pub async fn nfsboot( path_to_rts: Utf8PathBuf, + mount_rts_path: Option, mut mounts: Vec, persistent_img_path: Option<&Path>, rng: impl rand::Rng + Send + 'static, @@ -57,9 +63,28 @@ pub async fn nfsboot( "we expected a directory called `rts` after extracting" ); - for m in mounts.iter_mut().filter(|m| m.host_path == "/rtsdir") { - m.host_path = rts_dir.clone().try_into().unwrap(); - } + let mount_rts_tmp_dir = if let Some(mount_rts) = mount_rts_path { + let tmp = tokio::task::spawn_blocking(move || extract(&mount_rts)) + .await + .wrap_err("task panicked")??; + debug!("mount rts temp dir: {tmp:?}"); + let mount_rts_dir = tmp.path().join("rts"); + assert!( + tokio::fs::try_exists(&mount_rts_dir) + .await + .unwrap_or(false), + "we expected a directory called `rts` in mount RTS tarball" + ); + for m in mounts.iter_mut().filter(|m| m.host_path == "/rtsdir") { + m.host_path = mount_rts_dir.clone().try_into().unwrap(); + } + Some(tmp) + } else { + for m in mounts.iter_mut().filter(|m| m.host_path == "/rtsdir") { + m.host_path = rts_dir.clone().try_into().unwrap(); + } + None + }; if let Some(persistent_img_path) = persistent_img_path { crate::rts::populate_persistent(tmp_dir.path(), persistent_img_path, rng) @@ -74,7 +99,7 @@ pub async fn nfsboot( let tmp_dir_path = tmp_dir.path().to_path_buf(); let rts_dir = tmp_dir.path().join("rts"); let mounter = tokio::task::spawn_blocking(move || { - let mut mounter = Mounter::new(tmp_dir); + let mut mounter = Mounter::new(tmp_dir, mount_rts_tmp_dir); mounter .do_mounting(&rts_dir, &scratch_dir, &mounts) .map(|()| mounter) @@ -108,14 +133,16 @@ pub async fn nfsboot( struct Mounter { mounts: Vec, tmp: Option, + mount_rts_tmp: Option, } #[bon::bon] impl Mounter { - fn new(temp_dir: TempDir) -> Self { + fn new(temp_dir: TempDir, mount_rts_tmp: Option) -> Self { Self { mounts: Vec::new(), tmp: Some(temp_dir), + mount_rts_tmp, } } @@ -157,7 +184,7 @@ impl Mounter { .iter() .map(|m| inner_mount_dir.join(&m.orb_mount_name)) { - run_fun!(sudo mkdir $d) + run_fun!(sudo mkdir -p $d) .wrap_err_with(|| format!("failed to create {d:?}"))?; } @@ -243,15 +270,15 @@ impl Drop for Mounter { } } - // The regular destructor of TempDir doesn't work, because the directory contains - // root-owned files. We need to delete manually with sudo. - let tmp = self.tmp.take().expect("always Some until drop"); - let tmp_path = tmp.path(); - debug!("deleting tempdir {tmp_path:?}"); - let result = run_fun!(sudo rm -rf $tmp_path) - .wrap_err("failed to remove tempdir with sudo"); - if let Err(err) = result { - warn!("{err:?}"); + for tmp in [self.mount_rts_tmp.take(), self.tmp.take()] { + let Some(tmp) = tmp else { continue }; + let tmp_path = tmp.path(); + debug!("deleting tempdir {tmp_path:?}"); + let result = run_fun!(sudo rm -rf $tmp_path) + .wrap_err("failed to remove tempdir with sudo"); + if let Err(err) = result { + warn!("{err:?}"); + } } } } diff --git a/hil/src/rts.rs b/hil/src/rts.rs index 5fd17c285..64e63a5b1 100644 --- a/hil/src/rts.rs +++ b/hil/src/rts.rs @@ -85,7 +85,7 @@ pub(crate) fn extract(path_to_rts: &Utf8Path) -> Result { let result = run_cmd! { cd $extract_dir; info extracting rts $path_to_rts; - tar xvf $path_to_rts; + tar xf $path_to_rts; info finished extract!; }; result From 1f9f4cea4112d3666fa96144d40438d0d5ae713b Mon Sep 17 00:00:00 2001 From: AlexKaravaev Date: Tue, 24 Feb 2026 22:00:36 +0100 Subject: [PATCH 2/5] format --- hil/src/nfsboot.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hil/src/nfsboot.rs b/hil/src/nfsboot.rs index 64f937288..a63f4a3a4 100644 --- a/hil/src/nfsboot.rs +++ b/hil/src/nfsboot.rs @@ -70,9 +70,7 @@ pub async fn nfsboot( debug!("mount rts temp dir: {tmp:?}"); let mount_rts_dir = tmp.path().join("rts"); assert!( - tokio::fs::try_exists(&mount_rts_dir) - .await - .unwrap_or(false), + tokio::fs::try_exists(&mount_rts_dir).await.unwrap_or(false), "we expected a directory called `rts` in mount RTS tarball" ); for m in mounts.iter_mut().filter(|m| m.host_path == "/rtsdir") { From e6e50e309dfaa7a3ebd8d000304e618a6b748d1a Mon Sep 17 00:00:00 2001 From: AlexKaravaev <30314738+AlexKaravaev@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:28:57 +0100 Subject: [PATCH 3/5] fix(hil): disambiguate mount rts download filename --- hil/src/commands/nfsboot.rs | 105 +++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/hil/src/commands/nfsboot.rs b/hil/src/commands/nfsboot.rs index 556e83893..cdb686936 100644 --- a/hil/src/commands/nfsboot.rs +++ b/hil/src/commands/nfsboot.rs @@ -100,12 +100,9 @@ impl Nfsboot { self.rts_path.is_none(), "sanity: mutual exclusion guaranteed by clap" ); - let download_dir = - self.download_dir.clone().unwrap_or_else(crate::current_dir); - let download_path = download_dir.join( - crate::download_s3::parse_filename(s3_url) - .wrap_err("failed to parse filename")?, - ); + let download_path = self + .download_path_for_s3_url(s3_url) + .wrap_err("failed to resolve boot rts download path")?; crate::download_s3::download_url( s3_url, @@ -140,12 +137,9 @@ impl Nfsboot { self.mount_rts_path.is_none(), "sanity: mutual exclusion guaranteed by clap" ); - let download_dir = - self.download_dir.clone().unwrap_or_else(crate::current_dir); - let download_path = download_dir.join( - crate::download_s3::parse_filename(s3_url) - .wrap_err("failed to parse mount rts filename")?, - ); + let download_path = self + .mount_download_path(s3_url) + .wrap_err("failed to resolve mount rts download path")?; crate::download_s3::download_url( s3_url, @@ -162,4 +156,91 @@ impl Nfsboot { Ok(path) } + + fn download_dir(&self) -> Utf8PathBuf { + self.download_dir.clone().unwrap_or_else(crate::current_dir) + } + + fn download_path_for_s3_url(&self, s3_url: &S3Uri) -> Result { + let file_name = crate::download_s3::parse_filename(s3_url) + .wrap_err("failed to parse s3 filename")?; + + Ok(self.download_dir().join(file_name)) + } + + fn mount_download_path(&self, mount_s3_url: &S3Uri) -> Result { + let mount_download_path = self + .download_path_for_s3_url(mount_s3_url) + .wrap_err("failed to parse mount s3 filename")?; + + let Some(ref boot_s3_url) = self.s3_url else { + return Ok(mount_download_path); + }; + + let boot_download_path = self + .download_path_for_s3_url(boot_s3_url) + .wrap_err("failed to parse boot s3 filename")?; + if mount_download_path != boot_download_path { + return Ok(mount_download_path); + } + + let mount_file_name = mount_download_path + .file_name() + .ok_or_else(|| color_eyre::eyre::eyre!("mount rts path has no filename"))?; + let disambiguated_mount_file_name = format!("mount-{mount_file_name}"); + + Ok(self.download_dir().join(disambiguated_mount_file_name)) + } +} + +#[cfg(test)] +mod tests { + use super::Nfsboot; + use camino::Utf8PathBuf; + use orb_s3_helpers::S3Uri; + + #[test] + fn mount_download_path_is_disambiguated_when_file_names_collide() { + let s3_url = S3Uri::parse("s3://test-bucket/rts.tar.zst").unwrap(); + let command = Nfsboot { + s3_url: Some(s3_url.clone()), + download_dir: Some(Utf8PathBuf::from("/tmp/downloads")), + rts_path: None, + mount_s3_url: Some(s3_url.clone()), + mount_rts_path: None, + overwrite_existing: false, + mounts: vec![], + persistent_img_path: None, + }; + + let mount_download_path = command.mount_download_path(&s3_url).unwrap(); + + assert_eq!( + mount_download_path, + Utf8PathBuf::from("/tmp/downloads/mount-rts.tar.zst") + ); + } + + #[test] + fn mount_download_path_is_unchanged_when_file_names_do_not_collide() { + let boot_s3_url = S3Uri::parse("s3://test-bucket/boot-rts.tar.zst").unwrap(); + let mount_s3_url = S3Uri::parse("s3://test-bucket/mount-rts.tar.zst").unwrap(); + let command = Nfsboot { + s3_url: Some(boot_s3_url), + download_dir: Some(Utf8PathBuf::from("/tmp/downloads")), + rts_path: None, + mount_s3_url: Some(mount_s3_url.clone()), + mount_rts_path: None, + overwrite_existing: false, + mounts: vec![], + persistent_img_path: None, + }; + + let mount_download_path = command.mount_download_path(&mount_s3_url).unwrap(); + + assert_eq!( + mount_download_path, + Utf8PathBuf::from("/tmp/downloads/mount-rts.tar.zst") + ); + } } From 58d41f9fd23c0e218499c2f802f54fa90af320c2 Mon Sep 17 00:00:00 2001 From: AlexKaravaev Date: Wed, 25 Feb 2026 00:09:34 +0100 Subject: [PATCH 4/5] fix --- hil/src/commands/nfsboot.rs | 95 +++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/hil/src/commands/nfsboot.rs b/hil/src/commands/nfsboot.rs index cdb686936..5f32285ee 100644 --- a/hil/src/commands/nfsboot.rs +++ b/hil/src/commands/nfsboot.rs @@ -173,23 +173,26 @@ impl Nfsboot { .download_path_for_s3_url(mount_s3_url) .wrap_err("failed to parse mount s3 filename")?; - let Some(ref boot_s3_url) = self.s3_url else { - return Ok(mount_download_path); + let collides_with_boot = if let Some(ref boot_s3_url) = self.s3_url { + let boot_download_path = self + .download_path_for_s3_url(boot_s3_url) + .wrap_err("failed to parse boot s3 filename")?; + mount_download_path == boot_download_path + } else if let Some(ref rts_path) = self.rts_path { + mount_download_path == *rts_path + } else { + false }; - let boot_download_path = self - .download_path_for_s3_url(boot_s3_url) - .wrap_err("failed to parse boot s3 filename")?; - if mount_download_path != boot_download_path { + if !collides_with_boot { return Ok(mount_download_path); } let mount_file_name = mount_download_path .file_name() .ok_or_else(|| color_eyre::eyre::eyre!("mount rts path has no filename"))?; - let disambiguated_mount_file_name = format!("mount-{mount_file_name}"); - Ok(self.download_dir().join(disambiguated_mount_file_name)) + Ok(self.download_dir().join(format!("mount-{mount_file_name}"))) } } @@ -199,12 +202,20 @@ mod tests { use camino::Utf8PathBuf; use orb_s3_helpers::S3Uri; + const DEV_S3: &str = "s3://worldcoin-orb-resources/worldcoin/orb-os/rts/2025-08-14-heads-main-0-g0a8d01b-diamond/rts-diamond-dev.tar.zstd"; + const STAGE_S3: &str = "s3://worldcoin-orb-resources/worldcoin/orb-os/rts/2025-08-14-heads-main-0-g0a8d01b-diamond/rts-diamond-stage.tar.zstd"; + + const DEV_FILENAME: &str = + "2025-08-14-heads-main-0-g0a8d01b-diamond-rts-diamond-dev.tar.zstd"; + const STAGE_FILENAME: &str = + "2025-08-14-heads-main-0-g0a8d01b-diamond-rts-diamond-stage.tar.zstd"; + #[test] - fn mount_download_path_is_disambiguated_when_file_names_collide() { - let s3_url = S3Uri::parse("s3://test-bucket/rts.tar.zst").unwrap(); + fn mount_download_path_disambiguated_when_both_s3_urls_collide() { + let s3_url: S3Uri = DEV_S3.parse().unwrap(); let command = Nfsboot { s3_url: Some(s3_url.clone()), - download_dir: Some(Utf8PathBuf::from("/tmp/downloads")), + download_dir: Some(Utf8PathBuf::from("/tmp/dl")), rts_path: None, mount_s3_url: Some(s3_url.clone()), mount_rts_path: None, @@ -213,34 +224,70 @@ mod tests { persistent_img_path: None, }; - let mount_download_path = command.mount_download_path(&s3_url).unwrap(); - + let path = command.mount_download_path(&s3_url).unwrap(); assert_eq!( - mount_download_path, - Utf8PathBuf::from("/tmp/downloads/mount-rts.tar.zst") + path, + Utf8PathBuf::from(format!("/tmp/dl/mount-{DEV_FILENAME}")) ); } #[test] - fn mount_download_path_is_unchanged_when_file_names_do_not_collide() { - let boot_s3_url = S3Uri::parse("s3://test-bucket/boot-rts.tar.zst").unwrap(); - let mount_s3_url = S3Uri::parse("s3://test-bucket/mount-rts.tar.zst").unwrap(); + fn mount_download_path_unchanged_when_s3_urls_differ() { + let boot: S3Uri = DEV_S3.parse().unwrap(); + let mount: S3Uri = STAGE_S3.parse().unwrap(); let command = Nfsboot { - s3_url: Some(boot_s3_url), - download_dir: Some(Utf8PathBuf::from("/tmp/downloads")), + s3_url: Some(boot), + download_dir: Some(Utf8PathBuf::from("/tmp/dl")), rts_path: None, - mount_s3_url: Some(mount_s3_url.clone()), + mount_s3_url: Some(mount.clone()), mount_rts_path: None, overwrite_existing: false, mounts: vec![], persistent_img_path: None, }; - let mount_download_path = command.mount_download_path(&mount_s3_url).unwrap(); + let path = command.mount_download_path(&mount).unwrap(); + assert_eq!(path, Utf8PathBuf::from(format!("/tmp/dl/{STAGE_FILENAME}"))); + } + + #[test] + fn mount_download_path_disambiguated_when_collides_with_local_rts() { + let mount: S3Uri = DEV_S3.parse().unwrap(); + let local_rts_path = Utf8PathBuf::from(format!("/tmp/dl/{DEV_FILENAME}")); + let command = Nfsboot { + s3_url: None, + download_dir: Some(Utf8PathBuf::from("/tmp/dl")), + rts_path: Some(local_rts_path), + mount_s3_url: Some(mount.clone()), + mount_rts_path: None, + overwrite_existing: false, + mounts: vec![], + persistent_img_path: None, + }; + let path = command.mount_download_path(&mount).unwrap(); assert_eq!( - mount_download_path, - Utf8PathBuf::from("/tmp/downloads/mount-rts.tar.zst") + path, + Utf8PathBuf::from(format!("/tmp/dl/mount-{DEV_FILENAME}")) ); } + + #[test] + fn mount_download_path_unchanged_when_local_rts_differs() { + let mount: S3Uri = STAGE_S3.parse().unwrap(); + let local_rts_path = Utf8PathBuf::from(format!("/tmp/dl/{DEV_FILENAME}")); + let command = Nfsboot { + s3_url: None, + download_dir: Some(Utf8PathBuf::from("/tmp/dl")), + rts_path: Some(local_rts_path), + mount_s3_url: Some(mount.clone()), + mount_rts_path: None, + overwrite_existing: false, + mounts: vec![], + persistent_img_path: None, + }; + + let path = command.mount_download_path(&mount).unwrap(); + assert_eq!(path, Utf8PathBuf::from(format!("/tmp/dl/{STAGE_FILENAME}"))); + } } From 0134d438b5371d2299e766b3735d1bf6e0a1a212 Mon Sep 17 00:00:00 2001 From: AlexKaravaev Date: Wed, 25 Feb 2026 00:17:29 +0100 Subject: [PATCH 5/5] fix --- hil/src/commands/nfsboot.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/hil/src/commands/nfsboot.rs b/hil/src/commands/nfsboot.rs index 5f32285ee..31259e1b8 100644 --- a/hil/src/commands/nfsboot.rs +++ b/hil/src/commands/nfsboot.rs @@ -1,4 +1,4 @@ -use camino::Utf8PathBuf; +use camino::{Utf8Path, Utf8PathBuf}; use clap::Parser; use color_eyre::{ eyre::{bail, WrapErr}, @@ -179,7 +179,7 @@ impl Nfsboot { .wrap_err("failed to parse boot s3 filename")?; mount_download_path == boot_download_path } else if let Some(ref rts_path) = self.rts_path { - mount_download_path == *rts_path + same_file(rts_path, &mount_download_path) } else { false }; @@ -196,6 +196,16 @@ impl Nfsboot { } } +/// Compare two paths for equality by filename only, to catch cases where one +/// is relative (e.g. `./rts.tar.zstd`) and the other absolute +/// (e.g. `/home/user/rts.tar.zstd`). +fn same_file(a: &Utf8Path, b: &Utf8Path) -> bool { + match (a.canonicalize_utf8(), b.canonicalize_utf8()) { + (Ok(a), Ok(b)) => a == b, + _ => a.file_name() == b.file_name(), + } +} + #[cfg(test)] mod tests { use super::Nfsboot; @@ -272,6 +282,28 @@ mod tests { ); } + #[test] + fn mount_download_path_disambiguated_with_relative_local_rts() { + let mount: S3Uri = DEV_S3.parse().unwrap(); + let local_rts_path = Utf8PathBuf::from(format!("./{DEV_FILENAME}")); + let command = Nfsboot { + s3_url: None, + download_dir: Some(Utf8PathBuf::from("/tmp/dl")), + rts_path: Some(local_rts_path), + mount_s3_url: Some(mount.clone()), + mount_rts_path: None, + overwrite_existing: false, + mounts: vec![], + persistent_img_path: None, + }; + + let path = command.mount_download_path(&mount).unwrap(); + assert_eq!( + path, + Utf8PathBuf::from(format!("/tmp/dl/mount-{DEV_FILENAME}")) + ); + } + #[test] fn mount_download_path_unchanged_when_local_rts_differs() { let mount: S3Uri = STAGE_S3.parse().unwrap();