Skip to content

Commit 2a3c5f7

Browse files
authored
Merge pull request #175 from serokell/philtaken/remote-building
Add option to build on the target host
2 parents be40823 + d0c8665 commit 2a3c5f7

File tree

5 files changed

+158
-69
lines changed

5 files changed

+158
-69
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ This is a set of options that can be put in any of the above definitions, with t
183183
# If not specified, this will default to `/tmp`
184184
# (if `magicRollback` is in use, this _must_ be writable by `user`)
185185
tempPath = "/home/someuser/.deploy-rs";
186+
187+
# Build the derivation on the target system.
188+
# Will also fetch all external dependencies from the target system's substituters.
189+
# This default to `false`
190+
remoteBuild = true;
186191
}
187192
```
188193

src/cli.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ pub struct Opts {
5656
#[clap(short, long)]
5757
skip_checks: bool,
5858

59+
/// Build on remote host
60+
#[clap(long)]
61+
remote_build: bool,
62+
5963
/// Override the SSH user with the given value
6064
#[clap(long)]
6165
ssh_user: Option<String>,
@@ -138,9 +142,7 @@ async fn check_deployment(
138142
.arg(format!("let r = import {}/.; x = (if builtins.isFunction r then (r {{}}) else r); in if x ? checks then x.checks.${{builtins.currentSystem}} else {{}}", repo));
139143
}
140144

141-
for extra_arg in extra_build_args {
142-
check_command.arg(extra_arg);
143-
}
145+
check_command.args(extra_build_args);
144146

145147
let check_status = check_command.status().await?;
146148

@@ -239,9 +241,7 @@ async fn get_deployment_data(
239241
.arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo))
240242
};
241243

242-
for extra_arg in extra_build_args {
243-
c.arg(extra_arg);
244-
}
244+
c.args(extra_build_args);
245245

246246
let build_child = c
247247
.stdout(Stdio::piped())
@@ -640,6 +640,7 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> {
640640
temp_path: opts.temp_path,
641641
confirm_timeout: opts.confirm_timeout,
642642
dry_activate: opts.dry_activate,
643+
remote_build: opts.remote_build,
643644
sudo: opts.sudo,
644645
};
645646

src/data.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub struct GenericSettings {
3030
pub magic_rollback: Option<bool>,
3131
#[serde(rename(deserialize = "sudo"))]
3232
pub sudo: Option<String>,
33+
#[serde(default,rename(deserialize = "remoteBuild"))]
34+
pub remote_build: Option<bool>,
3335
}
3436

3537
#[derive(Deserialize, Debug, Clone)]

src/lib.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ pub struct CmdOverrides {
163163
pub confirm_timeout: Option<u16>,
164164
pub sudo: Option<String>,
165165
pub dry_activate: bool,
166+
pub remote_build: bool,
166167
}
167168

168169
#[derive(PartialEq, Debug)]
@@ -395,10 +396,10 @@ impl<'a> DeployData<'a> {
395396
}
396397

397398
fn get_sudo(&'a self) -> String {
398-
return match self.merged_settings.sudo {
399-
Some(ref x) => x.clone(),
400-
None => "sudo -u".to_string()
401-
};
399+
match self.merged_settings.sudo {
400+
Some(ref x) => x.clone(),
401+
None => "sudo -u".to_string(),
402+
}
402403
}
403404
}
404405

@@ -416,6 +417,10 @@ pub fn make_deploy_data<'a, 's>(
416417
merged_settings.merge(node.generic_settings.clone());
417418
merged_settings.merge(top_settings.clone());
418419

420+
// build all machines remotely when the command line flag is set
421+
if cmd_overrides.remote_build {
422+
merged_settings.remote_build = Some(cmd_overrides.remote_build);
423+
}
419424
if cmd_overrides.ssh_user.is_some() {
420425
merged_settings.ssh_user = cmd_overrides.ssh_user.clone();
421426
}

src/push.rs

Lines changed: 135 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ pub enum PushProfileError {
4141
Copy(std::io::Error),
4242
#[error("Nix copy command resulted in a bad exit code: {0:?}")]
4343
CopyExit(Option<i32>),
44+
#[error("The remote building option is not supported when using legacy nix")]
45+
RemoteBuildWithLegacyNix,
4446
}
4547

4648
pub struct PushProfileData<'a> {
@@ -54,40 +56,7 @@ pub struct PushProfileData<'a> {
5456
pub extra_build_args: &'a [String],
5557
}
5658

57-
pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileError> {
58-
debug!(
59-
"Finding the deriver of store path for {}",
60-
&data.deploy_data.profile.profile_settings.path
61-
);
62-
63-
// `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :(
64-
let mut show_derivation_command = Command::new("nix");
65-
66-
show_derivation_command
67-
.arg("show-derivation")
68-
.arg(&data.deploy_data.profile.profile_settings.path);
69-
70-
let show_derivation_output = show_derivation_command
71-
.output()
72-
.await
73-
.map_err(PushProfileError::ShowDerivation)?;
74-
75-
match show_derivation_output.status.code() {
76-
Some(0) => (),
77-
a => return Err(PushProfileError::ShowDerivationExit(a)),
78-
};
79-
80-
let derivation_info: HashMap<&str, serde_json::value::Value> = serde_json::from_str(
81-
std::str::from_utf8(&show_derivation_output.stdout)
82-
.map_err(PushProfileError::ShowDerivationUtf8)?,
83-
)
84-
.map_err(PushProfileError::ShowDerivationParse)?;
85-
86-
let derivation_name = derivation_info
87-
.keys()
88-
.next()
89-
.ok_or(PushProfileError::ShowDerivationEmpty)?;
90-
59+
pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: &str) -> Result<(), PushProfileError> {
9160
info!(
9261
"Building profile `{}` for node `{}`",
9362
data.deploy_data.profile_name, data.deploy_data.node_name
@@ -118,9 +87,7 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr
11887
(false, true) => build_command.arg("--no-link"),
11988
};
12089

121-
for extra_arg in data.extra_build_args {
122-
build_command.arg(extra_arg);
123-
}
90+
build_command.args(data.extra_build_args);
12491

12592
let build_exit_status = build_command
12693
// Logging should be in stderr, this just stops the store path from printing for no reason
@@ -179,22 +146,77 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr
179146
a => return Err(PushProfileError::SignExit(a)),
180147
};
181148
}
149+
Ok(())
150+
}
182151

