Skip to content

Commit 9b584e4

Browse files
authored
Merge pull request #430 from nix-community/notashelf/push-tznonkqvxxyz
nixos: implement `--run` for VM builders
2 parents a30ace8 + 3c7c158 commit 9b584e4

File tree

4 files changed

+157
-9
lines changed

4 files changed

+157
-9
lines changed

src/commands.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ static PASSWORD_CACHE: OnceLock<Mutex<HashMap<String, SecretString>>> =
2222

2323
fn get_cached_password(host: &str) -> Option<SecretString> {
2424
let cache = PASSWORD_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
25-
let guard = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
25+
let guard = cache
26+
.lock()
27+
.unwrap_or_else(std::sync::PoisonError::into_inner);
2628
guard.get(host).cloned()
2729
}
2830

2931
fn cache_password(host: &str, password: SecretString) {
3032
let cache = PASSWORD_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
31-
let mut guard = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
33+
let mut guard = cache
34+
.lock()
35+
.unwrap_or_else(std::sync::PoisonError::into_inner);
3236
guard.insert(host.to_string(), password);
3337
}
3438

src/generations.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ pub fn get_closure_size(generation_dir: &Path) -> Option<String> {
117117
let closure_size = json.as_array().and_then(|arr| {
118118
arr.iter().find_map(|entry| {
119119
let path = entry.get("path").and_then(|v| v.as_str());
120-
let size = entry.get("closureSize").and_then(serde_json::Value::as_u64);
120+
let size =
121+
entry.get("closureSize").and_then(serde_json::Value::as_u64);
121122
if let (Some(path), Some(size)) = (path, size) {
122123
if path == store_path_str {
123124
return Some(size);
@@ -133,9 +134,9 @@ pub fn get_closure_size(generation_dir: &Path) -> Option<String> {
133134
arr
134135
.iter()
135136
.filter_map(|entry| {
136-
entry
137-
.get("path")
138-
.and_then(|v| v.as_str().map(std::string::ToString::to_string))
137+
entry.get("path").and_then(|v| {
138+
v.as_str().map(std::string::ToString::to_string)
139+
})
139140
})
140141
.collect()
141142
})

src/interface.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ pub struct OsBuildVmArgs {
191191
/// Build with bootloader. Bootloader is bypassed by default.
192192
#[arg(long, short = 'B')]
193193
pub with_bootloader: bool,
194+
195+
/// Run the VM immediately after building
196+
#[arg(long, short = 'r')]
197+
pub run: bool,
194198
}
195199

196200
#[derive(Debug, Args)]

src/nixos.rs

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,27 @@ enum OsRebuildVariant {
6464
impl OsBuildVmArgs {
6565
fn build_vm(self, elevation: ElevationStrategy) -> Result<()> {
6666
let final_attr = get_final_attr(true, self.with_bootloader);
67-
debug!("Building VM with attribute: {}", final_attr);
68-
self
67+
let should_run = self.run;
68+
let out_path = self
69+
.common
6970
.common
70-
.rebuild(&OsRebuildVariant::BuildVm, Some(final_attr), elevation)
71+
.out_link
72+
.clone()
73+
.unwrap_or_else(|| PathBuf::from("result"));
74+
75+
debug!("Building VM with attribute: {}", final_attr);
76+
self.common.rebuild(
77+
&OsRebuildVariant::BuildVm,
78+
Some(final_attr),
79+
elevation,
80+
)?;
81+
82+
// If --run flag is set, execute the VM
83+
if should_run {
84+
run_vm(&out_path)?;
85+
}
86+
87+
Ok(())
7188
}
7289
}
7390

@@ -261,6 +278,12 @@ impl OsRebuildArgs {
261278
if self.common.ask {
262279
warn!("--ask has no effect as dry run was requested");
263280
}
281+
282+
// For VM builds, print instructions on how to run the VM
283+
if matches!(variant, BuildVm) && !self.common.dry {
284+
print_vm_instructions(&out_path)?;
285+
}
286+
264287
return Ok(());
265288
}
266289

@@ -540,6 +563,122 @@ impl OsRollbackArgs {
540563
}
541564
}
542565

566+
/// Finds the VM runner script in the given build output directory.
567+
///
568+
/// Searches for a file matching `run-*-vm` in the `bin` subdirectory of
569+
/// `out_path`.
570+
///
571+
/// # Arguments
572+
///
573+
/// * `out_path` - The path to the build output directory (usually `result`).
574+
///
575+
/// # Returns
576+
///
577+
/// * `Ok(PathBuf)` with the path to the VM runner script if found.
578+
/// * `Err` if the script cannot be found or the bin directory is missing.
579+
///
580+
/// # Errors
581+
///
582+
/// Returns an error if the bin directory does not exist or if no matching
583+
/// script is found.
584+
fn find_vm_script(out_path: &Path) -> Result<PathBuf> {
585+
let bin_dir = out_path.join("bin");
586+
587+
if !bin_dir.exists() {
588+
bail!(
589+
"VM build output missing bin directory at {}",
590+
bin_dir.display()
591+
);
592+
}
593+
594+
let entries = fs::read_dir(&bin_dir).wrap_err_with(|| {
595+
format!("Failed to read bin directory at {}", bin_dir.display())
596+
})?;
597+
598+
let vm_script = entries
599+
.filter_map(std::result::Result::ok)
600+
.find(|entry| {
601+
entry
602+
.file_name()
603+
.to_str()
604+
.is_some_and(|name| name.starts_with("run-") && name.ends_with("-vm"))
605+
})
606+
.map(|entry| entry.path())
607+
.ok_or_else(|| {
608+
eyre!("Could not find VM runner script in {}", bin_dir.display())
609+
})?;
610+
611+
Ok(vm_script)
612+
}
613+
614+
/// Prints instructions for running the built VM to the user.
615+
///
616+
/// Attempts to locate the VM runner script in the build output directory and
617+
/// prints a message with the path to the script. If the script cannot be found,
618+
/// prints a warning and a generic path pattern.
619+
///
620+
/// # Arguments
621+
///
622+
/// * `out_path` - The path to the build output directory (usually `result`).
623+
///
624+
/// # Returns
625+
///
626+
/// * `Ok(())` on success.
627+
/// * `Err` if there is an error searching for the VM script.
628+
fn print_vm_instructions(out_path: &Path) -> Result<()> {
629+
match find_vm_script(out_path) {
630+
Ok(script) => {
631+
info!(
632+
"Done. The virtual machine can be started by running {}",
633+
script.display()
634+
);
635+
},
636+
Err(e) => {
637+
warn!("VM build completed, but could not find run script: {}", e);
638+
info!(
639+
"Done. The virtual machine script should be at {}/bin/run-*-vm",
640+
out_path.display()
641+
);
642+
},
643+
}
644+
645+
Ok(())
646+
}
647+
648+
/// Runs the built NixOS VM by executing the VM runner script.
649+
///
650+
/// Locates the VM runner script in the build output directory and executes it,
651+
/// streaming its output to the user. Returns an error if the script cannot be
652+
/// found or if execution fails.
653+
///
654+
/// # Arguments
655+
///
656+
/// * `out_path` - The path to the build output directory (usually `result`).
657+
///
658+
/// # Returns
659+
///
660+
/// * `Ok(())` if the VM was started successfully.
661+
/// * `Err` if the script cannot be found or execution fails.
662+
fn run_vm(out_path: &Path) -> Result<()> {
663+
let vm_script = find_vm_script(out_path)?;
664+
665+
info!(
666+
"Running VM... Starting virtual machine with {}",
667+
vm_script.display()
668+
);
669+
670+
Command::new(&vm_script)
671+
.message("Running VM")
672+
.show_output(true)
673+
.with_required_env()
674+
.run()
675+
.wrap_err_with(|| {
676+
format!("Failed to run VM script at {}", vm_script.display())
677+
})?;
678+
679+
Ok(())
680+
}
681+
543682
fn find_previous_generation() -> Result<generations::GenerationInfo> {
544683
let profile_path = PathBuf::from(SYSTEM_PROFILE);
545684

0 commit comments

Comments
 (0)