1use std::{env::current_dir, process::Stdio, time::Duration};23use crate::{command::CommandExt, host::Config};4use anyhow::Result;5use clap::Parser;6use tokio::{process::Command, task::LocalSet, time::sleep};7use tracing::{error, field, info, info_span, warn, Instrument};89#[derive(Parser, Clone)]10pub struct BuildSystems {11 12 #[clap(long)]13 fail_fast: bool,14 15 #[clap(long)]16 privileged_build: bool,17 #[clap(subcommand)]18 subcommand: Subcommand,19}2021enum UploadAction {22 Test,23 Boot,24 Switch,25}26impl UploadAction {27 fn name(&self) -> &'static str {28 match self {29 UploadAction::Test => "test",30 UploadAction::Boot => "boot",31 UploadAction::Switch => "switch",32 }33 }3435 pub(crate) fn should_switch_profile(&self) -> bool {36 matches!(self, Self::Switch | Self::Test)37 }38}3940enum PackageAction {41 SdImage,42 InstallationCd,43}44impl PackageAction {45 fn build_attr(&self) -> String {46 match self {47 PackageAction::SdImage => "sdImage".to_owned(),48 PackageAction::InstallationCd => "installationCd".to_owned(),49 }50 }51}5253enum Action {54 Upload { action: Option<UploadAction> },55 Package(PackageAction),56}57impl Action {58 fn build_attr(&self) -> String {59 match self {60 Action::Upload { .. } => "toplevel".to_owned(),61 Action::Package(p) => p.build_attr(),62 }63 }64}6566impl From<Subcommand> for Action {67 fn from(s: Subcommand) -> Self {68 match s {69 Subcommand::Upload => Self::Upload { action: None },70 Subcommand::Test => Self::Upload {71 action: Some(UploadAction::Test),72 },73 Subcommand::Boot => Self::Upload {74 action: Some(UploadAction::Boot),75 },76 Subcommand::Switch => Self::Upload {77 action: Some(UploadAction::Switch),78 },79 Subcommand::SdImage => Self::Package(PackageAction::SdImage),80 Subcommand::InstallationCd => Self::Package(PackageAction::InstallationCd),81 }82 }83}8485#[derive(Parser, Clone)]86enum Subcommand {87 88 Upload,89 90 Test,91 92 Boot,93 94 Switch,9596 97 SdImage,98 99 InstallationCd,100}101102impl BuildSystems {103 async fn build_task(self, config: Config, host: String) -> Result<()> {104 info!("building");105 let action = Action::from(self.subcommand.clone());106 let built = {107 let dir = tempfile::tempdir()?;108 dir.path().to_owned()109 };110111 let mut nix_build = if self.privileged_build {112 let mut out = Command::new("sudo");113 out.arg("nix");114 out115 } else {116 Command::new("nix")117 };118 nix_build119 .args([120 "build",121 "--impure",122 "--json",123 124 "--no-link",125 "--out-link",126 ])127 .arg(&built)128 .arg(129 config.configuration_attr_name(&format!(130 "buildSystems.{}.{host}",131 action.build_attr()132 )),133 )134 .args(&config.nix_args);135136 nix_build.run_nix().await?;137 let built = std::fs::canonicalize(built)?;138139 match action {140 Action::Upload { action } => {141 if !config.is_local(&host) {142 info!("uploading system closure");143 let mut tries = 0;144 loop {145 match Command::new("nix")146 .args(["copy", "--to"])147 .arg(format!("ssh://root@{}", host))148 .arg(&built)149 .inherit_stdio()150 .run_nix()151 .await152 {153 Ok(()) => break,154 Err(e) if tries < 3 => {155 tries += 1;156 warn!("Copy failure ({}/3): {}", tries, e);157 sleep(Duration::from_millis(5000)).await;158 }159 Err(e) => return Err(e),160 }161 }162 }163 if let Some(action) = action {164 if action.should_switch_profile() {165 info!("switching generation");166 config167 .command_on(&host, "nix-env", true)168 .args(["-p", "/nix/var/nix/profiles/system", "--set"])169 .arg(&built)170 .inherit_stdio()171 .run()172 .await?;173 }174 info!("executing activation script");175 let mut switch_script = built.clone();176 switch_script.push("bin");177 switch_script.push("switch-to-configuration");178 config179 .command_on(&host, switch_script, true)180 .arg(action.name())181 .stdout(Stdio::inherit())182 .run()183 .await?;184 }185 }186 Action::Package(PackageAction::SdImage) => {187 let mut out = current_dir()?;188 out.push(format!("sd-image-{}", host));189190 info!("building sd image to {:?}", out);191 let mut nix_build = if self.privileged_build {192 let mut out = Command::new("sudo");193 out.arg("nix");194 out195 } else {196 Command::new("nix")197 };198 nix_build199 .args(["build", "--impure", "--no-link", "--out-link"])200 .arg(&out)201 .arg(config.configuration_attr_name(&format!("buildSystems.sdImage.{}", host,)))202 .args(&config.nix_args);203 if !self.fail_fast {204 nix_build.arg("--keep-going");205 }206207 nix_build.inherit_stdio().run_nix().await?;208 }209 Action::Package(PackageAction::InstallationCd) => {210 let mut out = current_dir()?;211 out.push(format!("installation-cd-{}", host));212213 info!("building sd image to {:?}", out);214 let mut nix_build = if self.privileged_build {215 let mut out = Command::new("sudo");216 out.arg("nix");217 out218 } else {219 Command::new("nix")220 };221 nix_build222 .args(["build", "--impure", "--no-link", "--out-link"])223 .arg(&out)224 .arg(225 config.configuration_attr_name(&format!(226 "buildSystems.installationCd.{}",227 host,228 )),229 )230 .args(&config.nix_args);231 if !self.fail_fast {232 nix_build.arg("--keep-going");233 }234235 nix_build.inherit_stdio().run_nix().await?;236 }237 };238 Ok(())239 }240241 pub async fn run(self, config: &Config) -> Result<()> {242 let hosts = config.list_hosts().await?;243 let set = LocalSet::new();244 let this = &self;245 for host in hosts.iter() {246 if config.should_skip(host) {247 continue;248 }249 let config = config.clone();250 let host = host.clone();251 let this = this.clone();252 let span = info_span!("deployment", host = field::display(&host));253 set.spawn_local(254 (async move {255 match this.build_task(config, host).await {256 Ok(_) => {}257 Err(e) => {258 error!("failed to deploy host: {}", e)259 }260 }261 })262 .instrument(span),263 );264 }265 set.await;266 Ok(())267 }268}