Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ static PASSWORD_CACHE: OnceLock<Mutex<HashMap<String, SecretString>>> =

fn get_cached_password(host: &str) -> Option<SecretString> {
let cache = PASSWORD_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let guard = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let guard = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.get(host).cloned()
}

fn cache_password(host: &str, password: SecretString) {
let cache = PASSWORD_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut guard = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut guard = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.insert(host.to_string(), password);
}

Expand Down
9 changes: 5 additions & 4 deletions src/generations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ pub fn get_closure_size(generation_dir: &Path) -> Option<String> {
let closure_size = json.as_array().and_then(|arr| {
arr.iter().find_map(|entry| {
let path = entry.get("path").and_then(|v| v.as_str());
let size = entry.get("closureSize").and_then(serde_json::Value::as_u64);
let size =
entry.get("closureSize").and_then(serde_json::Value::as_u64);
if let (Some(path), Some(size)) = (path, size) {
if path == store_path_str {
return Some(size);
Expand All @@ -133,9 +134,9 @@ pub fn get_closure_size(generation_dir: &Path) -> Option<String> {
arr
.iter()
.filter_map(|entry| {
entry
.get("path")
.and_then(|v| v.as_str().map(std::string::ToString::to_string))
entry.get("path").and_then(|v| {
v.as_str().map(std::string::ToString::to_string)
})
})
.collect()
})
Expand Down
4 changes: 4 additions & 0 deletions src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ pub struct OsBuildVmArgs {
/// Build with bootloader. Bootloader is bypassed by default.
#[arg(long, short = 'B')]
pub with_bootloader: bool,

/// Run the VM immediately after building
#[arg(long, short = 'r')]
pub run: bool,
}

#[derive(Debug, Args)]
Expand Down
145 changes: 142 additions & 3 deletions src/nixos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,27 @@ enum OsRebuildVariant {
impl OsBuildVmArgs {
fn build_vm(self, elevation: ElevationStrategy) -> Result<()> {
let final_attr = get_final_attr(true, self.with_bootloader);
debug!("Building VM with attribute: {}", final_attr);
self
let should_run = self.run;
let out_path = self
.common
.common
.rebuild(&OsRebuildVariant::BuildVm, Some(final_attr), elevation)
.out_link
.clone()
.unwrap_or_else(|| PathBuf::from("result"));

debug!("Building VM with attribute: {}", final_attr);
self.common.rebuild(
&OsRebuildVariant::BuildVm,
Some(final_attr),
elevation,
)?;

// If --run flag is set, execute the VM
if should_run {
run_vm(&out_path)?;
}

Ok(())
}
}

Expand Down Expand Up @@ -261,6 +278,12 @@ impl OsRebuildArgs {
if self.common.ask {
warn!("--ask has no effect as dry run was requested");
}

// For VM builds, print instructions on how to run the VM
if matches!(variant, BuildVm) && !self.common.dry {
print_vm_instructions(&out_path)?;
}

return Ok(());
}

Expand Down Expand Up @@ -540,6 +563,122 @@ impl OsRollbackArgs {
}
}

/// Finds the VM runner script in the given build output directory.
///
/// Searches for a file matching `run-*-vm` in the `bin` subdirectory of
/// `out_path`.
///
/// # Arguments
///
/// * `out_path` - The path to the build output directory (usually `result`).
///
/// # Returns
///
/// * `Ok(PathBuf)` with the path to the VM runner script if found.
/// * `Err` if the script cannot be found or the bin directory is missing.
///
/// # Errors
///
/// Returns an error if the bin directory does not exist or if no matching
/// script is found.
fn find_vm_script(out_path: &Path) -> Result<PathBuf> {
let bin_dir = out_path.join("bin");

if !bin_dir.exists() {
bail!(
"VM build output missing bin directory at {}",
bin_dir.display()
);
}

let entries = fs::read_dir(&bin_dir).wrap_err_with(|| {
format!("Failed to read bin directory at {}", bin_dir.display())
})?;

let vm_script = entries
.filter_map(std::result::Result::ok)
.find(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.starts_with("run-") && name.ends_with("-vm"))
})
.map(|entry| entry.path())
.ok_or_else(|| {
eyre!("Could not find VM runner script in {}", bin_dir.display())
})?;

Ok(vm_script)
}

/// Prints instructions for running the built VM to the user.
///
/// Attempts to locate the VM runner script in the build output directory and
/// prints a message with the path to the script. If the script cannot be found,
/// prints a warning and a generic path pattern.
///
/// # Arguments
///
/// * `out_path` - The path to the build output directory (usually `result`).
///
/// # Returns
///
/// * `Ok(())` on success.
/// * `Err` if there is an error searching for the VM script.
fn print_vm_instructions(out_path: &Path) -> Result<()> {
match find_vm_script(out_path) {
Ok(script) => {
info!(
"Done. The virtual machine can be started by running {}",
script.display()
);
},
Err(e) => {
warn!("VM build completed, but could not find run script: {}", e);
info!(
"Done. The virtual machine script should be at {}/bin/run-*-vm",
out_path.display()
);
},
}

Ok(())
}

/// Runs the built NixOS VM by executing the VM runner script.
///
/// Locates the VM runner script in the build output directory and executes it,
/// streaming its output to the user. Returns an error if the script cannot be
/// found or if execution fails.
///
/// # Arguments
///
/// * `out_path` - The path to the build output directory (usually `result`).
///
/// # Returns
///
/// * `Ok(())` if the VM was started successfully.
/// * `Err` if the script cannot be found or execution fails.
fn run_vm(out_path: &Path) -> Result<()> {
let vm_script = find_vm_script(out_path)?;

info!(
"Running VM... Starting virtual machine with {}",
vm_script.display()
);

Command::new(&vm_script)
.message("Running VM")
.show_output(true)
.with_required_env()
.run()
.wrap_err_with(|| {
format!("Failed to run VM script at {}", vm_script.display())
})?;

Ok(())
}

fn find_previous_generation() -> Result<generations::GenerationInfo> {
let profile_path = PathBuf::from(SYSTEM_PROFILE);

Expand Down