From 50a2b8f9628bacdc8f13848e98b909e7be44c640 Mon Sep 17 00:00:00 2001 From: tv Date: Fri, 13 Nov 2026 16:23:34 +0100 Subject: populate: add a Rust implementation, selectable for the tests pkgs.populate takes implementation: "shell", the default, generates a script from populate's arguments, and "rust" passes them as JSON to the program in pkgs/populate, built from the crate there by program.nix. The program checks the spec as checks.nix does during evaluation, with the same messages, and reports that delivering sources isn't implemented yet. rustProgram replaces the program, e.g. with one built some other way. checks.nix holds the checks of populate's arguments during evaluation. Both implementations check the names of sources and the target path with it, and the Rust one's wrapper also checks drawing inputs, which the shell implementation checks where it draws them. nix-build tests --argstr implementation rust runs the scenarios with the Rust implementation. They expect it to refuse copying flakes to targets using sudo when it runs, not during evaluation, because only then it decides whether the target is local. --- pkgs/populate/src/spec.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 pkgs/populate/src/spec.rs (limited to 'pkgs/populate/src/spec.rs') diff --git a/pkgs/populate/src/spec.rs b/pkgs/populate/src/spec.rs new file mode 100644 index 0000000..089d330 --- /dev/null +++ b/pkgs/populate/src/spec.rs @@ -0,0 +1,121 @@ +//! The spec: populate's arguments as JSON, as docs/design.md describes them. + +// Delivering sources isn't implemented yet, so most of the spec is unused. +#![allow(dead_code)] + +use serde::Deserialize; +use serde_json::{Map, Value}; +use std::collections::BTreeMap; + +/// The version of the spec this populate reads. +pub const VERSION: u64 = 1; + +/// The source types populate knows. +pub const SOURCE_TYPES: &[&str] = &[ + "derivation", + "file", + "flake", + "flake-input", + "git", + "pass", + "passage", + "pipe", + "symlink", +]; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Spec { + pub version: u64, + #[serde(default)] + pub backup: bool, + #[serde(default)] + pub force: bool, + #[serde(default)] + pub prune: bool, + pub source: BTreeMap, + pub target: Target, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct Target { + #[serde(default)] + pub user: Option, + pub host: String, + #[serde(default)] + pub port: Option, + pub path: String, + #[serde(default)] + pub sudo: bool, + #[serde(default)] + pub extra_options: Vec, +} + +/// A source: its type, and the settings of that type, under the type's name. +#[derive(Debug)] +pub struct Source { + pub kind: String, + pub settings: Map, +} + +impl<'de> Deserialize<'de> for Source { + fn deserialize>(deserializer: D) -> Result { + use serde::de::Error; + let mut object = Map::deserialize(deserializer)?; + let kind = match object.remove("type") { + Some(Value::String(kind)) => kind, + Some(_) => return Err(D::Error::custom("type must be a string")), + None => return Err(D::Error::missing_field("type")), + }; + if !SOURCE_TYPES.contains(&kind.as_str()) { + return Err(D::Error::custom(format!("unknown source type {kind:?}"))); + } + let settings = match object.remove(&kind) { + Some(Value::Object(settings)) => settings, + Some(_) => return Err(D::Error::custom(format!("{kind} must be an object"))), + None => return Err(D::Error::missing_field("settings named after the type")), + }; + if let Some(other) = object.keys().next() { + return Err(D::Error::custom(format!( + "unknown field {other:?} of a {kind} source" + ))); + } + Ok(Source { kind, settings }) + } +} + +/// An input drawn from another source: the path of input names of the flake +/// in the source drawing it, and the name of the source it's drawn from. +#[derive(Debug, PartialEq)] +pub struct Follows { + pub path: Vec, + pub source: String, +} + +impl Source { + /// The inputs this source draws from other sources, like krops' lib's + /// flakeFollows. + pub fn follows(&self) -> Vec { + fn walk(prefix: &[String], inputs: &Map, out: &mut Vec) { + for (name, input) in inputs { + let mut path = prefix.to_vec(); + path.push(name.clone()); + if let Some(Value::String(source)) = input.get("follows") { + out.push(Follows { + path: path.clone(), + source: source.clone(), + }); + } + if let Some(Value::Object(inputs)) = input.get("inputs") { + walk(&path, inputs, out); + } + } + } + let mut out = Vec::new(); + if let Some(Value::Object(inputs)) = self.settings.get("inputs") { + walk(&[], inputs, &mut out); + } + out + } +} -- cgit v1.3.1