diff --git a/crates/omnix-cli/crate.nix b/crates/omnix-cli/crate.nix index 70ddeca4..aeba2580 100644 --- a/crates/omnix-cli/crate.nix +++ b/crates/omnix-cli/crate.nix @@ -26,6 +26,7 @@ in # > error: don't yet have a `targetPackages.darwin.LibsystemCross for x86_64-apple-darwin` (if (stdenv.isDarwin && stdenv.isAarch64) then pkgsStatic.libiconv else pkgs.libiconv) pkgs.pkg-config + pkgs.cachix ]; buildInputs = lib.optionals pkgs.stdenv.isDarwin ( diff --git a/crates/omnix-hack/crate.nix b/crates/omnix-hack/crate.nix index ce94f5c8..20206310 100644 --- a/crates/omnix-hack/crate.nix +++ b/crates/omnix-hack/crate.nix @@ -1,5 +1,4 @@ -{ flake -, pkgs +{ pkgs , lib , rust-project , ... @@ -15,6 +14,7 @@ IOKit ] ); + nativeBuildInputs = [ pkgs.cachix ]; inherit (rust-project.crates."nix_rs".crane.args) DEFAULT_FLAKE_SCHEMAS INSPECT_FLAKE diff --git a/crates/omnix-hack/src/core.rs b/crates/omnix-hack/src/core.rs index bbd7b66c..9b10b111 100644 --- a/crates/omnix-hack/src/core.rs +++ b/crates/omnix-hack/src/core.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use nix_rs::{flake::url::FlakeUrl, info::NixInfo}; use omnix_common::markdown::print_markdown; -use omnix_health::{traits::Checkable, NixHealth}; +use omnix_health::{check::caches::CachixCache, traits::Checkable, NixHealth}; use crate::config::HackConfig; @@ -36,28 +36,40 @@ pub async fn hack_on_pre_shell(prj: &Project) -> anyhow::Result<()> { .await .as_ref() .with_context(|| "Unable to gather nix info")?; - let relevant_checks: Vec<&'_ dyn Checkable> = vec![ - &health.nix_version, - &health.rosetta, - &health.max_jobs, - // TODO: Run this only when a cache is configured - &health.trusted_users, - &health.caches, - ]; + + let mut relevant_checks: Vec<&'_ dyn Checkable> = + vec![&health.nix_version, &health.rosetta, &health.max_jobs]; + if !health.caches.required.is_empty() { + relevant_checks.push(&health.trusted_users); + }; + + // Run cache related checks, and try to resolve it automatically using `cachix use` as appropriate + if !health.caches.required.is_empty() { + let missing = health.caches.get_missing_caches(nix_info); + let (missing_cachix, missing_other) = parse_many(&missing, CachixCache::from_url); + for cachix_cache in &missing_cachix { + tracing::info!("🐦 Running `cachix use` for {}", cachix_cache.0); + cachix_cache.cachix_use().await?; + } + if !missing_other.is_empty() { + // We cannot add these caches automatically, so defer to `om health` + relevant_checks.push(&health.caches); + }; + // TODO: Re-calculate NixInfo since our nix.conf has changed (due to `cachix use`) + // To better implement this, we need a mutable database of NixInfo, NixConfig, etc. OnceCell is not sufficient + }; + for check_kind in relevant_checks.into_iter() { for check in check_kind.check(nix_info, Some(&prj.flake)) { if !check.result.green() { check.tracing_log().await?; if !check.result.green() && check.required { - tracing::error!("ERROR: Your Nix invironment is not properly setup. Run `om health` for details."); - anyhow::bail!("Cannot proceed"); + anyhow::bail!("ERROR: Your Nix invironment is not properly setup. See suggestions above, or run `om health` for details."); }; }; } } - if !health.caches.required.is_empty() { - // TODO: Auto-resolve some problems; like running 'cachix use' automatically - }; + Ok(()) } @@ -66,3 +78,21 @@ pub async fn hack_on_post_shell(prj: &Project) -> anyhow::Result<()> { print_markdown(&prj.dir, prj.cfg.readme.get_markdown()).await?; Ok(()) } + +/// Parse all items using the given parse function +fn parse_many<'a, T, Q, F>(vec: &'a [T], f: F) -> (Vec, Vec<&'a T>) +where + F: Fn(&T) -> Option, +{ + let mut successes: Vec = Vec::new(); + let mut failures: Vec<&'a T> = Vec::new(); + + for item in vec { + match f(item) { + Some(transformed) => successes.push(transformed), + None => failures.push(item), + } + } + + (successes, failures) +} diff --git a/crates/omnix-hack/src/lib.rs b/crates/omnix-hack/src/lib.rs index 4dc24858..2f3c9b86 100644 --- a/crates/omnix-hack/src/lib.rs +++ b/crates/omnix-hack/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(let_chains)] pub mod config; pub mod core; pub mod readme; diff --git a/crates/omnix-hack/src/readme.rs b/crates/omnix-hack/src/readme.rs index 1afec84a..72bfa62e 100644 --- a/crates/omnix-hack/src/readme.rs +++ b/crates/omnix-hack/src/readme.rs @@ -2,7 +2,7 @@ use serde::Deserialize; const DEFAULT: &str = r#"🍾 Welcome to the project -*(Want to add more instructions here? Add them to the `om.hack.default.readme` field in your `flake.nix` file)* +*(Want to show custom instructions here? Add them to the `om.hack.default.readme` field in your `flake.nix` file)* "#; /// The README to display at the end. diff --git a/crates/omnix-health/src/check/caches.rs b/crates/omnix-health/src/check/caches.rs index 43149f91..c6c871d3 100644 --- a/crates/omnix-health/src/check/caches.rs +++ b/crates/omnix-health/src/check/caches.rs @@ -26,12 +26,7 @@ impl Checkable for Caches { nix_info: &info::NixInfo, _: Option<&nix_rs::flake::url::FlakeUrl>, ) -> Vec { - let val = &nix_info.nix_config.substituters.value; - let missing_caches = self - .required - .iter() - .filter(|required_cache| !val.contains(required_cache)) - .collect::>(); + let missing_caches = self.get_missing_caches(nix_info); let result = if missing_caches.is_empty() { CheckResult::Green } else { @@ -54,7 +49,11 @@ impl Checkable for Caches { title: "Nix Caches in use".to_string(), info: format!( "substituters = {}", - val.iter() + nix_info + .nix_config + .substituters + .value + .iter() .map(|url| url.to_string()) .collect::>() .join(" ") @@ -65,3 +64,42 @@ impl Checkable for Caches { vec![check] } } + +impl Caches { + /// Get subset of required caches not already in use + pub fn get_missing_caches(&self, nix_info: &info::NixInfo) -> Vec { + let val = &nix_info.nix_config.substituters.value; + self.required + .iter() + .filter(|required_cache| !val.contains(required_cache)) + .cloned() + .collect() + } +} + +pub struct CachixCache(pub String); + +impl CachixCache { + /// Parse the https URL into a CachixCache + pub fn from_url(url: &Url) -> Option { + // Parse https://foo.cachix.org into CachixCache("foo") + // If domain is not cachix.org, return None. + let host = url.host_str()?; + if host.ends_with(".cachix.org") { + Some(CachixCache(host.split('.').next()?.to_string())) + } else { + None + } + } + + /// Run `cachix use` for this cache + pub async fn cachix_use(&self) -> anyhow::Result<()> { + let mut cmd = tokio::process::Command::new("cachix"); + cmd.arg("use").arg(&self.0); + let status = cmd.spawn()?.wait().await?; + if !status.success() { + anyhow::bail!("Failed to run `cachix use {}`", self.0); + } + Ok(()) + } +} diff --git a/doc/src/om/hack.md b/doc/src/om/hack.md index 5cf16b26..cd6a19d6 100644 --- a/doc/src/om/hack.md +++ b/doc/src/om/hack.md @@ -1,4 +1,7 @@ # Hack > [!TODO] -> `om hack` is a work in progress +> `om hack` is a work in progress, but checkout [Emanote's `.envrc`](https://github.com/srid/emanote/blob/master/.envrc) to see what is available so far. + +- Tight `direnv` integration +- Health check; `cachix use`