diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a77ce80..fa3357d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,11 +1,15 @@ name: Rust on: - merge_group: + push: branches: [ "canon" ] pull_request: branches: [ "canon" ] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always diff --git a/Cargo.lock b/Cargo.lock index 6a7fa3b..85935d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -282,9 +288,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -292,9 +298,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -304,21 +310,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "clru" @@ -365,6 +371,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "combine" version = "4.6.8" @@ -598,7 +614,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1291,6 +1307,7 @@ dependencies = [ "parking_lot", "smallvec", "thiserror", + "uluru", ] [[package]] @@ -2116,6 +2133,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.189" @@ -2664,6 +2687,7 @@ name = "rix-cli" version = "0.1.0" dependencies = [ "clap", + "colored", "rix-eval", "rix-parser", "rootcause", @@ -2676,9 +2700,9 @@ name = "rix-eval" version = "0.1.0" dependencies = [ "base32", - "clap", "codespan", "codespan-reporting", + "colored", "criterion", "flate2", "gix", @@ -2950,7 +2974,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3093,9 +3117,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -3178,7 +3202,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3256,9 +3280,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.4+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ "indexmap", "serde_core", @@ -3375,6 +3399,15 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uluru" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" +dependencies = [ + "arrayvec", +] + [[package]] name = "unarray" version = "0.1.4" @@ -3617,6 +3650,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -3818,7 +3860,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a3b0bed..07ebcce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/Sunrise-OS/rix" rix-parser = { path = "rix-parser" } rix-eval = { path = "rix-eval" } -clap = { version = "4.6.6", features = ["derive"] } +clap = { version = "4.6.7", features = ["derive"] } thiserror = "2.0.20" rootcause = "0.13.0" rowan = "0.17.0" @@ -30,11 +30,12 @@ criterion = "0.8.2" expect-test = "1.5.1" proptest = "1.11.0" quick-xml = "0.42.0" -toml = "1.1.4" +toml = "1.1.6" gix = { version = "0.87.1", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "blocking-network-client", "worktree-mutation", "revision", "credentials", "sha1", "sha256", "parallel", "pack-cache-lru-static"] } reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] } -flate2 = { version = "1.0.17", features = ["zlib-rs"], default-features = false } +flate2 = { version = "1.1.10", features = ["zlib-rs"], default-features = false } tar = "0.4" +colored = "2.2" [profile.dev.package."*"] opt-level = 3 diff --git a/rix-cli/Cargo.toml b/rix-cli/Cargo.toml index 0665d25..0a204b8 100644 --- a/rix-cli/Cargo.toml +++ b/rix-cli/Cargo.toml @@ -12,4 +12,5 @@ thiserror.workspace = true rix-parser.workspace = true rix-eval.workspace = true serde_json.workspace = true +colored.workspace = true rootcause.workspace = true diff --git a/rix-cli/src/main.rs b/rix-cli/src/main.rs index 8158387..e651d0d 100644 --- a/rix-cli/src/main.rs +++ b/rix-cli/src/main.rs @@ -1,55 +1,318 @@ -use clap::Parser; -use nix_eval::Evaluator; +use clap::{Parser, Subcommand}; +use nix_eval::{BuildOptions, Builder, Evaluator, NixValue}; use rootcause::{Report, report}; +use std::collections::HashMap; use std::io::{self, Read}; +use std::path::{Path, PathBuf}; -/// A pure Rust Nix expression evaluator -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// Nix expression to evaluate (or file path if --file is used) - #[arg(short, long = "expr", value_name = "EXPRESSION")] - expression: Option, +/// Pure Rust Nix expression evaluator and package manager +#[derive(Parser, Debug, Clone)] +#[command(name = "rix", author, version, about = "Nix-compatible package manager", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Option, + + /// File to evaluate or build + #[arg(short = 'f', long = "file", global = true)] + file: Option, + + /// Attribute path to select + #[arg(short = 'A', long = "attr", global = true)] + attr: Option, - /// Read expression from file instead of command line - #[arg(short, long)] - file: bool, + /// Output symlink path (default: result) + #[arg(short = 'o', long = "out-link", global = true)] + out_link: Option, - /// Produce output in JSON format, suitable for consumption by another program. - #[arg(long)] + /// Nix expression to evaluate + #[arg(short = 'e', long = "expr", global = true)] + expression: Option, + + /// Produce output in JSON format + #[arg(long, global = true)] json: bool, + + /// Verbose output + #[arg(short = 'v', long = "verbose", global = true)] + verbose: bool, + + /// Keep failed build temporary directory + #[arg(long = "keep-failed", global = true)] + keep_failed: bool, + + /// Dry run (do not execute build commands) + #[arg(long = "dry-run", global = true)] + dry_run: bool, } -fn main() -> Result<(), Report> { - let args = Args::parse(); +#[derive(Subcommand, Debug, Clone)] +enum Commands { + /// Build a derivation from a Nix expression or file + Build { + /// File to build (default: default.nix) + #[arg(short = 'f', long = "file")] + file: Option, + + /// Attribute path to build + #[arg(short = 'A', long = "attr")] + attr: Option, + + /// Output symlink path (default: result) + #[arg(short = 'o', long = "out-link")] + out_link: Option, + + /// Nix expression to evaluate and build + #[arg(short = 'e', long = "expr")] + expr: Option, + }, - // Create evaluator + /// Evaluate a Nix expression + Eval { + /// Nix expression to evaluate + #[arg(short = 'e', long = "expr")] + expr: Option, + + /// File to evaluate + #[arg(short = 'f', long = "file")] + file: Option, + + /// Attribute path to select + #[arg(short = 'A', long = "attr")] + attr: Option, + }, + + /// Show derivation plan (ATerm or JSON) + ShowDerivation { + /// File to evaluate (default: default.nix) + #[arg(short = 'f', long = "file")] + file: Option, + + /// Attribute path to show + #[arg(short = 'A', long = "attr")] + attr: Option, + }, +} + +fn main() -> Result<(), Report> { + let cli = Cli::parse(); let evaluator = Evaluator::new(); - let value = if args.file { - let path = args - .expression - .ok_or_else(|| report!("File path required when using --file"))?; + // Determine command: default to Build if -f or -A is provided at top-level or program name is nix-build + let prog_name = std::env::var("RIX_PROG_NAME") + .ok() + .or_else(|| std::env::args().next()) + .unwrap_or_default(); + let prog_base = prog_name + .rsplit('/') + .next() + .unwrap_or(&prog_name) + .to_string(); + let is_nix_build = prog_base.contains("nix-build") || prog_base.contains("opendarwin-nix"); + let is_nix_instantiate = prog_base.contains("nix-instantiate"); + + match cli.command.clone() { + Some(Commands::Build { + file, + attr, + out_link, + expr, + }) => run_build( + &evaluator, + file.or(cli.file.clone()), + attr.or(cli.attr.clone()), + out_link.or(cli.out_link.clone()), + expr.or(cli.expression.clone()), + &cli, + ), + Some(Commands::Eval { expr, file, attr }) => run_eval( + &evaluator, + file.or(cli.file.clone()), + attr.or(cli.attr.clone()), + expr.or(cli.expression.clone()), + &cli, + ), + Some(Commands::ShowDerivation { file, attr }) => run_show_derivation( + &evaluator, + file.or(cli.file.clone()), + attr.or(cli.attr.clone()), + &cli, + ), + None => { + if is_nix_instantiate { + run_show_derivation(&evaluator, cli.file.clone(), cli.attr.clone(), &cli) + } else if is_nix_build || cli.file.is_some() || cli.attr.is_some() { + run_build( + &evaluator, + cli.file.clone(), + cli.attr.clone(), + cli.out_link.clone(), + cli.expression.clone(), + &cli, + ) + } else if let Some(expr) = cli.expression.clone() { + run_eval(&evaluator, None, None, Some(expr), &cli) + } else { + // If nothing specified and default.nix exists, build default.nix + if Path::new("default.nix").exists() { + run_build( + &evaluator, + Some("default.nix".to_string()), + None, + cli.out_link.clone(), + None, + &cli, + ) + } else { + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + if buffer.trim().is_empty() { + println!("Usage: rix [build|eval|show-derivation] [-f FILE] [-A ATTR]"); + return Ok(()); + } + run_eval(&evaluator, None, None, Some(buffer), &cli) + } + } + } + } +} + +fn resolve_expression( + evaluator: &Evaluator, + file: Option, + attr: Option, + expr: Option, +) -> Result { + let mut root_val = if let Some(path_str) = file { + evaluator + .evaluate_from_file(Path::new(&path_str)) + .map_err(|e| report!("{}", e))? + } else if let Some(expression_str) = expr { + evaluator + .evaluate(&expression_str) + .map_err(|e| report!("{}", e))? + } else if Path::new("default.nix").exists() { evaluator - .evaluate_from_file(std::path::Path::new(&path)) + .evaluate_from_file(Path::new("default.nix")) .map_err(|e| report!("{}", e))? } else { - let expr = if let Some(expr) = args.expression { - expr - } else { - let mut buffer = String::new(); - io::stdin().read_to_string(&mut buffer)?; - buffer - }; - evaluator.evaluate(&expr).map_err(|e| report!("{}", e))? + return Err(report!("No expression or file provided to evaluate")); + }; + + // Auto-call function if root value is a function taking an attribute set + root_val = unwrap_function(evaluator, root_val)?; + + // Select attribute path if requested + if let Some(attr_path) = attr { + for part in attr_path.split('.') { + root_val = root_val.force(evaluator).map_err(|e| report!("{}", e))?; + match root_val { + NixValue::AttributeSet(map) => { + let next = map + .get(part) + .cloned() + .ok_or_else(|| report!("Attribute '{}' not found in package set", part))?; + root_val = unwrap_function(evaluator, next)?; + } + other => { + return Err(report!( + "Cannot select attribute '{}' from non-attribute-set: {}", + part, + other + )); + } + } + } + } + + unwrap_function(evaluator, root_val) +} + +fn unwrap_function(evaluator: &Evaluator, mut val: NixValue) -> Result { + val = val.force(evaluator).map_err(|e| report!("{}", e))?; + while matches!(val, NixValue::Function(_)) { + if let NixValue::Function(func) = val { + let empty_set = NixValue::AttributeSet(HashMap::new()); + val = func + .apply(evaluator, empty_set) + .map_err(|e| report!("{}", e))?; + val = val.force(evaluator).map_err(|e| report!("{}", e))?; + } + } + Ok(val) +} + +fn run_build( + evaluator: &Evaluator, + file: Option, + attr: Option, + out_link: Option, + expr: Option, + cli: &Cli, +) -> Result<(), Report> { + let value = resolve_expression(evaluator, file, attr, expr)?; + + let builder = Builder::new(); + let options = BuildOptions { + verbose: cli.verbose, + keep_failed: cli.keep_failed, + dry_run: cli.dry_run, + out_link: out_link + .map(PathBuf::from) + .or_else(|| Some(PathBuf::from("result"))), }; - // Output the result - if args.json { + let result = builder + .build(evaluator, &value, &options) + .map_err(|e| report!("{}", e))?; + + if cli.json { + let json_map: HashMap = result + .outputs + .into_iter() + .map(|(k, v)| (k, v.to_string_lossy().to_string())) + .collect(); + println!("{}", serde_json::to_string_pretty(&json_map)?); + } + + Ok(()) +} + +fn run_eval( + evaluator: &Evaluator, + file: Option, + attr: Option, + expr: Option, + cli: &Cli, +) -> Result<(), Report> { + let value = resolve_expression(evaluator, file, attr, expr)?; + if cli.json { println!("{}", serde_json::to_string_pretty(&value)?); } else { println!("{}", value); } + Ok(()) +} + +fn run_show_derivation( + evaluator: &Evaluator, + file: Option, + attr: Option, + cli: &Cli, +) -> Result<(), Report> { + let value = resolve_expression(evaluator, file, attr, None)?; + let forced = value.force(evaluator).map_err(|e| report!("{}", e))?; + match forced { + NixValue::AttributeSet(map) => { + if cli.json { + println!("{}", serde_json::to_string_pretty(&map)?); + } else { + println!("{}", NixValue::AttributeSet(map)); + } + } + other => { + println!("{}", other); + } + } Ok(()) } diff --git a/rix-eval/Cargo.toml b/rix-eval/Cargo.toml index 07aa9ad..761fce9 100644 --- a/rix-eval/Cargo.toml +++ b/rix-eval/Cargo.toml @@ -9,7 +9,6 @@ keywords = ["nix", "evaluator", "parser", "expression"] categories = ["parsing", "development-tools"] [dependencies] -clap.workspace = true thiserror.workspace = true rowan.workspace = true serde.workspace = true @@ -28,6 +27,7 @@ gix.workspace = true reqwest.workspace = true flate2.workspace = true tar.workspace = true +colored.workspace = true rix-parser.workspace = true [dev-dependencies] diff --git a/rix-eval/src/builder.rs b/rix-eval/src/builder.rs new file mode 100644 index 0000000..07e4cb7 --- /dev/null +++ b/rix-eval/src/builder.rs @@ -0,0 +1,600 @@ +//! Derivation build execution engine for Rix + +use crate::error::{Error, Result}; +use crate::store::Store; +use crate::value::NixValue; +use colored::Colorize; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Build configuration options +#[derive(Debug, Clone)] +pub struct BuildOptions { + pub verbose: bool, + pub keep_failed: bool, + pub dry_run: bool, + pub out_link: Option, +} + +impl Default for BuildOptions { + fn default() -> Self { + Self { + verbose: false, + keep_failed: false, + dry_run: false, + out_link: Some(PathBuf::from("result")), + } + } +} + +/// Result of building a derivation +#[derive(Debug, Clone)] +pub struct BuildResult { + pub drv_name: String, + pub drv_path: String, + pub outputs: HashMap, + pub cached: bool, +} + +/// Derivation Builder +pub struct Builder { + pub store: Store, +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +impl Builder { + pub fn new() -> Self { + Self { + store: Store::new(), + } + } + + /// Build a derivation value or attribute set + pub fn build( + &self, + evaluator: &crate::eval::Evaluator, + value: &NixValue, + options: &BuildOptions, + ) -> Result { + self.store.ensure_store_dir().map_err(Error::IoError)?; + + let forced = value.clone().force(evaluator)?; + let attrs = match &forced { + NixValue::AttributeSet(map) => map, + other => { + return Err(Error::UnsupportedExpression { + reason: format!("Cannot build non-attribute-set value: {}", other), + }); + } + }; + + // Extract derivation attributes + let name = match attrs.get("name") { + Some(v) => v.clone().force(evaluator)?.as_string()?, + None => { + let pname = match attrs.get("pname") { + Some(v) => v.clone().force(evaluator)?.as_string()?, + None => "unnamed".to_string(), + }; + let version = match attrs.get("version") { + Some(v) => v.clone().force(evaluator)?.as_string()?, + None => "0.0.0".to_string(), + }; + format!("{}-{}", pname, version) + } + }; + + let system = match attrs.get("system") { + Some(v) => v.clone().force(evaluator)?.as_string()?, + None => "unknown".to_string(), + }; + + let builder = match attrs.get("builder") { + Some(v) => v.clone().force(evaluator)?.to_string(), + None => "/bin/sh".to_string(), + }; + + let args = match attrs.get("args") { + Some(v) => match v.clone().force(evaluator)? { + NixValue::List(l) => { + let mut res = Vec::new(); + for item in l { + res.push(item.force(evaluator)?.to_string()); + } + res + } + _ => Vec::new(), + }, + None => Vec::new(), + }; + + // Extract outputs + let mut output_names = Vec::new(); + if let Some(v) = attrs.get("outputs") + && let Ok(NixValue::List(l)) = v.clone().force(evaluator) + { + for item in l { + if let Ok(s) = item.force(evaluator)?.as_string() { + output_names.push(s); + } + } + } + if output_names.is_empty() { + output_names.push("out".to_string()); + } + + // Collect environment variables + let mut env = HashMap::new(); + for (k, v) in attrs { + let key = k.to_string(); + let forced_v = v.clone().force(evaluator)?; + let str_val = match &forced_v { + NixValue::String(s) => s.clone(), + NixValue::Path(p) => p.to_string_lossy().to_string(), + NixValue::StorePath(p) => p.clone(), + NixValue::Integer(i) => i.to_string(), + NixValue::Boolean(b) => { + if *b { + "1".to_string() + } else { + "".to_string() + } + } + NixValue::List(l) => { + let mut parts = Vec::new(); + for item in l { + parts.push(item.clone().force(evaluator)?.to_string()); + } + parts.join(" ") + } + NixValue::AttributeSet(sub) => { + if let Some(out_p) = sub.get("outPath") { + out_p.clone().force(evaluator)?.to_string() + } else { + forced_v.to_string() + } + } + _ => forced_v.to_string(), + }; + env.insert(key, str_val); + } + + // Use the store paths computed during derivation evaluation + let drv_path = match attrs.get("drvPath") { + Some(v) => v.clone().force(evaluator)?.as_string()?, + None => { + let (drv, _) = self + .store + .compute_derivation_paths(&name, &output_names, &env); + drv + } + }; + + let mut output_paths = HashMap::new(); + for out_name in &output_names { + if let Some(v) = attrs.get(out_name) + && let Ok(s) = v.clone().force(evaluator)?.as_string() + { + output_paths.insert(out_name.clone(), s); + continue; + } + if out_name == "out" + && let Some(v) = attrs.get("outPath") + && let Ok(s) = v.clone().force(evaluator)?.as_string() + { + output_paths.insert("out".to_string(), s); + continue; + } + let (_, computed_outs) = + self.store + .compute_derivation_paths(&name, &output_names, &env); + if let Some(p) = computed_outs.get(out_name) { + output_paths.insert(out_name.clone(), p.clone()); + } + } + + let out_path = output_paths + .get("out") + .cloned() + .unwrap_or_else(|| drv_path.clone()); + + // Check if outputs already exist in store + let all_outputs_exist = output_paths + .values() + .all(|p| self.store.is_valid_path(Path::new(p))); + + if all_outputs_exist { + if options.verbose { + println!( + "{} Derivation {} is already cached in store at {}", + "✓".green(), + name.bold(), + out_path.cyan() + ); + } + if let Some(link) = &options.out_link { + create_symlink(Path::new(&out_path), link)?; + } + return Ok(BuildResult { + drv_name: name, + drv_path, + outputs: output_paths + .into_iter() + .map(|(k, v)| (k, PathBuf::from(v))) + .collect(), + cached: true, + }); + } + + if options.dry_run { + println!("Would build derivation: {}", name); + return Ok(BuildResult { + drv_name: name, + drv_path, + outputs: output_paths + .into_iter() + .map(|(k, v)| (k, PathBuf::from(v))) + .collect(), + cached: false, + }); + } + + println!( + "{} {}", + "==> Building derivation:".cyan().bold(), + name.bold() + ); + + // Build prerequisite input derivations first + if let Some(inputs_val) = attrs.get("__inputs") + && let Ok(NixValue::List(inputs)) = inputs_val.clone().force(evaluator) + { + for input_drv in inputs { + self.build(evaluator, &input_drv, options)?; + } + } + + // Scan environment for input store paths and build dependencies if any + self.resolve_and_build_inputs(evaluator, &env, options)?; + + // Prepare temporary sandbox directory + let temp_dir = + std::env::temp_dir().join(format!("rix-build-{}-{}", std::process::id(), name)); + let _ = std::fs::remove_dir_all(&temp_dir); + std::fs::create_dir_all(&temp_dir).map_err(Error::IoError)?; + + // Ensure output directories exist in store + for path_str in output_paths.values() { + let p = Path::new(path_str); + let _ = std::fs::remove_dir_all(p); + std::fs::create_dir_all(p).map_err(Error::IoError)?; + } + + // Setup execution environment + let mut cmd_env = env.clone(); + for (out_name, out_p) in &output_paths { + cmd_env.insert(out_name.clone(), out_p.clone()); + } + cmd_env.insert("name".to_string(), name.clone()); + cmd_env.insert("system".to_string(), system.clone()); + cmd_env.insert("NIX_BUILD_CORES".to_string(), num_cpus().to_string()); + cmd_env.insert("TMPDIR".to_string(), temp_dir.to_string_lossy().to_string()); + cmd_env.insert("TEMP".to_string(), temp_dir.to_string_lossy().to_string()); + cmd_env.insert("TMP".to_string(), temp_dir.to_string_lossy().to_string()); + cmd_env.insert("HOME".to_string(), temp_dir.to_string_lossy().to_string()); + + // Set Darwin / macOS defaults + if cfg!(target_os = "macos") { + cmd_env.insert("MACOSX_DEPLOYMENT_TARGET".to_string(), "14.0".to_string()); + if let Ok(sdk) = get_macos_sdk_path() { + cmd_env.insert("SDKROOT".to_string(), sdk); + } + } + + // Construct PATH with store bin directories and host toolchain + let mut path_entries = Vec::new(); + for out_p in output_paths.values() { + path_entries.push(format!("{}/bin", out_p)); + } + // Extract bin paths from env values that look like store paths + for v in env.values() { + for token in v.split_whitespace() { + if token.contains("/store/") { + path_entries.push(format!("{}/bin", token)); + } + } + } + // Extract bin paths from input derivations + if let Some(inputs_val) = attrs.get("__inputs") + && let Ok(NixValue::List(inputs)) = inputs_val.clone().force(evaluator) + { + for input in inputs { + if let Ok(NixValue::AttributeSet(m)) = input.force(evaluator) + && let Some(out_p) = m.get("outPath") + && let Ok(s) = out_p.clone().force(evaluator)?.as_string() + { + path_entries.push(format!("{}/bin", s)); + } + } + } + // Add host paths + path_entries.push("/usr/local/bin".to_string()); + path_entries.push("/opt/homebrew/bin".to_string()); + path_entries.push("/usr/bin".to_string()); + path_entries.push("/bin".to_string()); + path_entries.push("/usr/sbin".to_string()); + path_entries.push("/sbin".to_string()); + if let Ok(home) = std::env::var("HOME") { + path_entries.push(format!("{}/.cargo/bin", home)); + path_entries.push(format!("{}/.local/bin", home)); + path_entries.push(format!("{}/.gentoo/usr/bin", home)); + } + if let Ok(existing_path) = std::env::var("PATH") { + path_entries.push(existing_path); + } + cmd_env.insert("PATH".to_string(), path_entries.join(":")); + + // Build execution + let build_script = generate_build_script(&cmd_env, &builder, &args); + let script_path = temp_dir.join("builder.sh"); + std::fs::write(&script_path, &build_script).map_err(Error::IoError)?; + + let mut cmd = Command::new("/bin/bash"); + cmd.arg("-e").arg(&script_path); + cmd.current_dir(&temp_dir); + cmd.envs(&cmd_env); + + let output = cmd.output().map_err(Error::IoError)?; + + if !output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let code = output.status.code().unwrap_or(1); + + eprintln!( + "{} Build command failed with code {}:\n{}\n{}", + "Error:".red().bold(), + code, + stdout, + stderr + ); + + if !options.keep_failed { + for p in output_paths.values() { + let _ = std::fs::remove_dir_all(p); + } + let _ = std::fs::remove_dir_all(&temp_dir); + } + + return Err(Error::UnsupportedExpression { + reason: format!("Derivation {} build failed with exit code {}", name, code), + }); + } + + // Clean up temporary build sandbox + let _ = std::fs::remove_dir_all(&temp_dir); + + // Verify output exists and is populated + let out_dir = Path::new(&out_path); + if !out_dir.exists() { + return Err(Error::UnsupportedExpression { + reason: format!( + "Derivation {} succeeded but output path {} was not created", + name, out_path + ), + }); + } + + let size_str = compute_dir_size_str(out_dir); + println!( + " {} Successfully built {} ({})", + "✓".green(), + name.bold(), + size_str + ); + println!(" Installed to: {}", out_path.blue()); + + // Create result symlink + if let Some(link) = &options.out_link { + create_symlink(out_dir, link)?; + println!( + " {} Symlink created: {} -> {}", + "✓".green(), + link.display(), + out_path + ); + } + + Ok(BuildResult { + drv_name: name, + drv_path, + outputs: output_paths + .into_iter() + .map(|(k, v)| (k, PathBuf::from(v))) + .collect(), + cached: false, + }) + } + + /// Scan environment variables for referenced store paths and ensure they exist + fn resolve_and_build_inputs( + &self, + _evaluator: &crate::eval::Evaluator, + env: &HashMap, + options: &BuildOptions, + ) -> Result<()> { + let store_prefix = self.store.store_dir.to_str().unwrap_or("/nix/store"); + for val in env.values() { + if val.contains(store_prefix) { + // If it references a store path that doesn't exist yet, we check if it's in evaluator + for token in val.split_whitespace() { + if token.starts_with(store_prefix) + && !Path::new(token).exists() + && options.verbose + { + println!("Input store path not found: {}", token); + } + } + } + } + Ok(()) + } +} + +/// Generate default builder shell script covering standard phases +fn generate_build_script( + env: &HashMap, + _builder: &str, + _args: &[String], +) -> String { + let mut script = String::new(); + script.push_str("#!/usr/bin/env bash\nset -e\n\n"); + + // If a custom buildCommand is provided (like crane/cargo-vendor-dir) + if let Some(cmd) = env.get("buildCommand") { + script.push_str("# Custom buildCommand\n"); + script.push_str(cmd); + script.push('\n'); + return script; + } + + // Standard phases + script.push_str(r#" +mkdir -p "$out" + +# Unpack Phase +if [ -n "$unpackPhase" ]; then + eval "$unpackPhase" +elif [ -n "$src" ]; then + if [ -f "$src" ]; then + mkdir -p source + if tar -tf "$src" >/dev/null 2>&1; then + tar -xf "$src" -C source --strip-components=1 2>/dev/null || tar -xf "$src" -C source 2>/dev/null || tar -xf "$src" + if [ -d source ] && [ "$(ls -A source 2>/dev/null)" ]; then + cd source + fi + elif unzip -t "$src" >/dev/null 2>&1; then + unzip -q "$src" -d source 2>/dev/null || unzip -q "$src" + if [ -d source ] && [ "$(ls -A source 2>/dev/null)" ]; then + cd source + fi + else + cp "$src" . + fi + # Only enter single subdirectory if configure/Makefile/etc is NOT in current directory + if [ ! -f "configure" ] && [ ! -f "configure.py" ] && [ ! -f "Makefile" ] && [ ! -f "makefile" ] && [ ! -f "CMakeLists.txt" ] && [ ! -f "Cargo.toml" ]; then + shopt -s nullglob + subdirs=(*/) + if [ ${#subdirs[@]} -eq 1 ] && [ -d "${subdirs[0]}" ]; then + cd "${subdirs[0]}" + fi + fi + fi +fi + +# Patch Phase +if [ -n "$patchPhase" ]; then + eval "$patchPhase" +elif [ -n "$patches" ]; then + for p in $patches; do + patch -p1 < "$p" 2>/dev/null || patch -p0 < "$p" + done +fi + +# Configure Phase +if [ -n "$configurePhase" ]; then + eval "$configurePhase" +elif [ -x "./configure" ]; then + ./configure --prefix="$out" $configureFlags +fi + +# Build Phase +if [ -n "$buildPhase" ]; then + eval "$buildPhase" +elif [ -f "Makefile" ] || [ -f "makefile" ] || [ -f "GNUmakefile" ]; then + make -j"$NIX_BUILD_CORES" $makeFlags +fi + +# Check Phase +if [ "$doCheck" = "1" ] || [ "$doCheck" = "true" ]; then + if [ -n "$checkPhase" ]; then + eval "$checkPhase" + elif [ -f "Makefile" ] || [ -f "makefile" ] || [ -f "GNUmakefile" ]; then + make check $checkFlags 2>/dev/null || make test $checkFlags 2>/dev/null || true + fi +fi + +# Install Phase +if [ -n "$installPhase" ]; then + eval "$installPhase" +elif [ -f "Makefile" ] || [ -f "makefile" ] || [ -f "GNUmakefile" ]; then + make install $installFlags +fi + +# Fixup Phase +if [ -n "$fixupPhase" ]; then + eval "$fixupPhase" +fi +"#); + + script +} + +fn create_symlink(target: &Path, link: &Path) -> std::io::Result<()> { + if link.exists() || link.is_symlink() { + let _ = std::fs::remove_file(link); + } + #[cfg(unix)] + { + std::os::unix::fs::symlink(target, link)?; + } + Ok(()) +} + +fn num_cpus() -> usize { + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4) +} + +fn get_macos_sdk_path() -> std::io::Result { + let output = Command::new("xcrun").args(["--show-sdk-path"]).output()?; + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } else { + Ok("/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk".to_string()) + } +} + +fn compute_dir_size_str(path: &Path) -> String { + fn dir_size(path: &Path) -> u64 { + let mut total: u64 = 0; + if let Ok(entries) = std::fs::read_dir(path) { + for entry in entries.flatten() { + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + total += dir_size(&entry.path()); + } else { + total += metadata.len(); + } + } + } + } + total + } + + let total_bytes = dir_size(path); + if total_bytes > 1024 * 1024 { + format!("{:.1} MB", total_bytes as f64 / (1024.0 * 1024.0)) + } else if total_bytes > 1024 { + format!("{:.1} KB", total_bytes as f64 / 1024.0) + } else { + format!("{} bytes", total_bytes) + } +} diff --git a/rix-eval/src/builtins.rs b/rix-eval/src/builtins.rs index 8027220..902a5e3 100644 --- a/rix-eval/src/builtins.rs +++ b/rix-eval/src/builtins.rs @@ -1134,9 +1134,37 @@ impl Builtin for DerivationBuiltin { outputs.insert("out".to_string(), String::new()); } + let mut input_drvs = Vec::new(); let mut result_attrs_original = HashMap::new(); for (key, value) in attrs.iter() { let key = key.to_string(); + let forced_v = value.clone().force(evaluator)?; + + // Check if this attribute is a derivation or contains derivations + match &forced_v { + NixValue::AttributeSet(m) => { + if let Some(t) = m.get("type") + && let Ok(s) = t.clone().force(evaluator)?.as_string() + && s == "derivation" + { + input_drvs.push(forced_v.clone()); + } + } + NixValue::List(l) => { + for item in l { + if let Ok(forced_item) = item.clone().force(evaluator) + && let NixValue::AttributeSet(m) = &forced_item + && let Some(t) = m.get("type") + && let Ok(s) = t.clone().force(evaluator)?.as_string() + && s == "derivation" + { + input_drvs.push(forced_item.clone()); + } + } + } + _ => {} + } + if key != "name" && key != "system" && key != "builder" @@ -1144,7 +1172,6 @@ impl Builtin for DerivationBuiltin { && key != "outputs" { // All other attributes become environment variables - let forced_v = value.clone().force(evaluator)?; result_attrs_original.insert(key.clone(), forced_v.clone()); let env_value = match forced_v { NixValue::String(s) => s, @@ -1166,26 +1193,17 @@ impl Builtin for DerivationBuiltin { outputs: HashMap::new(), // Will be populated after computing store path }; - // Compute store path and write .drv file - // Note: In a full implementation, we'd also need to: - // - Compute output paths based on the derivation hash - // - Handle input derivations and sources properly - // - Set up the $out environment variable - - // Compute output paths (simplified - in reality these depend on the derivation hash) - // For now, we'll use placeholder paths that would be computed properly - // in a full implementation - for output_name in outputs.keys() { - // In a real implementation, output paths would be computed as: - // /nix/store/-- - // For now, we'll leave them empty as they require proper store path computation + let store = crate::store::Store::new(); + let output_names: Vec = outputs.keys().cloned().collect(); + let (drv_path, output_paths) = + store.compute_derivation_paths(&name, &output_names, &env); + + for (output_name, out_path) in &output_paths { derivation .outputs - .insert(output_name.clone(), String::new()); + .insert(output_name.clone(), out_path.clone()); } - // In a real implementation, we'd compute the actual store path here. - // For now, we'll return an attribute set that looks like a derivation. let mut result_attrs = HashMap::new(); // Copy all original values into the result set @@ -1194,7 +1212,7 @@ impl Builtin for DerivationBuiltin { } // Add required derivation attributes - result_attrs.insert("name".to_string(), NixValue::String(name)); + result_attrs.insert("name".to_string(), NixValue::String(name.clone())); result_attrs.insert("system".to_string(), NixValue::String(system)); result_attrs.insert("builder".to_string(), NixValue::String(builder)); result_attrs.insert( @@ -1206,15 +1224,19 @@ impl Builtin for DerivationBuiltin { NixValue::String("derivation".to_string()), ); - // Add dummy drvPath and outPath (in a real system these would be computed) - result_attrs.insert( - "drvPath".to_string(), - NixValue::String("/nix/store/placeholder.drv".to_string()), - ); - result_attrs.insert( - "outPath".to_string(), - NixValue::String("/nix/store/placeholder".to_string()), - ); + result_attrs.insert("drvPath".to_string(), NixValue::String(drv_path.clone())); + result_attrs.insert("__inputs".to_string(), NixValue::List(input_drvs)); + + let default_out = output_paths + .get("out") + .cloned() + .unwrap_or_else(|| drv_path.clone()); + + result_attrs.insert("outPath".to_string(), NixValue::String(default_out.clone())); + + for (out_name, out_p) in output_paths { + result_attrs.insert(out_name, NixValue::String(out_p)); + } Ok(NixValue::AttributeSet(result_attrs)) } @@ -4145,6 +4167,126 @@ impl FromTOMLBuiltin { } } +/// ImportCargoLock builtin +/// +/// Parses a `Cargo.lock` file and exposes a structured representation of its +/// packages. Git dependencies are enriched with `git`, `rev` and +/// `gitArchiveUrl` attributes so that vendoring build-support code can fetch +/// and lay them out without hardcoding crate-specific logic. +pub struct ImportCargoLockBuiltin; + +impl Builtin for ImportCargoLockBuiltin { + fn name(&self) -> &str { + "importCargoLock" + } + + fn call_with_evaluator(&self, args: &[NixValue], evaluator: &Evaluator) -> Result { + if args.len() != 1 { + return Err(Error::UnsupportedExpression { + reason: format!("importCargoLock takes 1 argument, got {}", args.len()), + }); + } + + let contents = match args[0].clone().force(evaluator)? { + NixValue::String(s) => s, + NixValue::Path(p) => std::fs::read_to_string(&p).map_err(Error::IoError)?, + NixValue::StorePath(p) => std::fs::read_to_string(&p).map_err(Error::IoError)?, + other => { + return Err(Error::UnsupportedExpression { + reason: format!("importCargoLock expects a path or string, got {}", other), + }); + } + }; + + let value: toml::Value = + toml::from_str(&contents).map_err(|e| Error::UnsupportedExpression { + reason: format!("importCargoLock: parse error: {}", e), + })?; + + let packages = value + .get("package") + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_default(); + + let mut out_packages = Vec::new(); + for pkg in packages { + let mut attrs = HashMap::new(); + + let name = pkg + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let version = pkg + .get("version") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + attrs.insert("name".to_string(), NixValue::String(name.clone())); + attrs.insert("version".to_string(), NixValue::String(version.clone())); + + if let Some(checksum) = pkg.get("checksum").and_then(|v| v.as_str()) { + attrs.insert( + "checksum".to_string(), + NixValue::String(checksum.to_string()), + ); + } + + if let Some(source) = pkg.get("source").and_then(|v| v.as_str()) { + attrs.insert("source".to_string(), NixValue::String(source.to_string())); + + if let Some(rest) = source.strip_prefix("git+") { + // `rest` looks like: + // https://host/owner/repo?rev=# + let (before_fragment, fragment) = match rest.split_once('#') { + Some((b, f)) => (b, Some(f)), + None => (rest, None), + }; + let (git_url, query) = match before_fragment.split_once('?') { + Some((g, q)) => (g, Some(q)), + None => (before_fragment, None), + }; + + let mut rev = fragment.map(|s| s.to_string()); + if rev.is_none() + && let Some(q) = query + { + for pair in q.split('&') { + if let Some((k, v)) = pair.split_once('=') + && (k == "rev" || k == "tag" || k == "branch") + { + rev = Some(v.to_string()); + break; + } + } + } + + attrs.insert("git".to_string(), NixValue::String(git_url.to_string())); + let rev = rev.unwrap_or_else(|| "HEAD".to_string()); + attrs.insert("rev".to_string(), NixValue::String(rev.clone())); + attrs.insert( + "gitArchiveUrl".to_string(), + NixValue::String(format!("{}/archive/{}.tar.gz", git_url, rev)), + ); + } + } + + out_packages.push(NixValue::AttributeSet(attrs)); + } + + let mut result = HashMap::new(); + result.insert("packages".to_string(), NixValue::List(out_packages)); + Ok(NixValue::AttributeSet(result)) + } + + fn call(&self, _args: &[NixValue]) -> Result { + Err(Error::UnsupportedExpression { + reason: "importCargoLock requires evaluator context".to_string(), + }) + } +} + /// GetEnv builtin - returns the value of an environment variable pub struct GetEnvBuiltin; @@ -4290,6 +4432,69 @@ fn get_rix_cache_dir(sub: &str) -> std::path::PathBuf { } } +/// Build a blocking HTTP client used for all fetch operations. +fn build_fetch_client() -> Result { + reqwest::blocking::Client::builder() + .use_rustls_tls() + .redirect(reqwest::redirect::Policy::limited(10)) + .user_agent("rix/0.1.0 (Nix-compatible; OpenDarwin)") + .build() + .map_err(|e| Error::EvaluationError { + reason: format!("fetch: failed to build HTTP client: {}", e), + }) +} + +/// Stream an HTTP response body to `dest`, returning the SHA-256 hex digest. +/// +/// Streaming avoids buffering very large tarballs (e.g. LLVM) fully in memory +/// and produces a meaningful error if the connection is interrupted. +fn download_to_file( + client: &reqwest::blocking::Client, + url: &str, + dest: &std::path::Path, +) -> Result { + use sha2::{Digest, Sha256}; + + let mut resp = client.get(url).send().map_err(|e| Error::EvaluationError { + reason: format!("fetch: failed to download '{}': {}", url, e), + })?; + + if !resp.status().is_success() { + return Err(Error::EvaluationError { + reason: format!("fetch: HTTP error {} downloading '{}'", resp.status(), url), + }); + } + + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent).map_err(Error::IoError)?; + } + + let mut tmp = dest.to_path_buf(); + tmp.set_extension("part"); + + let mut file = std::fs::File::create(&tmp).map_err(Error::IoError)?; + let mut hasher = Sha256::new(); + let mut buf = [0u8; 65536]; + loop { + let n = std::io::Read::read(&mut resp, &mut buf).map_err(|e| { + let _ = std::fs::remove_file(&tmp); + Error::EvaluationError { + reason: format!("fetch: failed to read response body from '{}': {}", url, e), + } + })?; + if n == 0 { + break; + } + hasher.update(&buf[..n]); + std::io::Write::write_all(&mut file, &buf[..n]).map_err(Error::IoError)?; + } + std::io::Write::flush(&mut file).map_err(Error::IoError)?; + drop(file); + + std::fs::rename(&tmp, dest).map_err(Error::IoError)?; + Ok(hex::encode(hasher.finalize())) +} + fn clone_shallow_repo( url: &str, ref_name: Option<&str>, @@ -4526,37 +4731,22 @@ impl Builtin for FetchurlBuiltin { let target_file = file_cache.join(&hash_key); if !target_file.exists() { - let client = reqwest::blocking::Client::builder() - .use_rustls_tls() - .build() - .map_err(|e| Error::UnsupportedExpression { - reason: format!("fetchurl: failed to build HTTP client: {}", e), - })?; - let resp = client - .get(&url) - .send() - .map_err(|e| Error::UnsupportedExpression { - reason: format!("fetchurl: failed to download '{}': {}", url, e), - })?; - let bytes = resp.bytes().map_err(|e| Error::UnsupportedExpression { - reason: format!("fetchurl: failed to read response from '{}': {}", url, e), - })?; + let client = build_fetch_client()?; + let actual = download_to_file(&client, &url, &target_file)?; if let Some(expected) = &expected_sha256 { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(&bytes); - let actual = hex::encode(hasher.finalize()); if &actual != expected { - return Err(Error::UnsupportedExpression { + let _ = std::fs::remove_file(&target_file); + return Err(Error::EvaluationError { reason: format!( - "fetchurl: sha256 mismatch for '{}': expected {}, got {}", + "fetchurl: sha256 mismatch for '{}' (expected {}, actual {}) - if the upstream content changed, update the sha256 in the package definition to the actual hash", url, expected, actual ), }); } + } else { + eprintln!("fetchurl: {} -> sha256: {}", url, actual); } - std::fs::write(&target_file, bytes).map_err(Error::IoError)?; } Ok(NixValue::Path(target_file)) @@ -4639,46 +4829,32 @@ impl Builtin for FetchTarballBuiltin { let target_dir = tarball_cache.join(&hash_key); if !target_dir.exists() { - let client = reqwest::blocking::Client::builder() - .use_rustls_tls() - .build() - .map_err(|e| Error::UnsupportedExpression { - reason: format!("fetchTarball: failed to build HTTP client: {}", e), - })?; - let resp = client - .get(&url) - .send() - .map_err(|e| Error::UnsupportedExpression { - reason: format!("fetchTarball: failed to download '{}': {}", url, e), - })?; - let bytes = resp.bytes().map_err(|e| Error::UnsupportedExpression { - reason: format!( - "fetchTarball: failed to read response from '{}': {}", - url, e - ), - })?; + let client = build_fetch_client()?; + let archive_file = tarball_cache.join(format!("{}.tar.gz", hash_key)); + let actual = download_to_file(&client, &url, &archive_file)?; if let Some(expected) = &expected_sha256 { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(&bytes); - let actual = hex::encode(hasher.finalize()); if &actual != expected { - return Err(Error::UnsupportedExpression { + let _ = std::fs::remove_file(&archive_file); + return Err(Error::EvaluationError { reason: format!( - "fetchTarball: sha256 mismatch for '{}': expected {}, got {}", + "fetchTarball: sha256 mismatch for '{}' (expected {}, actual {}) - if the upstream content changed, update the sha256 in the package definition to the actual hash", url, expected, actual ), }); } + } else { + eprintln!("fetchTarball: {} -> sha256: {}", url, actual); } - let tar_gz = flate2::read::GzDecoder::new(&bytes[..]); + let file = std::fs::File::open(&archive_file).map_err(Error::IoError)?; + let tar_gz = flate2::read::GzDecoder::new(file); let mut archive = tar::Archive::new(tar_gz); - let temp_dir = tarball_cache.join(format!("{}.tmp", &hash_key)); + let temp_dir = tarball_cache.join(format!("{hash_key}.tmp")); let _ = std::fs::remove_dir_all(&temp_dir); std::fs::create_dir_all(&temp_dir).map_err(Error::IoError)?; archive.unpack(&temp_dir).map_err(Error::IoError)?; + let _ = std::fs::remove_file(&archive_file); // If the unpacked directory contains a single top-level directory, strip it let entries = std::fs::read_dir(&temp_dir) diff --git a/rix-eval/src/error.rs b/rix-eval/src/error.rs index 60728a8..a44b948 100644 --- a/rix-eval/src/error.rs +++ b/rix-eval/src/error.rs @@ -42,6 +42,12 @@ pub enum Error { #[error("cannot evaluate unsupported expression type: {reason}")] UnsupportedExpression { reason: String }, + /// A builtin function failed at runtime (bad arguments, fetch failures, + /// hash mismatches, etc.). Unlike [`Error::UnsupportedExpression`], the + /// message is presented to the user verbatim without an internal prefix. + #[error("{reason}")] + EvaluationError { reason: String }, + /// Literal value could not be parsed or is unsupported #[error("cannot evaluate unsupported literal: {literal}")] UnsupportedLiteral { literal: String }, diff --git a/rix-eval/src/eval/context.rs b/rix-eval/src/eval/context.rs index 97e7d44..09f8fe5 100644 --- a/rix-eval/src/eval/context.rs +++ b/rix-eval/src/eval/context.rs @@ -141,35 +141,35 @@ impl Drop for VariableScope { } } ScopeLayer::Recursive(mutex) => { - if let Ok(mutex) = Arc::try_unwrap(mutex) { - if let Ok(mut map) = mutex.into_inner() { - for (_, v) in map.drain() { - to_drop.push(v); - } + if let Ok(mutex) = Arc::try_unwrap(mutex) + && let Ok(mut map) = mutex.into_inner() + { + for (_, v) in map.drain() { + to_drop.push(v); } } } } } while let Some(v) = to_drop.pop() { - if let NixValue::Thunk(thunk) = v { - if let Ok(mut thunk_inner) = Arc::try_unwrap(thunk) { - for layer in thunk_inner.closure.layers.drain(..) { - match layer { - ScopeLayer::Lexical(mut map) => { + if let NixValue::Thunk(thunk) = v + && let Ok(mut thunk_inner) = Arc::try_unwrap(thunk) + { + for layer in thunk_inner.closure.layers.drain(..) { + match layer { + ScopeLayer::Lexical(mut map) => { + for (_, v) in map.drain() { + to_drop.push(v); + } + } + ScopeLayer::Recursive(mutex) => { + if let Ok(mutex) = Arc::try_unwrap(mutex) + && let Ok(mut map) = mutex.into_inner() + { for (_, v) in map.drain() { to_drop.push(v); } } - ScopeLayer::Recursive(mutex) => { - if let Ok(mutex) = Arc::try_unwrap(mutex) { - if let Ok(mut map) = mutex.into_inner() { - for (_, v) in map.drain() { - to_drop.push(v); - } - } - } - } } } } diff --git a/rix-eval/src/eval/evaluator.rs b/rix-eval/src/eval/evaluator.rs index 57b501f..6d77dd0 100644 --- a/rix-eval/src/eval/evaluator.rs +++ b/rix-eval/src/eval/evaluator.rs @@ -178,6 +178,7 @@ impl Evaluator { self.register_builtin(Box::new(crate::builtins::GenericClosureBuiltin)); self.register_builtin(Box::new(crate::builtins::DirOfBuiltin)); self.register_builtin(Box::new(crate::builtins::FromTOMLBuiltin)); + self.register_builtin(Box::new(crate::builtins::ImportCargoLockBuiltin)); self.register_builtin(Box::new(crate::builtins::GetEnvBuiltin)); self.register_builtin(Box::new(crate::builtins::HashFileBuiltin)); self.register_builtin(Box::new(crate::builtins::FetchGitBuiltin)); @@ -875,9 +876,25 @@ impl Evaluator { for name in self.builtins.keys() { builtins_attrs.insert(name.clone(), NixValue::Builtin(name.clone())); } + let current_sys = if cfg!(target_os = "macos") { + if cfg!(target_arch = "aarch64") { + "aarch64-darwin" + } else { + "x86_64-darwin" + } + } else if cfg!(target_os = "linux") { + if cfg!(target_arch = "aarch64") { + "aarch64-linux" + } else { + "x86_64-linux" + } + } else { + "unknown-system" + }; + builtins_attrs.insert( "currentSystem".to_string(), - NixValue::String("x86_64-linux".to_string()), + NixValue::String(current_sys.to_string()), ); builtins_attrs.insert( "builtins".to_string(), diff --git a/rix-eval/src/eval/expressions/operators.rs b/rix-eval/src/eval/expressions/operators.rs index 9fef0aa..633c70d 100644 --- a/rix-eval/src/eval/expressions/operators.rs +++ b/rix-eval/src/eval/expressions/operators.rs @@ -452,33 +452,32 @@ impl Evaluator { rhs: &NixValue, is_nested: bool, ) -> Result { - if let (NixValue::Thunk(a), NixValue::Thunk(b)) = (lhs, rhs) { - if std::sync::Arc::ptr_eq(a, b) { - return Ok(true); - } + if let (NixValue::Thunk(a), NixValue::Thunk(b)) = (lhs, rhs) + && std::sync::Arc::ptr_eq(a, b) + { + return Ok(true); } - if let (NixValue::DeferredLookup(a, _), NixValue::DeferredLookup(b, _)) = (lhs, rhs) { - if a == b { - return Ok(true); - } + if let (NixValue::DeferredLookup(a, _), NixValue::DeferredLookup(b, _)) = (lhs, rhs) + && a == b + { + return Ok(true); } let lhs_forced = lhs.clone().force(self)?; let rhs_forced = rhs.clone().force(self)?; - if let (NixValue::Thunk(a), NixValue::Thunk(b)) = (&lhs_forced, &rhs_forced) { - if std::sync::Arc::ptr_eq(a, b) { - return Ok(true); - } + if let (NixValue::Thunk(a), NixValue::Thunk(b)) = (&lhs_forced, &rhs_forced) + && std::sync::Arc::ptr_eq(a, b) + { + return Ok(true); } if let (NixValue::DeferredLookup(a, _), NixValue::DeferredLookup(b, _)) = (&lhs_forced, &rhs_forced) + && a == b { - if a == b { - return Ok(true); - } + return Ok(true); } match (&lhs_forced, &rhs_forced) { @@ -517,12 +516,11 @@ impl Evaluator { } for (key, a_val) in a { if let Some(b_val) = b.get(key) { - if key == "builtins" { - if matches!(a_val, NixValue::DeferredLookup(n, _) if n == "builtins") - || matches!(b_val, NixValue::DeferredLookup(n, _) if n == "builtins") - { - continue; - } + if key == "builtins" + && (matches!(a_val, NixValue::DeferredLookup(n, _) if n == "builtins") + || matches!(b_val, NixValue::DeferredLookup(n, _) if n == "builtins")) + { + continue; } if !self.evaluate_equal_inner(a_val, b_val, true)? { return Ok(false); diff --git a/rix-eval/src/function.rs b/rix-eval/src/function.rs index 2affcd4..771816c 100644 --- a/rix-eval/src/function.rs +++ b/rix-eval/src/function.rs @@ -677,68 +677,64 @@ impl Function { if let Some(builtin_marker) = current_func .closure .get(&format!("__builtin_{}", builtin_name)) + && let NixValue::Builtin(_) = builtin_marker + && let Some(builtin) = evaluator.get_builtin(builtin_name) { - if let NixValue::Builtin(_) = builtin_marker { - if let Some(builtin) = evaluator.get_builtin(builtin_name) { - // Collect args from closure - let mut args = Vec::new(); - if let Some(first_arg) = current_func.closure.get("__curried_first_arg") + // Collect args from closure + let mut args = Vec::new(); + if let Some(first_arg) = current_func.closure.get("__curried_first_arg") { + args.push(first_arg.clone()); + } else { + let arg_count = current_func + .closure + .get("__curried_arg_count") + .and_then(|v| match v { + NixValue::Integer(n) => Some(n as usize), + _ => None, + }) + .unwrap_or(0); + for i in 1..=arg_count { + if let Some(arg) = + current_func.closure.get(&format!("__curried_arg{}", i)) { - args.push(first_arg.clone()); - } else { - let arg_count = current_func - .closure - .get("__curried_arg_count") - .and_then(|v| match v { - NixValue::Integer(n) => Some(n as usize), - _ => None, - }) - .unwrap_or(0); - for i in 1..=arg_count { - if let Some(arg) = - current_func.closure.get(&format!("__curried_arg{}", i)) - { - args.push(arg.clone()); - } - } + args.push(arg.clone()); } - args.push(current_arg.clone()); - - match builtin.call_with_evaluator(&args, evaluator) { - Ok(result) => return Ok(result), - Err(Error::UnsupportedExpression { reason }) - if reason.contains("takes") && reason.contains("arguments") => - { - // Still needs more args - create another curried function - let file_id = evaluator.current_file_id(); - let mut closure = VariableScope::new(); - closure.insert( - format!("__builtin_{}", builtin_name), - NixValue::Builtin(builtin_name.to_string()), - ); - for (i, arg) in args.iter().enumerate() { - closure - .insert(format!("__curried_arg{}", i + 1), arg.clone()); - } - closure.insert( - "__curried_arg_count".to_string(), - NixValue::Integer(args.len() as i64), - ); - let next_curried = Function::new_curried_builtin_internal( - Parameter::Simple(format!( - "__curried_{}_arg{}", - builtin_name, - args.len() + 1 - )), - format!("__curried_builtin_call:{}", builtin_name), - closure, - file_id, - ); - return Ok(NixValue::Function(Arc::new(next_curried))); - } - Err(e) => return Err(e), + } + } + args.push(current_arg.clone()); + + match builtin.call_with_evaluator(&args, evaluator) { + Ok(result) => return Ok(result), + Err(Error::UnsupportedExpression { reason }) + if reason.contains("takes") && reason.contains("arguments") => + { + // Still needs more args - create another curried function + let file_id = evaluator.current_file_id(); + let mut closure = VariableScope::new(); + closure.insert( + format!("__builtin_{}", builtin_name), + NixValue::Builtin(builtin_name.to_string()), + ); + for (i, arg) in args.iter().enumerate() { + closure.insert(format!("__curried_arg{}", i + 1), arg.clone()); } + closure.insert( + "__curried_arg_count".to_string(), + NixValue::Integer(args.len() as i64), + ); + let next_curried = Function::new_curried_builtin_internal( + Parameter::Simple(format!( + "__curried_{}_arg{}", + builtin_name, + args.len() + 1 + )), + format!("__curried_builtin_call:{}", builtin_name), + closure, + file_id, + ); + return Ok(NixValue::Function(Arc::new(next_curried))); } + Err(e) => return Err(e), } } return Err(Error::UnsupportedExpression { diff --git a/rix-eval/src/lib.rs b/rix-eval/src/lib.rs index 5e8f84b..3178fa8 100644 --- a/rix-eval/src/lib.rs +++ b/rix-eval/src/lib.rs @@ -2,20 +2,24 @@ //! //! A pure Rust library for evaluating Nix expressions. +pub mod builder; mod builtin; mod builtins; mod error; mod eval; mod function; pub mod prelude; +pub mod store; mod thunk; mod value; mod xml; // Re-export public API +pub use builder::{BuildOptions, BuildResult, Builder}; pub use builtin::Builtin; pub use error::{Error, Result}; pub use eval::{EvaluationContext, Evaluator, VariableScope}; pub use function::Function; +pub use store::Store; pub use thunk::Thunk; pub use value::{Derivation, NixValue}; diff --git a/rix-eval/src/store.rs b/rix-eval/src/store.rs new file mode 100644 index 0000000..0d60bc2 --- /dev/null +++ b/rix-eval/src/store.rs @@ -0,0 +1,169 @@ +//! Nix store path calculations, receipts, and management + +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +const NIX_BASE32_CHARS: &[u8; 32] = b"0123456789abcdfghijklmnpqrsvwxyz"; + +/// Nix base32 encoding (little-endian, 5 bits per character) +pub fn nix_base32_encode(bytes: &[u8]) -> String { + let len = 32; + let mut result = String::with_capacity(len); + for n in (0..len).rev() { + let b = n * 5; + let i = b / 8; + let o = b % 8; + let mut v = if i < bytes.len() { + (bytes[i] >> o) as u32 + } else { + 0 + }; + if i + 1 < bytes.len() { + v |= (bytes[i + 1] as u32) << (8 - o); + } + let c = (v & 0x1f) as usize; + result.push(NIX_BASE32_CHARS[c] as char); + } + result +} + +/// Compute a 32-character Nix store hash for arbitrary input bytes +pub fn compute_store_hash(data: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(data); + let hash = hasher.finalize(); + nix_base32_encode(&hash) +} + +/// Nix Store manager +#[derive(Debug, Clone)] +pub struct Store { + pub store_dir: PathBuf, +} + +impl Default for Store { + fn default() -> Self { + Self::new() + } +} + +impl Store { + /// Initialize the Nix store, determining the active store directory + pub fn new() -> Self { + let store_dir = if let Ok(dir) = std::env::var("NIX_STORE_DIR") { + PathBuf::from(dir) + } else if Path::new("/nix/store").is_dir() && is_dir_writable(Path::new("/nix/store")) { + PathBuf::from("/nix/store") + } else { + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(home).join(".rix").join("store") + }; + + Self { store_dir } + } + + /// Ensure the store directory exists + pub fn ensure_store_dir(&self) -> std::io::Result<()> { + std::fs::create_dir_all(&self.store_dir) + } + + /// Compute a store path for a named derivation or file + pub fn compute_store_path(&self, name: &str, content: &[u8], is_drv: bool) -> (String, String) { + let hash = compute_store_hash(content); + let ext = if is_drv { ".drv" } else { "" }; + let store_path = self + .store_dir + .join(format!("{}-{}{}", hash, name, ext)) + .to_string_lossy() + .to_string(); + (store_path, hash) + } + + /// Compute derivation store path and its output paths + pub fn compute_derivation_paths( + &self, + name: &str, + outputs: &[String], + env: &HashMap, + ) -> (String, HashMap) { + // Collect sorted environment entries to produce a deterministic hash + let mut sorted_env: Vec<(&String, &String)> = env.iter().collect(); + sorted_env.sort_by_key(|(k, _)| *k); + + let mut hash_data = format!("name={};outputs={:?};", name, outputs); + for (k, v) in sorted_env { + hash_data.push_str(k); + hash_data.push('='); + hash_data.push_str(v); + hash_data.push(';'); + } + + let drv_hash = compute_store_hash(hash_data.as_bytes()); + let drv_path = self + .store_dir + .join(format!("{}-{}.drv", drv_hash, name)) + .to_string_lossy() + .to_string(); + + let mut output_paths = HashMap::new(); + for output in outputs { + let out_data = format!("drv={};out={};name={}", drv_hash, output, name); + let out_hash = compute_store_hash(out_data.as_bytes()); + let out_suffix = if output == "out" { + name.to_string() + } else { + format!("{}-{}", name, output) + }; + let out_path = self + .store_dir + .join(format!("{}-{}", out_hash, out_suffix)) + .to_string_lossy() + .to_string(); + output_paths.insert(output.clone(), out_path); + } + + (drv_path, output_paths) + } + + /// Check if a store path already exists and is populated + pub fn is_valid_path(&self, path: &Path) -> bool { + if !path.exists() { + return false; + } + if path.is_file() { + return true; + } + if path.is_dir() + && let Ok(entries) = std::fs::read_dir(path) + { + return entries.count() > 0; + } + false + } +} + +fn is_dir_writable(path: &Path) -> bool { + let test_file = path.join(".writable_test"); + if std::fs::write(&test_file, b"test").is_ok() { + let _ = std::fs::remove_file(test_file); + true + } else { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nix_base32_hash() { + let data = b"hello world"; + let hash = compute_store_hash(data); + assert_eq!(hash.len(), 32); + for c in hash.chars() { + assert!(NIX_BASE32_CHARS.contains(&(c as u8))); + } + } +} diff --git a/rix-eval/src/value/display.rs b/rix-eval/src/value/display.rs index 599c857..3855bdb 100644 --- a/rix-eval/src/value/display.rs +++ b/rix-eval/src/value/display.rs @@ -7,7 +7,7 @@ use std::sync::Arc; thread_local! { /// Recursion depth guard for Display to prevent stack overflow on recursive structures - static DISPLAY_DEPTH: Cell = Cell::new(0); + static DISPLAY_DEPTH: Cell = const { Cell::new(0) }; } const MAX_DISPLAY_DEPTH: usize = 20; @@ -44,7 +44,7 @@ fn format_nix_float(f: f64) -> String { // Nix/C sprintf("%.6g") format: // Uses scientific notation if exponent is < -4 or >= precision (6). // Otherwise uses standard decimal notation with up to 6 significant digits. - if abs >= 1e-4 && abs < 1e6 { + if (1e-4..1e6).contains(&abs) { // Find how many decimal digits needed for up to 6 significant digits let magnitude = f.abs().log10().floor() as i32; let decimals = (5 - magnitude).max(0) as usize;