diff --git a/.gitignore b/.gitignore index fd04cdc..2d5df85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target .direnv -justfile diff --git a/Cargo.lock b/Cargo.lock index 832f771..61b1bed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,12 +90,6 @@ 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" @@ -110,11 +104,9 @@ dependencies = [ "bincode", "clap", "microxdg", - "rusqlite", "serde", "serde_json", "sled", - "whoami", ] [[package]] @@ -193,24 +185,6 @@ 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" @@ -230,24 +204,6 @@ 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" @@ -281,27 +237,6 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" -dependencies = [ - "pkg-config", - "vcpkg", -] - [[package]] name = "lock_api" version = "0.4.13" @@ -356,17 +291,11 @@ dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", "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" @@ -391,30 +320,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 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" -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", + "bitflags", ] [[package]] @@ -529,34 +435,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" 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 e75887c..b032084 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,6 @@ 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" -whoami = { version = "1.6.1", default-features = false } diff --git a/flake.nix b/flake.nix index a8d3bb4..ce9d71d 100644 --- a/flake.nix +++ b/flake.nix @@ -23,22 +23,8 @@ rust-analyzer rustfmt clippy - just - sqlite ; }; - shellHook = - let - justfile = '' - build: - nix build --offline - fresh: - nix build - ''; - in - '' - echo "${justfile}" > justfile - ''; }; }); packages = forAllSystems (pkgs: { @@ -52,7 +38,6 @@ src = ./.; cargoLock.lockFile = ./Cargo.lock; RUSTFLAGS = "-C prefer-dynamic=yes"; - buildInputs = [ pkgs.sqlite ]; }); }); }; diff --git a/src/actions.rs b/src/actions.rs new file mode 100644 index 0000000..6c4b5e2 --- /dev/null +++ b/src/actions.rs @@ -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)>; + +#[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) -> 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()); + } + 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 { + 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 { + 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) -> Result { + 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 { + 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 { + 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")) + } +} + +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::>(); + + 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")?, + ); + + 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(¤t_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); + }) + .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 { + 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) +} diff --git a/src/cli/clap.rs b/src/cli/clap.rs index 726fe31..fef8320 100644 --- a/src/cli/clap.rs +++ b/src/cli/clap.rs @@ -2,7 +2,6 @@ use clap::{Parser, Subcommand}; #[derive(Subcommand)] pub enum CliAction { Enter, - Exit, Allow, Disallow, Edit, diff --git a/src/cli/parse.rs b/src/cli/parse.rs index a93bd2a..9b93e40 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -1,7 +1,9 @@ +use crate::Debug; use crate::types::*; use std::{fmt::Display, str::FromStr}; -pub enum ParseError { +#[derive(Debug)] +enum ParseError { InvalidKeyword, UnknownLoadable, TooManyOptions, @@ -10,12 +12,7 @@ pub enum ParseError { impl Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - 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"), - } + Debug::fmt(self, f) } } @@ -47,19 +44,12 @@ 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), } } @@ -68,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::InnerHook { + Hook(crate::types::Hook { kind: match p.peek() { None => return Err(ParseError::TooFewOptions), Some(&"preload") => { diff --git a/src/core.rs b/src/core.rs index 1bfab74..351ea4b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,206 +1,11 @@ -use crate::types::{CadeAction, CadeLayer, EnvSet, Keyword, Loadable}; -use anyhow::{Context, Result, anyhow, bail}; -use rusqlite::named_params; +use crate::types::{CadeActions, Keyword, Loadable}; +use anyhow::{Context, Result, anyhow}; use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, io::BufRead, - os::unix::process::CommandExt, path::{Path, PathBuf}, }; -pub struct Cade { - db: rusqlite::Connection, - 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, - 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 (Path, 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 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()) - } - - pub fn do_activation(&mut self) -> Result<()> { - 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) - } - - 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::>(), - ) - }) - .collect::>>(); - 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!(); - } - - 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 - 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() - { - state_dir - } else { - let mut p = PathBuf::new(); - p.push("home"); - p.push(whoami::username()); - p.push(".local"); - 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(); @@ -208,73 +13,32 @@ pub fn read_cade(path: &Path) -> Result> { while let Some(Ok(line)) = lines.next() { let instruction: Keyword = line .parse() - .map_err(|e| anyhow!("parse cade file at {}: {e}", path.display()))?; + .map_err(|e| anyhow!("while parsing cade file at {}: {e:?}", path.display()))?; accum.push(instruction); } Ok(accum) } -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 load_envs(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 { +pub fn realise(cascade: HashMap>) -> Result> { + let mut actions: Vec = Vec::new(); + use crate::actions::*; + for (path, layer) in cascade { + for keyword in layer { use Keyword::*; use Loadable::*; - let act = match kw { - Pure => Ok(CadeAction::Purify), - Call(argv) => call(&path, argv) - .context("calling process") - .map(CadeAction::Environ), + let acts = match keyword { + Pure => Ok(CadeActions::Purify), + Call(argv) => call(&path, argv).context("calling process"), 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"), - Cass(filename) => load_cass(&path, filename).context("loading cass shell"), - } - .map(CadeAction::Environ), - Hook(hook) => Ok(CadeAction::Hook(hook)), + }, + Hook(hook) => Ok(CadeActions::Hook(hook)), }?; - layer.push_action(act); + actions.push(acts); } - layer.origin = path; - layer.layer = layer_count; - actions.push(layer); } Ok(actions) } diff --git a/src/envs.rs b/src/envs.rs deleted file mode 100644 index 07651dd..0000000 --- a/src/envs.rs +++ /dev/null @@ -1,137 +0,0 @@ -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 = 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: 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}") - } // 1 => Clear(split[0].to_string()), - // _ => Add(split[0].to_string(), split[1..].concat().to_string()), - }; - } - 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, _)| { - !(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())) - }) - .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/loaders.rs b/src/loaders.rs deleted file mode 100644 index d303f78..0000000 --- a/src/loaders.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::types::EnvSet; -use anyhow::{Context, Result}; -use std::{io::Read, path::Path}; - -pub fn load_cass(path: &Path, filename: String) -> Result { - todo!() -} - -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()); - } - 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; - EnvSet::from_json(&output) -} - -pub fn load_shell(path: &Path, filename: String) -> Result { - let mut nix_cmd = String::from("nix print-dev-env --json "); - if !filename.is_empty() { - nix_cmd.push_str(&["-F ", &filename].concat()); - } else { - nix_cmd.push_str(&["-F ./shell.nix"].concat()); - } - let mut proc = std::process::Command::new(nix_cmd); - proc.env_clear(); - proc.current_dir(path); - let output = proc - .output() - .with_context(|| format!("loading shell at {}", path.display()))? - .stdout; - EnvSet::from_json(&output) -} - -pub fn load_env(path: &Path, filename: String) -> Result { - 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")?; - EnvSet::from_envs(&buf) -} - -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()); - process.current_dir(path); - process.args(it); - let output = process - .output() - .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()))?; - EnvSet::from_envs(&text) -} - -// TODO cache results by hash/storepath and read from db so we don't -// need to eat eval every time diff --git a/src/main.rs b/src/main.rs index 02de32c..08bf644 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,28 @@ +mod actions; mod cli; mod core; -mod envs; -mod loaders; -mod shells; mod types; -use std::ffi::OsString; +use std::{ + collections::HashMap, + ffi::OsString, + fmt::{Debug, Display}, + io::{BufRead, Read}, + path::{Path, PathBuf}, +}; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, anyhow, bail}; use clap::Parser; -use crate::core::Cade; - fn try_main() -> Result<()> { let args = cli::clap::Cli::parse(); + use crate::actions::*; use cli::clap::CliAction::*; - 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)?, + // 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)?, Edit => { let editor = std::env::var("EDITOR").context("find EDITOR variable")?; let mut session = std::process::Command::new(Into::::into(editor)) @@ -29,7 +30,7 @@ fn try_main() -> Result<()> { .spawn() .context("spawn editor process")?; session.wait().context("wait for editor process")?; - cade.set_permission(true)?; + set_permission(Permission::Allowed)?; } }; Ok(()) diff --git a/src/shells.rs b/src/shells.rs deleted file mode 100644 index fb1f0a4..0000000 --- a/src/shells.rs +++ /dev/null @@ -1 +0,0 @@ -// TODO write an Output trait and impls for each shell diff --git a/src/types.rs b/src/types.rs index 798d8fa..004ce4b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,22 +1,19 @@ -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, -}; +use std::path::PathBuf; #[derive(Debug)] -pub enum CadeAction { +pub enum CadeActions { Purify, - Environ(EnvSet), - Hook(InnerHook), + Environ(Vec), + Hook(Hook), } #[derive(Debug)] -pub struct CadeLayer { - pub envs: EnvSet, - pub hooks: Vec, - pub purify: bool, +pub struct Env { + pub name: String, + pub value: Vec, pub origin: PathBuf, - pub layer: usize, + pub kind: Loadable, + pub layer: u8, } #[derive(Debug)] @@ -24,7 +21,7 @@ pub enum Keyword { Pure, Call(Vec), Load(Loadable), - Hook(InnerHook), + Hook(Hook), } #[derive(Debug)] @@ -33,10 +30,9 @@ pub enum Loadable { Flake(String), Shell(String), Env(String), - Cass(String), } -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Debug)] pub enum HookType { LoadPre, LoadPost, @@ -45,10 +41,7 @@ pub enum HookType { } #[derive(Debug)] -pub struct InnerHook { +pub struct Hook { pub content: Vec, pub kind: HookType, } - -#[derive(Debug)] -pub struct EnvSet(pub HashMap>);