From 93b2b5c51275e77c69da269c4e51e20c6cb10c58 Mon Sep 17 00:00:00 2001 From: atagen Date: Mon, 17 Nov 2025 09:17:43 +1100 Subject: [PATCH 1/4] working --- Cargo.lock | 81 +++++++++++++++++++++++++- Cargo.toml | 1 + flake.nix | 14 +++++ src/actions.rs | 147 ++++++++++++++--------------------------------- src/cli/parse.rs | 2 +- src/core.rs | 10 +++- src/envs.rs | 45 +++++++++++++++ src/main.rs | 1 + src/types.rs | 68 ++++++++++++++++++++-- 9 files changed, 255 insertions(+), 114 deletions(-) create mode 100644 src/envs.rs diff --git a/Cargo.lock b/Cargo.lock index 61b1bed..7d22f0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + [[package]] name = "byteorder" version = "1.5.0" @@ -104,6 +110,7 @@ dependencies = [ "bincode", "clap", "microxdg", + "rusqlite", "serde", "serde_json", "sled", @@ -185,6 +192,24 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fs2" version = "0.4.3" @@ -204,6 +229,24 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + [[package]] name = "heck" version = "0.5.0" @@ -237,6 +280,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.13" @@ -296,6 +349,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "proc-macro2" version = "1.0.97" @@ -320,7 +379,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.9.4", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", ] [[package]] @@ -435,6 +508,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "virtue" version = "0.0.18" diff --git a/Cargo.toml b/Cargo.toml index b032084..f2cd26d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ anyhow = "1.0.99" bincode = "2.0.1" clap = { version = "4.5.45", features = ["derive"] } microxdg = "0.2.0" +rusqlite = "0.37.0" serde = { version = "1.0.226", features = ["derive"] } serde_json = "1.0.145" sled = "0.34.7" diff --git a/flake.nix b/flake.nix index ce9d71d..f965e92 100644 --- a/flake.nix +++ b/flake.nix @@ -23,8 +23,21 @@ rust-analyzer rustfmt clippy + just ; }; + shellHook = + let + justfile = '' + build: + nix build --offline + fresh: + nix build + ''; + in + '' + echo "${justfile}" > justfile + ''; }; }); packages = forAllSystems (pkgs: { @@ -38,6 +51,7 @@ src = ./.; cargoLock.lockFile = ./Cargo.lock; RUSTFLAGS = "-C prefer-dynamic=yes"; + buildInputs = [ pkgs.sqlite ]; }); }); }; diff --git a/src/actions.rs b/src/actions.rs index 6c4b5e2..45d0e9e 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,6 +1,5 @@ -use crate::types::{CadeActions, Env}; +use crate::types::{CadeActions, Env, EnvSet, InnerHook}; use anyhow::{Context, Result, anyhow, bail}; -use serde_json::Value; use std::{ collections::HashMap, ffi::OsString, @@ -8,7 +7,6 @@ use std::{ io::{BufRead, Read}, path::{Path, PathBuf}, }; -type RawEnv = Vec<(String, Vec)>; #[repr(u8)] #[derive(bincode::Encode, bincode::Decode, PartialEq, Debug)] @@ -37,11 +35,10 @@ pub fn load_flake(path: &Path, output: Option) -> Result { .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) + parse_json(output) } -pub fn load_shell(path: &Path, filename: String) -> Result { +pub fn load_shell(path: &Path, filename: String) -> Result { let mut nix_cmd = String::from("nix print-dev-env --json "); // let mut nix_cmd = String::from("nix-shell "); if !filename.is_empty() { @@ -59,11 +56,10 @@ pub fn load_shell(path: &Path, filename: String) -> Result { .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) + EnvSet::from_json(output)? } -pub fn load_env(path: &Path, filename: String) -> Result { +pub fn load_env(path: &Path, filename: String) -> Result { let mut p = path.to_path_buf(); if filename.is_empty() { p.push(".env"); @@ -77,7 +73,7 @@ pub fn load_env(path: &Path, filename: String) -> Result { parse_envs(buf) } -pub fn call(path: &Path, argv: Vec) -> Result { +pub fn call(path: &Path, argv: Vec) -> Result { let mut it = argv.iter(); // safety: already checked at parsing let mut process = std::process::Command::new(it.next().unwrap()); @@ -94,117 +90,54 @@ pub fn call(path: &Path, argv: Vec) -> Result { parse_envs(text) } -fn parse_envs(text: String) -> Result { - 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) -} +// TODO cache results by hash/storepath and read from db so we don't +// need to eat eval every time -fn parse_json(json: Value) -> Result { - 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::>(), - ) - }) - .collect() - }) - .context("collecting env vars")?; - Ok(vars) - } else { - Err(anyhow!("failed to parse PATH value from JSON output")) - } +fn check_permission(dir: &Path) -> Permission { + // TODO check database for permission + todo!(); } 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(); + // TODO finish sqlite impl + let db = rusqlite::Connection::open(dir)?; - 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 - { + // let db = sled::open(dir).context("open database")?; + let mut cwd = std::env::current_dir().context("determine CWD")?; + let cwd_str = cwd + .clone() + .into_os_string() + .into_string() + .map_err(|_| anyhow!("cwd has invalid unicode"))?; + + let permission = db.query_one( + "SELECT Permission FROM WorkingPaths WHERE Path=(:path)", + &[(":path", &cwd_str)], + |row| Ok(row.get(0).unwrap_or(false)), + )?; + if !permission { 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::>(); use crate::core::{read_cade, realise}; let mut cascade = HashMap::new(); cascade.insert( - current_dir.clone(), - read_cade(¤t_dir.join(".cade")).context("reading cade file")?, + cwd.clone(), + read_cade(&cwd.join(".cade")).context("reading cade file")?, ); - while let Some(parent) = current_dir.parent() + // recurse into parent dirs + while let Some(parent) = cwd.parent() && std::fs::exists(parent.join(".cade")) .context("check for .cade file in parent directory")? { - current_dir = parent.to_path_buf(); + cwd = parent.to_path_buf(); cascade.insert( - current_dir.clone(), - read_cade(¤t_dir.join(".cade")).context("reading cade file")?, + cwd.clone(), + read_cade(&cwd.join(".cade")).context("reading cade file")?, ); } @@ -217,24 +150,27 @@ pub fn do_activation() -> Result<()> { for action in env_actions { env.entry(action.name) .and_modify(|iv: &mut Vec| { - iv.extend(action.value); + iv.extend(action.value.clone()); }) .or_insert(action.value); } } Purify => { + // FIXME this is a dumb way to purify an env 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) => {} + Hook(hook) => match hook.kind { + _ => todo!(), + }, }; } for (k, v) in env { - println!("{k}={v}"); + let value: String = v.into_iter().map(|s| [&s, ":"].concat()).collect(); + println!("{k}={}", &value[..value.len() - 1]); } Ok(()) } @@ -265,6 +201,7 @@ fn ensure_dir() -> Result { if !std::fs::exists(&path).is_ok_and(|v| v) { std::fs::create_dir(&path).context("create cade's state path")?; } + // FIXME should this be appended after ? we only guarantee the dir here .. path.push("permissions.db"); Ok(path) } diff --git a/src/cli/parse.rs b/src/cli/parse.rs index 9b93e40..1922626 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -58,7 +58,7 @@ impl FromStr for Keyword { let mut words = lower[4..].split_whitespace(); let mut p = words.clone().peekable(); use crate::types::HookType::*; - Hook(crate::types::Hook { + Hook(crate::types::InnerHook { kind: match p.peek() { None => return Err(ParseError::TooFewOptions), Some(&"preload") => { diff --git a/src/core.rs b/src/core.rs index 351ea4b..b5e2971 100644 --- a/src/core.rs +++ b/src/core.rs @@ -19,10 +19,13 @@ pub fn read_cade(path: &Path) -> Result> { Ok(accum) } -pub fn realise(cascade: HashMap>) -> Result> { - let mut actions: Vec = Vec::new(); +pub fn realise( + cascade: HashMap>, +) -> Result>> { + let mut actions: HashMap> = HashMap::new(); use crate::actions::*; for (path, layer) in cascade { + let mut layer_actions = Vec::new(); for keyword in layer { use Keyword::*; use Loadable::*; @@ -37,8 +40,9 @@ pub fn realise(cascade: HashMap>) -> Result Ok(CadeActions::Hook(hook)), }?; - actions.push(acts); + layer_actions.push(acts); } + actions.insert(path, layer_actions); } Ok(actions) } diff --git a/src/envs.rs b/src/envs.rs new file mode 100644 index 0000000..7e74959 --- /dev/null +++ b/src/envs.rs @@ -0,0 +1,45 @@ +use std::str::FromStr; + +use crate::types::EnvSet; +use anyhow::{Result, anyhow}; +use serde_json::Value; + +pub enum EnvProviders { + RawEnvs, + NixJson, + Cade, +} +pub enum EnvSetParseErr { + Bad, +} + +impl FromStr for EnvSet { + type Err = EnvSetParseErr; +} + +pub fn parse(text: &str) -> Result {} + +pub trait EnvProvider { + fn parse(text: &str) -> Result; +} + +impl EnvProvider for RawEnvs { + fn parse(text: &str) -> Result { + let mut envs = Vec::new(); + for line in text.lines() { + let split: Vec<&str> = line.split('=').collect(); + match split.len() { + 2 => { + let key = split[0].to_owned(); + let values: Vec = split[1].split(":").map(|s| s.to_owned()).collect(); + envs.push((key, values)); + } + _ => { + return Err(anyhow!("parsing variable from {text}")); + } // 1 => Clear(split[0].to_string()), + // _ => Add(split[0].to_string(), split[1..].concat().to_string()), + }; + } + Ok(envs) + } +} diff --git a/src/main.rs b/src/main.rs index 08bf644..8de7ab2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod actions; mod cli; mod core; +mod envs; mod types; use std::{ diff --git a/src/types.rs b/src/types.rs index 004ce4b..26ac9b6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,27 +1,35 @@ use std::path::PathBuf; +use anyhow::{Context, Result, anyhow}; + #[derive(Debug)] pub enum CadeActions { Purify, Environ(Vec), - Hook(Hook), + Hook(InnerHook), } #[derive(Debug)] pub struct Env { pub name: String, pub value: Vec, + // FIXME wtf, at this level we don't need to know origin.. + // that is for a higher structure pub origin: PathBuf, - pub kind: Loadable, pub layer: u8, } +impl Env { + // upcast envset to env + fn from_set(env: EnvSet, origin: &Path, layer: u8) -> Env {} +} + #[derive(Debug)] pub enum Keyword { Pure, Call(Vec), Load(Loadable), - Hook(Hook), + Hook(InnerHook), } #[derive(Debug)] @@ -41,7 +49,59 @@ pub enum HookType { } #[derive(Debug)] -pub struct Hook { +pub struct InnerHook { + // FIXME should be a pathbuf ? pub content: Vec, pub kind: HookType, } + +pub struct EnvSet(Vec<(String, Vec)>); + +impl EnvSet { + pub fn from_json(raw: &[u8]) -> Result { + let json: serde_json::Value = serde_json::from_slice(raw).context("parsing json")?; + 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::>(), + ) + }) + .collect() + }) + .context("collecting env vars")?; + Ok(EnvSet(vars)) + } else { + Err(anyhow!("failed to parse values from JSON output")) + } + } +} From eaaecb831f1d65f607f9a0eb7527dcd3bad3ca20 Mon Sep 17 00:00:00 2001 From: atagen Date: Mon, 17 Nov 2025 21:02:20 +1100 Subject: [PATCH 2/4] working refactor 1 --- Cargo.lock | 39 +++++++++++++- Cargo.toml | 1 + src/actions.rs | 130 ++++---------------------------------------- src/cli/parse.rs | 7 +++ src/core.rs | 137 ++++++++++++++++++++++++++++++++++++++++++++++- src/envs.rs | 78 +++++++++++++++++---------- src/main.rs | 7 +-- src/types.rs | 65 +++------------------- 8 files changed, 255 insertions(+), 209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d22f0f..832f771 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ dependencies = [ "serde", "serde_json", "sled", + "whoami", ] [[package]] @@ -280,6 +281,17 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.4", + "libc", + "redox_syscall 0.5.18", +] + [[package]] name = "libsqlite3-sys" version = "0.35.0" @@ -344,7 +356,7 @@ dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi", ] @@ -382,6 +394,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -520,6 +541,22 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index f2cd26d..e75887c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ rusqlite = "0.37.0" serde = { version = "1.0.226", features = ["derive"] } serde_json = "1.0.145" sled = "0.34.7" +whoami = { version = "1.6.1", default-features = false } diff --git a/src/actions.rs b/src/actions.rs index 45d0e9e..f39e7b7 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -21,7 +21,7 @@ impl Display for Permission { } } -pub fn load_flake(path: &Path, output: Option) -> Result { +pub fn load_flake(path: &Path, output: Option) -> Result { let mut nix_cmd = String::from("nix print-dev-env --json"); if let Some(flake_output) = output { nix_cmd.push_str(&[" ", &flake_output, " "].concat()); @@ -35,28 +35,24 @@ pub fn load_flake(path: &Path, output: Option) -> Result { .output() .with_context(|| format!("loading flake at {}", path.display()))? .stdout; - parse_json(output) + EnvSet::from_json(&output) } -pub fn load_shell(path: &Path, filename: String) -> Result { +pub fn load_shell(path: &Path, filename: String) -> Result { 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"); + let mut proc = std::process::Command::new(nix_cmd); 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; - EnvSet::from_json(output)? + EnvSet::from_json(&output) } pub fn load_env(path: &Path, filename: String) -> Result { @@ -70,7 +66,7 @@ pub fn load_env(path: &Path, filename: String) -> Result { .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) + EnvSet::from_envs(&buf) } pub fn call(path: &Path, argv: Vec) -> Result { @@ -84,10 +80,10 @@ pub fn call(path: &Path, argv: Vec) -> Result { .with_context(|| format!("running process {}", argv.concat()))? .stdout; + // FIXME locale ? let text = String::from_utf8(output) - .with_context(|| format!("converting call {} output to text", argv.concat()))? - .replace(' ', "\0"); - parse_envs(text) + .with_context(|| format!("converting call {} output to text", argv.concat()))?; + EnvSet::from_envs(&text) } // TODO cache results by hash/storepath and read from db so we don't @@ -98,110 +94,4 @@ fn check_permission(dir: &Path) -> Permission { todo!(); } -pub fn do_activation() -> Result<()> { - let dir = ensure_dir()?; - // TODO finish sqlite impl - let db = rusqlite::Connection::open(dir)?; - - // let db = sled::open(dir).context("open database")?; - let mut cwd = std::env::current_dir().context("determine CWD")?; - let cwd_str = cwd - .clone() - .into_os_string() - .into_string() - .map_err(|_| anyhow!("cwd has invalid unicode"))?; - - let permission = db.query_one( - "SELECT Permission FROM WorkingPaths WHERE Path=(:path)", - &[(":path", &cwd_str)], - |row| Ok(row.get(0).unwrap_or(false)), - )?; - if !permission { - bail!("cade is not permitted to operate here; use 'cade allow'."); - } - - let base_env = std::env::vars().collect::>(); - - use crate::core::{read_cade, realise}; - let mut cascade = HashMap::new(); - cascade.insert( - cwd.clone(), - read_cade(&cwd.join(".cade")).context("reading cade file")?, - ); - - // recurse into parent dirs - while let Some(parent) = cwd.parent() - && std::fs::exists(parent.join(".cade")) - .context("check for .cade file in parent directory")? - { - cwd = parent.to_path_buf(); - cascade.insert( - cwd.clone(), - read_cade(&cwd.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| { - iv.extend(action.value.clone()); - }) - .or_insert(action.value); - } - } - Purify => { - // FIXME this is a dumb way to purify an env - for (bk, bv) in base_env.iter() { - if env.get(bk).is_some_and(|inner| inner.contains(bv)) { - env.remove(bk); - } - } - } - Hook(hook) => match hook.kind { - _ => todo!(), - }, - }; - } - for (k, v) in env { - let value: String = v.into_iter().map(|s| [&s, ":"].concat()).collect(); - println!("{k}={}", &value[..value.len() - 1]); - } - 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 { - 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")?; - } - // FIXME should this be appended after ? we only guarantee the dir here .. - path.push("permissions.db"); - Ok(path) -} +// FIXME TODO make these methods on Cade struct diff --git a/src/cli/parse.rs b/src/cli/parse.rs index 1922626..206f167 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -44,12 +44,19 @@ impl FromStr for Keyword { Some("shell") => { Load(Loadable::Shell(s.get(11..).unwrap_or("").to_string())) } + Some("flake") => { Load(Loadable::Flake(s.get(11..).unwrap_or("").to_string())) } + Some("env") => { Load(Loadable::Env(s.get(9..).unwrap_or("").to_string())) } + + Some("cass") => { + Load(Loadable::Cass(s.get(10..).unwrap_or("").to_string())) + } + Some(_) => return Err(ParseError::UnknownLoadable), } } diff --git a/src/core.rs b/src/core.rs index b5e2971..b26c28b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,11 +1,146 @@ use crate::types::{CadeActions, Keyword, Loadable}; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result, anyhow, bail}; +use rusqlite::named_params; use std::{ collections::HashMap, io::BufRead, path::{Path, PathBuf}, }; +pub struct Cade { + db: rusqlite::Connection, + layers: HashMap, + cwd: PathBuf, +} + +impl Cade { + pub fn init() -> anyhow::Result { + let mut db_path = Cade::ensure_dir()?; + db_path.push("cade.db"); + let mut db = rusqlite::Connection::open(db_path)?; + Cade::ensure_db(&mut db)?; + Ok(Self { + db, + layers: HashMap::new(), + cwd: std::env::current_dir().context("determine cwd")?, + }) + } + + fn ensure_db(conn: &mut rusqlite::Connection) -> Result<()> { + conn.execute( + " + CREATE TABLE IF NOT EXISTS WorkingPaths ( + Path TEXT PRIMARY KEY, + Permission INTEGER NOT NULL DEFAULT 0 + ); + ", + [], + ) + .context("create table in database") + .map(|_| ()) + } + + pub fn set_permission(&mut self, permission: bool) -> Result<()> { + self.db.execute( + "INSERT OR REPLACE INTO WorkingPaths (Permission) VALUES ((:cwd), (:perm));", + named_params! { + ":cwd": self.cwd.to_str().context("parse cwd as unicode")?, + ":perm": permission, + }, + )?; + eprintln!( + "cade is now {} here.", + if permission { "allowed" } else { "disallowed" } + ); + Ok(()) + } + + pub fn do_activation(&mut self) -> Result<()> { + let permission = self.db.query_one( + "SELECT Permission FROM WorkingPaths WHERE Path=(:path)", + &[(":path", &self.cwd.to_str().context("parse cwd as unicode")?)], + |row| Ok(row.get(0).unwrap_or(false)), + )?; + if !permission { + bail!("cade is not permitted to operate here; use 'cade allow'."); + } + + let base_env = std::env::vars().collect::>(); + + use crate::core::{read_cade, realise}; + let mut cascade = HashMap::new(); + cascade.insert( + cwd.clone(), + read_cade(&cwd.join(".cade")).context("reading cade file")?, + ); + + // recurse into parent dirs + while let Some(parent) = cwd.parent() + && std::fs::exists(parent.join(".cade")) + .context("check for .cade file in parent directory")? + { + cwd = parent.to_path_buf(); + cascade.insert( + cwd.clone(), + read_cade(&cwd.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| { + iv.extend(action.value.clone()); + }) + .or_insert(action.value); + } + } + Purify => { + // FIXME this is a dumb way to purify an env + for (bk, bv) in base_env.iter() { + if env.get(bk).is_some_and(|inner| inner.contains(bv)) { + env.remove(bk); + } + } + } + Hook(hook) => match hook.kind { + _ => todo!(), + }, + }; + } + for (k, v) in env { + let value: String = v.into_iter().map(|s| [&s, ":"].concat()).collect(); + println!("{k}={}", &value[..value.len() - 1]); + } + Ok(()) + } + + fn ensure_dir() -> Result { + let mut path = if let Ok(xdg) = microxdg::Xdg::new() + && let Ok(state_dir) = xdg.state() + { + state_dir + } else { + let mut p = PathBuf::new(); + p.push("home"); + p.push(whoami::username()); + p.push(".state"); + p + }; + path.push("cade"); + + if !std::fs::exists(&path).is_ok_and(|v| v) { + std::fs::create_dir(&path).context("create cade state path")?; + } + Ok(path) + } +} + pub fn read_cade(path: &Path) -> Result> { let contents = std::fs::read(path).context("reading cade file")?; let mut accum = Vec::new(); diff --git a/src/envs.rs b/src/envs.rs index 7e74959..2691930 100644 --- a/src/envs.rs +++ b/src/envs.rs @@ -1,30 +1,8 @@ -use std::str::FromStr; - use crate::types::EnvSet; -use anyhow::{Result, anyhow}; -use serde_json::Value; +use anyhow::{Context, Result, anyhow, bail}; -pub enum EnvProviders { - RawEnvs, - NixJson, - Cade, -} -pub enum EnvSetParseErr { - Bad, -} - -impl FromStr for EnvSet { - type Err = EnvSetParseErr; -} - -pub fn parse(text: &str) -> Result {} - -pub trait EnvProvider { - fn parse(text: &str) -> Result; -} - -impl EnvProvider for RawEnvs { - fn parse(text: &str) -> Result { +impl EnvSet { + pub fn from_envs(text: &str) -> Result { let mut envs = Vec::new(); for line in text.lines() { let split: Vec<&str> = line.split('=').collect(); @@ -35,11 +13,57 @@ impl EnvProvider for RawEnvs { envs.push((key, values)); } _ => { - return Err(anyhow!("parsing variable from {text}")); + bail!("parsing variable from {text}") } // 1 => Clear(split[0].to_string()), // _ => Add(split[0].to_string(), split[1..].concat().to_string()), }; } - Ok(envs) + Ok(EnvSet(envs)) + } + pub fn from_json(raw: &[u8]) -> Result { + let json: serde_json::Value = serde_json::from_slice(raw).context("parsing json")?; + 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::>(), + ) + }) + .collect() + }) + .context("collecting env vars")?; + Ok(EnvSet(vars)) + } else { + Err(anyhow!("failed to parse values from JSON output")) + } } } diff --git a/src/main.rs b/src/main.rs index 8de7ab2..b4ba3c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,20 +7,21 @@ mod types; use std::{ collections::HashMap, ffi::OsString, - fmt::{Debug, Display}, - io::{BufRead, Read}, path::{Path, PathBuf}, }; use anyhow::{Context, Result, anyhow, bail}; use clap::Parser; +use rusqlite::named_params; + +use crate::types::CadeLayer; fn try_main() -> Result<()> { let args = cli::clap::Cli::parse(); use crate::actions::*; use cli::clap::CliAction::*; + let cade = Cade::init()?; match args.action { - // TODO recursively ascend until we hit a top level .cade, then activate downwards (ie. in reverse) Enter => do_activation().context("activate cade environment")?, Allow => set_permission(Permission::Allowed)?, Disallow => set_permission(Permission::Disallowed)?, diff --git a/src/types.rs b/src/types.rs index 26ac9b6..0f8f164 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,6 +9,12 @@ pub enum CadeActions { Hook(InnerHook), } +pub struct CadeLayer { + pub envs: EnvSet, + pub origin: PathBuf, + pub layer: u8, +} + #[derive(Debug)] pub struct Env { pub name: String, @@ -16,12 +22,6 @@ pub struct Env { // FIXME wtf, at this level we don't need to know origin.. // that is for a higher structure pub origin: PathBuf, - pub layer: u8, -} - -impl Env { - // upcast envset to env - fn from_set(env: EnvSet, origin: &Path, layer: u8) -> Env {} } #[derive(Debug)] @@ -38,6 +38,7 @@ pub enum Loadable { Flake(String), Shell(String), Env(String), + Cass(String), } #[derive(Debug)] @@ -50,58 +51,8 @@ pub enum HookType { #[derive(Debug)] pub struct InnerHook { - // FIXME should be a pathbuf ? pub content: Vec, pub kind: HookType, } -pub struct EnvSet(Vec<(String, Vec)>); - -impl EnvSet { - pub fn from_json(raw: &[u8]) -> Result { - let json: serde_json::Value = serde_json::from_slice(raw).context("parsing json")?; - 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::>(), - ) - }) - .collect() - }) - .context("collecting env vars")?; - Ok(EnvSet(vars)) - } else { - Err(anyhow!("failed to parse values from JSON output")) - } - } -} +pub struct EnvSet(pub Vec<(String, Vec)>); From 870ab86ba48c90ed1384e962275f9b0361835393 Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 20 Nov 2025 15:31:19 +1100 Subject: [PATCH 3/4] refactor --- .gitignore | 1 + flake.nix | 1 + src/cli/parse.rs | 11 +- src/core.rs | 213 +++++++++++++++++++++++---------- src/envs.rs | 106 +++++++++++++--- src/{actions.rs => loaders.rs} | 32 +---- src/main.rs | 24 ++-- src/types.rs | 28 ++--- 8 files changed, 270 insertions(+), 146 deletions(-) rename src/{actions.rs => loaders.rs} (77%) diff --git a/.gitignore b/.gitignore index 2d5df85..fd04cdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .direnv +justfile diff --git a/flake.nix b/flake.nix index f965e92..a8d3bb4 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,7 @@ rustfmt clippy just + sqlite ; }; shellHook = diff --git a/src/cli/parse.rs b/src/cli/parse.rs index 206f167..a93bd2a 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -1,9 +1,7 @@ -use crate::Debug; use crate::types::*; use std::{fmt::Display, str::FromStr}; -#[derive(Debug)] -enum ParseError { +pub enum ParseError { InvalidKeyword, UnknownLoadable, TooManyOptions, @@ -12,7 +10,12 @@ enum ParseError { impl Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(self, f) + match self { + Self::InvalidKeyword => f.write_str("invalid keyword"), + Self::UnknownLoadable => f.write_str("unknown loadable"), + Self::TooManyOptions => f.write_str("too many options"), + Self::TooFewOptions => f.write_str("too few options"), + } } } diff --git a/src/core.rs b/src/core.rs index b26c28b..86ab313 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,15 +1,15 @@ -use crate::types::{CadeActions, Keyword, Loadable}; +use crate::types::{CadeAction, CadeLayer, EnvSet, Keyword, Loadable}; use anyhow::{Context, Result, anyhow, bail}; use rusqlite::named_params; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, io::BufRead, + os::unix::process::CommandExt, path::{Path, PathBuf}, }; pub struct Cade { db: rusqlite::Connection, - layers: HashMap, cwd: PathBuf, } @@ -21,7 +21,6 @@ impl Cade { Cade::ensure_db(&mut db)?; Ok(Self { db, - layers: HashMap::new(), cwd: std::env::current_dir().context("determine cwd")?, }) } @@ -42,7 +41,7 @@ impl Cade { pub fn set_permission(&mut self, permission: bool) -> Result<()> { self.db.execute( - "INSERT OR REPLACE INTO WorkingPaths (Permission) VALUES ((:cwd), (:perm));", + "INSERT OR REPLACE INTO WorkingPaths (Path, Permission) VALUES (:cwd, :perm);", named_params! { ":cwd": self.cwd.to_str().context("parse cwd as unicode")?, ":perm": permission, @@ -55,68 +54,116 @@ impl Cade { Ok(()) } + pub fn get_permission(&mut self, path: &Path) -> Result { + self.db + .query_one( + "SELECT Permission FROM WorkingPaths WHERE Path=(:path)", + &[(":path", &path.to_str().context("parse cwd as unicode")?)], + |row| Ok(row.get(0).unwrap_or(false)), + ) + .map_err(|e| e.into()) + } + + // TODO break this function up pub fn do_activation(&mut self) -> Result<()> { - let permission = self.db.query_one( - "SELECT Permission FROM WorkingPaths WHERE Path=(:path)", - &[(":path", &self.cwd.to_str().context("parse cwd as unicode")?)], - |row| Ok(row.get(0).unwrap_or(false)), - )?; - if !permission { + let mut working_dir = self.cwd.clone(); + if !self.get_permission(&working_dir)? { bail!("cade is not permitted to operate here; use 'cade allow'."); } - let base_env = std::env::vars().collect::>(); + let base_env = std::env::vars() + .map(|(k, v)| { + ( + k, + v.split(':') + .map(|i| i.to_string()) + .collect::>(), + ) + }) + .collect::>>(); use crate::core::{read_cade, realise}; - let mut cascade = HashMap::new(); - cascade.insert( - cwd.clone(), - read_cade(&cwd.join(".cade")).context("reading cade file")?, + let mut cade_files = HashMap::new(); + cade_files.insert( + working_dir.clone(), + read_cade(&working_dir.join(".cade")).context("reading cade file")?, ); // recurse into parent dirs - while let Some(parent) = cwd.parent() + while let Some(parent) = working_dir.parent() && std::fs::exists(parent.join(".cade")) .context("check for .cade file in parent directory")? { - cwd = parent.to_path_buf(); - cascade.insert( - cwd.clone(), - read_cade(&cwd.join(".cade")).context("reading cade file")?, + working_dir = parent.to_path_buf(); + cade_files.insert( + working_dir.clone(), + read_cade(&working_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| { - iv.extend(action.value.clone()); - }) - .or_insert(action.value); - } - } - Purify => { - // FIXME this is a dumb way to purify an env - for (bk, bv) in base_env.iter() { - if env.get(bk).is_some_and(|inner| inner.contains(bv)) { - env.remove(bk); - } - } - } - Hook(hook) => match hook.kind { - _ => todo!(), - }, - }; + let cade_layers = realise(cade_files)?; + let mut purified = false; + + for layer in cade_layers { + for (k, v) in layer.envs.0 { + env.entry(k) + .and_modify(|iv: &mut HashSet| { + iv.extend(v.clone()); + }) + .or_insert(v); + } + if layer.purify { + // && !purified { + // for (bk, bv) in base_env.iter() { + // // FIXME is this even worth doing ? + // // no, just unset them in the export.. + // env.entry(bk.clone()).and_modify(|v| { + // *v = v + // .iter() + // .filter(|iv| !bv.contains(*iv)) + // .cloned() + // .collect::>(); + // }); + // } + purified = true; + } + // TODO hooks don't exist yet + } + // if !purified { + // for (bk, bv) in base_env { + // env.entry(bk) + // .and_modify(|v| { + // v.extend(bv.clone()); + // }) + // .or_insert(bv); + // } + // } + + // TODO save these env sets for the unexport hook + // base_envs will have to be fully restored + // while envs must be carefully subtraced + if purified { + for (k, _) in base_env { + print!("set -u {k};") + } } for (k, v) in env { - let value: String = v.into_iter().map(|s| [&s, ":"].concat()).collect(); - println!("{k}={}", &value[..value.len() - 1]); + let len = v.len(); + let value: String = v + .into_iter() + .enumerate() + .map(|(i, s)| { + if i < len.saturating_sub(1) { + [&s, ":"].concat() + } else { + s + } + }) + .collect(); + print!("set -x -g '{k}' '{value}';"); } + println!(); Ok(()) } @@ -129,7 +176,8 @@ impl Cade { let mut p = PathBuf::new(); p.push("home"); p.push(whoami::username()); - p.push(".state"); + p.push(".local"); + p.push("state"); p }; path.push("cade"); @@ -148,36 +196,71 @@ pub fn read_cade(path: &Path) -> Result> { while let Some(Ok(line)) = lines.next() { let instruction: Keyword = line .parse() - .map_err(|e| anyhow!("while parsing cade file at {}: {e:?}", path.display()))?; + .map_err(|e| anyhow!("parse cade file at {}: {e}", path.display()))?; accum.push(instruction); } Ok(accum) } -pub fn realise( - cascade: HashMap>, -) -> Result>> { - let mut actions: HashMap> = HashMap::new(); - use crate::actions::*; - for (path, layer) in cascade { - let mut layer_actions = Vec::new(); - for keyword in layer { +impl CadeLayer { + pub fn new(layer: usize, origin: &Path) -> Self { + Self { + envs: EnvSet(HashMap::new()), + hooks: Vec::new(), + purify: false, + origin: origin.to_path_buf(), + layer, + } + } + + pub fn push_action(&mut self, action: CadeAction) { + use CadeAction::*; + match action { + Purify => { + self.purify = true; + } + Environ(env) => { + for (k, v) in env.0 { + self.envs + .0 + .entry(k) + .and_modify(|iv| iv.extend(v.clone())) + .or_insert(v); + } + } + Hook(hook) => { + self.hooks.push(hook); + } + } + } +} + +pub fn realise(cascade: HashMap>) -> Result> { + let mut actions = Vec::new(); + use crate::loaders::*; + for (layer_count, (path, keywords)) in cascade.into_iter().enumerate() { + let mut layer = CadeLayer::new(layer_count, &path); + for kw in keywords { use Keyword::*; use Loadable::*; - let acts = match keyword { - Pure => Ok(CadeActions::Purify), - Call(argv) => call(&path, argv).context("calling process"), + let act = match kw { + Pure => Ok(CadeAction::Purify), + Call(argv) => call(&path, argv) + .context("calling process") + .map(CadeAction::Environ), Load(loadable) => match loadable { Default => load_flake(&path, None).context("loading flake"), Flake(output) => load_flake(&path, Some(output)), Shell(filename) => load_shell(&path, filename).context("loading shell"), Env(filename) => load_env(&path, filename).context("loading env file"), - }, - Hook(hook) => Ok(CadeActions::Hook(hook)), + Cass(filename) => load_cass(&path, filename).context("loading cass shell"), + } + .map(CadeAction::Environ), + Hook(hook) => Ok(CadeAction::Hook(hook)), }?; - layer_actions.push(acts); + layer.push_action(act); } - actions.insert(path, layer_actions); + actions.push(layer); } Ok(actions) } diff --git a/src/envs.rs b/src/envs.rs index 2691930..07651dd 100644 --- a/src/envs.rs +++ b/src/envs.rs @@ -1,16 +1,23 @@ +use std::collections::{HashMap, HashSet}; + use crate::types::EnvSet; use anyhow::{Context, Result, anyhow, bail}; impl EnvSet { pub fn from_envs(text: &str) -> Result { - let mut envs = Vec::new(); + let mut envs = HashMap::new(); for line in text.lines() { let split: Vec<&str> = line.split('=').collect(); match split.len() { 2 => { let key = split[0].to_owned(); - let values: Vec = split[1].split(":").map(|s| s.to_owned()).collect(); - envs.push((key, values)); + let values: HashSet = + split[1].split(":").map(|s| s.to_owned()).collect(); + envs.entry(key) + .and_modify(|v: &mut HashSet| { + v.extend(values.clone()); + }) + .or_insert(values); } _ => { bail!("parsing variable from {text}") @@ -30,21 +37,82 @@ impl EnvSet { .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(|(var, _)| { + !(var.starts_with("NIX_") + || var.starts_with("output") + || var.starts_with("deps") + || var.starts_with("enable") + || var.ends_with("Inputs") + || var.ends_with("Flags") + || var.ends_with("TYPE") + || var.to_lowercase().contains("phase") + || matches!( + var.as_str(), + "SHELL" + | "pkg" + | "prefix" + | "guess" + | "_substituteStream_has_warned_replace_deprecation" + | "LINENO" + | "OPTERROR" + | "OLDPWD" + | "BASH" + | "IFS" + | "PS4" + | "initialPath" + | "out" + | "shell" + | "STRINGS" + | "stdenv" + | "builder" + | "PWD" + | "SOURCE_DATE_EPOCH" + | "CXX" + | "TEMPDIR" + | "system" + | "HOST_PATH" + | "doInstallCheck" + | "buildCommandPath" + | "LS_COLORS" + | "cmakeFlakes" + | "TMPDIR" + | "LD" + | "READELF" + | "doCheck" + | "SIZE" + | "propagatedNativeBuildInputs" + | "strictDeps" + | "AR" + | "AS" + | "TEMP" + | "SHLVL" + | "NM" + | "patches" + | "passAsFile" + | "buildInputs" + | "SSL_CERT_FILE" + | "OBJCOPY" + | "STRIP" + | "TMP" + | "OBJDUMP" + | "propagatedBuildInputs" + | "CC" + | "__ETC_PROFILE_SOURCED" + | "CONFIG_SHELL" + | "__structuredAttrs" + | "RANLIB" + | "nativeBuildInputs" + | "name" + | "TEST" + | "TZ" + | "HOME" + | "GZIP_NO_TIMESTAMPS" + | "cmakeFlags" + | "TERM" + | "buildCommand" + | "preferLocalBuild" + | "dontAddDisableDepTrack" + )) }) .filter_map(|var| { Some((var.0.to_string(), var.1.get("value")?.as_str()?.to_owned())) @@ -55,7 +123,7 @@ impl EnvSet { value .split(':') .map(|s| s.to_string()) - .collect::>(), + .collect::>(), ) }) .collect() diff --git a/src/actions.rs b/src/loaders.rs similarity index 77% rename from src/actions.rs rename to src/loaders.rs index f39e7b7..d303f78 100644 --- a/src/actions.rs +++ b/src/loaders.rs @@ -1,24 +1,9 @@ -use crate::types::{CadeActions, Env, EnvSet, InnerHook}; -use anyhow::{Context, Result, anyhow, bail}; -use std::{ - collections::HashMap, - ffi::OsString, - fmt::{Debug, Display}, - io::{BufRead, Read}, - path::{Path, PathBuf}, -}; +use crate::types::EnvSet; +use anyhow::{Context, Result}; +use std::{io::Read, path::Path}; -#[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_cass(path: &Path, filename: String) -> Result { + todo!() } pub fn load_flake(path: &Path, output: Option) -> Result { @@ -88,10 +73,3 @@ pub fn call(path: &Path, argv: Vec) -> Result { // TODO cache results by hash/storepath and read from db so we don't // need to eat eval every time - -fn check_permission(dir: &Path) -> Permission { - // TODO check database for permission - todo!(); -} - -// FIXME TODO make these methods on Cade struct diff --git a/src/main.rs b/src/main.rs index b4ba3c2..2a773b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,30 +1,24 @@ -mod actions; mod cli; mod core; mod envs; +mod loaders; mod types; -use std::{ - collections::HashMap, - ffi::OsString, - path::{Path, PathBuf}, -}; +use std::ffi::OsString; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context, Result}; use clap::Parser; -use rusqlite::named_params; -use crate::types::CadeLayer; +use crate::core::Cade; fn try_main() -> Result<()> { let args = cli::clap::Cli::parse(); - use crate::actions::*; use cli::clap::CliAction::*; - let cade = Cade::init()?; + let mut cade = Cade::init()?; match args.action { - Enter => do_activation().context("activate cade environment")?, - Allow => set_permission(Permission::Allowed)?, - Disallow => set_permission(Permission::Disallowed)?, + Enter => cade.do_activation().context("activate cade environment")?, + Allow => cade.set_permission(true)?, + Disallow => cade.set_permission(false)?, Edit => { let editor = std::env::var("EDITOR").context("find EDITOR variable")?; let mut session = std::process::Command::new(Into::::into(editor)) @@ -32,7 +26,7 @@ fn try_main() -> Result<()> { .spawn() .context("spawn editor process")?; session.wait().context("wait for editor process")?; - set_permission(Permission::Allowed)?; + cade.set_permission(true)?; } }; Ok(()) diff --git a/src/types.rs b/src/types.rs index 0f8f164..17ece0f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,27 +1,22 @@ -use std::path::PathBuf; - -use anyhow::{Context, Result, anyhow}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; #[derive(Debug)] -pub enum CadeActions { +pub enum CadeAction { Purify, - Environ(Vec), + Environ(EnvSet), Hook(InnerHook), } +#[derive(Debug)] pub struct CadeLayer { pub envs: EnvSet, + pub hooks: Vec, + pub purify: bool, pub origin: PathBuf, - pub layer: u8, -} - -#[derive(Debug)] -pub struct Env { - pub name: String, - pub value: Vec, - // FIXME wtf, at this level we don't need to know origin.. - // that is for a higher structure - pub origin: PathBuf, + pub layer: usize, } #[derive(Debug)] @@ -55,4 +50,5 @@ pub struct InnerHook { pub kind: HookType, } -pub struct EnvSet(pub Vec<(String, Vec)>); +#[derive(Debug)] +pub struct EnvSet(pub HashMap>); From f8083521e17c7ce9716b0f8b9175edb397fd4547 Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 27 Nov 2025 09:54:57 +1100 Subject: [PATCH 4/4] work on exit commands --- src/cli/clap.rs | 1 + src/core.rs | 186 ++++++++++++++++++++++++++---------------------- src/main.rs | 3 + src/shells.rs | 1 + src/types.rs | 2 +- 5 files changed, 106 insertions(+), 87 deletions(-) create mode 100644 src/shells.rs diff --git a/src/cli/clap.rs b/src/cli/clap.rs index fef8320..726fe31 100644 --- a/src/cli/clap.rs +++ b/src/cli/clap.rs @@ -2,6 +2,7 @@ use clap::{Parser, Subcommand}; #[derive(Subcommand)] pub enum CliAction { Enter, + Exit, Allow, Disallow, Edit, diff --git a/src/core.rs b/src/core.rs index 86ab313..1bfab74 100644 --- a/src/core.rs +++ b/src/core.rs @@ -64,109 +64,121 @@ impl Cade { .map_err(|e| e.into()) } - // TODO break this function up pub fn do_activation(&mut self) -> Result<()> { - let mut working_dir = self.cwd.clone(); - if !self.get_permission(&working_dir)? { - bail!("cade is not permitted to operate here; use 'cade allow'."); - } - - let base_env = std::env::vars() - .map(|(k, v)| { - ( - k, - v.split(':') - .map(|i| i.to_string()) - .collect::>(), - ) - }) - .collect::>>(); - - use crate::core::{read_cade, realise}; - let mut cade_files = HashMap::new(); - cade_files.insert( - working_dir.clone(), - read_cade(&working_dir.join(".cade")).context("reading cade file")?, - ); - - // recurse into parent dirs - while let Some(parent) = working_dir.parent() - && std::fs::exists(parent.join(".cade")) - .context("check for .cade file in parent directory")? - { - working_dir = parent.to_path_buf(); + fn collect_cade_files(mut working_dir: PathBuf) -> Result>> { + use crate::core::read_cade; + let mut cade_files = HashMap::new(); cade_files.insert( working_dir.clone(), read_cade(&working_dir.join(".cade")).context("reading cade file")?, ); + + // recurse into parent dirs + while let Some(parent) = working_dir.parent() + && std::fs::exists(parent.join(".cade")) + .context("check for .cade file in parent directory")? + { + working_dir = parent.to_path_buf(); + cade_files.insert( + working_dir.clone(), + read_cade(&working_dir.join(".cade")).context("reading cade file")?, + ); + } + Ok(cade_files) + } + // TODO use faststr + + fn rollup_envs(cade_layers: Vec) -> (HashMap>, bool) { + let mut purified = false; + let mut env = HashMap::new(); + let mut hooks = HashMap::new(); + for layer in cade_layers { + for (k, v) in layer.envs.0 { + env.entry(k) + .and_modify(|iv: &mut HashSet| { + iv.extend(v.clone()); + }) + .or_insert(v); + } + if !purified && layer.purify { + purified = true; + } + // FIXME not really happy with this impl of hooks + // should each line just be a `call` ? + for hook in layer.hooks { + hooks + .entry(hook.kind) + .and_modify(|h: &mut Vec| h.extend(hook.content.clone())) + .or_insert(hook.content); + } + } + + (env, purified) } - let mut env = HashMap::new(); - let cade_layers = realise(cade_files)?; - let mut purified = false; - - for layer in cade_layers { - for (k, v) in layer.envs.0 { - env.entry(k) - .and_modify(|iv: &mut HashSet| { - iv.extend(v.clone()); + fn output_changes(env: HashMap>, purified: bool) { + if purified { + let base_env = std::env::vars() + .map(|(k, v)| { + ( + k, + v.split(':') + .map(|i| i.to_string()) + .collect::>(), + ) }) - .or_insert(v); + .collect::>>(); + for (k, _) in base_env { + print!("set -u {k};") + } } - if layer.purify { - // && !purified { - // for (bk, bv) in base_env.iter() { - // // FIXME is this even worth doing ? - // // no, just unset them in the export.. - // env.entry(bk.clone()).and_modify(|v| { - // *v = v - // .iter() - // .filter(|iv| !bv.contains(*iv)) - // .cloned() - // .collect::>(); - // }); - // } - purified = true; + for (k, v) in env { + let len = v.len(); + let value: String = v + .into_iter() + .enumerate() + .map(|(i, s)| { + if i < len.saturating_sub(1) { + [&s, ":"].concat() + } else { + s + } + }) + .collect(); + print!("set -x -g '{k}' '{value}';"); } - // TODO hooks don't exist yet + println!(); } - // if !purified { - // for (bk, bv) in base_env { - // env.entry(bk) - // .and_modify(|v| { - // v.extend(bv.clone()); - // }) - // .or_insert(bv); - // } - // } + + let working_dir = self.cwd.clone(); + if !self.get_permission(&working_dir)? { + bail!("cade is not permitted to operate here; use 'cade allow'."); + } + + let cade_files = collect_cade_files(working_dir)?; + let cade_layers = load_envs(cade_files)?; + let (env, purified) = rollup_envs(cade_layers); + output_changes(env, purified); // TODO save these env sets for the unexport hook // base_envs will have to be fully restored // while envs must be carefully subtraced - if purified { - for (k, _) in base_env { - print!("set -u {k};") - } - } - for (k, v) in env { - let len = v.len(); - let value: String = v - .into_iter() - .enumerate() - .map(|(i, s)| { - if i < len.saturating_sub(1) { - [&s, ":"].concat() - } else { - s - } - }) - .collect(); - print!("set -x -g '{k}' '{value}';"); - } - println!(); Ok(()) } + // this is more complex than it seems - + // we need to understand what layers are in place, where our root is, and determine + // which ones should remain (in case we are leaving only an inner layer) + // then diff those against the current environment and output the appropriate set commands + // + // this will play into the caching story also, naturally + // perhaps we could store related stateful information into our own env vars + // + // alternatively, we just traverse to root and reinit up to our current dir.. + pub fn do_restore(&mut self) -> Result<()> { + todo!() + } + fn ensure_dir() -> Result { let mut path = if let Ok(xdg) = microxdg::Xdg::new() && let Ok(state_dir) = xdg.state() @@ -235,7 +247,7 @@ impl CadeLayer { } } -pub fn realise(cascade: HashMap>) -> Result> { +pub fn load_envs(cascade: HashMap>) -> Result> { let mut actions = Vec::new(); use crate::loaders::*; for (layer_count, (path, keywords)) in cascade.into_iter().enumerate() { @@ -260,6 +272,8 @@ pub fn realise(cascade: HashMap>) -> Result }?; layer.push_action(act); } + layer.origin = path; + layer.layer = layer_count; actions.push(layer); } Ok(actions) diff --git a/src/main.rs b/src/main.rs index 2a773b9..02de32c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod cli; mod core; mod envs; mod loaders; +mod shells; mod types; use std::ffi::OsString; @@ -17,6 +18,8 @@ fn try_main() -> Result<()> { let mut cade = Cade::init()?; match args.action { Enter => cade.do_activation().context("activate cade environment")?, + // TODO + Exit => cade.do_restore().context("deactivate cade environment")?, Allow => cade.set_permission(true)?, Disallow => cade.set_permission(false)?, Edit => { diff --git a/src/shells.rs b/src/shells.rs new file mode 100644 index 0000000..fb1f0a4 --- /dev/null +++ b/src/shells.rs @@ -0,0 +1 @@ +// TODO write an Output trait and impls for each shell diff --git a/src/types.rs b/src/types.rs index 17ece0f..798d8fa 100644 --- a/src/types.rs +++ b/src/types.rs @@ -36,7 +36,7 @@ pub enum Loadable { Cass(String), } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Hash)] pub enum HookType { LoadPre, LoadPost,