This commit is contained in:
atagen 2025-10-02 08:52:15 +10:00
parent b2e0936d4a
commit 757020689c
10 changed files with 537 additions and 344 deletions

270
src/actions.rs Normal file
View file

@ -0,0 +1,270 @@
use crate::types::{CadeActions, Env};
use anyhow::{Context, Result, anyhow, bail};
use serde_json::Value;
use std::{
collections::HashMap,
ffi::OsString,
fmt::{Debug, Display},
io::{BufRead, Read},
path::{Path, PathBuf},
};
type RawEnv = Vec<(String, Vec<String>)>;
#[repr(u8)]
#[derive(bincode::Encode, bincode::Decode, PartialEq, Debug)]
pub enum Permission {
Allowed,
Disallowed,
}
impl Display for Permission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, f)
}
}
pub fn load_flake(path: &Path, output: Option<String>) -> Result<RawEnv> {
let mut nix_cmd = String::from("nix print-dev-env --json");
if let Some(flake_output) = output {
nix_cmd.push_str(&[" ", &flake_output, " "].concat());
}
let mut proc = std::process::Command::new("sh");
// proc.env_clear();
proc.arg("-c");
proc.arg(nix_cmd);
proc.current_dir(path);
let output = proc
.output()
.with_context(|| format!("loading flake at {}", path.display()))?
.stdout;
let json: Value = serde_json::from_slice(&output).context("parsing output as json")?;
parse_json(json)
}
pub fn load_shell(path: &Path, filename: String) -> Result<CadeActions> {
let mut nix_cmd = String::from("nix print-dev-env --json ");
// let mut nix_cmd = String::from("nix-shell ");
if !filename.is_empty() {
nix_cmd.push_str(&["-F ", &filename].concat());
} else {
nix_cmd.push_str(&["-F ./shell.nix"].concat());
}
// nix_cmd.push_str("--pure --command env --null");
let mut proc = std::process::Command::new("sh");
proc.env_clear();
proc.arg("-c");
proc.arg(nix_cmd);
proc.current_dir(path);
let output = proc
.output()
.with_context(|| format!("loading shell at {}", path.display()))?
.stdout;
let json: Value = serde_json::from_slice(&output).context("parsing output as json")?;
parse_json(json)
}
pub fn load_env(path: &Path, filename: String) -> Result<CadeActions> {
let mut p = path.to_path_buf();
if filename.is_empty() {
p.push(".env");
} else {
p.push(filename);
}
let mut file = std::fs::File::open(p)
.with_context(|| format!("opening env file at {}", path.display()))?;
let mut buf = String::new();
file.read_to_string(&mut buf).context("reading env file")?;
parse_envs(buf)
}
pub fn call(path: &Path, argv: Vec<String>) -> Result<CadeActions> {
let mut it = argv.iter();
// safety: already checked at parsing
let mut process = std::process::Command::new(it.next().unwrap());
process.current_dir(path);
process.args(it);
let output = process
.output()
.with_context(|| format!("running process {}", argv.concat()))?
.stdout;
let text = String::from_utf8(output)
.with_context(|| format!("converting call {} output to text", argv.concat()))?
.replace(' ', "\0");
parse_envs(text)
}
fn parse_envs(text: String) -> Result<RawEnv> {
let mut envs = Vec::new();
for line in text.lines() {
let split: Vec<&str> = line.split('=').collect();
let parse = match split.len() {
3 => {
let key = split[0];
let values = &split[2].split(":").to_owned().collect();
}
2 => {
// TODO
}
_ => {
return Err(anyhow!("parsing variable from {text}"));
}
// 1 => Clear(split[0].to_string()),
// _ => Add(split[0].to_string(), split[1..].concat().to_string()),
};
envs.push(parse);
}
Ok(envs)
}
fn parse_json(json: Value) -> Result<RawEnv> {
if json.is_object()
&& let Some(all_vars) = json.get("variables")
{
let vars = all_vars
.as_object()
.map(|inner| {
inner
.iter()
.filter(|(var, _)| match var.as_str() {
"NIX_BUILD_TOP"
| "NIX_BUILD_CORES"
| "NIX_STORE"
| "TEMP"
| "TEMPDIR"
| "TMP"
| "TMPDIR"
| "builder"
| "out"
| "stdenv"
| "system"
| "dontAddDisableDepTrack"
| "outputs" => false,
_ => true,
})
.filter_map(|var| {
Some((var.0.to_string(), var.1.get("value")?.as_str()?.to_owned()))
})
.map(|(name, value)| {
(
name,
value
.split(':')
.map(|s| s.to_string())
.collect::<Vec<String>>(),
)
})
.collect()
})
.context("collecting env vars")?;
Ok(vars)
} else {
Err(anyhow!("failed to parse PATH value from JSON output"))
}
}
pub fn do_activation() -> Result<()> {
let dir = ensure_dir()?;
let db = sled::open(dir).context("open database")?;
let cwd = std::env::current_dir()
.context("determine CWD")?
.into_os_string();
if db
.get(cwd.into_encoded_bytes())
.map(|perm| {
perm.map(|inner| {
let (p, _) =
bincode::decode_from_slice(inner.as_ref(), bincode::config::standard())
.unwrap_or((Permission::Disallowed, 0));
p
})
.unwrap_or(Permission::Disallowed)
})
.unwrap_or(Permission::Disallowed)
== Permission::Disallowed
{
bail!("cade is not permitted to operate here; use 'cade allow'.");
}
let mut current_dir = std::env::current_dir().context("determine CWD")?;
let base_env = std::env::vars().collect::<HashMap<String, String>>();
use crate::core::{read_cade, realise};
let mut cascade = HashMap::new();
cascade.insert(
current_dir.clone(),
read_cade(&current_dir.join(".cade")).context("reading cade file")?,
);
while let Some(parent) = current_dir.parent()
&& std::fs::exists(parent.join(".cade"))
.context("check for .cade file in parent directory")?
{
current_dir = parent.to_path_buf();
cascade.insert(
current_dir.clone(),
read_cade(&current_dir.join(".cade")).context("reading cade file")?,
);
}
let mut env = HashMap::new();
let adjustments = realise(cascade)?;
use CadeActions::*;
for action in adjustments {
match action {
Environ(env_actions) => {
for action in env_actions {
env.entry(action.name)
.and_modify(|iv: &mut Vec<String>| {
iv.extend(action.value);
})
.or_insert(action.value);
}
}
Purify => {
for (bk, bv) in base_env.iter() {
env.entry(bk).and_modify(|inner| inner)
if env.get(bk).is_some_and(|inner| inner.contains(bv)) {
env.remove(bk);
}
}
}
Hook(hook) => {}
};
}
for (k, v) in env {
println!("{k}={v}");
}
Ok(())
}
pub fn set_permission(permission: Permission) -> Result<()> {
let dir = ensure_dir()?;
let db = sled::open(dir).context("open database")?;
let cwd = std::env::current_dir()
.context("determine CWD")?
.into_os_string();
let encoded_permission = bincode::encode_to_vec(&permission, bincode::config::standard())
.context("encode permission value")?;
let _ = db
.insert(cwd.into_encoded_bytes(), encoded_permission)
.context("update permissions database")?;
eprintln!(
"cade is now {} here.",
permission.to_string().to_lowercase()
);
Ok(())
}
fn ensure_dir() -> Result<PathBuf> {
let xdg = microxdg::Xdg::new().context("establish XDG paths")?;
let mut path = xdg.state().context("find xdg state dir")?;
path.push("cade");
if !std::fs::exists(&path).is_ok_and(|v| v) {
std::fs::create_dir(&path).context("create cade's state path")?;
}
path.push("permissions.db");
Ok(path)
}