152+
pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: &str) -> Result<(), PushProfileError> {
183153
info!(
184-
"Copying profile `{}` to node `{}`",
154+
"Building profile `{}` for node `{}` on remote host",
185155
data.deploy_data.profile_name, data.deploy_data.node_name
186156
);
187157

188-
let mut copy_command = Command::new("nix");
189-
copy_command.arg("copy");
158+
let store_address = format!("ssh-ng://{}@{}",
159+
if data.deploy_data.profile.generic_settings.ssh_user.is_some() {
160+
&data.deploy_data.profile.generic_settings.ssh_user.as_ref().unwrap()
161+
} else {
162+
&data.deploy_defs.ssh_user
163+
},
164+
data.deploy_data.node.node_settings.hostname
165+
);
190166

191-
if data.deploy_data.merged_settings.fast_connection != Some(true) {
192-
copy_command.arg("--substitute-on-destination");
193-
}
167+
let ssh_opts_str = data.deploy_data.merged_settings.ssh_opts.join(" ");
194168

195-
if !data.check_sigs {
196-
copy_command.arg("--no-check-sigs");
197-
}
169+
170+
// copy the derivation to remote host so it can be built there
171+
let copy_command_status = Command::new("nix").arg("copy")
172+
.arg("-s") // fetch dependencies from substitures, not localhost
173+
.arg("--to").arg(&store_address)
174+
.arg("--derivation").arg(derivation_name)
175+
.env("NIX_SSHOPTS", ssh_opts_str.clone())
176+
.stdout(Stdio::null())
177+
.status()
178+
.await
179+
.map_err(PushProfileError::Copy)?;
180+
181+
match copy_command_status.code() {
182+
Some(0) => (),
183+
a => return Err(PushProfileError::CopyExit(a)),
184+
};
185+
186+
let mut build_command = Command::new("nix");
187+
build_command
188+
.arg("build").arg(derivation_name)
189+
.arg("--eval-store").arg("auto")
190+
.arg("--store").arg(&store_address)
191+
.args(data.extra_build_args)
192+
.env("NIX_SSHOPTS", ssh_opts_str.clone());
193+
194+
debug!("build command: {:?}", build_command);
195+
196+
let build_exit_status = build_command
197+
// Logging should be in stderr, this just stops the store path from printing for no reason
198+
.stdout(Stdio::null())
199+
.status()
200+
.await
201+
.map_err(PushProfileError::Build)?;
202+
203+
match build_exit_status.code() {
204+
Some(0) => (),
205+
a => return Err(PushProfileError::BuildExit(a)),
206+
};
207+
208+
209+
Ok(())
210+
}
211+
212+
pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileError> {
213+
debug!(
214+
"Finding the deriver of store path for {}",
215+
&data.deploy_data.profile.profile_settings.path
216+
);
217+
218+
// `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :(
219+
let mut show_derivation_command = Command::new("nix");
198220

199221
let ssh_opts_str = data
200222
.deploy_data
@@ -206,24 +228,78 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr
206228
// .collect::<Vec<String>>()
207229
.join(" ");
208230

209-
let hostname = match data.deploy_data.cmd_overrides.hostname {
210-
Some(ref x) => x,
211-
None => &data.deploy_data.node.node_settings.hostname,
212-
};
213231

214-
let copy_exit_status = copy_command
215-
.arg("--to")
216-
.arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname))
217-
.arg(&data.deploy_data.profile.profile_settings.path)
218-
.env("NIX_SSHOPTS", ssh_opts_str)
219-
.status()
232+
show_derivation_command
233+
.arg("show-derivation")
234+
.arg(&data.deploy_data.profile.profile_settings.path);
235+
236+
let show_derivation_output = show_derivation_command
237+
.output()
220238
.await
221-
.map_err(PushProfileError::Copy)?;
239+
.map_err(PushProfileError::ShowDerivation)?;
222240

