This commit is contained in:
2026-03-12 17:47:46 +08:00
parent 7a7229d70e
commit 0c9a391618
511 changed files with 234 additions and 12772 deletions

773
fix/src/store/daemon.rs Normal file
View File

@@ -0,0 +1,773 @@
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
use std::path::Path;
use nix_compat::nix_daemon::types::{AddToStoreNarRequest, UnkeyedValidPathInfo};
use nix_compat::nix_daemon::worker_protocol::{ClientSettings, Operation};
use nix_compat::store_path::StorePath;
use nix_compat::wire::ProtocolVersion;
use nix_compat::wire::de::{NixRead, NixReader};
use nix_compat::wire::ser::{NixSerialize, NixWrite, NixWriter, NixWriterBuilder};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf, split};
use tokio::net::UnixStream;
use tokio::sync::Mutex;
use super::Store;
use crate::error::{Error, Result};
pub struct DaemonStore {
runtime: tokio::runtime::Runtime,
connection: NixDaemonConnection,
}
impl DaemonStore {
pub fn connect(socket_path: &Path) -> Result<Self> {
let runtime = tokio::runtime::Runtime::new()
.map_err(|e| Error::internal(format!("Failed to create tokio runtime: {}", e)))?;
let connection = runtime.block_on(async {
NixDaemonConnection::connect(socket_path)
.await
.map_err(|e| {
Error::internal(format!(
"Failed to connect to nix-daemon at {}: {}",
socket_path.display(),
e
))
})
})?;
Ok(Self {
runtime,
connection,
})
}
fn block_on<F>(&self, future: F) -> F::Output
where
F: std::future::Future,
{
self.runtime.block_on(future)
}
}
impl Store for DaemonStore {
fn get_store_dir(&self) -> &str {
"/nix/store"
}
fn is_valid_path(&self, path: &str) -> Result<bool> {
self.block_on(async {
self.connection
.is_valid_path(path)
.await
.map_err(|e| Error::internal(format!("Daemon error in is_valid_path: {}", e)))
})
}
fn ensure_path(&self, path: &str) -> Result<()> {
self.block_on(async {
self.connection.ensure_path(path).await.map_err(|e| {
Error::eval_error(
format!(
"builtins.storePath: path '{}' is not valid in nix store: {}",
path, e
),
None,
)
})
})
}
fn add_to_store(
&self,
name: &str,
content: &[u8],
recursive: bool,
references: Vec<String>,
) -> Result<String> {
use std::fs;
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
use nix_compat::nixhash::{CAHash, NixHash};
use nix_compat::store_path::{StorePath, build_ca_path};
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new()
.map_err(|e| Error::internal(format!("Failed to create temp file: {}", e)))?;
fs::write(temp_file.path(), content)
.map_err(|e| Error::internal(format!("Failed to write temp file: {}", e)))?;
let nar_data = crate::nar::pack_nar(temp_file.path())?;
let nar_hash_hex = {
let mut hasher = Sha256::new();
hasher.update(&nar_data);
hex::encode(hasher.finalize())
};
let nar_hash_bytes = hex::decode(&nar_hash_hex)
.map_err(|e| Error::internal(format!("Invalid nar hash: {}", e)))?;
let mut nar_hash_arr = [0u8; 32];
nar_hash_arr.copy_from_slice(&nar_hash_bytes);
let ca_hash = if recursive {
CAHash::Nar(NixHash::Sha256(nar_hash_arr))
} else {
let mut content_hasher = Sha256::new();
content_hasher.update(content);
let content_hash = content_hasher.finalize();
let mut content_hash_arr = [0u8; 32];
content_hash_arr.copy_from_slice(&content_hash);
CAHash::Flat(NixHash::Sha256(content_hash_arr))
};
let ref_store_paths: std::result::Result<Vec<StorePath<String>>, _> = references
.iter()
.map(|r| StorePath::<String>::from_absolute_path(r.as_bytes()))
.collect();
let ref_store_paths = ref_store_paths
.map_err(|e| Error::internal(format!("Invalid reference path: {}", e)))?;
let store_path: StorePath<String> =
build_ca_path(name, &ca_hash, references.clone(), false)
.map_err(|e| Error::internal(format!("Failed to build store path: {}", e)))?;
let store_path_str = store_path.to_absolute_path();
if self.is_valid_path(&store_path_str)? {
return Ok(store_path_str);
}
let request = AddToStoreNarRequest {
path: store_path,
deriver: None,
nar_hash: unsafe {
std::mem::transmute::<[u8; 32], nix_compat::nix_daemon::types::NarHash>(
nar_hash_arr,
)
},
references: ref_store_paths,
registration_time: 0,
nar_size: nar_data.len() as u64,
ultimate: false,
signatures: vec![],
ca: Some(ca_hash),
repair: false,
dont_check_sigs: false,
};
self.block_on(async {
self.connection
.add_to_store_nar(request, &nar_data)
.await
.map_err(|e| Error::internal(format!("Failed to add to store: {}", e)))
})?;
Ok(store_path_str)
}
fn add_to_store_from_path(
&self,
name: &str,
source_path: &std::path::Path,
references: Vec<String>,
) -> Result<String> {
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
use nix_compat::nixhash::{CAHash, NixHash};
use nix_compat::store_path::{StorePath, build_ca_path};
use sha2::{Digest, Sha256};
let nar_data = crate::nar::pack_nar(source_path)?;
let nar_hash: [u8; 32] = {
let mut hasher = Sha256::new();
hasher.update(&nar_data);
hasher.finalize().into()
};
let ca_hash = CAHash::Nar(NixHash::Sha256(nar_hash));
let ref_store_paths: std::result::Result<Vec<StorePath<String>>, _> = references
.iter()
.map(|r| StorePath::<String>::from_absolute_path(r.as_bytes()))
.collect();
let ref_store_paths = ref_store_paths
.map_err(|e| Error::internal(format!("Invalid reference path: {}", e)))?;
let store_path: StorePath<String> =
build_ca_path(name, &ca_hash, references.clone(), false)
.map_err(|e| Error::internal(format!("Failed to build store path: {}", e)))?;
let store_path_str = store_path.to_absolute_path();
if self.is_valid_path(&store_path_str)? {
return Ok(store_path_str);
}
let request = AddToStoreNarRequest {
path: store_path,
deriver: None,
nar_hash: unsafe {
std::mem::transmute::<[u8; 32], nix_compat::nix_daemon::types::NarHash>(nar_hash)
},
references: ref_store_paths,
registration_time: 0,
nar_size: nar_data.len() as u64,
ultimate: false,
signatures: vec![],
ca: Some(ca_hash),
repair: false,
dont_check_sigs: false,
};
self.block_on(async {
self.connection
.add_to_store_nar(request, &nar_data)
.await
.map_err(|e| Error::internal(format!("Failed to add to store: {}", e)))
})?;
Ok(store_path_str)
}
fn add_text_to_store(
&self,
name: &str,
content: &str,
references: Vec<String>,
) -> Result<String> {
use std::fs;
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
use nix_compat::nixhash::CAHash;
use nix_compat::store_path::{StorePath, build_text_path};
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new()
.map_err(|e| Error::internal(format!("Failed to create temp file: {}", e)))?;
fs::write(temp_file.path(), content.as_bytes())
.map_err(|e| Error::internal(format!("Failed to write temp file: {}", e)))?;
let nar_data = crate::nar::pack_nar(temp_file.path())?;
let nar_hash: [u8; 32] = {
let mut hasher = Sha256::new();
hasher.update(&nar_data);
hasher.finalize().into()
};
let content_hash = {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
hasher.finalize().into()
};
let ref_store_paths: std::result::Result<Vec<StorePath<String>>, _> = references
.iter()
.map(|r| StorePath::<String>::from_absolute_path(r.as_bytes()))
.collect();
let ref_store_paths = ref_store_paths
.map_err(|e| Error::internal(format!("Invalid reference path: {}", e)))?;
let store_path: StorePath<String> = build_text_path(name, content, references.clone())
.map_err(|e| Error::internal(format!("Failed to build text store path: {}", e)))?;
let store_path_str = store_path.to_absolute_path();
if self.is_valid_path(&store_path_str)? {
return Ok(store_path_str);
}
let request = AddToStoreNarRequest {
path: store_path,
deriver: None,
nar_hash: unsafe {
std::mem::transmute::<[u8; 32], nix_compat::nix_daemon::types::NarHash>(nar_hash)
},
references: ref_store_paths,
registration_time: 0,
nar_size: nar_data.len() as u64,
ultimate: false,
signatures: vec![],
ca: Some(CAHash::Text(content_hash)),
repair: false,
dont_check_sigs: false,
};
self.block_on(async {
self.connection
.add_to_store_nar(request, &nar_data)
.await
.map_err(|e| Error::internal(format!("Failed to add text to store: {}", e)))
})?;
Ok(store_path_str)
}
}
const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::from_parts(1, 37);
// Protocol magic numbers (from nix-compat worker_protocol.rs)
const WORKER_MAGIC_1: u64 = 0x6e697863; // "nixc"
const WORKER_MAGIC_2: u64 = 0x6478696f; // "dxio"
const STDERR_LAST: u64 = 0x616c7473; // "alts"
const STDERR_ERROR: u64 = 0x63787470; // "cxtp"
/// Performs the client handshake with a nix-daemon server
///
/// This is the client-side counterpart to `server_handshake_client`.
/// It exchanges magic numbers, negotiates protocol version, and sends client settings.
async fn client_handshake<RW>(
conn: &mut RW,
client_settings: &ClientSettings,
) -> IoResult<ProtocolVersion>
where
RW: AsyncReadExt + AsyncWriteExt + Unpin,
{
// 1. Send magic number 1
conn.write_u64_le(WORKER_MAGIC_1).await?;
// 2. Receive magic number 2
let magic2 = conn.read_u64_le().await?;
if magic2 != WORKER_MAGIC_2 {
return Err(IoError::new(
IoErrorKind::InvalidData,
format!("Invalid magic number from server: {}", magic2),
));
}
// 3. Receive server protocol version
let server_version_raw = conn.read_u64_le().await?;
let server_version: ProtocolVersion = server_version_raw.try_into().map_err(|e| {
IoError::new(
IoErrorKind::InvalidData,
format!("Invalid protocol version: {}", e),
)
})?;
// 4. Send our protocol version
conn.write_u64_le(PROTOCOL_VERSION.into()).await?;
// Pick the minimum version
let protocol_version = std::cmp::min(PROTOCOL_VERSION, server_version);
// 5. Send obsolete fields based on protocol version
if protocol_version.minor() >= 14 {
// CPU affinity (obsolete, send 0)
conn.write_u64_le(0).await?;
}
if protocol_version.minor() >= 11 {
// Reserve space (obsolete, send 0)
conn.write_u64_le(0).await?;
}
if protocol_version.minor() >= 33 {
// Read Nix version string
let version_len = conn.read_u64_le().await? as usize;
let mut version_bytes = vec![0u8; version_len];
conn.read_exact(&mut version_bytes).await?;
// Padding
let padding = (8 - (version_len % 8)) % 8;
if padding > 0 {
let mut pad = vec![0u8; padding];
conn.read_exact(&mut pad).await?;
}
}
if protocol_version.minor() >= 35 {
// Read trust level
let _trust = conn.read_u64_le().await?;
}
// 6. Read STDERR_LAST
let stderr_last = conn.read_u64_le().await?;
if stderr_last != STDERR_LAST {
return Err(IoError::new(
IoErrorKind::InvalidData,
format!("Expected STDERR_LAST, got: {}", stderr_last),
));
}
// 7. Send SetOptions operation with client settings
conn.write_u64_le(Operation::SetOptions.into()).await?;
conn.flush().await?;
// Serialize client settings
let mut settings_buf = Vec::new();
{
let mut writer = NixWriterBuilder::default()
.set_version(protocol_version)
.build(&mut settings_buf);
writer.write_value(client_settings).await?;
writer.flush().await?;
}
conn.write_all(&settings_buf).await?;
conn.flush().await?;
// 8. Read response to SetOptions
let response = conn.read_u64_le().await?;
if response != STDERR_LAST {
return Err(IoError::new(
IoErrorKind::InvalidData,
format!("Expected STDERR_LAST after SetOptions, got: {}", response),
));
}
Ok(protocol_version)
}
/// Low-level Nix Daemon client
///
/// This struct manages communication with a nix-daemon using the wire protocol.
/// It is NOT thread-safe and should be wrapped in a Mutex for concurrent access.
pub struct NixDaemonClient {
protocol_version: ProtocolVersion,
reader: NixReader<ReadHalf<UnixStream>>,
writer: NixWriter<WriteHalf<UnixStream>>,
_marker: std::marker::PhantomData<std::cell::Cell<()>>,
}
impl NixDaemonClient {
/// Connect to a nix-daemon at the given Unix socket path
pub async fn connect(socket_path: &Path) -> IoResult<Self> {
let stream = UnixStream::connect(socket_path).await?;
Self::from_stream(stream).await
}
/// Create a client from an existing Unix stream
pub async fn from_stream(mut stream: UnixStream) -> IoResult<Self> {
let client_settings = ClientSettings::default();
// Perform handshake
let protocol_version = client_handshake(&mut stream, &client_settings).await?;
// Split stream into reader and writer
let (read_half, write_half) = split(stream);
let reader = NixReader::builder()
.set_version(protocol_version)
.build(read_half);
let writer = NixWriterBuilder::default()
.set_version(protocol_version)
.build(write_half);
Ok(Self {
protocol_version,
reader,
writer,
_marker: Default::default(),
})
}
/// Execute an operation with a single parameter
async fn execute_with<P, T>(&mut self, operation: Operation, param: &P) -> IoResult<T>
where
P: NixSerialize + Send,
T: nix_compat::wire::de::NixDeserialize,
{
// Send operation
self.writer.write_value(&operation).await?;
// Send parameter
self.writer.write_value(param).await?;
self.writer.flush().await?;
self.read_response().await
}
/// Read a response from the daemon
///
/// The daemon sends either:
/// - STDERR_LAST followed by the result
/// - STDERR_ERROR followed by a structured error
async fn read_response<T>(&mut self) -> IoResult<T>
where
T: nix_compat::wire::de::NixDeserialize,
{
loop {
let msg = self.reader.read_number().await?;
if msg == STDERR_LAST {
let result: T = self.reader.read_value().await?;
return Ok(result);
} else if msg == STDERR_ERROR {
let error_msg = self.read_daemon_error().await?;
return Err(IoError::other(error_msg));
} else {
let _data: String = self.reader.read_value().await?;
continue;
}
}
}
async fn read_daemon_error(&mut self) -> IoResult<NixDaemonError> {
let type_marker: String = self.reader.read_value().await?;
assert_eq!(type_marker, "Error");
let level = NixDaemonErrorLevel::try_from_primitive(
self.reader
.read_number()
.await?
.try_into()
.map_err(|_| IoError::other("invalid nix-daemon error level"))?,
)
.map_err(|_| IoError::other("invalid nix-daemon error level"))?;
// removed
let _name: String = self.reader.read_value().await?;
let msg: String = self.reader.read_value().await?;
let have_pos: u64 = self.reader.read_number().await?;
assert_eq!(have_pos, 0);
let nr_traces: u64 = self.reader.read_number().await?;
let mut traces = Vec::new();
for _ in 0..nr_traces {
let _trace_pos: u64 = self.reader.read_number().await?;
let trace_hint: String = self.reader.read_value().await?;
traces.push(trace_hint);
}
Ok(NixDaemonError { level, msg, traces })
}
/// Check if a path is valid in the store
pub async fn is_valid_path(&mut self, path: &str) -> IoResult<bool> {
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
self.execute_with(Operation::IsValidPath, &store_path).await
}
/// Query information about a store path
#[allow(dead_code)]
pub async fn query_path_info(&mut self, path: &str) -> IoResult<Option<UnkeyedValidPathInfo>> {
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
self.writer.write_value(&Operation::QueryPathInfo).await?;
self.writer.write_value(&store_path).await?;
self.writer.flush().await?;
loop {
let msg = self.reader.read_number().await?;
if msg == STDERR_LAST {
let has_value: bool = self.reader.read_value().await?;
if has_value {
use nix_compat::narinfo::Signature;
use nix_compat::nixhash::CAHash;
let deriver = self.reader.read_value().await?;
let nar_hash: String = self.reader.read_value().await?;
let references = self.reader.read_value().await?;
let registration_time = self.reader.read_value().await?;
let nar_size = self.reader.read_value().await?;
let ultimate = self.reader.read_value().await?;
let signatures: Vec<Signature<String>> = self.reader.read_value().await?;
let ca: Option<CAHash> = self.reader.read_value().await?;
let value = UnkeyedValidPathInfo {
deriver,
nar_hash,
references,
registration_time,
nar_size,
ultimate,
signatures,
ca,
};
return Ok(Some(value));
} else {
return Ok(None);
}
} else if msg == STDERR_ERROR {
let error_msg = self.read_daemon_error().await?;
return Err(IoError::other(error_msg));
} else {
let _data: String = self.reader.read_value().await?;
continue;
}
}
}
/// Ensure a path is available in the store
pub async fn ensure_path(&mut self, path: &str) -> IoResult<()> {
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
self.writer.write_value(&Operation::EnsurePath).await?;
self.writer.write_value(&store_path).await?;
self.writer.flush().await?;
loop {
let msg = self.reader.read_number().await?;
if msg == STDERR_LAST {
return Ok(());
} else if msg == STDERR_ERROR {
let error_msg = self.read_daemon_error().await?;
return Err(IoError::other(error_msg));
} else {
let _data: String = self.reader.read_value().await?;
continue;
}
}
}
/// Query which paths are valid
#[allow(dead_code)]
pub async fn query_valid_paths(&mut self, paths: Vec<String>) -> IoResult<Vec<String>> {
let store_paths: IoResult<Vec<StorePath<String>>> = paths
.iter()
.map(|p| {
StorePath::<String>::from_absolute_path(p.as_bytes())
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))
})
.collect();
let store_paths = store_paths?;
// Send operation
self.writer.write_value(&Operation::QueryValidPaths).await?;
// Manually serialize the request since QueryValidPaths doesn't impl NixSerialize
// QueryValidPaths = { paths: Vec<StorePath>, substitute: bool }
self.writer.write_value(&store_paths).await?;
// For protocol >= 1.27, send substitute flag
if self.protocol_version.minor() >= 27 {
self.writer.write_value(&false).await?;
}
self.writer.flush().await?;
let result: Vec<StorePath<String>> = self.read_response().await?;
Ok(result.into_iter().map(|p| p.to_absolute_path()).collect())
}
/// Add a NAR to the store
pub async fn add_to_store_nar(
&mut self,
request: AddToStoreNarRequest,
nar_data: &[u8],
) -> IoResult<()> {
tracing::debug!(
"add_to_store_nar: path={}, nar_size={}",
request.path.to_absolute_path(),
request.nar_size,
);
self.writer.write_value(&Operation::AddToStoreNar).await?;
self.writer.write_value(&request.path).await?;
self.writer.write_value(&request.deriver).await?;
let nar_hash_hex = hex::encode(request.nar_hash.as_ref());
self.writer.write_value(&nar_hash_hex).await?;
self.writer.write_value(&request.references).await?;
self.writer.write_value(&request.registration_time).await?;
self.writer.write_value(&request.nar_size).await?;
self.writer.write_value(&request.ultimate).await?;
self.writer.write_value(&request.signatures).await?;
self.writer.write_value(&request.ca).await?;
self.writer.write_value(&request.repair).await?;
self.writer.write_value(&request.dont_check_sigs).await?;
if self.protocol_version.minor() >= 23 {
self.writer.write_number(nar_data.len() as u64).await?;
self.writer.write_all(nar_data).await?;
self.writer.write_number(0u64).await?;
} else {
self.writer.write_slice(nar_data).await?;
}
self.writer.flush().await?;
loop {
let msg = self.reader.read_number().await?;
if msg == STDERR_LAST {
return Ok(());
} else if msg == STDERR_ERROR {
let error_msg = self.read_daemon_error().await?;
return Err(IoError::other(error_msg));
} else {
let _data: String = self.reader.read_value().await?;
continue;
}
}
}
}
/// Thread-safe wrapper around NixDaemonClient
pub struct NixDaemonConnection {
client: Mutex<NixDaemonClient>,
}
impl NixDaemonConnection {
/// Connect to a nix-daemon at the given socket path
pub async fn connect(socket_path: &Path) -> IoResult<Self> {
let client = NixDaemonClient::connect(socket_path).await?;
Ok(Self {
client: Mutex::new(client),
})
}
/// Check if a path is valid in the store
pub async fn is_valid_path(&self, path: &str) -> IoResult<bool> {
let mut client = self.client.lock().await;
client.is_valid_path(path).await
}
/// Query information about a store path
#[allow(dead_code)]
pub async fn query_path_info(&self, path: &str) -> IoResult<Option<UnkeyedValidPathInfo>> {
let mut client = self.client.lock().await;
client.query_path_info(path).await
}
/// Ensure a path is available in the store
pub async fn ensure_path(&self, path: &str) -> IoResult<()> {
let mut client = self.client.lock().await;
client.ensure_path(path).await
}
/// Query which paths are valid
#[allow(dead_code)]
pub async fn query_valid_paths(&self, paths: Vec<String>) -> IoResult<Vec<String>> {
let mut client = self.client.lock().await;
client.query_valid_paths(paths).await
}
/// Add a NAR to the store
pub async fn add_to_store_nar(
&self,
request: AddToStoreNarRequest,
nar_data: &[u8],
) -> IoResult<()> {
let mut client = self.client.lock().await;
client.add_to_store_nar(request, nar_data).await
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum NixDaemonErrorLevel {
Error = 0,
Warn,
Notice,
Info,
Talkative,
Chatty,
Debug,
Vomit,
}
#[derive(Debug, Error)]
#[error("{msg}")]
pub struct NixDaemonError {
level: NixDaemonErrorLevel,
msg: String,
traces: Vec<String>,
}