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

om hack: Auto cachix use missing cachix caches #330

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions crates/omnix-cli/crate.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
(
Expand Down
4 changes: 2 additions & 2 deletions crates/omnix-hack/crate.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{ flake
, pkgs
{ pkgs
, lib
, rust-project
, ...
Expand All @@ -15,6 +14,7 @@
IOKit
]
);
nativeBuildInputs = [ pkgs.cachix ];
inherit (rust-project.crates."nix_rs".crane.args)
DEFAULT_FLAKE_SCHEMAS
INSPECT_FLAKE
Expand Down
58 changes: 44 additions & 14 deletions crates/omnix-hack/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(())
}

Expand All @@ -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<Q>, Vec<&'a T>)
where
F: Fn(&T) -> Option<Q>,
{
let mut successes: Vec<Q> = 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)
}
1 change: 1 addition & 0 deletions crates/omnix-hack/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![feature(let_chains)]
pub mod config;
pub mod core;
pub mod readme;
2 changes: 1 addition & 1 deletion crates/omnix-hack/src/readme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
52 changes: 45 additions & 7 deletions crates/omnix-health/src/check/caches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@ impl Checkable for Caches {
nix_info: &info::NixInfo,
_: Option<&nix_rs::flake::url::FlakeUrl>,
) -> Vec<Check> {
let val = &nix_info.nix_config.substituters.value;
let missing_caches = self
.required
.iter()
.filter(|required_cache| !val.contains(required_cache))
.collect::<Vec<_>>();
let missing_caches = self.get_missing_caches(nix_info);
let result = if missing_caches.is_empty() {
CheckResult::Green
} else {
Expand All @@ -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::<Vec<_>>()
.join(" ")
Expand All @@ -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<Url> {
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<Self> {
// 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(())
}
}
5 changes: 4 additions & 1 deletion doc/src/om/hack.md
Original file line number Diff line number Diff line change
@@ -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`