223-
match copy_exit_status.code() {
241+
match show_derivation_output.status.code() {
224242
Some(0) => (),
225-
a => return Err(PushProfileError::CopyExit(a)),
243+
a => return Err(PushProfileError::ShowDerivationExit(a)),
226244
};
227245

246+
let derivation_info: HashMap<&str, serde_json::value::Value> = serde_json::from_str(
247+
std::str::from_utf8(&show_derivation_output.stdout)
248+
.map_err(PushProfileError::ShowDerivationUtf8)?,
249+
)
250+
.map_err(PushProfileError::ShowDerivationParse)?;
251+
252+
let derivation_name = derivation_info
253+
.keys()
254+
.next()
255+
.ok_or(PushProfileError::ShowDerivationEmpty)?;
256+
257+
if data.deploy_data.merged_settings.remote_build.unwrap_or(false) {
258+
if !data.supports_flakes {
259+
return Err(PushProfileError::RemoteBuildWithLegacyNix)
260+
}
261+
262+
// remote building guarantees that the resulting derivation is stored on the target system
263+
// no need to copy after building
264+
build_profile_remotely(&data, derivation_name).await?;
265+
} else {
266+
build_profile_locally(&data, derivation_name).await?;
267+
268+
info!(
269+
"Copying profile `{}` to node `{}`",
270+
data.deploy_data.profile_name, data.deploy_data.node_name
271+
);
272+
273+
let mut copy_command = Command::new("nix");
274+
copy_command.arg("copy");
275+
276+
if data.deploy_data.merged_settings.fast_connection != Some(true) {
277+
copy_command.arg("--substitute-on-destination");
278+
}
279+
280+
if !data.check_sigs {
281+
copy_command.arg("--no-check-sigs");
282+
}
283+
284+
let hostname = match data.deploy_data.cmd_overrides.hostname {
285+
Some(ref x) => x,
286+
None => &data.deploy_data.node.node_settings.hostname,
287+
};
288+
289+
let copy_exit_status = copy_command
290+
.arg("--to")
291+
.arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname))
292+
.arg(&data.deploy_data.profile.profile_settings.path)
293+
.env("NIX_SSHOPTS", ssh_opts_str)
294+
.status()
295+
.await
296+
.map_err(PushProfileError::Copy)?;
297+
298+
match copy_exit_status.code() {
299+
Some(0) => (),
300+
a => return Err(PushProfileError::CopyExit(a)),
301+
};
302+
}
303+
228304
Ok(())
229305
}

0 commit comments

Comments
 (0)