From 216930027dfc4a76c317d50d1ab307dff3fd0bea Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Sun, 1 Feb 2026 11:14:38 +0800 Subject: [PATCH] refactor: move ops to runtime/ops.rs --- nix-js/src/context.rs | 3 +- nix-js/src/fetcher.rs | 53 ++-- nix-js/src/runtime.rs | 640 +------------------------------------ nix-js/src/runtime/ops.rs | 641 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 686 insertions(+), 651 deletions(-) create mode 100644 nix-js/src/runtime/ops.rs diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index dc0eb2b..7d67c7e 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -30,8 +30,7 @@ macro_rules! eval { let code = self.compile(source)?; tracing::debug!("Executing JavaScript"); - self.runtime - .eval(format!($wrapper, code), &mut self.ctx) + self.runtime.eval(format!($wrapper, code), &mut self.ctx) } }; } diff --git a/nix-js/src/fetcher.rs b/nix-js/src/fetcher.rs index 54ffecf..345c324 100644 --- a/nix-js/src/fetcher.rs +++ b/nix-js/src/fetcher.rs @@ -18,7 +18,7 @@ pub use download::Downloader; pub use metadata_cache::MetadataCache; use crate::nar; -use crate::runtime::NixError; +use crate::runtime::NixRuntimeError; #[derive(Serialize)] pub struct FetchUrlResult { @@ -62,14 +62,15 @@ pub fn op_fetch_url( #[string] expected_hash: Option, #[string] name: Option, executable: bool, -) -> Result { +) -> Result { let _span = tracing::info_span!("op_fetch_url", url = %url).entered(); info!("fetchurl started"); let file_name = name.unwrap_or_else(|| url.rsplit('/').next().unwrap_or("download").to_string()); - let metadata_cache = MetadataCache::new(3600).map_err(|e| NixError::from(e.to_string()))?; + let metadata_cache = + MetadataCache::new(3600).map_err(|e| NixRuntimeError::from(e.to_string()))?; let input = serde_json::json!({ "type": "file", @@ -80,7 +81,7 @@ pub fn op_fetch_url( if let Some(cached_entry) = metadata_cache .lookup(&input) - .map_err(|e| NixError::from(e.to_string()))? + .map_err(|e| NixRuntimeError::from(e.to_string()))? { let cached_hash = cached_entry .info @@ -112,7 +113,7 @@ pub fn op_fetch_url( let downloader = Downloader::new(); let data = downloader .download(&url) - .map_err(|e| NixError::from(e.to_string()))?; + .map_err(|e| NixRuntimeError::from(e.to_string()))?; info!(bytes = data.len(), "Download complete"); @@ -121,7 +122,7 @@ pub fn op_fetch_url( if let Some(ref expected) = expected_hash { let normalized_expected = normalize_hash(expected); if hash != normalized_expected { - return Err(NixError::from(format!( + return Err(NixRuntimeError::from(format!( "hash mismatch for '{}': expected {}, got {}", url, normalized_expected, hash ))); @@ -132,7 +133,7 @@ pub fn op_fetch_url( let store = ctx.get_store(); let store_path = store .add_to_store(&file_name, &data, false, vec![]) - .map_err(|e| NixError::from(e.to_string()))?; + .map_err(|e| NixRuntimeError::from(e.to_string()))?; info!(store_path = %store_path, "Added to store"); @@ -153,7 +154,7 @@ pub fn op_fetch_url( metadata_cache .add(&input, &info, &store_path, true) - .map_err(|e| NixError::from(e.to_string()))?; + .map_err(|e| NixRuntimeError::from(e.to_string()))?; Ok(FetchUrlResult { store_path, hash }) } @@ -166,12 +167,13 @@ pub fn op_fetch_tarball( #[string] expected_hash: Option, #[string] expected_nar_hash: Option, #[string] name: Option, -) -> Result { +) -> Result { let _span = tracing::info_span!("op_fetch_tarball", url = %url).entered(); info!("fetchTarball started"); let dir_name = name.unwrap_or_else(|| "source".to_string()); - let metadata_cache = MetadataCache::new(3600).map_err(|e| NixError::from(e.to_string()))?; + let metadata_cache = + MetadataCache::new(3600).map_err(|e| NixRuntimeError::from(e.to_string()))?; let input = serde_json::json!({ "type": "tarball", @@ -181,7 +183,7 @@ pub fn op_fetch_tarball( if let Some(cached_entry) = metadata_cache .lookup(&input) - .map_err(|e| NixError::from(e.to_string()))? + .map_err(|e| NixRuntimeError::from(e.to_string()))? { let cached_nar_hash = cached_entry .info @@ -218,7 +220,7 @@ pub fn op_fetch_tarball( let downloader = Downloader::new(); let data = downloader .download(&url) - .map_err(|e| NixError::from(e.to_string()))?; + .map_err(|e| NixRuntimeError::from(e.to_string()))?; info!(bytes = data.len(), "Download complete"); @@ -227,7 +229,7 @@ pub fn op_fetch_tarball( if let Some(ref expected) = expected_hash { let normalized_expected = normalize_hash(expected); if tarball_hash != normalized_expected { - return Err(NixError::from(format!( + return Err(NixRuntimeError::from(format!( "Tarball hash mismatch for '{}': expected {}, got {}", url, normalized_expected, tarball_hash ))); @@ -235,14 +237,14 @@ pub fn op_fetch_tarball( } info!("Extracting tarball"); - let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?; + let cache = FetcherCache::new().map_err(|e| NixRuntimeError::from(e.to_string()))?; let (extracted_path, _temp_dir) = cache .extract_tarball_to_temp(&data) - .map_err(|e| NixError::from(e.to_string()))?; + .map_err(|e| NixRuntimeError::from(e.to_string()))?; info!("Computing NAR hash"); let nar_hash = - nar::compute_nar_hash(&extracted_path).map_err(|e| NixError::from(e.to_string()))?; + nar::compute_nar_hash(&extracted_path).map_err(|e| NixRuntimeError::from(e.to_string()))?; debug!( tarball_hash = %tarball_hash, @@ -253,7 +255,7 @@ pub fn op_fetch_tarball( if let Some(ref expected) = expected_nar_hash { let normalized_expected = normalize_hash(expected); if nar_hash != normalized_expected { - return Err(NixError::from(format!( + return Err(NixRuntimeError::from(format!( "NAR hash mismatch for '{}': expected {}, got {}", url, normalized_expected, nar_hash ))); @@ -265,7 +267,7 @@ pub fn op_fetch_tarball( let store = ctx.get_store(); let store_path = store .add_to_store_from_path(&dir_name, &extracted_path, vec![]) - .map_err(|e| NixError::from(e.to_string()))?; + .map_err(|e| NixRuntimeError::from(e.to_string()))?; info!(store_path = %store_path, "Added to store"); @@ -278,7 +280,7 @@ pub fn op_fetch_tarball( let immutable = expected_nar_hash.is_some(); metadata_cache .add(&input, &info, &store_path, immutable) - .map_err(|e| NixError::from(e.to_string()))?; + .map_err(|e| NixRuntimeError::from(e.to_string()))?; Ok(FetchTarballResult { store_path, @@ -298,10 +300,10 @@ pub fn op_fetch_git( submodules: bool, all_refs: bool, #[string] name: Option, -) -> Result { +) -> Result { let _span = tracing::info_span!("op_fetch_git", url = %url).entered(); info!("fetchGit started"); - let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?; + let cache = FetcherCache::new().map_err(|e| NixRuntimeError::from(e.to_string()))?; let dir_name = name.unwrap_or_else(|| "source".to_string()); let ctx: &Ctx = state.get_ctx(); @@ -318,7 +320,7 @@ pub fn op_fetch_git( all_refs, &dir_name, ) - .map_err(|e| NixError::from(e.to_string())) + .map_err(|e| NixRuntimeError::from(e.to_string())) } #[op2] @@ -327,11 +329,12 @@ pub fn op_fetch_hg( #[string] url: String, #[string] rev: Option, #[string] name: Option, -) -> Result { - let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?; +) -> Result { + let cache = FetcherCache::new().map_err(|e| NixRuntimeError::from(e.to_string()))?; let dir_name = name.unwrap_or_else(|| "source".to_string()); - hg::fetch_hg(&cache, &url, rev.as_deref(), &dir_name).map_err(|e| NixError::from(e.to_string())) + hg::fetch_hg(&cache, &url, rev.as_deref(), &dir_name) + .map_err(|e| NixRuntimeError::from(e.to_string())) } fn normalize_hash(hash: &str) -> String { diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 8bd243d..3ea78d5 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -1,15 +1,16 @@ use std::borrow::Cow; use std::marker::PhantomData; -use std::path::{Component, Path, PathBuf}; -use std::sync::{Arc, Once}; +use std::path::Path; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8}; -use rust_embed::Embed; use crate::error::{Error, Result, Source}; use crate::store::Store; use crate::value::{AttrSet, List, Symbol, Value}; +mod ops; +use ops::*; + type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>; type LocalValue<'a> = v8::Local<'a, v8::Value>; type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>; @@ -87,633 +88,20 @@ mod private { } impl std::error::Error for SimpleErrorWrapper {} - js_error_wrapper!(SimpleErrorWrapper, NixError, "Error"); + js_error_wrapper!(SimpleErrorWrapper, NixRuntimeError, "Error"); - impl From for NixError { + impl From for NixRuntimeError { fn from(value: String) -> Self { - NixError(SimpleErrorWrapper(value)) + NixRuntimeError(SimpleErrorWrapper(value)) } } - impl From<&str> for NixError { + impl From<&str> for NixRuntimeError { fn from(value: &str) -> Self { - NixError(SimpleErrorWrapper(value.to_string())) + NixRuntimeError(SimpleErrorWrapper(value.to_string())) } } } -pub(crate) use private::NixError; - -#[derive(Embed)] -#[folder = "src/runtime/corepkgs"] -pub(crate) struct CorePkgs; - -#[deno_core::op2] -#[string] -fn op_import( - state: &mut OpState, - #[string] path: String, -) -> std::result::Result { - let _span = tracing::info_span!("op_import", path = %path).entered(); - let ctx: &mut Ctx = state.get_ctx_mut(); - - // FIXME: special path type - if path.starts_with("") { - let corepkg_name = &path[5..path.len() - 1]; - if let Some(file) = CorePkgs::get(corepkg_name) { - tracing::info!("Importing corepkg: {}", corepkg_name); - let source = Source { - ty: crate::error::SourceType::Eval(Arc::new(ctx.get_current_dir().to_path_buf())), - src: str::from_utf8(&file.data) - .expect("corrupted corepkgs file") - .into(), - }; - ctx.add_source(source.clone()); - return Ok(ctx.compile_code(source).map_err(|err| err.to_string())?); - } else { - return Err(format!("Corepkg not found: {}", corepkg_name).into()); - } - } - - let current_dir = ctx.get_current_dir(); - let mut absolute_path = current_dir.join(&path); - // Do NOT resolve symlinks (eval-okay-symlink-resolution) - // TODO: is this correct? - // .canonicalize() - // .map_err(|e| format!("Failed to resolve path {}: {}", path, e))?; - if absolute_path.is_dir() { - absolute_path.push("default.nix") - } - - tracing::info!("Importing file: {}", absolute_path.display()); - - let source = Source::new_file(absolute_path.clone()) - .map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?; - - tracing::debug!("Compiling file"); - ctx.add_source(source.clone()); - - Ok(ctx.compile_code(source).map_err(|err| err.to_string())?) -} - -#[deno_core::op2] -#[string] -fn op_read_file(#[string] path: String) -> std::result::Result { - Ok(std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {}", path, e))?) -} - -#[deno_core::op2(fast)] -fn op_path_exists(#[string] path: String) -> bool { - let must_be_dir = path.ends_with('/') || path.ends_with("/."); - let p = Path::new(&path); - - if must_be_dir { - match std::fs::metadata(p) { - Ok(m) => m.is_dir(), - Err(_) => false, - } - } else { - std::fs::symlink_metadata(p).is_ok() - } -} - -#[deno_core::op2] -#[string] -fn op_read_file_type(#[string] path: String) -> std::result::Result { - let path = Path::new(&path); - let metadata = std::fs::symlink_metadata(path) - .map_err(|e| format!("Failed to read file type for {}: {}", path.display(), e))?; - - let file_type = metadata.file_type(); - let type_str = if file_type.is_dir() { - "directory" - } else if file_type.is_symlink() { - "symlink" - } else if file_type.is_file() { - "regular" - } else { - "unknown" - }; - - Ok(type_str.to_string()) -} - -#[deno_core::op2] -#[serde] -fn op_read_dir( - #[string] path: String, -) -> std::result::Result, NixError> { - let path = Path::new(&path); - - if !path.is_dir() { - return Err(format!("{} is not a directory", path.display()).into()); - } - - let entries = std::fs::read_dir(path) - .map_err(|e| format!("Failed to read directory {}: {}", path.display(), e))?; - - let mut result = std::collections::HashMap::new(); - - for entry in entries { - let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; - let file_name = entry.file_name().to_string_lossy().to_string(); - - let file_type = entry.file_type().map_err(|e| { - format!( - "Failed to read file type for {}: {}", - entry.path().display(), - e - ) - })?; - - let type_str = if file_type.is_dir() { - "directory" - } else if file_type.is_symlink() { - "symlink" - } else if file_type.is_file() { - "regular" - } else { - "unknown" - }; - - result.insert(file_name, type_str.to_string()); - } - - Ok(result) -} - -#[deno_core::op2] -#[string] -fn op_resolve_path( - #[string] current_dir: String, - #[string] path: String, -) -> std::result::Result { - let _span = tracing::debug_span!("op_resolve_path").entered(); - tracing::debug!(current_dir, path); - - // If already absolute, return as-is - if path.starts_with('/') { - return Ok(path); - } - // Resolve relative path against current file directory (or CWD) - let current_dir = if !path.starts_with("~/") { - let mut dir = PathBuf::from(current_dir); - dir.push(path); - dir - } else { - let mut dir = std::env::home_dir().ok_or("home dir not defined")?; - dir.push(&path[2..]); - dir - }; - let mut normalized = PathBuf::new(); - for component in current_dir.components() { - match component { - Component::Prefix(p) => normalized.push(p.as_os_str()), - Component::RootDir => normalized.push("/"), - Component::CurDir => {} - Component::ParentDir => { - normalized.pop(); - } - Component::Normal(c) => normalized.push(c), - } - } - tracing::debug!(normalized = normalized.display().to_string()); - Ok(normalized.to_string_lossy().to_string()) -} - -#[deno_core::op2] -#[string] -fn op_sha256_hex(#[string] data: String) -> String { - crate::nix_hash::sha256_hex(&data) -} - -#[deno_core::op2] -#[string] -fn op_make_placeholder(#[string] output: String) -> String { - use sha2::{Digest, Sha256}; - let input = format!("nix-output:{}", output); - let mut hasher = Sha256::new(); - hasher.update(input.as_bytes()); - let hash: [u8; 32] = hasher.finalize().into(); - let encoded = crate::nix_hash::nix_base32_encode(&hash); - format!("/{}", encoded) -} - -#[deno_core::op2] -#[serde] -fn op_decode_span( - state: &mut OpState, - #[string] span_str: String, -) -> std::result::Result { - let parts: Vec<&str> = span_str.split(':').collect(); - if parts.len() != 3 { - return Ok(serde_json::json!({ - "file": serde_json::Value::Null, - "line": serde_json::Value::Null, - "column": serde_json::Value::Null - })); - } - - let source_id: usize = parts[0].parse().map_err(|_| "Invalid source ID")?; - let start: u32 = parts[1].parse().map_err(|_| "Invalid start offset")?; - - let ctx: &Ctx = state.get_ctx(); - let source = ctx.get_source(source_id); - let content = &source.src; - - let (line, column) = byte_offset_to_line_col(content, start as usize); - - Ok(serde_json::json!({ - "file": source.get_name(), - "line": line, - "column": column - })) -} - -fn byte_offset_to_line_col(content: &str, offset: usize) -> (u32, u32) { - let mut line = 1u32; - let mut col = 1u32; - - for (idx, ch) in content.char_indices() { - if idx >= offset { - break; - } - if ch == '\n' { - line += 1; - col = 1; - } else { - col += 1; - } - } - - (line, col) -} - -#[deno_core::op2] -#[string] -fn op_make_store_path( - state: &mut OpState, - #[string] ty: String, - #[string] hash_hex: String, - #[string] name: String, -) -> String { - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - let store_dir = store.get_store_dir(); - crate::nix_hash::make_store_path(store_dir, &ty, &hash_hex, &name) -} - -#[deno_core::op2] -#[string] -fn op_make_text_store_path( - state: &mut OpState, - #[string] hash_hex: String, - #[string] name: String, - #[serde] references: Vec, -) -> String { - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - let store_dir = store.get_store_dir(); - crate::nix_hash::make_text_store_path(store_dir, &hash_hex, &name, &references) -} - -#[deno_core::op2] -#[string] -fn op_output_path_name(#[string] drv_name: String, #[string] output_name: String) -> String { - crate::nix_hash::output_path_name(&drv_name, &output_name) -} - -#[deno_core::op2] -#[string] -fn op_make_fixed_output_path( - state: &mut OpState, - #[string] hash_algo: String, - #[string] hash: String, - #[string] hash_mode: String, - #[string] name: String, -) -> String { - use sha2::{Digest, Sha256}; - - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - let store_dir = store.get_store_dir(); - - if hash_algo == "sha256" && hash_mode == "recursive" { - crate::nix_hash::make_store_path(store_dir, "source", &hash, &name) - } else { - let prefix = if hash_mode == "recursive" { "r:" } else { "" }; - let inner_input = format!("fixed:out:{}{}:{}:", prefix, hash_algo, hash); - let mut hasher = Sha256::new(); - hasher.update(inner_input.as_bytes()); - let inner_hash = hex::encode(hasher.finalize()); - - crate::nix_hash::make_store_path(store_dir, "output:out", &inner_hash, &name) - } -} - -#[deno_core::op2] -#[string] -fn op_add_path( - state: &mut OpState, - #[string] path: String, - #[string] name: Option, - recursive: bool, - #[string] sha256: Option, -) -> std::result::Result { - use sha2::{Digest, Sha256}; - use std::fs; - use std::path::Path; - - let path_obj = Path::new(&path); - - if !path_obj.exists() { - return Err(NixError::from(format!("path '{}' does not exist", path))); - } - - let computed_name = name.unwrap_or_else(|| { - path_obj - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("source") - .to_string() - }); - - let computed_hash = if recursive { - crate::nar::compute_nar_hash(path_obj) - .map_err(|e| NixError::from(format!("failed to compute NAR hash: {}", e)))? - } else { - if !path_obj.is_file() { - return Err(NixError::from( - "when 'recursive' is false, path must be a regular file", - )); - } - let contents = fs::read(path_obj) - .map_err(|e| NixError::from(format!("failed to read '{}': {}", path, e)))?; - - let mut hasher = Sha256::new(); - hasher.update(&contents); - hex::encode(hasher.finalize()) - }; - - if let Some(expected_hash) = sha256 { - let expected_hex = crate::nix_hash::decode_hash_to_hex(&expected_hash) - .ok_or_else(|| NixError::from(format!("invalid hash format: {}", expected_hash)))?; - if computed_hash != expected_hex { - return Err(NixError::from(format!( - "hash mismatch for path '{}': expected {}, got {}", - path, expected_hex, computed_hash - ))); - } - } - - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - - let store_path = if recursive { - store - .add_to_store_from_path(&computed_name, path_obj, vec![]) - .map_err(|e| NixError::from(format!("failed to add path to store: {}", e)))? - } else { - let contents = fs::read(path_obj) - .map_err(|e| NixError::from(format!("failed to read '{}': {}", path, e)))?; - store - .add_to_store(&computed_name, &contents, false, vec![]) - .map_err(|e| NixError::from(format!("failed to add to store: {}", e)))? - }; - - Ok(store_path) -} - -#[deno_core::op2] -#[string] -fn op_store_path( - state: &mut OpState, - #[string] path: String, -) -> std::result::Result { - use crate::store::validate_store_path; - - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - let store_dir = store.get_store_dir(); - - validate_store_path(store_dir, &path).map_err(|e| NixError::from(e.to_string()))?; - - store - .ensure_path(&path) - .map_err(|e| NixError::from(e.to_string()))?; - - Ok(path) -} - -#[deno_core::op2] -#[string] -fn op_to_file( - state: &mut OpState, - #[string] name: String, - #[string] contents: String, - #[serde] references: Vec, -) -> std::result::Result { - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - let store_path = store - .add_text_to_store(&name, &contents, references) - .map_err(|e| NixError::from(format!("builtins.toFile failed: {}", e)))?; - - Ok(store_path) -} - -#[deno_core::op2] -#[string] -fn op_copy_path_to_store( - state: &mut OpState, - #[string] path: String, -) -> std::result::Result { - use std::path::Path; - - let path_obj = Path::new(&path); - - if !path_obj.exists() { - return Err(NixError::from(format!("path '{}' does not exist", path))); - } - - let name = path_obj - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("source") - .to_string(); - - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - let store_path = store - .add_to_store_from_path(&name, path_obj, vec![]) - .map_err(|e| NixError::from(format!("failed to copy path to store: {}", e)))?; - - Ok(store_path) -} - -#[deno_core::op2] -#[string] -fn op_get_env(#[string] key: String) -> std::result::Result { - match std::env::var(key) { - Ok(val) => Ok(val), - Err(std::env::VarError::NotPresent) => Ok("".into()), - Err(err) => Err(format!("Failed to read env var: {err}").into()), - } -} - -#[deno_core::op2] -#[serde] -fn op_walk_dir(#[string] path: String) -> std::result::Result, NixError> { - fn walk_recursive( - base: &Path, - current: &Path, - results: &mut Vec<(String, String)>, - ) -> std::result::Result<(), NixError> { - let entries = std::fs::read_dir(current) - .map_err(|e| NixError::from(format!("failed to read directory: {}", e)))?; - - for entry in entries { - let entry = - entry.map_err(|e| NixError::from(format!("failed to read entry: {}", e)))?; - let path = entry.path(); - let rel_path = path - .strip_prefix(base) - .map_err(|e| NixError::from(format!("failed to get relative path: {}", e)))? - .to_string_lossy() - .to_string(); - - let file_type = entry - .file_type() - .map_err(|e| NixError::from(format!("failed to get file type: {}", e)))?; - - let type_str = if file_type.is_dir() { - "directory" - } else if file_type.is_symlink() { - "symlink" - } else { - "regular" - }; - - results.push((rel_path.clone(), type_str.to_string())); - - if file_type.is_dir() { - walk_recursive(base, &path, results)?; - } - } - Ok(()) - } - - let path = Path::new(&path); - if !path.is_dir() { - return Err(NixError::from(format!( - "{} is not a directory", - path.display() - ))); - } - - let mut results = Vec::new(); - walk_recursive(path, path, &mut results)?; - Ok(results) -} - -#[deno_core::op2] -#[string] -fn op_add_filtered_path( - state: &mut OpState, - #[string] src_path: String, - #[string] name: Option, - recursive: bool, - #[string] sha256: Option, - #[serde] include_paths: Vec, -) -> std::result::Result { - use sha2::{Digest, Sha256}; - use std::fs; - - let src = Path::new(&src_path); - if !src.exists() { - return Err(NixError::from(format!( - "path '{}' does not exist", - src_path - ))); - } - - let computed_name = name.unwrap_or_else(|| { - src.file_name() - .and_then(|n| n.to_str()) - .unwrap_or("source") - .to_string() - }); - - let temp_dir = tempfile::tempdir() - .map_err(|e| NixError::from(format!("failed to create temp dir: {}", e)))?; - let dest = temp_dir.path().join(&computed_name); - - fs::create_dir_all(&dest) - .map_err(|e| NixError::from(format!("failed to create dest dir: {}", e)))?; - - for rel_path in &include_paths { - let src_file = src.join(rel_path); - let dest_file = dest.join(rel_path); - - if let Some(parent) = dest_file.parent() { - fs::create_dir_all(parent) - .map_err(|e| NixError::from(format!("failed to create dir: {}", e)))?; - } - - let metadata = fs::symlink_metadata(&src_file) - .map_err(|e| NixError::from(format!("failed to read metadata: {}", e)))?; - - if metadata.is_symlink() { - let target = fs::read_link(&src_file) - .map_err(|e| NixError::from(format!("failed to read symlink: {}", e)))?; - #[cfg(unix)] - std::os::unix::fs::symlink(&target, &dest_file) - .map_err(|e| NixError::from(format!("failed to create symlink: {}", e)))?; - #[cfg(not(unix))] - return Err(NixError::from("symlinks not supported on this platform")); - } else if metadata.is_dir() { - fs::create_dir_all(&dest_file) - .map_err(|e| NixError::from(format!("failed to create dir: {}", e)))?; - } else { - fs::copy(&src_file, &dest_file) - .map_err(|e| NixError::from(format!("failed to copy file: {}", e)))?; - } - } - - let computed_hash = if recursive { - crate::nar::compute_nar_hash(&dest) - .map_err(|e| NixError::from(format!("failed to compute NAR hash: {}", e)))? - } else { - if !dest.is_file() { - return Err(NixError::from( - "when 'recursive' is false, path must be a regular file", - )); - } - let contents = - fs::read(&dest).map_err(|e| NixError::from(format!("failed to read file: {}", e)))?; - let mut hasher = Sha256::new(); - hasher.update(&contents); - hex::encode(hasher.finalize()) - }; - - if let Some(expected_hash) = sha256 { - let expected_hex = crate::nix_hash::decode_hash_to_hex(&expected_hash) - .ok_or_else(|| NixError::from(format!("invalid hash format: {}", expected_hash)))?; - if computed_hash != expected_hex { - return Err(NixError::from(format!( - "hash mismatch for path '{}': expected {}, got {}", - src_path, expected_hex, computed_hash - ))); - } - } - - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - - let store_path = store - .add_to_store_from_path(&computed_name, &dest, vec![]) - .map_err(|e| NixError::from(format!("failed to add path to store: {}", e)))?; - - Ok(store_path) -} +pub(crate) use private::NixRuntimeError; pub(crate) struct Runtime { js_runtime: JsRuntime, @@ -727,11 +115,16 @@ pub(crate) struct Runtime { impl Runtime { pub(crate) fn new() -> Result { + use std::sync::Once; + // Initialize V8 once static INIT: Once = Once::new(); INIT.call_once(|| { // First flag is always not recognized - assert_eq!(deno_core::v8_set_flags(vec!["".into(), format!("--stack-size={}", 8 * 1024)]), [""]); + assert_eq!( + deno_core::v8_set_flags(vec!["".into(), format!("--stack-size={}", 8 * 1024)]), + [""] + ); JsRuntime::init_platform( Some(v8::new_default_platform(0, false).make_shared()), false, @@ -773,7 +166,6 @@ impl Runtime { .js_runtime .execute_script("", script) .map_err(|error| { - // Get current source from Context let op_state = self.js_runtime.op_state(); let op_state_borrow = op_state.borrow(); let ctx: &Ctx = op_state_borrow.get_ctx(); diff --git a/nix-js/src/runtime/ops.rs b/nix-js/src/runtime/ops.rs new file mode 100644 index 0000000..3913191 --- /dev/null +++ b/nix-js/src/runtime/ops.rs @@ -0,0 +1,641 @@ +use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; + +use deno_core::OpState; +use rust_embed::Embed; + +use crate::error::Source; + +use super::{NixRuntimeError, OpStateExt, RuntimeContext}; + +#[derive(Embed)] +#[folder = "src/runtime/corepkgs"] +pub(crate) struct CorePkgs; + +#[deno_core::op2] +#[string] +pub(super) fn op_import( + state: &mut OpState, + #[string] path: String, +) -> std::result::Result { + let _span = tracing::info_span!("op_import", path = %path).entered(); + let ctx: &mut Ctx = state.get_ctx_mut(); + + // FIXME: special path type + if path.starts_with("") { + let corepkg_name = &path[5..path.len() - 1]; + if let Some(file) = CorePkgs::get(corepkg_name) { + tracing::info!("Importing corepkg: {}", corepkg_name); + let source = Source { + ty: crate::error::SourceType::Eval(Arc::new(ctx.get_current_dir().to_path_buf())), + src: str::from_utf8(&file.data) + .expect("corrupted corepkgs file") + .into(), + }; + ctx.add_source(source.clone()); + return Ok(ctx.compile_code(source).map_err(|err| err.to_string())?); + } else { + return Err(format!("Corepkg not found: {}", corepkg_name).into()); + } + } + + let current_dir = ctx.get_current_dir(); + let mut absolute_path = current_dir.join(&path); + // Do NOT resolve symlinks (eval-okay-symlink-resolution) + // TODO: is this correct? + // .canonicalize() + // .map_err(|e| format!("Failed to resolve path {}: {}", path, e))?; + if absolute_path.is_dir() { + absolute_path.push("default.nix") + } + + tracing::info!("Importing file: {}", absolute_path.display()); + + let source = Source::new_file(absolute_path.clone()) + .map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?; + + tracing::debug!("Compiling file"); + ctx.add_source(source.clone()); + + Ok(ctx.compile_code(source).map_err(|err| err.to_string())?) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_read_file(#[string] path: String) -> std::result::Result { + Ok(std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {}", path, e))?) +} + +#[deno_core::op2(fast)] +pub(super) fn op_path_exists(#[string] path: String) -> bool { + let must_be_dir = path.ends_with('/') || path.ends_with("/."); + let p = Path::new(&path); + + if must_be_dir { + match std::fs::metadata(p) { + Ok(m) => m.is_dir(), + Err(_) => false, + } + } else { + std::fs::symlink_metadata(p).is_ok() + } +} + +#[deno_core::op2] +#[string] +pub(super) fn op_read_file_type( + #[string] path: String, +) -> std::result::Result { + let path = Path::new(&path); + let metadata = std::fs::symlink_metadata(path) + .map_err(|e| format!("Failed to read file type for {}: {}", path.display(), e))?; + + let file_type = metadata.file_type(); + let type_str = if file_type.is_dir() { + "directory" + } else if file_type.is_symlink() { + "symlink" + } else if file_type.is_file() { + "regular" + } else { + "unknown" + }; + + Ok(type_str.to_string()) +} + +#[deno_core::op2] +#[serde] +pub(super) fn op_read_dir( + #[string] path: String, +) -> std::result::Result, NixRuntimeError> { + let path = Path::new(&path); + + if !path.is_dir() { + return Err(format!("{} is not a directory", path.display()).into()); + } + + let entries = std::fs::read_dir(path) + .map_err(|e| format!("Failed to read directory {}: {}", path.display(), e))?; + + let mut result = std::collections::HashMap::new(); + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; + let file_name = entry.file_name().to_string_lossy().to_string(); + + let file_type = entry.file_type().map_err(|e| { + format!( + "Failed to read file type for {}: {}", + entry.path().display(), + e + ) + })?; + + let type_str = if file_type.is_dir() { + "directory" + } else if file_type.is_symlink() { + "symlink" + } else if file_type.is_file() { + "regular" + } else { + "unknown" + }; + + result.insert(file_name, type_str.to_string()); + } + + Ok(result) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_resolve_path( + #[string] current_dir: String, + #[string] path: String, +) -> std::result::Result { + let _span = tracing::debug_span!("op_resolve_path").entered(); + tracing::debug!(current_dir, path); + + // If already absolute, return as-is + if path.starts_with('/') { + return Ok(path); + } + // Resolve relative path against current file directory (or CWD) + let current_dir = if !path.starts_with("~/") { + let mut dir = PathBuf::from(current_dir); + dir.push(path); + dir + } else { + let mut dir = std::env::home_dir().ok_or("home dir not defined")?; + dir.push(&path[2..]); + dir + }; + let mut normalized = PathBuf::new(); + for component in current_dir.components() { + match component { + Component::Prefix(p) => normalized.push(p.as_os_str()), + Component::RootDir => normalized.push("/"), + Component::CurDir => {} + Component::ParentDir => { + normalized.pop(); + } + Component::Normal(c) => normalized.push(c), + } + } + tracing::debug!(normalized = normalized.display().to_string()); + Ok(normalized.to_string_lossy().to_string()) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_sha256_hex(#[string] data: String) -> String { + crate::nix_hash::sha256_hex(&data) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_make_placeholder(#[string] output: String) -> String { + use sha2::{Digest, Sha256}; + let input = format!("nix-output:{}", output); + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + let hash: [u8; 32] = hasher.finalize().into(); + let encoded = crate::nix_hash::nix_base32_encode(&hash); + format!("/{}", encoded) +} + +#[deno_core::op2] +#[serde] +pub(super) fn op_decode_span( + state: &mut OpState, + #[string] span_str: String, +) -> std::result::Result { + let parts: Vec<&str> = span_str.split(':').collect(); + if parts.len() != 3 { + return Ok(serde_json::json!({ + "file": serde_json::Value::Null, + "line": serde_json::Value::Null, + "column": serde_json::Value::Null + })); + } + + let source_id: usize = parts[0].parse().map_err(|_| "Invalid source ID")?; + let start: u32 = parts[1].parse().map_err(|_| "Invalid start offset")?; + + let ctx: &Ctx = state.get_ctx(); + let source = ctx.get_source(source_id); + let content = &source.src; + + let (line, column) = byte_offset_to_line_col(content, start as usize); + + Ok(serde_json::json!({ + "file": source.get_name(), + "line": line, + "column": column + })) +} + +fn byte_offset_to_line_col(content: &str, offset: usize) -> (u32, u32) { + let mut line = 1u32; + let mut col = 1u32; + + for (idx, ch) in content.char_indices() { + if idx >= offset { + break; + } + if ch == '\n' { + line += 1; + col = 1; + } else { + col += 1; + } + } + + (line, col) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_make_store_path( + state: &mut OpState, + #[string] ty: String, + #[string] hash_hex: String, + #[string] name: String, +) -> String { + let ctx: &Ctx = state.get_ctx(); + let store = ctx.get_store(); + let store_dir = store.get_store_dir(); + crate::nix_hash::make_store_path(store_dir, &ty, &hash_hex, &name) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_make_text_store_path( + state: &mut OpState, + #[string] hash_hex: String, + #[string] name: String, + #[serde] references: Vec, +) -> String { + let ctx: &Ctx = state.get_ctx(); + let store = ctx.get_store(); + let store_dir = store.get_store_dir(); + crate::nix_hash::make_text_store_path(store_dir, &hash_hex, &name, &references) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_output_path_name( + #[string] drv_name: String, + #[string] output_name: String, +) -> String { + crate::nix_hash::output_path_name(&drv_name, &output_name) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_make_fixed_output_path( + state: &mut OpState, + #[string] hash_algo: String, + #[string] hash: String, + #[string] hash_mode: String, + #[string] name: String, +) -> String { + use sha2::{Digest, Sha256}; + + let ctx: &Ctx = state.get_ctx(); + let store = ctx.get_store(); + let store_dir = store.get_store_dir(); + + if hash_algo == "sha256" && hash_mode == "recursive" { + crate::nix_hash::make_store_path(store_dir, "source", &hash, &name) + } else { + let prefix = if hash_mode == "recursive" { "r:" } else { "" }; + let inner_input = format!("fixed:out:{}{}:{}:", prefix, hash_algo, hash); + let mut hasher = Sha256::new(); + hasher.update(inner_input.as_bytes()); + let inner_hash = hex::encode(hasher.finalize()); + + crate::nix_hash::make_store_path(store_dir, "output:out", &inner_hash, &name) + } +} + +#[deno_core::op2] +#[string] +pub(super) fn op_add_path( + state: &mut OpState, + #[string] path: String, + #[string] name: Option, + recursive: bool, + #[string] sha256: Option, +) -> std::result::Result { + use sha2::{Digest, Sha256}; + use std::fs; + use std::path::Path; + + let path_obj = Path::new(&path); + + if !path_obj.exists() { + return Err(NixRuntimeError::from(format!( + "path '{}' does not exist", + path + ))); + } + + let computed_name = name.unwrap_or_else(|| { + path_obj + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("source") + .to_string() + }); + + let computed_hash = if recursive { + crate::nar::compute_nar_hash(path_obj) + .map_err(|e| NixRuntimeError::from(format!("failed to compute NAR hash: {}", e)))? + } else { + if !path_obj.is_file() { + return Err(NixRuntimeError::from( + "when 'recursive' is false, path must be a regular file", + )); + } + let contents = fs::read(path_obj) + .map_err(|e| NixRuntimeError::from(format!("failed to read '{}': {}", path, e)))?; + + let mut hasher = Sha256::new(); + hasher.update(&contents); + hex::encode(hasher.finalize()) + }; + + if let Some(expected_hash) = sha256 { + let expected_hex = + crate::nix_hash::decode_hash_to_hex(&expected_hash).ok_or_else(|| { + NixRuntimeError::from(format!("invalid hash format: {}", expected_hash)) + })?; + if computed_hash != expected_hex { + return Err(NixRuntimeError::from(format!( + "hash mismatch for path '{}': expected {}, got {}", + path, expected_hex, computed_hash + ))); + } + } + + let ctx: &Ctx = state.get_ctx(); + let store = ctx.get_store(); + + let store_path = if recursive { + store + .add_to_store_from_path(&computed_name, path_obj, vec![]) + .map_err(|e| NixRuntimeError::from(format!("failed to add path to store: {}", e)))? + } else { + let contents = fs::read(path_obj) + .map_err(|e| NixRuntimeError::from(format!("failed to read '{}': {}", path, e)))?; + store + .add_to_store(&computed_name, &contents, false, vec![]) + .map_err(|e| NixRuntimeError::from(format!("failed to add to store: {}", e)))? + }; + + Ok(store_path) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_store_path( + state: &mut OpState, + #[string] path: String, +) -> std::result::Result { + use crate::store::validate_store_path; + + let ctx: &Ctx = state.get_ctx(); + let store = ctx.get_store(); + let store_dir = store.get_store_dir(); + + validate_store_path(store_dir, &path).map_err(|e| NixRuntimeError::from(e.to_string()))?; + + store + .ensure_path(&path) + .map_err(|e| NixRuntimeError::from(e.to_string()))?; + + Ok(path) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_to_file( + state: &mut OpState, + #[string] name: String, + #[string] contents: String, + #[serde] references: Vec, +) -> std::result::Result { + let ctx: &Ctx = state.get_ctx(); + let store = ctx.get_store(); + let store_path = store + .add_text_to_store(&name, &contents, references) + .map_err(|e| NixRuntimeError::from(format!("builtins.toFile failed: {}", e)))?; + + Ok(store_path) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_copy_path_to_store( + state: &mut OpState, + #[string] path: String, +) -> std::result::Result { + use std::path::Path; + + let path_obj = Path::new(&path); + + if !path_obj.exists() { + return Err(NixRuntimeError::from(format!( + "path '{}' does not exist", + path + ))); + } + + let name = path_obj + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("source") + .to_string(); + + let ctx: &Ctx = state.get_ctx(); + let store = ctx.get_store(); + let store_path = store + .add_to_store_from_path(&name, path_obj, vec![]) + .map_err(|e| NixRuntimeError::from(format!("failed to copy path to store: {}", e)))?; + + Ok(store_path) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_get_env(#[string] key: String) -> std::result::Result { + match std::env::var(key) { + Ok(val) => Ok(val), + Err(std::env::VarError::NotPresent) => Ok("".into()), + Err(err) => Err(format!("Failed to read env var: {err}").into()), + } +} + +#[deno_core::op2] +#[serde] +pub(super) fn op_walk_dir( + #[string] path: String, +) -> std::result::Result, NixRuntimeError> { + fn walk_recursive( + base: &Path, + current: &Path, + results: &mut Vec<(String, String)>, + ) -> std::result::Result<(), NixRuntimeError> { + let entries = std::fs::read_dir(current) + .map_err(|e| NixRuntimeError::from(format!("failed to read directory: {}", e)))?; + + for entry in entries { + let entry = + entry.map_err(|e| NixRuntimeError::from(format!("failed to read entry: {}", e)))?; + let path = entry.path(); + let rel_path = path + .strip_prefix(base) + .map_err(|e| NixRuntimeError::from(format!("failed to get relative path: {}", e)))? + .to_string_lossy() + .to_string(); + + let file_type = entry + .file_type() + .map_err(|e| NixRuntimeError::from(format!("failed to get file type: {}", e)))?; + + let type_str = if file_type.is_dir() { + "directory" + } else if file_type.is_symlink() { + "symlink" + } else { + "regular" + }; + + results.push((rel_path.clone(), type_str.to_string())); + + if file_type.is_dir() { + walk_recursive(base, &path, results)?; + } + } + Ok(()) + } + + let path = Path::new(&path); + if !path.is_dir() { + return Err(NixRuntimeError::from(format!( + "{} is not a directory", + path.display() + ))); + } + + let mut results = Vec::new(); + walk_recursive(path, path, &mut results)?; + Ok(results) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_add_filtered_path( + state: &mut OpState, + #[string] src_path: String, + #[string] name: Option, + recursive: bool, + #[string] sha256: Option, + #[serde] include_paths: Vec, +) -> std::result::Result { + use sha2::{Digest, Sha256}; + use std::fs; + + let src = Path::new(&src_path); + if !src.exists() { + return Err(NixRuntimeError::from(format!( + "path '{}' does not exist", + src_path + ))); + } + + let computed_name = name.unwrap_or_else(|| { + src.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("source") + .to_string() + }); + + let temp_dir = tempfile::tempdir() + .map_err(|e| NixRuntimeError::from(format!("failed to create temp dir: {}", e)))?; + let dest = temp_dir.path().join(&computed_name); + + fs::create_dir_all(&dest) + .map_err(|e| NixRuntimeError::from(format!("failed to create dest dir: {}", e)))?; + + for rel_path in &include_paths { + let src_file = src.join(rel_path); + let dest_file = dest.join(rel_path); + + if let Some(parent) = dest_file.parent() { + fs::create_dir_all(parent) + .map_err(|e| NixRuntimeError::from(format!("failed to create dir: {}", e)))?; + } + + let metadata = fs::symlink_metadata(&src_file) + .map_err(|e| NixRuntimeError::from(format!("failed to read metadata: {}", e)))?; + + if metadata.is_symlink() { + let target = fs::read_link(&src_file) + .map_err(|e| NixRuntimeError::from(format!("failed to read symlink: {}", e)))?; + #[cfg(unix)] + std::os::unix::fs::symlink(&target, &dest_file) + .map_err(|e| NixRuntimeError::from(format!("failed to create symlink: {}", e)))?; + #[cfg(not(unix))] + return Err(NixRuntimeError::from( + "symlinks not supported on this platform", + )); + } else if metadata.is_dir() { + fs::create_dir_all(&dest_file) + .map_err(|e| NixRuntimeError::from(format!("failed to create dir: {}", e)))?; + } else { + fs::copy(&src_file, &dest_file) + .map_err(|e| NixRuntimeError::from(format!("failed to copy file: {}", e)))?; + } + } + + let computed_hash = if recursive { + crate::nar::compute_nar_hash(&dest) + .map_err(|e| NixRuntimeError::from(format!("failed to compute NAR hash: {}", e)))? + } else { + if !dest.is_file() { + return Err(NixRuntimeError::from( + "when 'recursive' is false, path must be a regular file", + )); + } + let contents = fs::read(&dest) + .map_err(|e| NixRuntimeError::from(format!("failed to read file: {}", e)))?; + let mut hasher = Sha256::new(); + hasher.update(&contents); + hex::encode(hasher.finalize()) + }; + + if let Some(expected_hash) = sha256 { + let expected_hex = + crate::nix_hash::decode_hash_to_hex(&expected_hash).ok_or_else(|| { + NixRuntimeError::from(format!("invalid hash format: {}", expected_hash)) + })?; + if computed_hash != expected_hex { + return Err(NixRuntimeError::from(format!( + "hash mismatch for path '{}': expected {}, got {}", + src_path, expected_hex, computed_hash + ))); + } + } + + let ctx: &Ctx = state.get_ctx(); + let store = ctx.get_store(); + + let store_path = store + .add_to_store_from_path(&computed_name, &dest, vec![]) + .map_err(|e| NixRuntimeError::from(format!("failed to add path to store: {}", e)))?; + + Ok(store_path) +}