Compare commits

..

No commits in common. "working" and "main" have entirely different histories.

13 changed files with 319 additions and 649 deletions

1
.gitignore vendored
View file

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

120
Cargo.lock generated
View file

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

View file

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

View file

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

270
src/actions.rs Normal file
View file

@ -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<String>)>;
#[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<String>) -> Result<RawEnv> {
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<CadeActions> {
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<CadeActions> {
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<String>) -> Result<CadeActions> {
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<RawEnv> {
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<RawEnv> {
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::<Vec<String>>(),
)
})
.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::<HashMap<String, String>>();
use crate::core::{read_cade, realise};
let mut cascade = HashMap::new();
cascade.insert(
current_dir.clone(),
read_cade(&current_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(&current_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);
})
.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<PathBuf> {
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)
}

View file

@ -2,7 +2,6 @@ use clap::{Parser, Subcommand};
#[derive(Subcommand)]
pub enum CliAction {
Enter,
Exit,
Allow,
Disallow,
Edit,

View file

@ -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") => {

View file

@ -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<Cade> {
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<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())
}
pub fn do_activation(&mut self) -> Result<()> {
fn collect_cade_files(mut working_dir: PathBuf) -> Result<HashMap<PathBuf, Vec<Keyword>>> {
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<CadeLayer>) -> (HashMap<String, HashSet<String>>, 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<String>| {
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<String>| h.extend(hook.content.clone()))
.or_insert(hook.content);
}
}
(env, purified)
}
fn output_changes(env: HashMap<String, HashSet<String>>, purified: bool) {
if purified {
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>>>();
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<PathBuf> {
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<Vec<Keyword>> {
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<Vec<Keyword>> {
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<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 {
pub fn realise(cascade: HashMap<PathBuf, Vec<Keyword>>) -> Result<Vec<CadeActions>> {
let mut actions: Vec<CadeActions> = 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)
}

View file

@ -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<EnvSet> {
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<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}")
} // 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<EnvSet> {
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::<HashSet<String>>(),
)
})
.collect()
})
.context("collecting env vars")?;
Ok(EnvSet(vars))
} else {
Err(anyhow!("failed to parse values from JSON output"))
}
}
}

View file

@ -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<EnvSet> {
todo!()
}
pub fn load_flake(path: &Path, output: Option<String>) -> Result<EnvSet> {
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<EnvSet> {
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<EnvSet> {
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<String>) -> Result<EnvSet> {
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

View file

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

View file

@ -1 +0,0 @@
// TODO write an Output trait and impls for each shell

View file

@ -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<Env>),
Hook(Hook),
}
#[derive(Debug)]
pub struct CadeLayer {
pub envs: EnvSet,
pub hooks: Vec<InnerHook>,
pub purify: bool,
pub struct Env {
pub name: String,
pub value: Vec<String>,
pub origin: PathBuf,
pub layer: usize,
pub kind: Loadable,
pub layer: u8,
}
#[derive(Debug)]
@ -24,7 +21,7 @@ pub enum Keyword {
Pure,
Call(Vec<String>),
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<String>,
pub kind: HookType,
}
#[derive(Debug)]
pub struct EnvSet(pub HashMap<String, HashSet<String>>);