feat: nix_nar

This commit is contained in:
2026-01-17 22:39:38 +08:00
parent 2ad662c765
commit 611255d42c
9 changed files with 108 additions and 131 deletions

34
Cargo.lock generated
View File

@@ -254,6 +254,12 @@ dependencies = [
"displaydoc",
]
[[package]]
name = "camino"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
[[package]]
name = "capacity_builder"
version = "0.5.0"
@@ -1480,6 +1486,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "is_executable"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4"
dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -1765,6 +1780,7 @@ dependencies = [
"mimalloc",
"nix-daemon",
"nix-js-macros",
"nix-nar",
"petgraph",
"regex",
"reqwest",
@@ -1794,6 +1810,18 @@ dependencies = [
"syn",
]
[[package]]
name = "nix-nar"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15dbfa157df89f4283825ff1c21d53344cfe0d222ea8fde0f9514206dc62d9e0"
dependencies = [
"camino",
"is_executable",
"symlink",
"thiserror 1.0.69",
]
[[package]]
name = "nom"
version = "7.1.3"
@@ -2729,6 +2757,12 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]]
name = "syn"
version = "2.0.104"

View File

@@ -36,6 +36,7 @@ regex = "1.11"
deno_core = "0.376"
deno_error = "0.7"
nix-nar = "0.3"
sha2 = "0.10"
hex = "0.4"

View File

@@ -7,12 +7,12 @@ pub(crate) mod cache;
mod download;
mod git;
mod hg;
mod nar;
pub use cache::FetcherCache;
pub use download::Downloader;
use crate::runtime::NixError;
use crate::nar;
#[derive(Serialize)]
pub struct FetchUrlResult {

View File

@@ -1,6 +1,6 @@
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use tracing::debug;
@@ -317,7 +317,7 @@ impl FetcherCache {
&self,
url: &str,
hash: &str,
extracted_path: &PathBuf,
extracted_path: &Path,
name: &str,
) -> Result<PathBuf, CacheError> {
let cache_dir = self.tarball_cache_dir();
@@ -377,7 +377,7 @@ impl FetcherCache {
}
}
fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), std::io::Error> {
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {

View File

@@ -1,127 +0,0 @@
use sha2::{Digest, Sha256};
use std::fs;
use std::io::{self, Write};
use std::path::Path;
pub fn compute_nar_hash(path: &Path) -> Result<String, io::Error> {
let mut hasher = Sha256::new();
dump_path(&mut hasher, path)?;
Ok(hex::encode(hasher.finalize()))
}
fn dump_path<W: Write>(sink: &mut W, path: &Path) -> io::Result<()> {
write_string(sink, "nix-archive-1")?;
write_string(sink, "(")?;
dump_entry(sink, path)?;
write_string(sink, ")")?;
Ok(())
}
fn dump_entry<W: Write>(sink: &mut W, path: &Path) -> io::Result<()> {
let metadata = fs::symlink_metadata(path)?;
if metadata.is_symlink() {
let target = fs::read_link(path)?;
write_string(sink, "type")?;
write_string(sink, "symlink")?;
write_string(sink, "target")?;
write_string(sink, &target.to_string_lossy())?;
} else if metadata.is_file() {
write_string(sink, "type")?;
write_string(sink, "regular")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if metadata.permissions().mode() & 0o111 != 0 {
write_string(sink, "executable")?;
write_string(sink, "")?;
}
}
let contents = fs::read(path)?;
write_string(sink, "contents")?;
write_contents(sink, &contents)?;
} else if metadata.is_dir() {
write_string(sink, "type")?;
write_string(sink, "directory")?;
let mut entries: Vec<_> = fs::read_dir(path)?
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
entries.sort();
for name in entries {
write_string(sink, "entry")?;
write_string(sink, "(")?;
write_string(sink, "name")?;
write_string(sink, &name)?;
write_string(sink, "node")?;
write_string(sink, "(")?;
dump_entry(sink, &path.join(&name))?;
write_string(sink, ")")?;
write_string(sink, ")")?;
}
}
Ok(())
}
fn write_string<W: Write>(sink: &mut W, s: &str) -> io::Result<()> {
let bytes = s.as_bytes();
let len = bytes.len() as u64;
sink.write_all(&len.to_le_bytes())?;
sink.write_all(bytes)?;
let padding = (8 - (len % 8)) % 8;
for _ in 0..padding {
sink.write_all(&[0])?;
}
Ok(())
}
fn write_contents<W: Write>(sink: &mut W, contents: &[u8]) -> io::Result<()> {
let len = contents.len() as u64;
sink.write_all(&len.to_le_bytes())?;
sink.write_all(contents)?;
let padding = (8 - (len % 8)) % 8;
for _ in 0..padding {
sink.write_all(&[0])?;
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_simple_file() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("test.txt");
fs::write(&file_path, "hello").unwrap();
let hash = compute_nar_hash(&file_path).unwrap();
assert!(!hash.is_empty());
assert_eq!(hash.len(), 64);
}
#[test]
fn test_directory() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("a.txt"), "aaa").unwrap();
fs::write(temp.path().join("b.txt"), "bbb").unwrap();
let hash = compute_nar_hash(temp.path()).unwrap();
assert!(!hash.is_empty());
assert_eq!(hash.len(), 64);
}
}

