initial commit

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a69644236ae18e7b46856fb6d6d6c998f8467
This commit is contained in:
raf 2025-10-05 21:12:25 +03:00
commit c07b295f71
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
29 changed files with 6780 additions and 0 deletions

86
cognos/Cargo.lock generated Normal file
View file

@ -0,0 +1,86 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "nous"
version = "0.1.0"
dependencies = [
"serde",
"serde_repr",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"

15
cognos/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "cognos"
description = "Minimalistic parser for Nix's ATerm .drv and internal-json log formats"
version.workspace = true
edition.workspace = true
authors.workspace = true
rust-version.workspace = true
[lib]
path = "src/lib.rs"
[dependencies]
serde.workspace = true
serde_repr.workspace = true
serde_json.workspace = true

414
cognos/src/aterm.rs Normal file
View file

@ -0,0 +1,414 @@
//! `ATerm` and Nix .drv file parser
//!
//! Parses Nix .drv files in `ATerm` format to extract dependency information.
use std::{fs, path::Path};
/// Parsed derivation information from a .drv file
#[derive(Debug, Clone)]
pub struct ParsedDerivation {
pub outputs: Vec<(String, String)>,
pub input_drvs: Vec<(String, Vec<String>)>,
pub input_srcs: Vec<String>,
pub platform: String,
pub builder: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
}
/// Parse a .drv file and extract its dependency information
pub fn parse_drv_file<P: AsRef<Path>>(
path: P,
) -> Result<ParsedDerivation, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read file: {e}"))?;
parse_drv_content(&content)
}
/// Parse the content of a .drv file
pub fn parse_drv_content(content: &str) -> Result<ParsedDerivation, String> {
let content = content.trim();
if !content.starts_with("Derive(") {
return Err(
"Invalid derivation format: must start with 'Derive('".to_string(),
);
}
let inner = content
.strip_prefix("Derive(")
.and_then(|s| s.strip_suffix(")"))
.ok_or("Invalid derivation format: missing closing parenthesis")?;
// XXX: The derivation has this structure:
// Derive(outputs, inputDrvs, inputSrcs, platform, builder, args, env)
let parts = parse_top_level_list(inner)?;
if parts.len() < 7 {
return Err(format!(
"Invalid derivation format: expected 7 parts, got {}",
parts.len()
));
}
let outputs = parse_outputs(&parts[0])?;
let input_drvs = parse_input_drvs(&parts[1])?;
let input_srcs = parse_string_list(&parts[2])?;
let platform = parse_string(&parts[3])?;
let builder = parse_string(&parts[4])?;
let args = parse_string_list(&parts[5])?;
let env = parse_env(&parts[6])?;
Ok(ParsedDerivation {
outputs,
input_drvs,
input_srcs,
platform,
builder,
args,
env,
})
}
/// Parse the top-level comma-separated list, respecting nested brackets
fn parse_top_level_list(s: &str) -> Result<Vec<String>, String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut depth = 0;
let mut in_string = false;
let mut escape = false;
for ch in s.chars() {
if escape {
current.push(ch);
escape = false;
continue;
}
match ch {
'\\' if in_string => {
escape = true;
current.push(ch);
},
'"' => {
in_string = !in_string;
current.push(ch);
},
'[' | '(' if !in_string => {
depth += 1;
current.push(ch);
},
']' | ')' if !in_string => {
depth -= 1;
current.push(ch);
},
',' if depth == 0 && !in_string => {
parts.push(current.trim().to_string());
current.clear();
},
_ => {
current.push(ch);
},
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
Ok(parts)
}
/// Parse outputs: [("out","/nix/store/...","",""),...]
fn parse_outputs(s: &str) -> Result<Vec<(String, String)>, String> {
let s = s.trim();
if s == "[]" {
return Ok(Vec::new());
}
let inner = s
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.ok_or("Invalid outputs format")?;
let tuples = parse_top_level_list(inner)?;
let mut outputs = Vec::new();
for tuple in tuples {
let tuple = tuple.trim();
let inner = tuple
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.ok_or("Invalid output tuple format")?;
let parts = parse_top_level_list(inner)?;
if parts.len() >= 2 {
let name = parse_string(&parts[0])?;
let path = parse_string(&parts[1])?;
outputs.push((name, path));
}
}
Ok(outputs)
}
/// Parse input derivations: [("/nix/store/foo.drv",["out"]),...]
fn parse_input_drvs(s: &str) -> Result<Vec<(String, Vec<String>)>, String> {
let s = s.trim();
if s == "[]" {
return Ok(Vec::new());
}
let inner = s
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.ok_or("Invalid input drvs format")?;
let tuples = parse_top_level_list(inner)?;
let mut input_drvs = Vec::new();
for tuple in tuples {
let tuple = tuple.trim();
let inner = tuple
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.ok_or("Invalid input drv tuple format")?;
let parts = parse_top_level_list(inner)?;
if parts.len() >= 2 {
let drv_path = parse_string(&parts[0])?;
let outputs = parse_string_list(&parts[1])?;
input_drvs.push((drv_path, outputs));
}
}
Ok(input_drvs)
}
/// Parse environment variables: [("name","value"),...]
fn parse_env(s: &str) -> Result<Vec<(String, String)>, String> {
let s = s.trim();
if s == "[]" {
return Ok(Vec::new());
}
let inner = s
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.ok_or("Invalid env format")?;
let tuples = parse_top_level_list(inner)?;
let mut env = Vec::new();
for tuple in tuples {
let tuple = tuple.trim();
let inner = tuple
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.ok_or("Invalid env tuple format")?;
let parts = parse_top_level_list(inner)?;
if parts.len() >= 2 {
let name = parse_string(&parts[0])?;
let value = parse_string(&parts[1])?;
env.push((name, value));
}
}
Ok(env)
}
/// Parse a list of strings: ["foo","bar",...]
fn parse_string_list(s: &str) -> Result<Vec<String>, String> {
let s = s.trim();
if s == "[]" {
return Ok(Vec::new());
}
let inner = s
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.ok_or("Invalid string list format")?;
let items = parse_top_level_list(inner)?;
items.into_iter().map(|item| parse_string(&item)).collect()
}
/// Parse a quoted string: "foo" -> foo
fn parse_string(s: &str) -> Result<String, String> {
let s = s.trim();
let inner = s
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.ok_or_else(|| format!("Invalid string format: {s}"))?;
// Unescape the string
Ok(unescape_string(inner))
}
/// Unescape a string (handle \n, \t, \\, \", etc.)
fn unescape_string(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('t') => result.push('\t'),
Some('r') => result.push('\r'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some(c) => {
result.push('\\');
result.push(c);
},
None => result.push('\\'),
}
} else {
result.push(ch);
}
}
result
}
/// Extract all input derivation paths from a .drv file
pub fn get_input_derivations<P: AsRef<Path>>(
path: P,
) -> Result<Vec<String>, String> {
let parsed = parse_drv_file(path)?;
Ok(
parsed
.input_drvs
.into_iter()
.map(|(path, _)| path)
.collect(),
)
}
/// Extract pname from environment variables
#[must_use]
pub fn extract_pname(env: &[(String, String)]) -> Option<String> {
env
.iter()
.find(|(k, _)| k == "pname")
.map(|(_, v)| v.clone())
}
/// Extract version from environment variables
#[must_use]
pub fn extract_version(env: &[(String, String)]) -> Option<String> {
env
.iter()
.find(|(k, _)| k == "version")
.map(|(_, v)| v.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_string() {
assert_eq!(parse_string(r#""hello""#).unwrap(), "hello");
assert_eq!(parse_string(r#""hello world""#).unwrap(), "hello world");
assert_eq!(parse_string(r#""hello\nworld""#).unwrap(), "hello\nworld");
}
#[test]
fn test_parse_string_list() {
let list = r#"["foo","bar","baz"]"#;
let result = parse_string_list(list).unwrap();
assert_eq!(result, vec!["foo", "bar", "baz"]);
let empty = "[]";
let result = parse_string_list(empty).unwrap();
assert_eq!(result, Vec::<String>::new());
}
#[test]
fn test_parse_outputs() {
let outputs = r#"[("out","/nix/store/abc-foo","","")]"#;
let result = parse_outputs(outputs).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "out");
assert_eq!(result[0].1, "/nix/store/abc-foo");
}
#[test]
fn test_parse_input_drvs() {
let input = r#"[("/nix/store/abc-foo.drv",["out"]),("/nix/store/def-bar.drv",["out","dev"])]"#;
let result = parse_input_drvs(input).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].0, "/nix/store/abc-foo.drv");
assert_eq!(result[0].1, vec!["out"]);
assert_eq!(result[1].0, "/nix/store/def-bar.drv");
assert_eq!(result[1].1, vec!["out", "dev"]);
}
#[test]
fn test_parse_minimal_drv() {
let drv = r#"Derive([("out","/nix/store/output","","")],[],[],"x86_64-linux","/bin/sh",[],[("name","value")])"#;
let result = parse_drv_content(drv).unwrap();
assert_eq!(result.outputs.len(), 1);
assert_eq!(result.outputs[0].0, "out");
assert_eq!(result.platform, "x86_64-linux");
assert_eq!(result.builder, "/bin/sh");
}
#[test]
fn test_parse_with_dependencies() {
let drv = r#"Derive([("out","/nix/store/abc-foo","","")],[("/nix/store/dep1.drv",["out"]),("/nix/store/dep2.drv",["out","dev"])],[],"x86_64-linux","/bin/sh",[],[("name","foo")])"#;
let result = parse_drv_content(drv).unwrap();
assert_eq!(result.input_drvs.len(), 2);
assert_eq!(result.input_drvs[0].0, "/nix/store/dep1.drv");
assert_eq!(result.input_drvs[0].1, vec!["out"]);
assert_eq!(result.input_drvs[1].0, "/nix/store/dep2.drv");
assert_eq!(result.input_drvs[1].1, vec!["out", "dev"]);
}
#[test]
fn test_parse_real_world_hello_drv() {
// Stripped down version of a real hello.drv
let drv = r#"Derive([("out","/nix/store/b1ayn0ln6n8bm2spz441csqc2ss66az3-hello-2.12.2","","")],[("/nix/store/1s1ir3vhwq86x0c7ikhhp3c9cin4095k-hello-2.12.2.tar.gz.drv",["out"]),("/nix/store/bjsb6wdjykafnkixq156qdvmxhsm2bai-bash-5.3p3.drv",["out"]),("/nix/store/lzvy25g887aypn07ah8igv72z7b9jb88-version-check-hook.drv",["out"]),("/nix/store/p76r0cwlf6k97ibprrpfd8xw0r8wc3nx-stdenv-linux.drv",["out"])],["/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh","/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh"],"x86_64-linux","/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin/bash",["-e","/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh"],[("name","hello-2.12.2"),("pname","hello"),("version","2.12.2"),("system","x86_64-linux")])"#;
let result = parse_drv_content(drv).unwrap();
// Verify outputs
assert_eq!(result.outputs.len(), 1);
assert_eq!(result.outputs[0].0, "out");
assert!(result.outputs[0].1.contains("hello-2.12.2"));
// Verify input derivations
assert_eq!(result.input_drvs.len(), 4);
assert!(result.input_drvs[0].0.contains("hello-2.12.2.tar.gz.drv"));
assert!(result.input_drvs[1].0.contains("bash-5.3p3.drv"));
assert!(result.input_drvs[2].0.contains("version-check-hook.drv"));
assert!(result.input_drvs[3].0.contains("stdenv-linux.drv"));
// Verify all inputs have "out" output
for (_, outputs) in &result.input_drvs {
assert_eq!(outputs, &vec!["out"]);
}
// Verify platform
assert_eq!(result.platform, "x86_64-linux");
// Verify builder
assert!(result.builder.contains("bash"));
// Verify environment
assert_eq!(extract_pname(&result.env), Some("hello".to_string()));
assert_eq!(extract_version(&result.env), Some("2.12.2".to_string()));
}
#[test]
fn test_get_input_derivations() {
let drv = r#"Derive([("out","/nix/store/out","","")],[("/nix/store/dep.drv",["out"])],[],"x86_64-linux","/bin/sh",[],[("pname","hello"),("version","1.0")])"#;
let result = parse_drv_content(drv).unwrap();
assert_eq!(result.input_drvs.len(), 1);
assert_eq!(result.input_drvs[0].0, "/nix/store/dep.drv");
assert_eq!(extract_pname(&result.env).unwrap(), "hello");
assert_eq!(extract_version(&result.env).unwrap(), "1.0");
}
}

View file

@ -0,0 +1,67 @@
use serde::Deserialize;
use serde_repr::Deserialize_repr;
#[derive(Deserialize_repr, Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Activities {
Unknown = 0,
CopyPath = 100,
FileTransfer = 101,
Realise = 102,
CopyPaths = 103,
Builds = 104,
Build = 105,
OptimiseStore = 106,
VerifyPath = 107,
Substitute = 108,
QueryPathInfo = 109,
PostBuildHook = 110,
BuildWaiting = 111,
FetchTree = 112,
}
#[derive(
Deserialize_repr, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord,
)]
#[repr(u8)]
pub enum Verbosity {
Error = 0,
Warning = 1,
Notice = 2,
Info = 3,
Talkative = 4,
Chatty = 5,
Debug = 6,
Vomit = 7,
}
pub type Id = u64;
#[derive(Deserialize, Debug, Clone)]
#[serde(tag = "action")]
pub enum Actions {
#[serde(rename = "start")]
Start {
id: Id,
level: Verbosity,
#[serde(default)]
parent: Id,
text: String,
#[serde(rename = "type")]
activity: Activities,
#[serde(default)]
fields: Vec<serde_json::Value>,
},
#[serde(rename = "stop")]
Stop { id: Id },
#[serde(rename = "msg")]
Message { level: Verbosity, msg: String },
#[serde(rename = "result")]
Result {
#[serde(default)]
fields: Vec<serde_json::Value>,
id: Id,
#[serde(rename = "type")]
activity: Activities,
},
}

12
cognos/src/lib.rs Normal file
View file

@ -0,0 +1,12 @@
pub mod aterm;
mod internal_json;
mod state;
pub use aterm::{
ParsedDerivation,
extract_pname,
extract_version,
parse_drv_file,
};
pub use internal_json::{Actions, Activities, Id, Verbosity};
pub use state::{BuildInfo, BuildStatus, Derivation, Host, State};

73
cognos/src/state.rs Normal file
View file

@ -0,0 +1,73 @@
use std::{collections::HashMap, path::PathBuf};
use crate::internal_json::Actions;
pub type Id = u64;
pub enum StorePath {
Downloading,
Uploading,
Downloaded,
Uploaded,
}
pub enum BuildStatus {
Planned,
Running,
Complete,
Failed,
}
pub enum Progress {
JustStarted,
InputReceived,
Finished,
}
pub enum OutputName {
Out,
Doc,
Dev,
Bin,
Info,
Lib,
Man,
Dist,
Other(String),
}
pub enum Host {
Local,
Host(String),
}
pub struct Derivation {
store_path: PathBuf,
}
pub struct BuildInfo {
start: f64,
host: Host,
estimate: Option<u64>,
activity_id: Id,
state: BuildStatus,
}
pub enum DependencyState {
Planned,
Running,
Completed,
}
pub struct Dependencies {
deps: HashMap<Id, BuildInfo>,
}
// #[derive(Default)]
pub struct State {
progress: Progress,
}
impl State {
pub fn imbibe(&mut self, update: Actions) {}
}