dynix/src/lib.rs
2026-03-10 18:47:06 +01:00

202 lines
5.5 KiB
Rust

// SPDX-FileCopyrightText: 2026 Qyriad <qyriad@qyriad.me>
//
// 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::{Display, Formatter, Result as FmtResult},
io::{Error as IoError, 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<dyn StdError + Send + Sync + 'static>;
pub use command_error::{CommandExt, OutputLike};
pub use fs_err::File;
#[cfg(unix)]
pub use fs_err::os::unix::fs::{FileExt, OpenOptionsExt};
pub use tap::{Pipe, Tap};
pub use tracing::{Level, debug, error, info, trace, warn};
}
use prelude::*;
pub mod args;
pub use args::{AppendCmd, Args};
mod color;
pub use color::{_CLI_ENABLE_COLOR, SHOULD_COLOR};
pub mod line;
mod nixcmd;
pub use line::Line;
pub mod source;
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};
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<Regex> = 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+\((?<priority>[\d-]+)\)").unwrap()
});
#[tracing::instrument(level = "debug")]
pub fn do_append(args: Arc<Args>, 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(())
}
#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)]
pub struct DefinitionWithLocation {
pub file: Box<Path>,
pub value: Box<serde_json::Value>,
}
pub fn expr_for_configuration(source_file: &Path) -> OsString {
[
OsStr::new("import <nixpkgs/nixos> { configuration = "),
source_file.as_os_str(),
OsStr::new("; }"),
]
.into_iter()
.collect()
}
fn maybe_extract_prio_from_line(line: &SourceLine) -> Option<i64> {
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<i64, BoxDynError> {
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<str>,
new_prio: i64,
new_value: Arc<str>,
) -> Result<SourceLine, BoxDynError> {
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(())
}