Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

health: Handle non-symlink dotfiles #366

Merged
merged 2 commits into from
Jan 6, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 84 additions & 44 deletions crates/omnix-health/src/check/shell.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};

use crate::traits::{Check, CheckResult, Checkable};

Expand Down Expand Up @@ -28,47 +31,40 @@ impl Checkable for ShellCheck {
if !self.enable {
return vec![];
}
let shell = match Shell::current_shell() {
Some(shell) => shell,
None => {
let msg = "Unsupported shell. Please file an issue at <https://github.com/juspay/omnix/issues>";
let user_shell_env = match CurrentUserShellEnv::new() {
Ok(shell) => shell,
Err(err) => {
tracing::error!("Skipping shell dotfile check! {:?}", err);
if self.required {
panic!("{}", msg);
panic!("Unable to determine user's shell environment (see above)");
} else {
tracing::warn!("Skipping shell dotfile check! {}", msg);
tracing::warn!("Skipping shell dotfile check! (see above)");
return vec![];
}
}
};

// Iterate over each dotfile and check if it is managed by nix
let mut managed: HashMap<PathBuf, PathBuf> = HashMap::new();
// Iterate over each dotfile and check if it is managed by Nix
let mut managed: HashMap<&'static str, PathBuf> = HashMap::new();
let mut unmanaged: Vec<PathBuf> = Vec::new();
for path in &shell.get_dotfiles() {
match std::fs::read_link(path) {
Ok(target) => {
if super::direnv::is_path_in_nix_store(&target) {
managed.insert(path.clone(), target);
} else {
unmanaged.push(path.clone());
};
}
Err(err) => {
tracing::warn!("Dotfile {:?} symlink error: {:?}; ignoring.", path, err);
}
for (name, path) in user_shell_env.dotfiles {
if super::direnv::is_path_in_nix_store(&path) {
managed.insert(name, path.clone());
} else {
unmanaged.push(path.clone());
}
}

let title = "Shell dotfiles".to_string();
let info = format!(
"Shell={:?}; Managed: {:?}; Unmanaged: {:?}",
shell, managed, unmanaged
"Shell={:?}; HOME={:?}; Managed: {:?}; Unmanaged: {:?}",
user_shell_env.shell, user_shell_env.home, managed, unmanaged
);
let result = if !managed.is_empty() {
CheckResult::Green
} else {
CheckResult::Red {
msg: format!("Default Shell: {:?} is not managed by Nix", shell),
msg: format!("Default Shell: {:?} is not managed by Nix", user_shell_env.shell),
suggestion: "You can use `home-manager` to manage shell configuration. See <https://github.com/juspay/nixos-unified-template>".to_string(),
}
};
Expand All @@ -83,37 +79,73 @@ impl Checkable for ShellCheck {
}
}

/// The shell environment of the current user
struct CurrentUserShellEnv {
/// The user's home directory
home: PathBuf,
/// Current shell
shell: Shell,
/// *Absolute* paths to the dotfiles
dotfiles: HashMap<&'static str, PathBuf>,
}

impl CurrentUserShellEnv {
/// Get the current user's shell environment
fn new() -> Result<Self, ShellError> {
let home = PathBuf::from(std::env::var("HOME")?);
let shell = Shell::current_shell()?;
let dotfiles = shell.get_dotfiles(&home)?;
let v = CurrentUserShellEnv {
home,
shell,
dotfiles,
};
Ok(v)
}
}

#[derive(thiserror::Error, Debug)]
enum ShellError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),

#[error("Environment variable error: {0}")]
Var(#[from] std::env::VarError),

#[error("Bad $SHELL value")]
BadShellPath,

#[error("Unsupported shell. Please file an issue at <https://github.com/juspay/omnix/issues>")]
UnsupportedShell,
}

/// An Unix shell
#[derive(Debug, Serialize, Deserialize, Clone, Hash, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Shell {
enum Shell {
Zsh,
Bash,
}

impl Shell {
/// Returns the user's current [Shell]
fn current_shell() -> Option<Self> {
let shell_path =
PathBuf::from(std::env::var("SHELL").expect("Environment variable `SHELL` not set"));
fn current_shell() -> Result<Self, ShellError> {
let shell_path = PathBuf::from(std::env::var("SHELL")?);
Self::from_path(shell_path)
}

/// Lookup [Shell] from the given executable path
/// For example if path is `/bin/zsh`, it would return `Zsh`
fn from_path(exe_path: PathBuf) -> Option<Self> {
fn from_path(exe_path: PathBuf) -> Result<Self, ShellError> {
let shell_name = exe_path
.file_name()
.expect("Path does not have a file name component")
.ok_or(ShellError::BadShellPath)?
.to_string_lossy();

match shell_name.as_ref() {
"zsh" => Some(Shell::Zsh),
"bash" => Some(Shell::Bash),
_ => {
tracing::warn!("Unrecognized shell: {:?}. Please file an issue at <https://github.com/juspay/omnix/issues>", exe_path);
None
}
"zsh" => Ok(Shell::Zsh),
"bash" => Ok(Shell::Bash),
_ => Err(ShellError::UnsupportedShell),
}
}

Expand All @@ -126,13 +158,21 @@ impl Shell {
}

/// Get the currently existing dotfiles under $HOME
fn get_dotfiles(&self) -> Vec<PathBuf> {
let home_dir =
PathBuf::from(std::env::var("HOME").expect("Environment variable `HOME` not set"));
self.dotfile_names()
.iter()
.map(|dotfile| home_dir.join(dotfile))
.filter(|path| path.exists())
.collect()
///
/// Returned paths will be absolute (i.e., symlinks are resolved).
fn get_dotfiles(&self, home_dir: &Path) -> std::io::Result<HashMap<&'static str, PathBuf>> {
let mut paths = HashMap::new();
for dotfile in self.dotfile_names() {
match std::fs::canonicalize(home_dir.join(dotfile)) {
Ok(path) => {
paths.insert(dotfile, path);
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
// If file not found, skip
}
Err(err) => return Err(err),
}
}
Ok(paths)
}
}
Loading