This commit is contained in:
atagen 2025-11-20 15:31:19 +11:00
parent eaaecb831f
commit 870ab86ba4
8 changed files with 270 additions and 146 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
.direnv
justfile

View file

@ -24,6 +24,7 @@
rustfmt
clippy
just
sqlite
;
};
shellHook =

View file

@ -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"),
}
}
}

View file

@ -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)
}

View file

@ -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()

View file

@ -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

View file

@ -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(())

View file

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