1use std::{env::current_dir, time::Duration};23use crate::command::MyCommand;4use crate::host::Config;5use anyhow::Result;6use clap::Parser;7use tokio::{task::LocalSet, time::sleep};8use tracing::{error, field, info, info_span, warn, Instrument};910#[derive(Parser, Clone)]11pub struct BuildSystems {12 13 #[clap(long)]14 fail_fast: bool,15 16 #[clap(long)]17 privileged_build: bool,18 #[clap(subcommand)]19 subcommand: Subcommand,20}2122enum UploadAction {23 Test,24 Boot,25 Switch,26}27impl UploadAction {28 fn name(&self) -> &'static str {29 match self {30 UploadAction::Test => "test",31 UploadAction::Boot => "boot",32 UploadAction::Switch => "switch",33 }34 }3536 pub(crate) fn should_switch_profile(&self) -> bool {37 matches!(self, Self::Switch | Self::Boot)38 }39 pub(crate) fn should_activate(&self) -> bool {40 matches!(self, Self::Switch | Self::Test)41 }42}4344enum PackageAction {45 SdImage,46 InstallationCd,47}48impl PackageAction {49 fn build_attr(&self) -> String {50 match self {51 PackageAction::SdImage => "sdImage".to_owned(),52 PackageAction::InstallationCd => "installationCd".to_owned(),53 }54 }55}5657enum Action {58 Upload { action: Option<UploadAction> },59 Package(PackageAction),60}61impl Action {62 fn build_attr(&self) -> String {63 match self {64 Action::Upload { .. } => "toplevel".to_owned(),65 Action::Package(p) => p.build_attr(),66 }67 }68}6970impl From<Subcommand> for Action {71 fn from(s: Subcommand) -> Self {72 match s {73 Subcommand::Upload => Self::Upload { action: None },74 Subcommand::Test => Self::Upload {75 action: Some(UploadAction::Test),76 },77 Subcommand::Boot => Self::Upload {78 action: Some(UploadAction::Boot),79 },80 Subcommand::Switch => Self::Upload {81 action: Some(UploadAction::Switch),82 },83 Subcommand::SdImage => Self::Package(PackageAction::SdImage),84 Subcommand::InstallationCd => Self::Package(PackageAction::InstallationCd),85 }86 }87}8889#[derive(Parser, Clone)]90enum Subcommand {91 92 Upload,93 94 Test,95 96 Boot,97 98 Switch,99100 101 SdImage,102 103 InstallationCd,104}105106impl BuildSystems {107 async fn build_task(self, config: Config, host: String) -> Result<()> {108 info!("building");109 let action = Action::from(self.subcommand.clone());110 let built = {111 let dir = tempfile::tempdir()?;112 dir.path().to_owned()113 };114115 let mut nix_build = MyCommand::new("nix");116 nix_build117 .args([118 "build",119 "--impure",120 "--json",121 122 "--no-link",123 ])124 .comparg("--out-link", &built)125 .arg(126 config.configuration_attr_name(&format!(127 "buildSystems.{}.{host}",128 action.build_attr()129 )),130 )131 .args(&config.nix_args);132133 if self.privileged_build {134 nix_build = nix_build.sudo();135 }136137 nix_build.run_nix().await.map_err(|e| {138 if action.build_attr() == "sdImage" {139 info!("sd-image build failed");140 info!("Make sure you have imported modulesPath/installer/sd-card/sd-image-<arch>[-installer].nix (For installer, you may want to check config)");141 info!("This module was automatically imported before, but was removed for better customization")142 }143 e144 })?;145 let built = std::fs::canonicalize(built)?;146147 match action {148 Action::Upload { action } => {149 if !config.is_local(&host) {150 info!("uploading system closure");151 let mut tries = 0;152 loop {153 let mut nix = MyCommand::new("nix");154 nix.arg("copy")155 .comparg("--to", format!("ssh://root@{host}"))156 .arg(&built);157 match nix.run_nix().await {158 Ok(()) => break,159 Err(e) if tries < 3 => {160 tries += 1;161 warn!("Copy failure ({}/3): {}", tries, e);162 sleep(Duration::from_millis(5000)).await;163 }164 Err(e) => return Err(e),165 }166 }167 }168 if let Some(action) = action {169 if action.should_switch_profile() {170 info!("switching generation");171 let mut cmd = MyCommand::new("nix-env");172 cmd.comparg("--profile", "/nix/var/nix/profiles/system")173 .comparg("--set", &built);174 config.run_on(&host, cmd, true).await?;175 }176 if action.should_activate() {177 info!("executing activation script");178 let mut switch_script = built.clone();179 switch_script.push("bin");180 switch_script.push("switch-to-configuration");181 let mut cmd = MyCommand::new(switch_script);182 cmd.arg(action.name());183 config.run_on(&host, cmd, true).await?;184 }185 }186 }187 Action::Package(PackageAction::SdImage) => {188 let mut out = current_dir()?;189 out.push(format!("sd-image-{}", host));190191 info!("building sd image to {:?}", out);192 let mut nix_build = MyCommand::new("nix");193 nix_build194 .args(["build", "--impure", "--no-link"])195 .comparg("--out-link", &out)196 .arg(config.configuration_attr_name(&format!("buildSystems.sdImage.{}", host,)))197 .args(&config.nix_args);198 if !self.fail_fast {199 nix_build.arg("--keep-going");200 }201 if self.privileged_build {202 nix_build = nix_build.sudo();203 }204205 nix_build.run_nix().await?;206 }207 Action::Package(PackageAction::InstallationCd) => {208 let mut out = current_dir()?;209 out.push(format!("installation-cd-{}", host));210211 info!("building sd image to {:?}", out);212 let mut nix_build = MyCommand::new("nix");213 nix_build214 .args(["build", "--impure", "--no-link"])215 .comparg("--out-link", &out)216 .arg(217 config.configuration_attr_name(&format!(218 "buildSystems.installationCd.{}",219 host,220 )),221 )222 .args(&config.nix_args);223 if !self.fail_fast {224 nix_build.arg("--keep-going");225 }226 if self.privileged_build {227 nix_build = nix_build.sudo();228 }229230 nix_build.run_nix().await?;231 }232 };233 Ok(())234 }235236 pub async fn run(self, config: &Config) -> Result<()> {237 let hosts = config.list_hosts().await?;238 let set = LocalSet::new();239 let this = &self;240 for host in hosts.iter() {241 if config.should_skip(host) {242 continue;243 }244 let config = config.clone();245 let host = host.clone();246 let this = this.clone();247 let span = info_span!("deployment", host = field::display(&host));248 set.spawn_local(249 (async move {250 match this.build_task(config, host).await {251 Ok(_) => {}252 Err(e) => {253 error!("failed to deploy host: {}", e)254 }255 }256 })257 .instrument(span),258 );259 }260 set.await;261 Ok(())262 }263}