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>);