1pub mod linux;
118pub mod macos;
119pub mod ostree;
120pub mod steam_deck;
121
122use std::{collections::HashMap, path::PathBuf, string::FromUtf8Error};
123
124use serde::{Deserialize, Serialize};
125
126use crate::{
127 action::{ActionError, StatefulAction},
128 error::HasExpectedErrors,
129 settings::{CommonSettings, InstallSettingsError},
130 Action, InstallPlan, NixInstallerError,
131};
132
133#[async_trait::async_trait]
135#[typetag::serde(tag = "planner")]
136pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
137 async fn default() -> Result<Self, PlannerError>
139 where
140 Self: Sized;
141 async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError>;
143 fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError>;
145
146 async fn configured_settings(&self)
147 -> Result<HashMap<String, serde_json::Value>, PlannerError>;
148
149 fn boxed(self) -> Box<dyn Planner>
151 where
152 Self: Sized + 'static,
153 {
154 Box::new(self)
155 }
156
157 async fn platform_check(&self) -> Result<(), PlannerError>;
158
159 async fn pre_uninstall_check(&self) -> Result<(), PlannerError> {
160 Ok(())
161 }
162
163 async fn pre_install_check(&self) -> Result<(), PlannerError> {
164 Ok(())
165 }
166
167 #[cfg(feature = "diagnostics")]
168 async fn diagnostic_data(&self) -> Result<crate::diagnostics::DiagnosticData, PlannerError>;
169}
170
171dyn_clone::clone_trait_object!(Planner);
172
173#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
175#[cfg_attr(feature = "cli", derive(clap::Subcommand))]
176pub enum BuiltinPlanner {
177 #[cfg_attr(not(target_os = "linux"), clap(hide = true))]
178 Linux(linux::Linux),
180 #[cfg_attr(not(target_os = "linux"), clap(hide = true))]
181 SteamDeck(steam_deck::SteamDeck),
183 #[cfg_attr(not(target_os = "linux"), clap(hide = true))]
184 Ostree(ostree::Ostree),
186 #[cfg_attr(not(target_os = "macos"), clap(hide = true))]
187 Macos(macos::Macos),
189}
190
191impl BuiltinPlanner {
192 pub async fn default() -> Result<Self, PlannerError> {
194 use target_lexicon::{Architecture, OperatingSystem};
195 match (Architecture::host(), OperatingSystem::host()) {
196 (Architecture::X86_64, OperatingSystem::Linux) => Self::detect_linux_distro().await,
197 (Architecture::X86_32(_), OperatingSystem::Linux) => {
198 Ok(Self::Linux(linux::Linux::default().await?))
199 },
200 (Architecture::Aarch64(_), OperatingSystem::Linux) => {
201 Ok(Self::Linux(linux::Linux::default().await?))
202 },
203 (Architecture::X86_64, OperatingSystem::MacOSX { .. })
204 | (Architecture::X86_64, OperatingSystem::Darwin) => {
205 Ok(Self::Macos(macos::Macos::default().await?))
206 },
207 (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. })
208 | (Architecture::Aarch64(_), OperatingSystem::Darwin) => {
209 Ok(Self::Macos(macos::Macos::default().await?))
210 },
211 _ => Err(PlannerError::UnsupportedArchitecture(target_lexicon::HOST)),
212 }
213 }
214
215 async fn detect_linux_distro() -> Result<Self, PlannerError> {
216 let is_steam_deck =
217 os_release::OsRelease::new().is_ok_and(|os_release| os_release.id == "steamos");
218 if is_steam_deck {
219 return Ok(Self::SteamDeck(steam_deck::SteamDeck::default().await?));
220 }
221
222 let is_ostree = std::process::Command::new("ostree")
223 .arg("remote")
224 .arg("list")
225 .output()
226 .is_ok_and(|output| output.status.success());
227 if is_ostree {
228 return Ok(Self::Ostree(ostree::Ostree::default().await?));
229 }
230
231 Ok(Self::Linux(linux::Linux::default().await?))
232 }
233
234 pub async fn from_common_settings(settings: CommonSettings) -> Result<Self, PlannerError> {
235 let mut built = Self::default().await?;
236 match &mut built {
237 BuiltinPlanner::Linux(inner) => inner.settings = settings,
238 BuiltinPlanner::SteamDeck(inner) => inner.settings = settings,
239 BuiltinPlanner::Ostree(inner) => inner.settings = settings,
240 BuiltinPlanner::Macos(inner) => inner.settings = settings,
241 }
242 Ok(built)
243 }
244
245 pub async fn configured_settings(
246 &self,
247 ) -> Result<HashMap<String, serde_json::Value>, PlannerError> {
248 match self {
249 BuiltinPlanner::Linux(inner) => inner.configured_settings().await,
250 BuiltinPlanner::SteamDeck(inner) => inner.configured_settings().await,
251 BuiltinPlanner::Ostree(inner) => inner.configured_settings().await,
252 BuiltinPlanner::Macos(inner) => inner.configured_settings().await,
253 }
254 }
255
256 pub async fn plan(self) -> Result<InstallPlan, NixInstallerError> {
257 match self {
258 BuiltinPlanner::Linux(planner) => InstallPlan::plan(planner).await,
259 BuiltinPlanner::SteamDeck(planner) => InstallPlan::plan(planner).await,
260 BuiltinPlanner::Ostree(planner) => InstallPlan::plan(planner).await,
261 BuiltinPlanner::Macos(planner) => InstallPlan::plan(planner).await,
262 }
263 }
264 pub fn boxed(self) -> Box<dyn Planner> {
265 match self {
266 BuiltinPlanner::Linux(i) => i.boxed(),
267 BuiltinPlanner::SteamDeck(i) => i.boxed(),
268 BuiltinPlanner::Ostree(i) => i.boxed(),
269 BuiltinPlanner::Macos(i) => i.boxed(),
270 }
271 }
272
273 pub fn typetag_name(&self) -> &'static str {
274 match self {
275 BuiltinPlanner::Linux(i) => i.typetag_name(),
276 BuiltinPlanner::SteamDeck(i) => i.typetag_name(),
277 BuiltinPlanner::Ostree(i) => i.typetag_name(),
278 BuiltinPlanner::Macos(i) => i.typetag_name(),
279 }
280 }
281
282 pub fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
283 match self {
284 BuiltinPlanner::Linux(i) => i.settings(),
285 BuiltinPlanner::SteamDeck(i) => i.settings(),
286 BuiltinPlanner::Ostree(i) => i.settings(),
287 BuiltinPlanner::Macos(i) => i.settings(),
288 }
289 }
290
291 #[cfg(feature = "diagnostics")]
292 pub async fn diagnostic_data(
293 &self,
294 ) -> Result<crate::diagnostics::DiagnosticData, PlannerError> {
295 match self {
296 BuiltinPlanner::Linux(i) => i.diagnostic_data().await,
297 BuiltinPlanner::SteamDeck(i) => i.diagnostic_data().await,
298 BuiltinPlanner::Ostree(i) => i.diagnostic_data().await,
299 BuiltinPlanner::Macos(i) => i.diagnostic_data().await,
300 }
301 }
302}
303
304#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
305pub struct ShellProfileLocations {
306 pub fish: FishShellProfileLocations,
307 pub bash: Vec<PathBuf>,
308 pub zsh: Vec<PathBuf>,
309}
310
311impl Default for ShellProfileLocations {
312 fn default() -> Self {
313 Self {
314 fish: FishShellProfileLocations::default(),
315 bash: vec![
316 "/etc/bashrc".into(),
317 "/etc/profile.d/nix.sh".into(),
318 "/etc/bash.bashrc".into(),
319 ],
320 zsh: vec![
321 "/etc/zshrc".into(),
323 "/etc/zsh/zshrc".into(),
324 ],
325 }
326 }
327}
328
329#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
330pub struct FishShellProfileLocations {
331 pub confd_suffix: PathBuf,
332 pub confd_prefixes: Vec<PathBuf>,
338 pub vendor_confd_suffix: PathBuf,
340 pub vendor_confd_prefixes: Vec<PathBuf>,
348}
349
350impl Default for FishShellProfileLocations {
351 fn default() -> Self {
352 Self {
353 confd_prefixes: vec![
354 "/etc/fish".into(), "/usr/local/etc/fish".into(), "/opt/homebrew/etc/fish".into(), "/opt/local/etc/fish".into(), ],
359 confd_suffix: "conf.d/nix.fish".into(),
360 vendor_confd_prefixes: vec!["/usr/share/fish/".into(), "/usr/local/share/fish/".into()],
361 vendor_confd_suffix: "vendor_conf.d/nix.fish".into(),
362 }
363 }
364}
365
366#[non_exhaustive]
368#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
369pub enum PlannerError {
370 #[error("The selected planner (`{planner}`) does not support the host's operating system (`{host_os}`)")]
371 IncompatibleOperatingSystem {
372 planner: &'static str,
373 host_os: target_lexicon::OperatingSystem,
374 },
375 #[error("`nix-installer` does not have a default planner for the `{0}` architecture right now, pass a specific archetype")]
377 UnsupportedArchitecture(target_lexicon::Triple),
378 #[error("Error executing action")]
380 Action(
381 #[source]
382 #[from]
383 ActionError,
384 ),
385 #[error(transparent)]
387 InstallSettings(#[from] InstallSettingsError),
388 #[error("Fetching `/etc/os-release`")]
390 OsRelease(#[source] std::io::Error),
391 #[error(transparent)]
393 Plist(#[from] plist::Error),
394 #[error(transparent)]
395 Sysctl(#[from] sysctl::SysctlError),
396 #[error("Detected that this process is running under Rosetta, using Nix in Rosetta is not supported (Please open an issue with your use case)")]
397 RosettaDetected,
398 #[error("Determinate Nix is not available. See: https://determinate.systems/enterprise")]
399 DeterminateNixUnavailable,
400 #[error("Unable to install on an SELinux system without common SELinux tooling, the binaries `restorecon`, and `semodule` are required")]
402 SelinuxRequirements,
403 #[error("UTF-8 error")]
405 Utf8(#[from] FromUtf8Error),
406 #[error("Custom planner error")]
408 Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
409 #[error("NixOS already has Nix installed")]
410 NixOs,
411 #[error("`nix` is already a valid command, so it is installed")]
412 NixExists,
413 #[error("WSL1 is not supported, please upgrade to WSL2: https://learn.microsoft.com/en-us/windows/wsl/install#upgrade-version-from-wsl-1-to-wsl-2")]
414 Wsl1,
415 #[error("Failed to execute command `{0}`")]
417 Command(String, #[source] std::io::Error),
418 #[cfg(feature = "diagnostics")]
419 #[error(transparent)]
420 Diagnostic(#[from] crate::diagnostics::DiagnosticError),
421}
422
423impl HasExpectedErrors for PlannerError {
424 fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
425 match self {
426 this @ PlannerError::UnsupportedArchitecture(_) => Some(Box::new(this)),
427 PlannerError::Action(_) => None,
428 PlannerError::InstallSettings(_) => None,
429 PlannerError::Plist(_) => None,
430 PlannerError::Sysctl(_) => None,
431 this @ PlannerError::IncompatibleOperatingSystem { .. } => Some(Box::new(this)),
432 this @ PlannerError::RosettaDetected => Some(Box::new(this)),
433 this @ PlannerError::DeterminateNixUnavailable => Some(Box::new(this)),
434 PlannerError::OsRelease(_) => None,
435 PlannerError::Utf8(_) => None,
436 PlannerError::SelinuxRequirements => Some(Box::new(self)),
437 PlannerError::Custom(_e) => {
438 #[cfg(target_os = "linux")]
439 if let Some(err) = _e.downcast_ref::<linux::LinuxErrorKind>() {
440 return err.expected();
441 }
442 #[cfg(target_os = "macos")]
443 if let Some(err) = _e.downcast_ref::<macos::MacosError>() {
444 return err.expected();
445 }
446 None
447 },
448 this @ PlannerError::NixOs => Some(Box::new(this)),
449 this @ PlannerError::NixExists => Some(Box::new(this)),
450 this @ PlannerError::Wsl1 => Some(Box::new(this)),
451 PlannerError::Command(_, _) => None,
452 #[cfg(feature = "diagnostics")]
453 PlannerError::Diagnostic(diagnostic_error) => Some(Box::new(diagnostic_error)),
454 }
455 }
456}
457
458#[cfg(feature = "diagnostics")]
459impl crate::diagnostics::ErrorDiagnostic for PlannerError {
460 fn diagnostic(&self) -> String {
461 let static_str: &'static str = (self).into();
462 static_str.to_string()
463 }
464}