Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

1use crate::command::{self, Spec, run_capture}; 2use crate::nix_config::{SPINDLE_RUN_DIR, nix_executable}; 3use crate::protocol::{self, Message, v1}; 4use anyhow::{Context, Result}; 5use std::fs; 6use std::path::{Path, PathBuf}; 7use std::time::Duration; 8use tokio::sync::mpsc::Sender; 9use tracing::info; 10 11const USER_CONFIG_DIR: &str = "/run/spindle/user-config"; 12 13pub async fn run(id: String, req: v1::ActivateConfig, out: Sender<Message>) { 14 let config_key = req.config_key.clone(); 15 let result = activate(&req).await; 16 let msg = Message { 17 id, 18 activate_config_result: Some(v1::ActivateConfigResult { 19 config_key, 20 toplevel: (result.as_ref()) 21 .map(|p| p.to_string_lossy().into_owned()) 22 .unwrap_or_default(), 23 error: protocol::error_or_empty(result.err().map(|e| format!("{e:#}"))), 24 }), 25 ..Default::default() 26 }; 27 let _ = out.send(msg).await; 28} 29 30async fn activate(req: &v1::ActivateConfig) -> Result<PathBuf> { 31 let need_build = req.toplevel.is_empty(); 32 let timeout = (req.timeout_seconds > 0) 33 .then(|| Duration::from_secs(u64::from(req.timeout_seconds))) 34 .or_else(|| need_build.then_some(Duration::from_secs(10 * 60))) 35 .unwrap_or(Duration::from_secs(2 * 60)); 36 37 let toplevel = if need_build { 38 build_toplevel(req, timeout).await? 39 } else { 40 realise_toplevel(&req.toplevel, timeout).await? 41 }; 42 43 if !toplevel.starts_with("/nix/store/") { 44 anyhow::bail!("config toplevel {toplevel:?} is not a nix store path"); 45 } 46 47 switch_to_configuration(&toplevel, timeout).await?; 48 info!( 49 config_key = %req.config_key, 50 base_config_hash = %req.base_config_hash, 51 ?toplevel, 52 "activated NixOS config" 53 ); 54 Ok(toplevel) 55} 56 57async fn build_toplevel(req: &v1::ActivateConfig, timeout: Duration) -> Result<PathBuf> { 58 let user_config = (req.user_config.is_empty()) 59 .then_some("{}") 60 .unwrap_or_else(|| &req.user_config); 61 62 info!("writing user config to {USER_CONFIG_DIR}/config.json"); 63 write_user_config(user_config).context("write user config")?; 64 65 info!("running nix build command for user config toplevel..."); 66 let output = run_capture( 67 Spec::new(nix_executable()) 68 .args([ 69 "build", 70 "--no-link", 71 "--show-trace", 72 "--json", 73 "--file", 74 "/etc/spindle/nixos/default.nix", 75 ]) 76 .cwd(SPINDLE_RUN_DIR) 77 .timeout(timeout), 78 ) 79 .await?; 80 81 if !output.success() { 82 anyhow::bail!( 83 "nix config build failed: exit={} error={:?} output={}", 84 output.exit.exit_code, 85 output.exit.error, 86 output.combined_lossy(), 87 ); 88 } 89 90 #[derive(Debug, serde::Deserialize)] 91 struct NixBuildResult { 92 outputs: NixBuildOutputs, 93 } 94 #[derive(Debug, serde::Deserialize)] 95 struct NixBuildOutputs { 96 out: PathBuf, 97 } 98 let [result] = serde_json::from_slice::<[NixBuildResult; 1]>(&output.stdout) 99 .context("parse nix build --json output")?; 100 Ok(result.outputs.out) 101} 102 103fn write_user_config(user_config: &str) -> Result<()> { 104 fs::create_dir_all(USER_CONFIG_DIR).with_context(|| format!("create {USER_CONFIG_DIR}"))?; 105 106 let config_path = format!("{USER_CONFIG_DIR}/config.json"); 107 fs::write(&config_path, user_config).with_context(|| format!("write {config_path}"))?; 108 Ok(()) 109} 110 111async fn realise_toplevel(toplevel: &str, timeout: Duration) -> Result<PathBuf> { 112 if !toplevel.starts_with("/nix/store/") { 113 anyhow::bail!("cached config toplevel {toplevel:?} is not a nix store path"); 114 } 115 let output = command::run_capture( 116 Spec::new(nix_executable()) 117 .args(["build", "--no-link", "--show-trace", toplevel]) 118 .timeout(timeout), 119 ) 120 .await?; 121 if !output.success() { 122 anyhow::bail!( 123 "realise cached config failed: exit={} error={:?} output={}", 124 output.exit.exit_code, 125 output.exit.error, 126 output.combined_lossy(), 127 ); 128 } 129 130 Ok(PathBuf::from(toplevel)) 131} 132 133async fn switch_to_configuration(toplevel: &Path, timeout: Duration) -> Result<()> { 134 info!("switching to new configuration: {:?}", toplevel); 135 let switch = toplevel.join("bin/switch-to-configuration"); 136 let output = run_capture(Spec::new(switch).args(["test"]).timeout(timeout)).await?; 137 if !output.success() { 138 anyhow::bail!( 139 "switch-to-configuration test failed: exit={} error={:?} output={}", 140 output.exit.exit_code, 141 output.exit.error, 142 output.combined_lossy(), 143 ); 144 } 145 Ok(()) 146}