View File

@@ -8,6 +8,7 @@ mod codegen;
mod fetcher;
mod ir;
mod nix_hash;
mod nar;
mod runtime;
mod store;

63
nix-js/src/nar.rs Normal file
View File

@@ -0,0 +1,63 @@
use nix_nar::Encoder;
use sha2::{Digest, Sha256};
use std::io::Read;
use std::path::Path;
use crate::error::{Error, Result};
pub fn compute_nar_hash(path: &Path) -> Result<String> {
let mut hasher = Sha256::new();
std::io::copy(
&mut Encoder::new(path).map_err(|err| Error::internal(err.to_string()))?,
&mut hasher,
)
.map_err(|err| Error::internal(err.to_string()))?;
Ok(hex::encode(hasher.finalize()))
}
pub fn pack_nar(path: &Path) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
Encoder::new(path)
.map_err(|err| Error::internal(err.to_string()))?
.read_to_end(&mut buffer)
.map_err(|err| Error::internal(err.to_string()))?;
Ok(buffer)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_simple_file() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("test.txt");
fs::write(&file_path, "hello").unwrap();
let hash = compute_nar_hash(&file_path).unwrap();
assert_eq!(
hash,
"0a430879c266f8b57f4092a0f935cf3facd48bbccde5760d4748ca405171e969"
);
assert!(!hash.is_empty());
assert_eq!(hash.len(), 64);
}
#[test]
fn test_directory() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("a.txt"), "aaa").unwrap();
fs::write(temp.path().join("b.txt"), "bbb").unwrap();
let hash = compute_nar_hash(temp.path()).unwrap();
assert_eq!(
hash,
"0036c14209749bc9b9631e2077b108b701c322ab53853cd26f2746268a86fc0f"
);
assert!(!hash.is_empty());
assert_eq!(hash.len(), 64);
}
}

View File

@@ -2,6 +2,8 @@ mod config;
mod error;
mod validation;
use std::path::Path;
pub use config::{StoreConfig, StoreMode};
pub use validation::validate_store_path;

View File

@@ -8,7 +8,10 @@ use utils::eval_result;
fn init() {
static INIT: Once = Once::new();
INIT.call_once(|| {
#[cfg(not(feature = "daemon"))]
unsafe { std::env::set_var("NIX_JS_STORE_MODE", "simulated") };
#[cfg(feature = "daemon")]
unsafe { std::env::set_var("NIX_JS_STORE_MODE", "daemon") };
});
}