diff --git a/.gitignore b/.gitignore index 2d5df85..fd04cdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .direnv +justfile diff --git a/Cargo.lock b/Cargo.lock index 61b1bed..832f771 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,9 +110,11 @@ dependencies = [ "bincode", "clap", "microxdg", + "rusqlite", "serde", "serde_json", "sled", + "whoami", ] [[package]] @@ -185,6 +193,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 +230,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 +281,27 @@ 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" @@ -291,11 +356,17 @@ dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "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" @@ -320,7 +391,30 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "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", ] [[package]] @@ -435,12 +529,34 @@ 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 b032084..e75887c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ 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 ce9d71d..a8d3bb4 100644 --- a/flake.nix +++ b/flake.nix @@ -23,8 +23,22 @@ rust-analyzer rustfmt clippy + just + sqlite ; }; + shellHook = + let + justfile = '' + build: + nix build --offline + fresh: + nix build + ''; + in + '' + echo "${justfile}" > justfile + ''; }; }); packages = forAllSystems (pkgs: { @@ -38,6 +52,7 @@ src = ./.; cargoLock.lockFile = ./Cargo.lock; RUSTFLAGS = "-C prefer-dynamic=yes"; + buildInputs = [ pkgs.sqlite ]; }); }); }; diff --git a/src/actions.rs b/src/actions.rs deleted file mode 100644 index 6c4b5e2..0000000 --- a/src/actions.rs +++ /dev/null @@ -1,270 +0,0 @@ -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 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/cli/parse.rs b/src/cli/parse.rs index 9b93e40..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"), + } } } @@ -44,12 +47,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), } } @@ -58,7 +68,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..1bfab74 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,11 +1,206 @@ -use crate::types::{CadeActions, Keyword, Loadable}; -use anyhow::{Context, Result, anyhow}; +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, + 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(); @@ -13,32 +208,73 @@ 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: Vec = Vec::new(); - use crate::actions::*; - for (path, layer) in cascade { - 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 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 { 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)), }?; - actions.push(acts); + layer.push_action(act); } + layer.origin = path; + layer.layer = layer_count; + actions.push(layer); } Ok(actions) } diff --git a/src/envs.rs b/src/envs.rs new file mode 100644 index 0000000..07651dd --- /dev/null +++ b/src/envs.rs @@ -0,0 +1,137 @@ +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 new file mode 100644 index 0000000..d303f78 --- /dev/null +++ b/src/loaders.rs @@ -0,0 +1,75 @@ +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 08bf644..02de32c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,27 @@ -mod actions; mod cli; mod core; +mod envs; +mod loaders; +mod shells; mod types; -use std::{ - collections::HashMap, - ffi::OsString, - fmt::{Debug, Display}, - io::{BufRead, Read}, - path::{Path, PathBuf}, -}; +use std::ffi::OsString; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context, Result}; 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 { - // 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)?, + 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 => { let editor = std::env::var("EDITOR").context("find EDITOR variable")?; let mut session = std::process::Command::new(Into::::into(editor)) @@ -30,7 +29,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/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 004ce4b..798d8fa 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,19 +1,22 @@ -use std::path::PathBuf; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; #[derive(Debug)] -pub enum CadeActions { +pub enum CadeAction { Purify, - Environ(Vec), - Hook(Hook), + Environ(EnvSet), + Hook(InnerHook), } #[derive(Debug)] -pub struct Env { - pub name: String, - pub value: Vec, +pub struct CadeLayer { + pub envs: EnvSet, + pub hooks: Vec, + pub purify: bool, pub origin: PathBuf, - pub kind: Loadable, - pub layer: u8, + pub layer: usize, } #[derive(Debug)] @@ -21,7 +24,7 @@ pub enum Keyword { Pure, Call(Vec), Load(Loadable), - Hook(Hook), + Hook(InnerHook), } #[derive(Debug)] @@ -30,9 +33,10 @@ pub enum Loadable { Flake(String), Shell(String), Env(String), + Cass(String), } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Hash)] pub enum HookType { LoadPre, LoadPost, @@ -41,7 +45,10 @@ pub enum HookType { } #[derive(Debug)] -pub struct Hook { +pub struct InnerHook { pub content: Vec, pub kind: HookType, } + +#[derive(Debug)] +pub struct EnvSet(pub HashMap>);