This commit is contained in:
atagen 2025-11-17 09:17:43 +11:00
parent 757020689c
commit 93b2b5c512
9 changed files with 255 additions and 114 deletions

81
Cargo.lock generated
View file

@ -90,6 +90,12 @@ 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"
@ -104,6 +110,7 @@ dependencies = [
"bincode",
"clap",
"microxdg",
"rusqlite",
"serde",
"serde_json",
"sled",
@ -185,6 +192,24 @@ 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"
@ -204,6 +229,24 @@ 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"
@ -237,6 +280,16 @@ version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[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"
@ -296,6 +349,12 @@ dependencies = [
"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"
@ -320,7 +379,21 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[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",
]
[[package]]
@ -435,6 +508,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"

View file

@ -15,6 +15,7 @@ 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"

View file

@ -23,8 +23,21 @@
rust-analyzer
rustfmt
clippy
just
;
};
shellHook =
let
justfile = ''
build:
nix build --offline
fresh:
nix build
'';
in
''
echo "${justfile}" > justfile
'';
};
});
packages = forAllSystems (pkgs: {
@ -38,6 +51,7 @@
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
RUSTFLAGS = "-C prefer-dynamic=yes";
buildInputs = [ pkgs.sqlite ];
});
});
};

View file

@ -1,6 +1,5 @@
use crate::types::{CadeActions, Env};
use crate::types::{CadeActions, Env, EnvSet, InnerHook};
use anyhow::{Context, Result, anyhow, bail};
use serde_json::Value;
use std::{
collections::HashMap,
ffi::OsString,
@ -8,7 +7,6 @@ use std::{
io::{BufRead, Read},
path::{Path, PathBuf},
};
type RawEnv = Vec<(String, Vec<String>)>;
#[repr(u8)]
#[derive(bincode::Encode, bincode::Decode, PartialEq, Debug)]
@ -37,11 +35,10 @@ pub fn load_flake(path: &Path, output: Option<String>) -> Result<RawEnv> {
.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)
parse_json(output)
}
pub fn load_shell(path: &Path, filename: String) -> Result<CadeActions> {
pub fn load_shell(path: &Path, filename: String) -> Result<RawEnv> {
let mut nix_cmd = String::from("nix print-dev-env --json ");
// let mut nix_cmd = String::from("nix-shell ");
if !filename.is_empty() {
@ -59,11 +56,10 @@ pub fn load_shell(path: &Path, filename: String) -> Result<CadeActions> {
.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)
EnvSet::from_json(output)?
}
pub fn load_env(path: &Path, filename: String) -> Result<CadeActions> {
pub fn load_env(path: &Path, filename: String) -> Result<EnvSet> {
let mut p = path.to_path_buf();
if filename.is_empty() {
p.push(".env");
@ -77,7 +73,7 @@ pub fn load_env(path: &Path, filename: String) -> Result<CadeActions> {
parse_envs(buf)
}
pub fn call(path: &Path, argv: Vec<String>) -> Result<CadeActions> {
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());
@ -94,117 +90,54 @@ pub fn call(path: &Path, argv: Vec<String>) -> Result<CadeActions> {
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)
}
// TODO cache results by hash/storepath and read from db so we don't
// need to eat eval every time
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"))
}
fn check_permission(dir: &Path) -> Permission {
// TODO check database for permission
todo!();
}
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();
// TODO finish sqlite impl
let db = rusqlite::Connection::open(dir)?;
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
{
// let db = sled::open(dir).context("open database")?;
let mut cwd = std::env::current_dir().context("determine CWD")?;
let cwd_str = cwd
.clone()
.into_os_string()
.into_string()
.map_err(|_| anyhow!("cwd has invalid unicode"))?;
let permission = db.query_one(
"SELECT Permission FROM WorkingPaths WHERE Path=(:path)",
&[(":path", &cwd_str)],
|row| Ok(row.get(0).unwrap_or(false)),
)?;
if !permission {
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")?,
cwd.clone(),
read_cade(&cwd.join(".cade")).context("reading cade file")?,
);
while let Some(parent) = current_dir.parent()
// recurse into parent dirs
while let Some(parent) = cwd.parent()
&& std::fs::exists(parent.join(".cade"))
.context("check for .cade file in parent directory")?
{
current_dir = parent.to_path_buf();
cwd = parent.to_path_buf();
cascade.insert(
current_dir.clone(),
read_cade(&current_dir.join(".cade")).context("reading cade file")?,
cwd.clone(),
read_cade(&cwd.join(".cade")).context("reading cade file")?,
);
}
@ -217,24 +150,27 @@ pub fn do_activation() -> Result<()> {
for action in env_actions {
env.entry(action.name)
.and_modify(|iv: &mut Vec<String>| {
iv.extend(action.value);
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() {
env.entry(bk).and_modify(|inner| inner)
if env.get(bk).is_some_and(|inner| inner.contains(bv)) {
env.remove(bk);
}
}
}
Hook(hook) => {}
Hook(hook) => match hook.kind {
_ => todo!(),
},
};
}
for (k, v) in env {
println!("{k}={v}");
let value: String = v.into_iter().map(|s| [&s, ":"].concat()).collect();
println!("{k}={}", &value[..value.len() - 1]);
}
Ok(())
}
@ -265,6 +201,7 @@ fn ensure_dir() -> Result<PathBuf> {
if !std::fs::exists(&path).is_ok_and(|v| v) {
std::fs::create_dir(&path).context("create cade's state path")?;
}
// FIXME should this be appended after ? we only guarantee the dir here ..
path.push("permissions.db");
Ok(path)
}

View file

@ -58,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::Hook {
Hook(crate::types::InnerHook {
kind: match p.peek() {
None => return Err(ParseError::TooFewOptions),
Some(&"preload") => {

View file

@ -19,10 +19,13 @@ pub fn read_cade(path: &Path) -> Result<Vec<Keyword>> {
Ok(accum)
}
pub fn realise(cascade: HashMap<PathBuf, Vec<Keyword>>) -> Result<Vec<CadeActions>> {
let mut actions: Vec<CadeActions> = Vec::new();
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 {
use Keyword::*;
use Loadable::*;
@ -37,8 +40,9 @@ pub fn realise(cascade: HashMap<PathBuf, Vec<Keyword>>) -> Result<Vec<CadeAction
},
Hook(hook) => Ok(CadeActions::Hook(hook)),
}?;
actions.push(acts);
layer_actions.push(acts);
}
actions.insert(path, layer_actions);
}
Ok(actions)
}

45
src/envs.rs Normal file
View file

@ -0,0 +1,45 @@
use std::str::FromStr;
use crate::types::EnvSet;
use anyhow::{Result, anyhow};
use serde_json::Value;
pub enum EnvProviders {
RawEnvs,
NixJson,
Cade,
}
pub enum EnvSetParseErr {
Bad,
}
impl FromStr for EnvSet {
type Err = EnvSetParseErr;
}
pub fn parse(text: &str) -> Result<EnvSet> {}
pub trait EnvProvider {
fn parse(text: &str) -> Result<EnvSet>;
}
impl EnvProvider for RawEnvs {
fn parse(text: &str) -> Result<EnvSet> {
let mut envs = Vec::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));
}
_ => {
return Err(anyhow!("parsing variable from {text}"));
} // 1 => Clear(split[0].to_string()),
// _ => Add(split[0].to_string(), split[1..].concat().to_string()),
};
}
Ok(envs)
}
}

View file

@ -1,6 +1,7 @@
mod actions;
mod cli;
mod core;
mod envs;
mod types;
use std::{

View file

@ -1,27 +1,35 @@
use std::path::PathBuf;
use anyhow::{Context, Result, anyhow};
#[derive(Debug)]
pub enum CadeActions {
Purify,
Environ(Vec<Env>),
Hook(Hook),
Hook(InnerHook),
}
#[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 kind: Loadable,
pub layer: u8,
}
impl Env {
// upcast envset to env
fn from_set(env: EnvSet, origin: &Path, layer: u8) -> Env {}
}
#[derive(Debug)]
pub enum Keyword {
Pure,
Call(Vec<String>),
Load(Loadable),
Hook(Hook),
Hook(InnerHook),
}
#[derive(Debug)]
@ -41,7 +49,59 @@ pub enum HookType {
}
#[derive(Debug)]
pub struct Hook {
pub struct InnerHook {
// FIXME should be a pathbuf ?
pub content: Vec<String>,
pub kind: HookType,
}
pub struct EnvSet(Vec<(String, Vec<String>)>);
impl EnvSet {
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, _)| 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(EnvSet(vars))
} else {
Err(anyhow!("failed to parse values from JSON output"))
}
}
}