Skip to content

Commit f17d79a

Browse files
committed
Add build-vm subcommand to nh os.
This commit adds the `nh os build-vm` subcommand to build a virtual machine activation script instead of a full system. This includes a new option --with-bootloader/-B that applies to just build-vm, to build a VM with a bootloader. Conditionally run nvd only if the system hostname and target hostnames match. See #208
1 parent e39b629 commit f17d79a

File tree

4 files changed

+94
-15
lines changed

4 files changed

+94
-15
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@
2222
support legacy/vulnerable versions of Nix, and encourage users to update if
2323
they have not yet done so.
2424

25+
- NixOS: Nh now accepts the subcommand `nh os build-vm`, which builds a virtual
26+
machine image activation script instead of a full system. This includes a new
27+
option `--with-bootloader/-B` that applies to just build-vm, to build a VM
28+
with a bootloader.
29+
2530
### Changed
2631

2732
- Darwin: Use `darwin-rebuild` directly for activation instead of old scripts
2833
- Darwin: Future-proof handling of `activate-user` script removal
2934
- Darwin: Improve compatibility with root-only activation in newer nix-darwin
3035
versions
36+
- NixOS: Check if the target hostname matches the running system hostname before
37+
running `nvd` to compare them.
3138

3239
## 4.0.3
3340

src/darwin.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ impl DarwinRebuildArgs {
9191
}
9292
}
9393

94-
let toplevel = toplevel_for(hostname, processed_installable);
94+
let toplevel = toplevel_for(hostname, processed_installable, "toplevel");
9595

9696
commands::Build::new(toplevel)
9797
.extra_arg("--out-link")

src/interface.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ pub enum OsSubcommand {
106106

107107
/// Rollback to a previous generation
108108
Rollback(OsRollbackArgs),
109+
110+
/// Build a NixOS VM image
111+
BuildVm(OsBuildVmArgs),
112+
}
113+
114+
#[derive(Debug, Args)]
115+
pub struct OsBuildVmArgs {
116+
#[command(flatten)]
117+
pub common: OsRebuildArgs,
118+
119+
/// Build with bootloader. Bootloader is bypassed by default.
120+
#[arg(long, short = 'B')]
121+
pub with_bootloader: bool,
109122
}
110123

111124
#[derive(Debug, Args)]

src/nixos.rs

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use crate::commands::Command;
1111
use crate::generations;
1212
use crate::installable::Installable;
1313
use crate::interface::OsSubcommand::{self};
14-
use crate::interface::{self, OsGenerationsArgs, OsRebuildArgs, OsReplArgs, OsRollbackArgs};
14+
use crate::interface::{
15+
self, OsBuildVmArgs, OsGenerationsArgs, OsRebuildArgs, OsReplArgs, OsRollbackArgs,
16+
};
1517
use crate::update::update;
1618
use crate::util::ensure_ssh_key_login;
1719
use crate::util::get_hostname;
@@ -25,15 +27,16 @@ impl interface::OsArgs {
2527
pub fn run(self) -> Result<()> {
2628
use OsRebuildVariant::*;
2729
match self.subcommand {
28-
OsSubcommand::Boot(args) => args.rebuild(Boot),
29-
OsSubcommand::Test(args) => args.rebuild(Test),
30-
OsSubcommand::Switch(args) => args.rebuild(Switch),
30+
OsSubcommand::Boot(args) => args.rebuild(Boot, None),
31+
OsSubcommand::Test(args) => args.rebuild(Test, None),
32+
OsSubcommand::Switch(args) => args.rebuild(Switch, None),
3133
OsSubcommand::Build(args) => {
3234
if args.common.ask || args.common.dry {
3335
warn!("`--ask` and `--dry` have no effect for `nh os build`");
3436
}
35-
args.rebuild(Build)
37+
args.rebuild(Build, None)
3638
}
39+
OsSubcommand::BuildVm(args) => args.build_vm(),
3740
OsSubcommand::Repl(args) => args.run(),
3841
OsSubcommand::Info(args) => args.info(),
3942
OsSubcommand::Rollback(args) => args.rollback(),
@@ -47,10 +50,20 @@ enum OsRebuildVariant {
4750
Switch,
4851
Boot,
4952
Test,
53+
BuildVm,
54+
}
55+
56+
impl OsBuildVmArgs {
57+
fn build_vm(self) -> Result<()> {
58+
let final_attr = get_final_attr(true, self.with_bootloader);
59+
self.common
60+
.rebuild(OsRebuildVariant::BuildVm, Some(final_attr))
61+
}
5062
}
5163

5264
impl OsRebuildArgs {
53-
fn rebuild(self, variant: OsRebuildVariant) -> Result<()> {
65+
// final_attr is the attribute of config.system.build.X to evaluate.
66+
fn rebuild(self, variant: OsRebuildVariant, final_attr: Option<String>) -> Result<()> {
5467
use OsRebuildVariant::*;
5568

5669
if self.build_host.is_some() || self.target_host.is_some() {
@@ -72,7 +85,24 @@ impl OsRebuildArgs {
7285
update(&self.common.installable, self.update_args.update_input)?;
7386
}
7487

75-
let hostname = self.hostname.ok_or(()).or_else(|()| get_hostname())?;
88+
let system_hostname = match get_hostname() {
89+
Ok(hostname) => Some(hostname),
90+
Err(err) => {
91+
tracing::warn!("{}", err.to_string());
92+
None
93+
}
94+
};
95+
96+
let target_hostname = match &self.hostname {
97+
Some(h) => h.to_owned(),
98+
None => match &system_hostname {
99+
Some(hostname) => {
100+
tracing::warn!("Guessing system is {hostname} for a VM image. If this isn't intended, use --hostname to change.");
101+
hostname.clone()
102+
}
103+
None => return Err(eyre!("Unable to fetch hostname, and no hostname supplied.")),
104+
},
105+
};
76106

77107
let out_path: Box<dyn crate::util::MaybeTempPath> = match self.common.out_link {
78108
Some(ref p) => Box::new(p.clone()),
@@ -103,14 +133,23 @@ impl OsRebuildArgs {
103133
self.common.installable.clone()
104134
};
105135

106-
let toplevel = toplevel_for(hostname, installable);
136+
let toplevel = toplevel_for(
137+
&target_hostname,
138+
installable,
139+
final_attr.unwrap_or(String::from("toplevel")).as_str(),
140+
);
141+
142+
let message = match variant {
143+
BuildVm => "Building NixOS VM image",
144+
_ => "Building NixOS configuration",
145+
};
107146

108147
commands::Build::new(toplevel)
109148
.extra_arg("--out-link")
110149
.extra_arg(out_path.get_path())
111150
.extra_args(&self.extra_args)
112151
.builder(self.build_host.clone())
113-
.message("Building NixOS configuration")
152+
.message(message)
114153
.nom(!self.common.no_nom)
115154
.run()?;
116155

@@ -133,16 +172,21 @@ impl OsRebuildArgs {
133172

134173
target_profile.try_exists().context("Doesn't exist")?;
135174

136-
if self.build_host.is_none() && self.target_host.is_none() {
175+
if self.build_host.is_none()
176+
&& self.target_host.is_none()
177+
&& system_hostname.map_or(true, |h| h == target_hostname)
178+
{
137179
Command::new("nvd")
138180
.arg("diff")
139181
.arg(CURRENT_PROFILE)
140182
.arg(&target_profile)
141183
.message("Comparing changes")
142184
.run()?;
185+
} else {
186+
debug!("Not running nvd as the target hostname is different from the system hostname.")
143187
}
144188

145-
if self.common.dry || matches!(variant, Build) {
189+
if self.common.dry || matches!(variant, Build | BuildVm) {
146190
if self.common.ask {
147191
warn!("--ask has no effect as dry run was requested");
148192
}
@@ -466,11 +510,26 @@ fn get_current_generation_number() -> Result<u64> {
466510
.map_err(|_| eyre!("Invalid generation number"))
467511
}
468512

469-
pub fn toplevel_for<S: AsRef<str>>(hostname: S, installable: Installable) -> Installable {
470-
let mut res = installable;
513+
pub fn get_final_attr(build_vm: bool, with_bootloader: bool) -> String {
514+
let attr = if build_vm && with_bootloader {
515+
"vmWithBootLoader"
516+
} else if build_vm {
517+
"vm"
518+
} else {
519+
"toplevel"
520+
};
521+
String::from(attr)
522+
}
523+
524+
pub fn toplevel_for<S: AsRef<str>>(
525+
hostname: S,
526+
installable: Installable,
527+
final_attr: &str,
528+
) -> Installable {
529+
let mut res = installable.clone();
471530
let hostname = hostname.as_ref().to_owned();
472531

473-
let toplevel = ["config", "system", "build", "toplevel"]
532+
let toplevel = ["config", "system", "build", final_attr]
474533
.into_iter()
475534
.map(String::from);
476535

0 commit comments

Comments
 (0)