Monorepo for Tangled tangled.org
2

Configure Feed

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

1use crate::command::{CaptureOutput, OutKind, Spec, run_capture, spawn_streaming}; 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"; 12const DEVSHELL_ENV_PATH: &str = "/run/spindle/devshell-env.sh"; 13const DEVSHELL_DRV: &str = "/etc/spindle/devshell-drv"; 14 15pub async fn run(id: String, req: v1::ActivateConfig, out: Sender<Message>) { 16 let config_key = req.config_key.clone(); 17 let result = activate(&id, &req, &out).await; 18 let msg = Message { 19 id, 20 activate_config_result: Some(v1::ActivateConfigResult { 21 config_key, 22 toplevel: (result.as_ref()) 23 .map(|p| p.to_string_lossy().into_owned()) 24 .unwrap_or_default(), 25 error: protocol::error_or_empty(result.err().map(|e| format!("{e:#}"))), 26 }), 27 ..Default::default() 28 }; 29 let _ = out.send(msg).await; 30} 31 32async fn activate(id: &str, req: &v1::ActivateConfig, out: &Sender<Message>) -> Result<PathBuf> { 33 let need_build = req.toplevel.is_empty(); 34 let timeout = (req.timeout_seconds > 0) 35 .then(|| Duration::from_secs(u64::from(req.timeout_seconds))) 36 .or_else(|| need_build.then_some(Duration::from_secs(10 * 60))) 37 .unwrap_or(Duration::from_secs(2 * 60)); 38 39 let toplevel = if need_build { 40 build_toplevel(id, req, timeout, out).await? 41 } else { 42 realise_toplevel(id, &req.toplevel, timeout, out).await? 43 }; 44 45 if !toplevel.starts_with("/nix/store/") { 46 anyhow::bail!("config toplevel {toplevel:?} is not a nix store path"); 47 } 48 49 switch_to_configuration(&toplevel, timeout).await?; 50 write_devshell_env(timeout).await?; 51 info!( 52 config_key = %req.config_key, 53 base_config_hash = %req.base_config_hash, 54 ?toplevel, 55 "activated NixOS config" 56 ); 57 Ok(toplevel) 58} 59 60async fn write_devshell_env(timeout: Duration) -> Result<()> { 61 let pointer = Path::new(DEVSHELL_DRV); 62 if !pointer.exists() { 63 let _ = fs::remove_file(DEVSHELL_ENV_PATH); 64 return Ok(()); 65 } 66 let drv = fs::read_to_string(&pointer) 67 .with_context(|| format!("read {pointer:?}"))? 68 .trim() 69 .to_owned(); 70 71 info!( 72 ?drv, 73 "running nix print-dev-env for dependencies devshell..." 74 ); 75 let output = run_capture( 76 Spec::new(nix_executable()) 77 .args(["print-dev-env", "--show-trace", &drv]) 78 .cwd(SPINDLE_RUN_DIR) 79 .timeout(timeout), 80 ) 81 .await?; 82 83 if !output.success() { 84 anyhow::bail!( 85 "nix print-dev-env failed: exit={} error={:?} output={}", 86 output.exit.exit_code, 87 output.exit.error, 88 output.combined_lossy(), 89 ); 90 } 91 92 fs::write(DEVSHELL_ENV_PATH, &output.stdout) 93 .with_context(|| format!("write {DEVSHELL_ENV_PATH}"))?; 94 info!(path = %DEVSHELL_ENV_PATH, "wrote devshell env"); 95 Ok(()) 96} 97 98async fn build_toplevel( 99 id: &str, 100 req: &v1::ActivateConfig, 101 timeout: Duration, 102 out: &Sender<Message>, 103) -> Result<PathBuf> { 104 let user_config = (req.user_config.is_empty()) 105 .then_some("{}") 106 .unwrap_or_else(|| &req.user_config); 107 108 info!("writing user config to {USER_CONFIG_DIR}/config.json"); 109 write_user_config(user_config).context("write user config")?; 110 111 info!("running nix build command for user config toplevel..."); 112 let output = run_streaming_stderr( 113 Spec::new(nix_executable()) 114 .args([ 115 "build", 116 "--no-link", 117 "--show-trace", 118 "--json", 119 "--file", 120 "/etc/spindle/nixos/default.nix", 121 ]) 122 .cwd(SPINDLE_RUN_DIR) 123 .timeout(timeout), 124 id, 125 out, 126 ) 127 .await?; 128 129 if !output.success() { 130 anyhow::bail!( 131 "nix config build failed: exit={} error={:?} output={}", 132 output.exit.exit_code, 133 output.exit.error, 134 output.combined_lossy(), 135 ); 136 } 137 138 #[derive(Debug, serde::Deserialize)] 139 struct NixBuildResult { 140 outputs: NixBuildOutputs, 141 } 142 #[derive(Debug, serde::Deserialize)] 143 struct NixBuildOutputs { 144 out: PathBuf, 145 } 146 let [result] = serde_json::from_slice::<[NixBuildResult; 1]>(&output.stdout) 147 .context("parse nix build --json output")?; 148 Ok(result.outputs.out) 149} 150 151fn write_user_config(user_config: &str) -> Result<()> { 152 fs::create_dir_all(USER_CONFIG_DIR).with_context(|| format!("create {USER_CONFIG_DIR}"))?; 153 154 let config_path = format!("{USER_CONFIG_DIR}/config.json"); 155 fs::write(&config_path, user_config).with_context(|| format!("write {config_path}"))?; 156 Ok(()) 157} 158 159async fn realise_toplevel( 160 id: &str, 161 toplevel: &str, 162 timeout: Duration, 163 out: &Sender<Message>, 164) -> Result<PathBuf> { 165 if !toplevel.starts_with("/nix/store/") { 166 anyhow::bail!("cached config toplevel {toplevel:?} is not a nix store path"); 167 } 168 let output = run_streaming_stderr( 169 Spec::new(nix_executable()) 170 .args(["build", "--no-link", "--show-trace", toplevel]) 171 .timeout(timeout), 172 id, 173 out, 174 ) 175 .await?; 176 if !output.success() { 177 anyhow::bail!( 178 "realise cached config failed: exit={} error={:?} output={}", 179 output.exit.exit_code, 180 output.exit.error, 181 output.combined_lossy(), 182 ); 183 } 184 185 Ok(PathBuf::from(toplevel)) 186} 187 188// streams stderr but captures stdout 189async fn run_streaming_stderr( 190 spec: Spec, 191 id: &str, 192 out: &Sender<Message>, 193) -> Result<CaptureOutput> { 194 let running = spawn_streaming(spec)?; 195 let mut stdout = Vec::new(); 196 let mut stderr = Vec::new(); 197 let (mut events, exit_task) = running.into_parts(); 198 199 while let Some(event) = events.recv().await { 200 match event.kind { 201 OutKind::Stdout => stdout.extend_from_slice(&event.data), 202 OutKind::Stderr => { 203 stderr.extend_from_slice(&event.data); 204 let data = String::from_utf8_lossy(&event.data).into_owned(); 205 let _ = out 206 .send(Message { 207 id: id.to_owned(), 208 exec_stderr: Some(v1::ExecStderr { data }), 209 ..Default::default() 210 }) 211 .await; 212 } 213 } 214 } 215 216 let exit = exit_task 217 .await 218 .unwrap_or_else(|error| Err(anyhow::anyhow!("command supervisor failed: {error}")))?; 219 Ok(CaptureOutput { 220 exit, 221 stdout, 222 stderr, 223 }) 224} 225 226async fn switch_to_configuration(toplevel: &Path, timeout: Duration) -> Result<()> { 227 info!("switching to new configuration: {:?}", toplevel); 228 let switch = toplevel.join("bin/switch-to-configuration"); 229 let output = run_capture(Spec::new(switch).args(["test"]).timeout(timeout)).await?; 230 if !output.success() { 231 anyhow::bail!( 232 "switch-to-configuration test failed: exit={} error={:?} output={}", 233 output.exit.exit_code, 234 output.exit.error, 235 output.combined_lossy(), 236 ); 237 } 238 Ok(()) 239}