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
1
.gitignore
vendored
|
|
@ -1,3 +1,2 @@
|
|||
/target
|
||||
.direnv
|
||||
justfile
|
||||
|
|
|
|||
120
Cargo.lock
generated
120
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
15
flake.nix
15
flake.nix
|
|
@ -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
270
src/actions.rs
Normal 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(¤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<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)
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ use clap::{Parser, Subcommand};
|
|||
#[derive(Subcommand)]
|
||||
pub enum CliAction {
|
||||
Enter,
|
||||
Exit,
|
||||
Allow,
|
||||
Disallow,
|
||||
Edit,
|
||||
|
|
|
|||
|
|
@ -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") => {
|
||||
|
|
|
|||
266
src/core.rs
266
src/core.rs
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
137
src/envs.rs
137
src/envs.rs
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
29
src/main.rs
29
src/main.rs
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
// TODO write an Output trait and impls for each shell
|
||||
31
src/types.rs
31
src/types.rs
|
|
@ -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>>);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue