Monorepo for Tangled tangled.org
6

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"; 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(&req).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(req: &v1::ActivateConfig) -> 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(req, timeout).await? 41 } else { 42 realise_toplevel(&req.toplevel, timeout).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 drv = match fs::canonicalize(DEVSHELL_DRV) { 62 Ok(p) => p, 63 Err(_) => { 64 let _ = fs::remove_file(DEVSHELL_ENV_PATH); 65 return Ok(()); 66 } 67 }; 68 69 info!( 70 ?drv, 71 "running nix print-dev-env for dependencies devshell..." 72 ); 73 let output = run_capture( 74 Spec::new(nix_executable()) 75 .args([ 76 "print-dev-env".into(), 77 "--show-trace".into(), 78 drv.into_os_string(), 79 ]) 80 .cwd(SPINDLE_RUN_DIR) 81 .timeout(timeout), 82 ) 83 .await?; 84 85 if !output.success() { 86 anyhow::bail!( 87 "nix print-dev-env failed: exit={} error={:?} output={}", 88 output.exit.exit_code, 89 output.exit.error, 90 output.combined_lossy(), 91 ); 92 } 93 94 fs::write(DEVSHELL_ENV_PATH, &output.stdout) 95 .with_context(|| format!("write {DEVSHELL_ENV_PATH}"))?; 96 info!(path = %DEVSHELL_ENV_PATH, "wrote devshell env"); 97 Ok(()) 98} 99 100async fn build_toplevel(req: &v1::ActivateConfig, timeout: Duration) -> Result<PathBuf> { 101 let user_config = (req.user_config.is_empty()) 102 .then_some("{}") 103 .unwrap_or_else(|| &req.user_config); 104 105 info!("writing user config to {USER_CONFIG_DIR}/config.json"); 106 write_user_config(user_config).context("write user config")?; 107 108 info!("running nix build command for user config toplevel..."); 109 let output = run_capture( 110 Spec::new(nix_executable()) 111 .args([ 112 "build", 113 "--no-link", 114 "--show-trace", 115 "--json", 116 "--file", 117 "/etc/spindle/nixos/default.nix", 118 ]) 119 .cwd(SPINDLE_RUN_DIR) 120 .timeout(timeout), 121 ) 122 .await?; 123 124 if !output.success() { 125 anyhow::bail!( 126 "nix config build failed: exit={} error={:?} output={}", 127 output.exit.exit_code, 128 output.exit.error, 129 output.combined_lossy(), 130 ); 131 } 132 133 #[derive(Debug, serde::Deserialize)] 134 struct NixBuildResult { 135 outputs: NixBuildOutputs, 136 } 137 #[derive(Debug, serde::Deserialize)] 138 struct NixBuildOutputs { 139 out: PathBuf, 140 } 141 let [result] = serde_json::from_slice::<[NixBuildResult; 1]>(&output.stdout) 142 .context("parse nix build --json output")?; 143 Ok(result.outputs.out) 144} 145 146fn write_user_config(user_config: &str) -> Result<()> { 147 fs::create_dir_all(USER_CONFIG_DIR).with_context(|| format!("create {USER_CONFIG_DIR}"))?; 148 149 let config_path = format!("{USER_CONFIG_DIR}/config.json"); 150 fs::write(&config_path, user_config).with_context(|| format!("write {config_path}"))?; 151 Ok(()) 152} 153 154async fn realise_toplevel(toplevel: &str, timeout: Duration) -> Result<PathBuf> { 155 if !toplevel.starts_with("/nix/store/") { 156 anyhow::bail!("cached config toplevel {toplevel:?} is not a nix store path"); 157 } 158 let output = command::run_capture( 159 Spec::new(nix_executable()) 160 .args(["build", "--no-link", "--show-trace", toplevel]) 161 .timeout(timeout), 162 ) 163 .await?; 164 if !output.success() { 165 anyhow::bail!( 166 "realise cached config failed: exit={} error={:?} output={}", 167 output.exit.exit_code, 168 output.exit.error, 169 output.combined_lossy(), 170 ); 171 } 172 173 Ok(PathBuf::from(toplevel)) 174} 175 176async fn switch_to_configuration(toplevel: &Path, timeout: Duration) -> Result<()> { 177 info!("switching to new configuration: {:?}", toplevel); 178 let switch = toplevel.join("bin/switch-to-configuration"); 179 let output = run_capture(Spec::new(switch).args(["test"]).timeout(timeout)).await?; 180 if !output.success() { 181 anyhow::bail!( 182 "switch-to-configuration test failed: exit={} error={:?} output={}", 183 output.exit.exit_code, 184 output.exit.error, 185 output.combined_lossy(), 186 ); 187 } 188 Ok(()) 189}