// SPDX-FileCopyrightText: 2026 Qyriad // // SPDX-License-Identifier: EUPL-1.1 use std::{ iter, sync::{Arc, LazyLock}, }; pub(crate) mod prelude { #![allow(unused_imports)] pub use std::{ error::Error as StdError, ffi::{OsStr, OsString}, fmt::{Debug, Display, Formatter, Result as FmtResult}, io::{Error as IoError, ErrorKind as IoErrorKind, Read, Seek, SeekFrom, Write}, path::{Path, PathBuf}, process::{Command, ExitCode}, str::FromStr, }; #[cfg(unix)] pub use std::os::{ fd::AsRawFd, unix::ffi::{OsStrExt, OsStringExt}, }; pub type BoxDynError = Box; pub use command_error::{CommandExt, OutputLike}; pub use fs_err::File; #[cfg(unix)] pub use fs_err::os::unix::fs::{FileExt, OpenOptionsExt}; pub use bstr::ByteSlice; pub use tap::{Pipe, Tap, TapFallible}; pub use tracing::{Level, debug, error, info, trace, warn}; pub use crate::boxext::BoxedPathExt; } use prelude::*; pub mod args; pub use args::{AppendCmd, Args}; mod boxext; mod color; pub use color::{_CLI_ENABLE_COLOR, SHOULD_COLOR}; mod daemon; pub use daemon::Daemon; mod daemon_io; pub use daemon_io::OwnedFdWithFlags; mod daemon_tokfd; pub(crate) use daemon_tokfd::TokenFd; pub mod line; pub use line::Line; mod nixcmd; pub mod source; use rustix::fs::Mode; pub use source::{SourceFile, SourceLine}; #[cfg(all(not(feature = "regex-full"), not(feature = "regex-lite")))] compile_error!("At least one of features `regex-full` or `regex-lite` must be used"); #[cfg(feature = "regex-full")] use regex as _regex; // Having both `regex-full` and `regex-lite` isn't an error; it's just wasteful. #[cfg(not(feature = "regex-full"))] use regex_lite as _regex; use _regex::Regex; use itertools::Itertools; use serde::{Deserialize, Serialize}; use crate::args::DaemonCmd; pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' ']; /// Regex pattern to extract the priority in a `lib.mkOverride` call. static MK_OVERRIDE_RE: LazyLock = LazyLock::new(|| { // Named capture group: priority // - Word boundary // - Literal `mkOverride` // - One or more whitespace characters // - Literal open parenthesis // - Named capture group "priority" // - One or more of: digit characters, or literal `-` // - Literal close parenthesis Regex::new(r"(?-u)\bmkOverride\s+\((?[\d-]+)\)").unwrap() }); #[tracing::instrument(level = "debug")] pub fn do_append(args: Arc, append_args: AppendCmd) -> Result<(), BoxDynError> { let filepath = Path::new(&args.file); let filepath: PathBuf = if filepath.is_relative() && !filepath.starts_with("./") { iter::once(OsStr::new("./")) .chain(filepath.iter()) .collect() } else { filepath.to_path_buf() }; let mut opts = File::options(); opts.read(true) .write(true) .create(false) .custom_flags(libc::O_CLOEXEC); let source_file = SourceFile::open_from(Arc::from(filepath), opts)?; let pri = get_where(source_file.clone())?; let new_pri = pri - 1; let new_pri_line = get_next_prio_line( source_file.clone(), append_args.name, new_pri, append_args.value, )?; debug!("new_pri_line={new_pri_line}"); write_next_prio(source_file, new_pri_line)?; Ok(()) } //#[tracing::instrument(level = "debug")] pub fn do_daemon(_args: Arc, daemon_args: DaemonCmd) -> Result<(), BoxDynError> { // FIXME: make configurable? let _ = rustix::process::umask(Mode::from_bits_retain(0o600).complement()); let mut daemon = match daemon_args { DaemonCmd { stdin: true, .. } => Daemon::from_stdin(), DaemonCmd { socket: None, .. } => Daemon::open_default_socket()?, DaemonCmd { socket: Some(socket), .. } => Daemon::from_unix_socket_path(&socket)?, }; daemon.enter_loop().unwrap(); info!("daemon has exited"); Ok(()) } #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)] pub struct DefinitionWithLocation { pub file: Box, pub value: Box, } pub fn expr_for_configuration(source_file: &Path) -> OsString { [ OsStr::new("import { configuration = "), source_file.as_os_str(), OsStr::new("; }"), ] .into_iter() .collect() } fn maybe_extract_prio_from_line(line: &SourceLine) -> Option { MK_OVERRIDE_RE .captures(line.text_ref()) .map(|caps| caps.name("priority").unwrap().as_str()) .map(|prio_str| { i64::from_str(prio_str).unwrap_or_else(|e| { panic!( "lib.mkOverride called with non-integer {}: {}. Nix source code is wrong!\n{}", prio_str, e, line, ); }) }) } pub fn get_where(dynamic_nix: SourceFile) -> Result { let lines = dynamic_nix.lines()?; let prio = lines .iter() .filter_map(maybe_extract_prio_from_line) .sorted_unstable() .next() // Priorities with lower integer values are "stronger" priorities. .unwrap_or(0); Ok(prio) } pub fn get_next_prio_line( source: SourceFile, option_name: Arc, new_prio: i64, new_value: Arc, ) -> Result { let source_lines = source.lines()?; let penultimate = source_lines.get(source_lines.len() - 2); // FIXME: don't rely on whitespace lol debug_assert_eq!(penultimate.map(SourceLine::text).as_deref(), Some(" ];")); let penultimate = penultimate.unwrap(); let new_generation = 0 - new_prio; let new_line = SourceLine { line: penultimate.line, path: source.path(), text: Arc::from(format!( " {} = lib.mkOverride ({}) ({}); # DYNIX GENERATION {}", option_name, new_prio, new_value, new_generation, )), }; Ok(new_line) } pub fn write_next_prio(mut source: SourceFile, new_line: SourceLine) -> Result<(), BoxDynError> { let new_mod_start = SourceLine { line: new_line.line.prev(), path: source.path(), text: Arc::from(" {"), }; let new_mod_end = SourceLine { line: new_line.line.next(), path: source.path(), text: Arc::from(" }"), }; source.insert_lines(&[new_mod_start, new_line, new_mod_end])?; Ok(()) }