refactor
This commit is contained in:
parent
eaaecb831f
commit
870ab86ba4
8 changed files with 270 additions and 146 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
.direnv
|
||||
justfile
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
rustfmt
|
||||
clippy
|
||||
just
|
||||
sqlite
|
||||
;
|
||||
};
|
||||
shellHook =
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
213
src/core.rs
213
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<u8, CadeLayer>,
|
||||
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<bool> {
|
||||
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::<HashMap<String, String>>();
|
||||
let base_env = std::env::vars()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
v.split(':')
|
||||
.map(|i| i.to_string())
|
||||
.collect::<HashSet<String>>(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<String, HashSet<String>>>();
|
||||
|
||||
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<String>| {
|
||||
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<String>| {
|
||||
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::<HashSet<String>>();
|
||||
// });
|
||||
// }
|
||||
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<Vec<Keyword>> {
|
|||
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<PathBuf, Vec<Keyword>>,
|
||||
) -> Result<HashMap<PathBuf, Vec<CadeActions>>> {
|
||||
let mut actions: HashMap<PathBuf, Vec<CadeActions>> = 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<PathBuf, Vec<Keyword>>) -> Result<Vec<CadeLayer>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
106
src/envs.rs
106
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<EnvSet> {
|
||||
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<String> = split[1].split(":").map(|s| s.to_owned()).collect();
|
||||
envs.push((key, values));
|
||||
let values: HashSet<String> =
|
||||
split[1].split(":").map(|s| s.to_owned()).collect();
|
||||
envs.entry(key)
|
||||
.and_modify(|v: &mut HashSet<String>| {
|
||||
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::<Vec<String>>(),
|
||||
.collect::<HashSet<String>>(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
|
|
|||
|
|
@ -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<EnvSet> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn load_flake(path: &Path, output: Option<String>) -> Result<EnvSet> {
|
||||
|
|
@ -88,10 +73,3 @@ pub fn call(path: &Path, argv: Vec<String>) -> Result<EnvSet> {
|
|||
|
||||
// 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
|
||||
24
src/main.rs
24
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::<OsString>::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(())
|
||||
|
|
|
|||
28
src/types.rs
28
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<Env>),
|
||||
Environ(EnvSet),
|
||||
Hook(InnerHook),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CadeLayer {
|
||||
pub envs: EnvSet,
|
||||
pub hooks: Vec<InnerHook>,
|
||||
pub purify: bool,
|
||||
pub origin: PathBuf,
|
||||
pub layer: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Env {
|
||||
pub name: String,
|
||||
pub value: Vec<String>,
|
||||
// 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<String>)>);
|
||||
#[derive(Debug)]
|
||||
pub struct EnvSet(pub HashMap<String, HashSet<String>>);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue