aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pkgs/populate/docs/design.md16
-rw-r--r--pkgs/populate/src/flake.rs1351
-rw-r--r--pkgs/populate/src/populate.rs2
-rw-r--r--pkgs/populate/src/sh.rs261
4 files changed, 1021 insertions, 609 deletions
diff --git a/pkgs/populate/docs/design.md b/pkgs/populate/docs/design.md
index 3340ccf..9e606b2 100644
--- a/pkgs/populate/docs/design.md
+++ b/pkgs/populate/docs/design.md
@@ -177,12 +177,12 @@ Populate runs rsync, Git, ssh, and Nix as programs, found in `PATH`.
## Status
The program delivers all source types, draws inputs from sources, makes
-backups, and prunes. Drawing inputs, in `src/flake.rs`, builds the same
-shell code that `shell.nix` generates, with every value quoted; it moves
-to Rust like delivering flake and flake-input sources did, in
-`src/flake_input.rs`, which reads what Nix prints as JSON itself, and runs
-fixed snippets on the target that send JSON or store paths back.
+backups, and prunes. It decides what to do itself: the snippets it runs
+on the target send back what it needs to know, e.g. what Nix prints as
+JSON, where paths resolve to, or store paths. Delivering flake and
+flake-input sources is in `src/flake_input.rs`, drawing inputs and
+deriving copies in `src/flake.rs`.
-The jq programs in `flake-jq.nix`, which the shell implementation and the
-jq test use, have equivalents in Rust, with the jq test's cases as unit
-tests.
+The jq programs and Nix functions in `flake-jq.nix`, which the shell
+implementation and the jq test use, have equivalents in Rust, with unit
+tests, among them the jq test's cases.
diff --git a/pkgs/populate/src/flake.rs b/pkgs/populate/src/flake.rs
index f03e443..b6b0790 100644
--- a/pkgs/populate/src/flake.rs
+++ b/pkgs/populate/src/flake.rs
@@ -1,37 +1,20 @@
-//! Drawing inputs of flakes in sources from other sources.
+//! Drawing inputs of flakes in sources from other sources, and deriving
+//! what flake and flake-input sources that get patched, get their lock
+//! files rewritten, or are materialized deliver.
//!
-//! This builds the same shell code as shell.nix generates, with every value
-//! quoted, so both implementations behave alike while the code moves to
-//! Rust piece by piece.
+//! Populate decides what to do. The target runs the fixed snippets of
+//! sh::draw, which send back what populate needs to know: where paths
+//! resolve to, the lock files of flakes, and what tells whether a derived
+//! copy needs to be made again.
-use crate::run::{quote as q, Context};
+use crate::run::{Context, Failure, Result};
use crate::sh;
use crate::spec::{Follows, Source};
use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet};
-/// Nix function returning 1 if the lock file locks has an input at path, a
-/// list of input names, resolving inputs that follow others, and else 0.
-/// Like lockHasInput in flake-jq.nix, for the shell code drawing inputs.
-pub const LOCK_HAS_INPUT: &str = r#"locks: path: let
- nodes = locks.nodes;
- input = key: name: (nodes.${key}.inputs or {}).${name} or null;
- # The node an input refers to, following follows from the root.
- node = ref:
- if ref == null then null
- else if builtins.isList ref then
- builtins.foldl'
- (key: name: if key == null then null else node (input key name))
- locks.root
- ref
- else ref;
- has = key: path:
- if path == [] then true
- else let next = node (input key (builtins.head path)); in
- next != null && has next (builtins.tail path);
-in
- if has locks.root path then "1" else "0"
-"#;
+/// What needs flakes, for the error if Nix on the target lacks them.
+const DRAWING: &str = "drawing flake inputs from sources";
fn bool_setting(settings: &Map<String, Value>, key: &str, default: bool) -> bool {
settings
@@ -69,630 +52,676 @@ pub fn derives_flake(source: &Source) -> bool {
|| (bool_setting(settings, "rewriteLock", true) && !source.follows().is_empty()))
}
-fn use_state(sentinel: &str, names: &[&str]) -> String {
- let mut call = format!("use_state {}", q(sentinel));
- for name in names {
- call.push(' ');
- call.push_str(&q(name));
+/// A path on the target, where it resolves to, and what's there.
+#[derive(Debug, Default)]
+struct Resolved {
+ path: String,
+ dir: bool,
+ git: bool,
+ submodules: bool,
+ flake: bool,
+}
+
+/// What a path is in a URL: with every byte percent-encoded that isn't a
+/// letter, a digit, or one of -._~/. Nix decodes that, and otherwise
+/// takes, e.g., ? and # for delimiters, and refuses blanks.
+fn url_path(path: &str) -> String {
+ path.bytes()
+ .map(|b| {
+ if b.is_ascii_alphanumeric() || b"-._~/".contains(&b) {
+ (b as char).to_string()
+ } else {
+ format!("%{b:02x}")
+ }
+ })
+ .collect()
+}
+
+/// Source names become input names in generated flakes, where Nix accepts
+/// fewer names than populate. Others get replaced by one derived from
+/// them.
+fn input_name(name: &str) -> String {
+ let ok = name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
+ && name
+ .chars()
+ .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
+ && name != "self";
+ if ok {
+ name.to_string()
+ } else {
+ format!("source-{}", &sha256_hex(name.as_bytes())[..16])
}
- call
}
-/// Draws inputs of flakes in sources from other sources, and derives what
-/// the target paths of flake and flake-input sources that get patched, get
-/// their lock files rewritten, or are materialized get. Sources get
-/// processed in the order they draw inputs from each other. Returns the
-/// shell code to run on the target, which also removes state that sources
-/// don't need anymore.
-///
-/// Inputs get drawn by rewriting the lock file of the flake in the source
-/// with nix flake lock --override-input: in place for file and git sources,
-/// and in a copy for flake and flake-input sources. Sources with
-/// rewriteLock set to false, and symlink sources, get a generated flake in
-/// .populate/flakes/<name> instead, which re-exports the outputs of the
-/// flake in the source, with those inputs drawn from the other sources.
-/// Lock files get rewritten or generated on every run, so they match the
-/// sources.
-pub fn flakes(context: &Context, dir: &str, sources: &BTreeMap<String, Source>) -> String {
- let sentinel = format!("{dir}/.populate");
- let flakes_dir = format!("{sentinel}/flakes");
- let derived_dir = format!("{sentinel}/derived");
- let patches_dir = format!("{sentinel}/patches");
- let follows: BTreeMap<&str, Vec<Follows>> = sources
- .iter()
- .map(|(n, s)| (n.as_str(), s.follows()))
- .collect();
- let drawing: BTreeSet<&str> = follows
- .iter()
- .filter(|(_, f)| !f.is_empty())
- .map(|(n, _)| *n)
- .collect();
- let generated: BTreeSet<&str> = sources
- .iter()
- .filter(|(_, s)| has_generated_flake(s))
- .map(|(n, _)| n.as_str())
- .collect();
- let derived: BTreeSet<&str> = sources
- .iter()
- .filter(|(_, s)| derives_flake(s))
- .map(|(n, _)| n.as_str())
- .collect();
- let patched: BTreeSet<&str> = sources
- .iter()
- .filter(|(_, s)| !patches(&s.settings).is_empty())
- .map(|(n, _)| n.as_str())
- .collect();
- let names: BTreeSet<&str> = drawing.union(&derived).copied().collect();
+/// The follows of a source that draws the input at path on the way to it,
+/// if any: the one with the longest path that path starts with.
+fn parent_of<'a>(follows: &'a [Follows], path: &[String]) -> Option<&'a Follows> {
+ (1..path.len())
+ .rev()
+ .find_map(|n| follows.iter().find(|f| f.path == path[..n]))
+}
- // Source names become input names in generated flakes, where Nix
- // accepts fewer names than populate. Others get replaced by one
- // derived from them.
- let input_name = |name: &str| -> String {
- let ok = name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
- && name
- .chars()
- .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
- && name != "self";
- if ok {
- name.to_string()
- } else {
- format!("source-{}", &sha256_hex(name.as_bytes())[..16])
- }
- };
- let drawn_from =
- |name: &str| -> Vec<String> { follows[name].iter().map(|f| f.source.clone()).collect() };
- let sorted = toposort(&names, &drawn_from);
+/// Where the input at path of the flake in the source named name is in the
+/// flake that the source's inputs get drawn into: the flake itself, or its
+/// generated flake. On the way, inputs that get drawn from sources are the
+/// flakes of those sources, where drawing went on, so the path continues
+/// there. A generated flake has its source and the sources that one draws
+/// from as inputs, and nothing else, so the path names them there.
+fn drawn_path(
+ name: &str,
+ path: &[String],
+ follows: &BTreeMap<&str, Vec<Follows>>,
+ generated: &BTreeSet<&str>,
+) -> Vec<String> {
+ let parent = parent_of(&follows[name], path);
+ let rest = |p: &Follows| drawn_path(&p.source, &path[p.path.len()..], follows, generated);
+ match (generated.contains(name), parent) {
+ (true, None) => [vec![input_name(name)], path.to_vec()].concat(),
+ (true, Some(p)) => [vec![input_name(&p.source)], rest(p)].concat(),
+ (false, None) => path.to_vec(),
+ (false, Some(p)) => [p.path.clone(), rest(p)].concat(),
+ }
+}
- // Shell word for the URL of the flake that inputs drawn from a source
- // use: its generated flake, if it has one, so overrides don't need to
- // be repeated, or else the flake in the source. Nix refuses path: URLs
- // with symlinks on the way, which the target directory may be, so paths
- // get resolved.
- let url = |name: &str| -> String {
- if generated.contains(name) {
- format!(
- "\"path:$(url_path \"$(readlink -f {})\")\"",
- q(&format!("{flakes_dir}/{name}"))
- )
- } else {
- format!("\"$(flake_url {})\"", q(name))
- }
- };
+/// The inputs that drawing the input at path of the flake in the source
+/// named name needs to exist, as (name, path) of the source whose flake
+/// has the input at path.
+fn input_checks(
+ name: &str,
+ path: &[String],
+ follows: &BTreeMap<&str, Vec<Follows>>,
+) -> Vec<(String, Vec<String>)> {
+ match parent_of(&follows[name], path) {
+ None => vec![(name.to_string(), path.to_vec())],
+ Some(p) => [
+ vec![(name.to_string(), p.path.clone())],
+ input_checks(&p.source, &path[p.path.len()..], follows),
+ ]
+ .concat(),
+ }
+}
- // Where the input at path of the flake in the source named name is in
- // the flake that the source's inputs get drawn into: the flake itself,
- // or its generated flake. On the way, inputs that get drawn from
- // sources are the flakes of those sources, where drawing went on, so
- // the path continues there. A generated flake has its source and the
- // sources that one draws from as inputs, and nothing else, so the path
- // names them there.
- fn parent_of<'a>(follows: &'a [Follows], path: &[String]) -> Option<&'a Follows> {
- (1..path.len())
- .rev()
- .find_map(|n| follows.iter().find(|f| f.path == path[..n]))
+/// Whether the lock file locks, as nix flake metadata prints it, has an
+/// input at path, a list of input names, resolving inputs that follow
+/// others.
+fn lock_has_input(locks: &Value, path: &[String]) -> bool {
+ let nodes = &locks["nodes"];
+ let Some(root) = locks["root"].as_str() else {
+ return false;
+ };
+ let input = |key: &str, name: &str| -> Option<&Value> {
+ Some(&nodes[key]["inputs"][name]).filter(|input| !input.is_null())
+ };
+ // The node an input refers to, following follows from the root.
+ fn node<'a>(
+ reference: Option<&'a Value>,
+ root: &'a str,
+ input: &dyn Fn(&str, &str) -> Option<&'a Value>,
+ depth: usize,
+ ) -> Option<&'a str> {
+ if depth > 64 {
+ return None;
+ }
+ match reference? {
+ Value::String(key) => Some(key),
+ Value::Array(names) => names.iter().try_fold(root, |key, name| {
+ node(input(key, name.as_str()?), root, input, depth + 1)
+ }),
+ _ => None,
+ }
}
- fn drawn_path(
- name: &str,
- path: &[String],
- follows: &BTreeMap<&str, Vec<Follows>>,
- generated: &BTreeSet<&str>,
- input_name: &dyn Fn(&str) -> String,
- ) -> Vec<String> {
- let parent = parent_of(&follows[name], path);
- match (generated.contains(name), parent) {
- (true, None) => [vec![input_name(name)], path.to_vec()].concat(),
- (true, Some(p)) => [
- vec![input_name(&p.source)],
- drawn_path(
- &p.source,
- &path[p.path.len()..],
- follows,
- generated,
- input_name,
- ),
- ]
- .concat(),
- (false, None) => path.to_vec(),
- (false, Some(p)) => [
- p.path.clone(),
- drawn_path(
- &p.source,
- &path[p.path.len()..],
- follows,
- generated,
- input_name,
- ),
- ]
- .concat(),
+ let mut key = root;
+ for name in path {
+ match node(input(key, name), root, &input, 0) {
+ Some(next) => key = next,
+ None => return false,
}
}
- let drawn_path =
- |name: &str, path: &[String]| drawn_path(name, path, &follows, &generated, &input_name);
+ true
+}
- // The inputs that drawing the input at path of the flake in the source
- // named name needs to exist, as (name, path) of the source whose flake
- // has the input at path.
- fn input_checks(
- name: &str,
- path: &[String],
- follows: &BTreeMap<&str, Vec<Follows>>,
- ) -> Vec<(String, Vec<String>)> {
- match parent_of(&follows[name], path) {
- None => vec![(name.to_string(), path.to_vec())],
- Some(p) => [
- vec![(name.to_string(), p.path.clone())],
- input_checks(&p.source, &path[p.path.len()..], follows),
- ]
- .concat(),
- }
+/// The store path a path below the store is in, and the rest of the path.
+fn split_store_path(path: &str) -> (&str, &str) {
+ let end = path
+ .match_indices('/')
+ .nth(3)
+ .map(|(i, _)| i)
+ .unwrap_or(path.len());
+ let rest = &path[end..];
+ (&path[..end], rest.strip_prefix('/').unwrap_or(rest))
+}
+
+/// The sources, and what drawing inputs and deriving copies needs to know
+/// about them.
+struct Flakes<'a> {
+ context: &'a Context,
+ dir: &'a str,
+ sources: &'a BTreeMap<String, Source>,
+ follows: BTreeMap<&'a str, Vec<Follows>>,
+ /// The sources drawing inputs from others.
+ drawing: BTreeSet<&'a str>,
+ /// The sources with a generated flake.
+ generated: BTreeSet<&'a str>,
+ /// The sources with derived state.
+ derived: BTreeSet<&'a str>,
+}
+
+impl<'a> Flakes<'a> {
+ fn sentinel(&self) -> String {
+ format!("{}/.populate", self.dir)
}
- // Sets the positional parameters to nix flake lock's arguments for
- // drawing the inputs of the flake in source name from other sources.
- let override_args = |name: &str| -> String {
- let mut text = String::from("set --\n");
- for f in &follows[name] {
- text += &format!(
- "set -- \"$@\" --override-input {} {}\n",
- q(&drawn_path(name, &f.path).join("/")),
- url(&f.source)
- );
- }
- text
- };
+ fn state(&self, kind: &str, name: &str) -> String {
+ format!("{}/{kind}/{name}", self.sentinel())
+ }
- // Rewrites the lock file of the flake in a file or git source in place.
- let rewrite = |name: &str| -> String {
- format!(
- "(\n{}own_lock \"$(readlink -f {})\"\nnix flake lock \"$@\" \"$(flake_url {})\" >&2\n)\n",
- override_args(name),
- q(&format!("{dir}/{name}")),
- q(name)
- )
- };
+ fn path(&self, name: &str) -> String {
+ format!("{}/{name}", self.dir)
+ }
- // Derives what the target path of a flake or flake-input source gets
- // from what got delivered to .populate/derived/<name>/original: a copy,
- // which gets patched and gets its lock file rewritten. Unless
- // materialized, the copy gets added to the store, and the target path
- // becomes a link to it. Copies get made again only if what they derive
- // from changes: materialized ones if the delivered store path, the
- // patches, or what inputs get drawn from which sources changes, and
- // others also if the sources they draw inputs from change.
- let derive = |name: &str| -> String {
- let source = &sources[name];
- let settings = &source.settings;
- let materialize = bool_setting(settings, "materialize", false);
- let rewrite_lock = bool_setting(settings, "rewriteLock", true) && drawing.contains(name);
- let target_path = format!("{dir}/{name}");
- let dir_ = format!("{derived_dir}/{name}");
- let has_patches = !patches(settings).is_empty();
- let follows_json: Vec<Value> = follows[name]
- .iter()
- .map(|f| serde_json::json!({ "path": f.path, "source": f.source }))
- .collect();
- // Like Nix' toJSON: attributes sorted by name.
- let spec = serde_json::to_string(&serde_json::json!({
- "follows": follows_json,
- "materialize": materialize,
- "rewriteLock": rewrite_lock,
- }))
- .expect("serializes");
- let patch_dir = format!("{patches_dir}/{name}");
- let apply_patches = if has_patches {
- format!(
- r#"ls {pd} | while read -r patch; do
- if ! patch -p1 -s --batch --no-backup-if-mismatch \
- -i {pd}/"$patch" >&2; then
- >&2 printf 'error: patch %s does not apply to source %s\n' \
- "$(printf %s "$patch" | cut -c6-)" {name}
- exit 1
- fi
-done
-"#,
- pd = q(&patch_dir),
- name = q(name)
- )
+ /// The shell code of snippet for the target, with the definitions it
+ /// uses, enabling flakes if flakes is set.
+ fn on_target(snippet: &str, flakes: bool) -> String {
+ let require = if flakes {
+ format!("require_flakes {}\n", crate::run::quote(DRAWING))
} else {
String::new()
};
- let remove_work = format!("remove_tree {}", q(&format!("{dir_}/work")));
- let key_patches = if has_patches {
- format!(
- "ls {pd} | while read -r patch; do\n sha256sum {pd}/\"$patch\"\ndone\n",
- pd = q(&patch_dir)
- )
- } else {
- String::new()
- };
- let key_drawn = if rewrite_lock && !materialize {
- let mut text = override_args(name);
- text += "printf '%s\\n' \"$@\"\n# Sources outside the store can change without changing their\n# path.\n";
- let mut seen = BTreeSet::new();
- for source in drawn_from(name) {
- if !seen.insert(source.clone()) {
- continue;
- }
- let source = source.as_str();
- let p = if generated.contains(source) {
- format!("{flakes_dir}/{source}")
- } else {
- format!("{dir}/{source}")
- };
- text += &format!(
- "drawn=$(readlink -f {})\ncase $drawn in\n /nix/store/*) ;;\n *) nix-hash --type sha256 \"$drawn\" ;;\nesac\n",
- q(&p)
- );
- }
- text
- } else {
- String::new()
- };
- let body = if materialize {
- let relock = if rewrite_lock {
- format!(
- "{}own_lock {}\nnix flake lock \"$@\" \"$(flake_url {})\" >&2\n",
- override_args(name),
- q(&target_path),
- q(name)
- )
- } else {
- String::new()
- };
- format!(
- r#"if ! test -e {key} ||
- test "$(cat {key})" != "$key" ||
- test -L {tp} ||
- ! test -d {tp}; then
- cp -R "$original" {work}
- chmod -R u+w {work}
- (
- cd {work}
- {apply_patches}
- )
- remove_foreign_link {tp} {tp_name}
- rm -fR {tp}
- mv {work} {tp}
- write_key "$key"
-fi
-{relock}
-"#,
- key = q(&format!("{dir_}/key")),
- tp = q(&target_path),
- tp_name = q(&context.name(&target_path)),
- work = q(&format!("{dir_}/work")),
- )
- } else {
- let relock = if rewrite_lock {
- format!(
- "{}# Not just the path, which Nix takes for the enclosing Git\n# work tree, if any.\nnix flake lock \"$@\" \"path:$(url_path \"$work\")${{subdir:+?dir=$subdir}}\" >&2\n",
- override_args(name)
- )
- } else {
- String::new()
- };
- format!(
- r#"if test -e {key} &&
- test "$(cat {key})" = "$key" &&
- result=$(readlink {result}) &&
- nix-store --check-validity "$result" 2>/dev/null; then
- :
-else
- # The copy keeps the name of the delivered store path.
- mkdir {work}
- work=$(readlink -f {work})/$(basename "$path" | cut -c34-)
- cp -R "$path" "$work"
- chmod -R u+w "$work"
- (
- cd "$work"/"$subdir"
- {apply_patches}
- )
- {relock}
- result=$(nix-store --add-fixed --recursive sha256 "$work")
- {remove_work}
- nix-store --realise "$result" \
- --add-root {result} --indirect >/dev/null
- write_key "$key"
-fi
-make_room_for_link {tp} {tp_name}
-# .populate/derived/<name>/result roots the store path.
-ln -fns "$result${{subdir:+/$subdir}}" {tp}
-"#,
- key = q(&format!("{dir_}/key")),
- result = q(&format!("{dir_}/result")),
- work = q(&format!("{dir_}/work")),
- tp = q(&target_path),
- tp_name = q(&context.name(&target_path)),
- )
- };
format!(
- r#"(
- # Writes the key $1 to a new file, which then replaces the key, so
- # nothing at its place gets written through, e.g. a symlink.
- write_key() {{
- key_file=$(mktemp {key}.XXXXXX)
- printf '%s\n' "$1" > "$key_file"
- mv -f "$key_file" {key}
- }}
- original=$(readlink -f {original})
- path=$(printf '%s\n' "$original" | cut -d/ -f1-4)
- subdir=${{original#"$path"}}
- subdir=${{subdir#/}}
- key=$(
- {{
- printf '%s\n' "$original" {spec}
- {key_patches}
- {key_drawn}
- }} | sha256sum | cut -c-64
- )
- {remove_work}
- {body}
-)
-"#,
- key = q(&format!("{dir_}/key")),
- original = q(&format!("{dir_}/original")),
- spec = q(&spec),
+ "{}{}{require}{snippet}",
+ sh::prelude(),
+ sh::draw::DEFINITIONS
)
- };
+ }
- // Generates the flake of a source in .populate/flakes/<name>.
- let generate = |name: &str| -> String {
- let mut drawn: Vec<&str> = Vec::new();
- for f in &follows[name] {
- if !drawn.contains(&f.source.as_str()) {
- drawn.push(&f.source);
- }
+ fn run(&self, snippet: &str, flakes: bool, args: &[&str]) -> Result<()> {
+ self.context.run(&Self::on_target(snippet, flakes), args)
+ }
+
+ /// What snippet prints, as the fields ending in null bytes.
+ fn fields(&self, snippet: &str, flakes: bool, args: &[&str]) -> Result<Vec<String>> {
+ let output = self
+ .context
+ .output(&Self::on_target(snippet, flakes), args)?;
+ let mut fields: Vec<String> = output.split('\0').map(str::to_string).collect();
+ // What follows the last null byte.
+ fields.pop();
+ Ok(fields)
+ }
+
+ /// Resolves paths on the target.
+ fn resolve(&self, paths: &[String], flakes: bool) -> Result<Vec<Resolved>> {
+ let args: Vec<&str> = paths.iter().map(String::as_str).collect();
+ let fields = self.fields(sh::draw::RESOLVE, flakes, &args)?;
+ if fields.len() != 2 * paths.len() {
+ return Err(Failure::Message(
+ "error: can't resolve paths on the target".to_string(),
+ ));
}
- let json = |s: &str| serde_json::to_string(s).expect("serializes");
- // Nix ignores overrides below an input that follows another input,
- // so an input of an input drawn from a source gets overridden at
- // that source.
- let override_line = |f: &Follows| -> String {
- format!(
- "inputs.{}.follows = {};",
- drawn_path(name, &f.path)
- .iter()
- .map(|n| json(n))
- .collect::<Vec<_>>()
- .join(".inputs."),
- json(&input_name(&f.source))
- )
- };
- let mut text = format!("(\nurl=$(flake_url {})\n", q(name));
- for (i, source) in drawn.iter().enumerate() {
- text += &format!("url{}={}\n", i + 1, url(source));
+ Ok(fields
+ .chunks(2)
+ .map(|field| Resolved {
+ path: field[1].clone(),
+ dir: field[0].contains('d'),
+ git: field[0].contains('g'),
+ submodules: field[0].contains('s'),
+ flake: field[0].contains('f'),
+ })
+ .collect())
+ }
+
+ /// The flake URL of the source named name, at resolved.
+ fn flake_url(name: &str, resolved: &Resolved) -> Result<String> {
+ if !resolved.dir {
+ return Err(Failure::Message(format!(
+ "error: source {name} is not a directory"
+ )));
}
- let fd = q(&format!("{flakes_dir}/{name}"));
- text += &format!("rm -fR {fd}\nmkdir {fd}\ncd {fd}\n{{\n echo '{{'\n");
- text += &format!(
- " printf ' inputs.%s.url = \"%s\";\\n' {} \"$url\"\n",
- q(&json(&input_name(name)))
- );
- for (i, source) in drawn.iter().enumerate() {
- text += &format!(
- " printf ' inputs.%s.url = \"%s\";\\n' {} \"$url{}\"\n",
- q(&json(&input_name(source))),
- i + 1
- );
- if !generated.contains(source) {
- text += &format!(
- " if ! test -e \"$(readlink -f {})\"/flake.nix; then\n printf ' inputs.%s.flake = false;\\n' {}\n fi\n",
- q(&format!("{dir}/{source}")),
- q(&json(&input_name(source)))
- );
+ let path = url_path(&resolved.path);
+ Ok(if resolved.git && resolved.submodules {
+ format!("git+file:{path}?submodules=1")
+ } else if resolved.git {
+ format!("git+file:{path}")
+ } else {
+ format!("path:{path}")
+ })
+ }
+
+ /// The flake URL of the source named name, as it is now.
+ fn own_url(&self, name: &str) -> Result<String> {
+ let resolved = self.resolve(&[self.path(name)], false)?;
+ Self::flake_url(name, &resolved[0])
+ }
+
+ /// The sources that the source named name draws inputs from, each
+ /// once.
+ fn drawn_from(&self, name: &str) -> Vec<&'a str> {
+ let mut drawn: Vec<&'a str> = Vec::new();
+ for f in &self.follows[name] {
+ let source = self
+ .sources
+ .get_key_value(&f.source)
+ .map(|(k, _)| k.as_str());
+ if let Some(source) = source.filter(|s| !drawn.contains(s)) {
+ drawn.push(source);
}
}
- for f in &follows[name] {
- text += &format!(" printf ' %s\\n' {}\n", q(&override_line(f)));
- }
- text += &format!(
- " printf ' outputs = inputs: inputs.%s;\\n' {}\n echo '}}'\n}} > flake.nix\n",
- q(&json(&input_name(name)))
- );
- text += "# Not just ., which Nix takes for the enclosing Git work tree, if any.\nnix flake lock \"path:$(url_path \"$(pwd -P)\")\" >&2\n)\n";
- text
- };
+ drawn
+ }
- let process = |name: &str| -> String {
- let mut text = String::new();
- if derived.contains(name) {
- text += &derive(name);
- }
- if generated.contains(name) {
- text += &generate(name);
- } else if drawing.contains(name) && !derived.contains(name) {
- text += &rewrite(name);
+ /// Where the flake that inputs drawn from the source named name use
+ /// is: its generated flake, if it has one, so overrides don't need to
+ /// be repeated, or else the flake in the source.
+ fn drawn_flake(&self, name: &str) -> String {
+ if self.generated.contains(name) {
+ self.state("flakes", name)
+ } else {
+ self.path(name)
}
- text
- };
+ }
- // Removes the entries of the state directory that sources named names
- // don't need anymore, and the directory once it's empty.
- let remove_stale = |state_dir: &str, names: &BTreeSet<&str>| -> String {
- let mut call = format!("remove_others {} ''", q(state_dir));
- for name in names {
- call += &format!(" {}", q(name));
+ /// The URLs of the flakes that inputs drawn from the sources use, and
+ /// whether they have a flake.nix. Nix refuses path: URLs with
+ /// symlinks on the way, which the target directory may be, so paths
+ /// get resolved.
+ fn drawn_urls(&self, sources: &[&str]) -> Result<BTreeMap<String, (String, bool)>> {
+ let paths: Vec<String> = sources.iter().map(|s| self.drawn_flake(s)).collect();
+ let resolved = self.resolve(&paths, false)?;
+ let mut urls = BTreeMap::new();
+ for (source, resolved) in sources.iter().zip(resolved) {
+ let url = if self.generated.contains(source) {
+ format!("path:{}", url_path(&resolved.path))
+ } else {
+ Self::flake_url(source, &resolved)?
+ };
+ // A generated flake is a flake.
+ let flake = resolved.flake || self.generated.contains(source);
+ urls.insert(source.to_string(), (url, flake));
}
- format!(
- "if test -d {d}; then\n {call}\n if test -z \"$(ls -A {d})\"; then\n rmdir {d}\n fi\nfi\n",
- d = q(state_dir)
- )
- };
+ Ok(urls)
+ }
- let all_names: Vec<&str> = sources.keys().map(String::as_str).collect();
- let mut text = format!("set -efu\n{}\n", use_state(&sentinel, &all_names));
- if !names.is_empty() {
- if !drawing.is_empty() {
- text += "require_flakes 'drawing flake inputs from sources'\n";
+ /// nix flake lock's arguments for drawing the inputs of the flake in
+ /// the source named name from other sources.
+ fn override_args(&self, name: &str) -> Result<Vec<String>> {
+ let urls = self.drawn_urls(&self.drawn_from(name))?;
+ let mut args = Vec::new();
+ for f in &self.follows[name] {
+ args.push("--override-input".to_string());
+ args.push(drawn_path(name, &f.path, &self.follows, &self.generated).join("/"));
+ args.push(urls[&f.source].0.clone());
}
- text += &format!(
- r#"
-# Nix rewrites lock files in place, which also changes what else is a hard
-# link to the lock file, e.g. in a backup, or what it is a symlink to. So
-# the lock file of the flake in the directory $1 gets replaced by a copy
-# first. The copy goes to a new file, so nothing that's there already gets
-# written through.
-own_lock() {{
- if test -e "$1"/flake.lock; then
- own_lock_copy=$(mktemp "$1"/.flake.lock.XXXXXX)
- cp -p "$1"/flake.lock "$own_lock_copy"
- mv -f "$own_lock_copy" "$1"/flake.lock
- fi
-}}
-
-# Prints the flake URL of the source named $1.
-flake_url() {{
- path=$(readlink -f {dir}/"$1")
- if ! test -d "$path"; then
- >&2 printf 'error: source %s is not a directory\n' "$1"
- exit 1
- fi
- if test -e "$path"/.git; then
- if test -e "$path"/.gitmodules; then
- printf 'git+file:%s?submodules=1' "$(url_path "$path")"
- else
- printf 'git+file:%s' "$(url_path "$path")"
- fi
- else
- printf 'path:%s' "$(url_path "$path")"
- fi
-}}
+ Ok(args)
+ }
-# Inputs can only be drawn into flakes, so check all sources before
-# changing anything.
-missing=
-"#,
- dir = q(dir)
- );
- for name in &drawing {
- let p = if derived.contains(name) {
- format!("{derived_dir}/{name}/original")
+ /// Fails unless each source drawing inputs contains a flake that has
+ /// the inputs drawn, before anything changes. Nix ignores inputs drawn
+ /// that a flake doesn't have, with a warning, so a misspelt input name
+ /// would draw nothing.
+ fn check(&self) -> Result<()> {
+ // The flake in a source as delivered.
+ let delivered = |name: &str| {
+ if self.derived.contains(name) {
+ format!("{}/original", self.state("derived", name))
} else {
- format!("{dir}/{name}")
- };
- text += &format!(
- "if ! test -e {}/flake.nix; then\n missing=\"$missing \"{}\nfi\n",
- q(&p),
- q(name)
- );
+ self.path(name)
+ }
+ };
+ let drawing: Vec<&str> = self.drawing.iter().copied().collect();
+ let paths: Vec<String> = drawing.iter().map(|n| delivered(n)).collect();
+ let resolved = self.resolve(&paths, true)?;
+ let missing: Vec<String> = drawing
+ .iter()
+ .zip(&resolved)
+ .filter(|(_, resolved)| !resolved.flake)
+ .map(|(name, _)| {
+ format!("error: source {name} draws flake inputs from other sources, but contains no flake.nix")
+ })
+ .collect();
+ if !missing.is_empty() {
+ return Err(Failure::Message(missing.join("\n")));
}
- let derived_names: Vec<&str> = derived.iter().copied().collect();
- text += &format!(
- r#"if test -n "$missing"; then
- >&2 printf 'error: source %s draws flake inputs from other sources, but contains no flake.nix\n' $missing
- exit 1
-fi
-# Nix ignores inputs drawn that the flake doesn't have, with a warning, so a
-# misspelt input name would draw nothing. Prints the flake URL of the
-# flake in the source named $1 as delivered.
-own_flake_url() {{
- case " "{derived_names}" " in
- *" $1 "*)
- printf 'path:%s' "$(url_path "$(readlink -f {derived_dir}/"$1"/original)")"
- ;;
- *)
- flake_url "$1"
- ;;
- esac
-}}
-# The lock files of the flakes in the sources, as Nix locks them now, with
-# an empty flake in place of each input drawn from a source, which may not
-# be reachable, as the one the flake's lock file pins.
-locks_dir=$(mktemp -d)
-echo '{{ outputs = _: {{}}; }}' > "$locks_dir"/flake.nix
-"#,
- derived_names = q(&derived_names.join(" ")),
- derived_dir = q(&derived_dir),
- );
+ // The lock files of the flakes in the sources, as Nix locks them
+ // now, with an empty flake in place of each input drawn from a
+ // source, which may not be reachable, as the one the flake's lock
+ // file pins.
let mut locked: Vec<String> = Vec::new();
for name in &drawing {
- for f in &follows[*name] {
- for (c, _) in input_checks(name, &f.path, &follows) {
+ for f in &self.follows[name] {
+ for (c, _) in input_checks(name, &f.path, &self.follows) {
if !locked.contains(&c) {
locked.push(c);
}
}
}
}
- for name in &locked {
- text += "set --\n";
- for f in &follows[name.as_str()] {
- text += &format!(
- "set -- \"$@\" --override-input {} \"path:$locks_dir\"\n",
- q(&f.path.join("/"))
- );
- }
- text += &format!(
- r#"if ! nix flake metadata --json --no-write-lock-file "$@" \
- "$(own_flake_url {n})" > "$locks_dir"/{n}.json; then
- >&2 printf 'error: source %s draws inputs from other sources, but its flake can'"'"'t be locked\n' \
- {n}
- exit 1
-fi
-"#,
- n = q(name)
- );
+ let paths: Vec<String> = locked.iter().map(|n| delivered(n)).collect();
+ let resolved = self.resolve(&paths, false)?;
+ let mut locks = BTreeMap::new();
+ for (name, resolved) in locked.iter().zip(&resolved) {
+ let url = if self.derived.contains(name.as_str()) {
+ format!("path:{}", url_path(&resolved.path))
+ } else {
+ Self::flake_url(name, resolved)?
+ };
+ let overrides: Vec<String> = self.follows[name.as_str()]
+ .iter()
+ .map(|f| f.path.join("/"))
+ .collect();
+ let mut args = vec![url.as_str(), name.as_str()];
+ args.extend(overrides.iter().map(String::as_str));
+ let output = self
+ .context
+ .output(&Self::on_target(sh::draw::METADATA, true), &args)?;
+ let metadata: Value = serde_json::from_str(&output).map_err(|e| {
+ Failure::Message(format!("error: can't read flake metadata of {url}: {e}"))
+ })?;
+ locks.insert(name.clone(), metadata["locks"].clone());
}
- text += &format!(
- r#"# Fails, naming the input, unless the flake in the source named $1 has
-# an input at the path $2, a JSON list of input names. $3 is the source
-# that the input $4 of the source $5 gets drawn from.
-require_input() {{
- if ! test "$(
- METADATA=$(cat "$locks_dir/$1.json") INPUT=$2 nix eval --impure --raw --expr {expr}
- )" = 1; then
- if test "$1" = "$5"; then
- >&2 printf 'error: source %s draws input %s from source %s, but its flake has no input %s\n' \
- "$5" "$4" "$3" "$4"
- else
- >&2 printf 'error: source %s draws input %s from source %s, but the flake of source %s has no input %s\n' \
- "$5" "$4" "$3" "$1" "$(printf %s "$2" | tr -d '[]"' | tr , /)"
- fi
- exit 1
- fi
-}}
-"#,
- expr = q(&format!(
- "({LOCK_HAS_INPUT})\n (builtins.fromJSON (builtins.getEnv \"METADATA\")).locks\n (builtins.fromJSON (builtins.getEnv \"INPUT\"))\n"
- ))
- );
+
for name in &drawing {
- for f in &follows[*name] {
- for (c, path) in input_checks(name, &f.path, &follows) {
- text += &format!(
- "require_input {} {} \\\n {} {} {}\n",
- q(&c),
- q(&serde_json::to_string(&path).expect("serializes")),
- q(&f.source),
- q(&f.path.join("/")),
- q(name)
- );
+ for f in &self.follows[name] {
+ let input = f.path.join("/");
+ for (c, path) in input_checks(name, &f.path, &self.follows) {
+ if lock_has_input(&locks[&c], &path) {
+ continue;
+ }
+ let source = &f.source;
+ return Err(Failure::Message(if c == *name {
+ format!("error: source {name} draws input {input} from source {source}, but its flake has no input {input}")
+ } else {
+ format!(
+ "error: source {name} draws input {input} from source {source}, but the flake of source {c} has no input {}",
+ path.join("/")
+ )
+ }));
}
}
}
- text += "rm -R \"$locks_dir\"\n\n";
- if !generated.is_empty() {
- text += &format!("mkdir -p {}\n", q(&flakes_dir));
+ Ok(())
+ }
+
+ /// Rewrites the lock file of the flake in a file or git source in
+ /// place.
+ fn rewrite(&self, name: &str) -> Result<()> {
+ let args = self.override_args(name)?;
+ let resolved = self.resolve(&[self.path(name)], false)?;
+ let url = Self::flake_url(name, &resolved[0])?;
+ let mut all = vec![resolved[0].path.as_str(), url.as_str()];
+ all.extend(args.iter().map(String::as_str));
+ self.run(sh::draw::LOCK, true, &all)
+ }
+
+ /// Derives what the target path of a flake or flake-input source gets
+ /// from what got delivered to .populate/derived/<name>/original: a
+ /// copy, which gets patched and gets its lock file rewritten. Unless
+ /// materialized, the copy gets added to the store, and the target path
+ /// becomes a link to it. Copies get made again only if what they
+ /// derive from changes: materialized ones if the delivered store path,
+ /// the patches, or what inputs get drawn from which sources changes,
+ /// and others also if the sources they draw inputs from change. The
+ /// key of a copy, in .populate/derived/<name>/key, is the hash of all
+ /// that.
+ fn derive(&self, name: &str) -> Result<()> {
+ let settings = &self.sources[name].settings;
+ let materialize = bool_setting(settings, "materialize", false);
+ let rewrite_lock =
+ bool_setting(settings, "rewriteLock", true) && self.drawing.contains(name);
+ let target_path = self.path(name);
+ let target_name = self.context.name(&target_path);
+ let state = self.state("derived", name);
+ let patch_dir = if patches(settings).is_empty() {
+ String::new()
+ } else {
+ self.state("patches", name)
+ };
+ let args = if rewrite_lock {
+ self.override_args(name)?
+ } else {
+ Vec::new()
+ };
+ // Sources outside the store can change without changing their
+ // path.
+ let drawn: Vec<String> = if rewrite_lock && !materialize {
+ self.drawn_from(name)
+ .iter()
+ .map(|s| self.drawn_flake(s))
+ .collect()
+ } else {
+ Vec::new()
+ };
+
+ let mut state_args = vec![state.as_str(), target_path.as_str(), patch_dir.as_str()];
+ state_args.extend(drawn.iter().map(String::as_str));
+ let fields = self.fields(sh::draw::DERIVED_STATE, false, &state_args)?;
+ let malformed =
+ || Failure::Message(format!("error: can't read the state of source {name}"));
+ let (head, rest) = fields.split_at_checked(4).ok_or_else(malformed)?;
+ let [original, key_file, result, target_kind] = head else {
+ return Err(malformed());
+ };
+ let separator = rest
+ .iter()
+ .position(String::is_empty)
+ .ok_or_else(malformed)?;
+ let (patch_sums, hashes) = (&rest[..separator], &rest[separator + 1..]);
+ if hashes.len() != drawn.len() {
+ return Err(malformed());
+ }
+
+ // Like Nix' toJSON: attributes sorted by name.
+ let follows: Vec<Value> = self.follows[name]
+ .iter()
+ .map(|f| serde_json::json!({ "path": f.path, "source": f.source }))
+ .collect();
+ let spec = serde_json::json!({
+ "follows": follows,
+ "materialize": materialize,
+ "rewriteLock": rewrite_lock,
+ });
+ let mut text = format!("{original}\n{spec}\n");
+ for sum in patch_sums {
+ text += &format!("{sum}\n");
+ }
+ if !drawn.is_empty() {
+ for arg in &args {
+ text += &format!("{arg}\n");
+ }
+ for hash in hashes.iter().filter(|h| !h.is_empty()) {
+ text += &format!("{hash}\n");
+ }
+ }
+ let key = sha256_hex(text.as_bytes());
+ let unchanged = *key_file == key;
+
+ if materialize {
+ if !unchanged || target_kind != "directory" {
+ let work = format!("{state}/work");
+ let key_path = format!("{state}/key");
+ self.run(
+ sh::draw::MATERIALIZE,
+ false,
+ &[
+ original,
+ &work,
+ &target_path,
+ &target_name,
+ &patch_dir,
+ name,
+ &key_path,
+ &key,
+ ],
+ )?;
+ }
+ if rewrite_lock {
+ let url = self.own_url(name)?;
+ let mut all = vec![target_path.as_str(), url.as_str()];
+ all.extend(args.iter().map(String::as_str));
+ self.run(sh::draw::LOCK, true, &all)?;
+ }
+ return Ok(());
}
- for name in &sorted {
- text += &process(name);
+
+ let (path, subdir) = split_store_path(original);
+ let result = if unchanged { result.as_str() } else { "" };
+ let mut all = vec![
+ state.as_str(),
+ path,
+ subdir,
+ &target_path,
+ &target_name,
+ result,
+ &patch_dir,
+ name,
+ &key,
+ ];
+ all.extend(args.iter().map(String::as_str));
+ self.run(sh::draw::ADD, rewrite_lock, &all)
+ }
+
+ /// Generates the flake of a source in .populate/flakes/<name>, which
+ /// re-exports the outputs of the flake in the source, with inputs drawn
+ /// from other sources.
+ fn generate(&self, name: &str) -> Result<()> {
+ let json = |s: &str| serde_json::to_string(s).expect("serializes");
+ let url = self.own_url(name)?;
+ let drawn = self.drawn_from(name);
+ let urls = self.drawn_urls(&drawn)?;
+ let mut text = format!(
+ "{{\n inputs.{}.url = \"{url}\";\n",
+ json(&input_name(name))
+ );
+ for source in &drawn {
+ let (url, flake) = &urls[*source];
+ let input = json(&input_name(source));
+ text += &format!(" inputs.{input}.url = \"{url}\";\n");
+ if !flake {
+ text += &format!(" inputs.{input}.flake = false;\n");
+ }
+ }
+ // Nix ignores overrides below an input that follows another input,
+ // so an input of an input drawn from a source gets overridden at
+ // that source.
+ for f in &self.follows[name] {
+ let path: Vec<String> = drawn_path(name, &f.path, &self.follows, &self.generated)
+ .iter()
+ .map(|n| json(n))
+ .collect();
+ text += &format!(
+ " inputs.{}.follows = {};\n",
+ path.join(".inputs."),
+ json(&input_name(&f.source))
+ );
}
+ text += &format!(
+ " outputs = inputs: inputs.{};\n}}\n",
+ json(&input_name(name))
+ );
+ self.run(
+ sh::draw::GENERATE,
+ true,
+ &[&self.state("flakes", name), &text],
+ )
+ }
+
+ /// Removes the state that sources don't need anymore.
+ fn clean_up(&self) -> Result<()> {
+ let list = |names: &mut dyn Iterator<Item = &str>| names.collect::<Vec<_>>().join(" ");
+ let all = list(&mut self.sources.keys().map(String::as_str));
+ let not_flakes = list(
+ &mut self
+ .sources
+ .iter()
+ .filter(|(_, s)| !matches!(s.kind.as_str(), "flake" | "flake-input"))
+ .map(|(n, _)| n.as_str()),
+ );
+ let generated = list(&mut self.generated.iter().copied());
+ let derived = list(&mut self.derived.iter().copied());
+ let patched = list(
+ &mut self
+ .sources
+ .iter()
+ .filter(|(_, s)| !patches(&s.settings).is_empty())
+ .map(|(n, _)| n.as_str()),
+ );
+ self.run(
+ sh::draw::CLEAN_UP,
+ false,
+ &[
+ &self.sentinel(),
+ &all,
+ &not_flakes,
+ &generated,
+ &derived,
+ &patched,
+ ],
+ )
}
- text += "# Remove roots of flake inputs of sources that are neither flake nor\n# flake-input sources anymore.\n";
- for (name, source) in sources {
- if !matches!(source.kind.as_str(), "flake" | "flake-input") {
- text += &format!("rm -fR {}\n", q(&format!("{sentinel}/flake-inputs/{name}")));
+}
+
+/// Draws inputs of flakes in sources from other sources, derives what the
+/// target paths of flake and flake-input sources that get patched, get
+/// their lock files rewritten, or are materialized get, and removes state
+/// that sources don't need anymore. Sources get processed in the order
+/// they draw inputs from each other.
+///
+/// Inputs get drawn by rewriting the lock file of the flake in the source
+/// with nix flake lock --override-input: in place for file and git sources,
+/// and in a copy for flake and flake-input sources. Sources with
+/// rewriteLock set to false, and symlink sources, get a generated flake in
+/// .populate/flakes/<name> instead, which re-exports the outputs of the
+/// flake in the source, with those inputs drawn from the other sources.
+/// Lock files get rewritten or generated on every run, so they match the
+/// sources.
+pub fn flakes(context: &Context, dir: &str, sources: &BTreeMap<String, Source>) -> Result<()> {
+ let follows: BTreeMap<&str, Vec<Follows>> = sources
+ .iter()
+ .map(|(n, s)| (n.as_str(), s.follows()))
+ .collect();
+ let names_where = |p: &dyn Fn(&Source) -> bool| -> BTreeSet<&str> {
+ sources
+ .iter()
+ .filter(|(_, s)| p(s))
+ .map(|(n, _)| n.as_str())
+ .collect()
+ };
+ let flakes = Flakes {
+ context,
+ dir,
+ sources,
+ drawing: names_where(&|s| !s.follows().is_empty()),
+ generated: names_where(&has_generated_flake),
+ derived: names_where(&derives_flake),
+ follows,
+ };
+ let names: BTreeSet<&str> = flakes.drawing.union(&flakes.derived).copied().collect();
+ if !names.is_empty() {
+ let mut args = vec![flakes.sentinel()];
+ args.extend(sources.keys().cloned());
+ let args: Vec<&str> = args.iter().map(String::as_str).collect();
+ flakes.run(sh::draw::USE_STATE, false, &args)?;
+ if !flakes.drawing.is_empty() {
+ flakes.check()?;
+ }
+ let drawn_from = |name: &str| -> Vec<String> {
+ flakes.follows[name]
+ .iter()
+ .map(|f| f.source.clone())
+ .collect()
+ };
+ for name in toposort(&names, &drawn_from) {
+ if flakes.derived.contains(name) {
+ flakes.derive(name)?;
+ }
+ if flakes.generated.contains(name) {
+ flakes.generate(name)?;
+ } else if flakes.drawing.contains(name) && !flakes.derived.contains(name) {
+ flakes.rewrite(name)?;
+ }
}
}
- let fi = q(&format!("{sentinel}/flake-inputs"));
- text += &format!(
- "if test -d {fi} &&\n test -z \"$(ls -A {fi})\"; then\n rmdir {fi}\nfi\n# Remove what sources left behind that don't need it anymore.\n"
- );
- text += &remove_stale(&flakes_dir, &generated);
- text += &remove_stale(&derived_dir, &derived);
- text += &remove_stale(&patches_dir, &patched);
- format!("{}\n{text}", sh::prelude())
+ flakes.clean_up()
}
/// The names sorted so each comes after the ones it draws inputs from, as
@@ -798,6 +827,7 @@ fn sha256_hex(data: &[u8]) -> String {
#[cfg(test)]
mod tests {
use super::*;
+ use serde_json::json;
#[test]
fn sha256() {
@@ -810,4 +840,127 @@ mod tests {
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
+
+ #[test]
+ fn url_paths() {
+ assert_eq!(url_path("/var/src/a-b_c.d~e"), "/var/src/a-b_c.d~e");
+ assert_eq!(url_path("/x y?#%"), "/x%20y%3f%23%25");
+ assert_eq!(url_path("/รค"), "/%c3%a4");
+ }
+
+ #[test]
+ fn input_names() {
+ assert_eq!(input_name("nixpkgs"), "nixpkgs");
+ assert_eq!(
+ input_name("self"),
+ format!("source-{}", &sha256_hex(b"self")[..16])
+ );
+ assert!(input_name("1x").starts_with("source-"));
+ assert!(input_name("a.b").starts_with("source-"));
+ }
+
+ #[test]
+ fn store_paths() {
+ assert_eq!(
+ split_store_path("/nix/store/abc-source/sub/dir"),
+ ("/nix/store/abc-source", "sub/dir")
+ );
+ assert_eq!(
+ split_store_path("/nix/store/abc-source"),
+ ("/nix/store/abc-source", "")
+ );
+ }
+
+ fn path(names: &[&str]) -> Vec<String> {
+ names.iter().map(|n| n.to_string()).collect()
+ }
+
+ /// A lock file with inputs a and b, where a has an input c, and its
+ /// input b follows the root's b.
+ fn locks() -> Value {
+ json!({
+ "root": "root",
+ "nodes": {
+ "root": { "inputs": { "a": "a", "b": "b_2" } },
+ "a": { "inputs": { "b": ["b"], "c": "c", "gone": [] } },
+ "b_2": { "inputs": { "d": "d" } },
+ "c": {},
+ "d": {}
+ }
+ })
+ }
+
+ #[test]
+ fn lock_inputs() {
+ let locks = locks();
+ assert!(lock_has_input(&locks, &path(&[])));
+ assert!(lock_has_input(&locks, &path(&["a"])));
+ assert!(lock_has_input(&locks, &path(&["a", "c"])));
+ assert!(lock_has_input(&locks, &path(&["a", "b", "d"])));
+ assert!(!lock_has_input(&locks, &path(&["x"])));
+ assert!(!lock_has_input(&locks, &path(&["a", "x"])));
+ assert!(!lock_has_input(&locks, &path(&["a", "b", "x"])));
+ // An empty follows path refers to the root.
+ assert!(lock_has_input(&locks, &path(&["a", "gone", "a"])));
+ }
+
+ #[test]
+ fn cyclic_follows() {
+ let mut locks = locks();
+ locks["nodes"]["a"]["inputs"]["b"] = json!(["a", "b"]);
+ assert!(!lock_has_input(&locks, &path(&["a", "b"])));
+ }
+
+ /// A source's name, and the paths of the inputs it draws, with the
+ /// sources it draws them from.
+ type Entry<'a> = (&'a str, &'a [(&'a [&'a str], &'a str)]);
+
+ fn follows(entries: &[Entry]) -> BTreeMap<&'static str, Vec<Follows>> {
+ entries
+ .iter()
+ .map(|(name, fs)| {
+ let name: &'static str = Box::leak(name.to_string().into_boxed_str());
+ let fs = fs
+ .iter()
+ .map(|(p, s)| Follows {
+ path: path(p),
+ source: s.to_string(),
+ })
+ .collect();
+ (name, fs)
+ })
+ .collect()
+ }
+
+ #[test]
+ fn drawn_paths() {
+ // top draws dep from source dep, and dep's inner from source
+ // inner; dep draws nothing itself.
+ let follows = follows(&[
+ ("top", &[(&["dep"], "dep"), (&["dep", "inner"], "inner")]),
+ ("dep", &[]),
+ ("inner", &[]),
+ ]);
+ let none = BTreeSet::new();
+ assert_eq!(
+ drawn_path("top", &path(&["dep", "inner"]), &follows, &none),
+ path(&["dep", "inner"])
+ );
+ let generated: BTreeSet<&str> = ["top"].into();
+ assert_eq!(
+ drawn_path("top", &path(&["dep"]), &follows, &generated),
+ path(&["top", "dep"])
+ );
+ assert_eq!(
+ drawn_path("top", &path(&["dep", "inner"]), &follows, &generated),
+ path(&["dep", "inner"])
+ );
+ assert_eq!(
+ input_checks("top", &path(&["dep", "inner"]), &follows),
+ vec![
+ ("top".to_string(), path(&["dep"])),
+ ("dep".to_string(), path(&["inner"]))
+ ]
+ );
+ }
}
diff --git a/pkgs/populate/src/populate.rs b/pkgs/populate/src/populate.rs
index ad0ec35..226cee2 100644
--- a/pkgs/populate/src/populate.rs
+++ b/pkgs/populate/src/populate.rs
@@ -34,7 +34,7 @@ pub fn populate(context: &Context, spec: &Spec) -> Result<()> {
})?;
deliver(context, name, source, &path, workdir)?;
}
- context.run(&flake::flakes(context, dir, &spec.source), &[])?;
+ flake::flakes(context, dir, &spec.source)?;
if spec.prune {
let flakes = spec
.source
diff --git a/pkgs/populate/src/sh.rs b/pkgs/populate/src/sh.rs
index 1a2d2c7..8282370 100644
--- a/pkgs/populate/src/sh.rs
+++ b/pkgs/populate/src/sh.rs
@@ -522,7 +522,7 @@ require_flakes() {
}
"#;
-/// The definitions shell code generated for flakes uses.
+/// The definitions the shell code for flakes uses.
pub fn prelude() -> String {
[
&use_state(),
@@ -730,3 +730,262 @@ nix eval --impure --raw --expr '
'
"#;
}
+
+/// Shell code for drawing inputs of flakes in sources from other sources,
+/// and for deriving what flake and flake-input sources that get patched,
+/// get their lock files rewritten, or are materialized deliver. Each gets
+/// the definitions of prelude and DEFINITIONS. Populate decides what to
+/// do, from what these send back.
+pub mod draw {
+ /// Defines own_lock DIR, apply_patches PATCH_DIR NAME, and write_key
+ /// KEY_FILE KEY.
+ ///
+ /// Nix rewrites lock files in place, which also changes what else is a
+ /// hard link to the lock file, e.g. in a backup, or what it is a
+ /// symlink to. So own_lock replaces the lock file of the flake in DIR
+ /// by a copy first. The copy goes to a new file, so nothing that's
+ /// there already gets written through.
+ ///
+ /// apply_patches applies the patches in PATCH_DIR, in order, to the
+ /// working directory, failing, naming the patch and the source NAME, if
+ /// one doesn't apply.
+ ///
+ /// write_key writes KEY to a new file, which then replaces KEY_FILE, so
+ /// nothing at its place gets written through, e.g. a symlink.
+ pub const DEFINITIONS: &str = r#"
+own_lock() {
+ if test -e "$1"/flake.lock; then
+ own_lock_copy=$(mktemp "$1"/.flake.lock.XXXXXX)
+ cp -p "$1"/flake.lock "$own_lock_copy"
+ mv -f "$own_lock_copy" "$1"/flake.lock
+ fi
+}
+apply_patches() {
+ ls "$1" | while read -r patch; do
+ if ! patch -p1 -s --batch --no-backup-if-mismatch -i "$1"/"$patch" >&2; then
+ >&2 printf 'error: patch %s does not apply to source %s\n' \
+ "$(printf %s "$patch" | cut -c6-)" "$2"
+ exit 1
+ fi
+ done
+}
+write_key() {
+ key_file=$(mktemp "$1".XXXXXX)
+ printf '%s\n' "$2" > "$key_file"
+ mv -f "$key_file" "$1"
+}
+"#;
+
+ /// use_state SENTINEL NAME...
+ pub const USE_STATE: &str = r#"
+use_state "$@"
+"#;
+
+ /// resolve PATH...: prints, for each PATH, what's there and where it
+ /// resolves to, as two fields, each ending in a null byte. The first
+ /// holds d if PATH is a directory, g if it has .git, s if it has
+ /// .gitmodules, and f if it has flake.nix.
+ pub const RESOLVE: &str = r#"
+for path; do
+ resolved=$(readlink -f "$path") || resolved=
+ flags=
+ if test -d "$path"; then flags=${flags}d; fi
+ if test -e "$path"/.git; then flags=${flags}g; fi
+ if test -e "$path"/.gitmodules; then flags=${flags}s; fi
+ if test -e "$path"/flake.nix; then flags=${flags}f; fi
+ printf '%s\0%s\0' "$flags" "$resolved"
+done
+"#;
+
+ /// metadata URL NAME OVERRIDE...: prints what nix flake metadata prints
+ /// for the flake at URL, of the source NAME, as Nix locks it now, with
+ /// an empty flake in place of each input at an OVERRIDE path, which
+ /// gets drawn from a source and may not be reachable as the flake's
+ /// lock file pins it.
+ pub const METADATA: &str = r#"
+url=$1 name=$2
+shift 2
+empty=$(mktemp -d)
+echo '{ outputs = _: {}; }' > "$empty"/flake.nix
+n=$#
+while test "$n" -gt 0; do
+ set -- "$@" --override-input "$1" "path:$empty"
+ shift
+ n=$((n - 1))
+done
+if ! nix flake metadata --json --no-write-lock-file "$@" "$url"; then
+ rm -R "$empty"
+ >&2 printf 'error: source %s draws inputs from other sources, but its flake can'"'"'t be locked\n' \
+ "$name"
+ exit 1
+fi
+rm -R "$empty"
+"#;
+
+ /// lock DIR URL ARG...: rewrites the lock file of the flake in DIR,
+ /// at URL, with nix flake lock's arguments ARG.
+ pub const LOCK: &str = r#"
+dir=$1 url=$2
+shift 2
+own_lock "$dir"
+nix flake lock "$@" "$url" >&2
+"#;
+
+ /// derived_state DIR TARGET_PATH PATCH_DIR DRAWN...: removes what an
+ /// interrupted run left in the source's derived state DIR, and prints
+ /// what tells whether the copy there needs to be made again, as fields
+ /// ending in null bytes: where DIR/original resolves to, the key of the
+ /// copy, the store path DIR/result links to, if the store has it, link
+ /// or directory, if TARGET_PATH is one, a line of sha256sum for each
+ /// patch in PATCH_DIR, an empty field, and the hash of each DRAWN path,
+ /// or nothing if it resolves into the store.
+ pub const DERIVED_STATE: &str = r#"
+dir=$1 target_path=$2 patch_dir=$3
+shift 3
+remove_tree "$dir"/work
+printf '%s\0' "$(readlink -f "$dir"/original)"
+if test -e "$dir"/key; then
+ printf '%s\0' "$(cat "$dir"/key)"
+else
+ printf '\0'
+fi
+if result=$(readlink "$dir"/result) &&
+ nix-store --check-validity "$result" 2>/dev/null; then
+ printf '%s\0' "$result"
+else
+ printf '\0'
+fi
+if test -L "$target_path"; then
+ printf 'link\0'
+elif test -d "$target_path"; then
+ printf 'directory\0'
+else
+ printf '\0'
+fi
+if test -n "$patch_dir"; then
+ ls "$patch_dir" | while read -r patch; do
+ printf '%s\0' "$(sha256sum "$patch_dir"/"$patch")"
+ done
+fi
+printf '\0'
+for drawn; do
+ drawn=$(readlink -f "$drawn")
+ case $drawn in
+ /nix/store/*) printf '\0' ;;
+ *) printf '%s\0' "$(nix-hash --type sha256 "$drawn")" ;;
+ esac
+done
+"#;
+
+ /// materialize ORIGINAL WORK TARGET_PATH TARGET_NAME PATCH_DIR NAME
+ /// KEY_FILE KEY: puts a copy of ORIGINAL, made in WORK and patched with
+ /// the patches in PATCH_DIR, if that isn't empty, at TARGET_PATH, and
+ /// writes its key. TARGET_NAME is TARGET_PATH's name in messages, and
+ /// NAME the source's.
+ pub const MATERIALIZE: &str = r#"
+original=$1 work=$2 target_path=$3 target_name=$4 patch_dir=$5 name=$6
+key_file=$7 key=$8
+cp -R "$original" "$work"
+chmod -R u+w "$work"
+if test -n "$patch_dir"; then
+ (
+ cd "$work"
+ apply_patches "$patch_dir" "$name"
+ )
+fi
+remove_foreign_link "$target_path" "$target_name"
+rm -fR "$target_path"
+mv "$work" "$target_path"
+write_key "$key_file" "$key"
+"#;
+
+ /// add DIR PATH SUBDIR TARGET_PATH TARGET_NAME RESULT PATCH_DIR NAME
+ /// KEY ARG...: links SUBDIR of the store path RESULT at TARGET_PATH.
+ /// If RESULT is empty, that's a copy of the store path PATH, made in
+ /// the source's derived state DIR, patched with the patches in
+ /// PATCH_DIR, if that isn't empty, with the lock file of its flake in
+ /// SUBDIR rewritten with nix flake lock's arguments ARG, if any, and
+ /// added to the store, rooted by DIR/result, with its key in DIR/key.
+ /// TARGET_NAME is TARGET_PATH's name in messages, and NAME the
+ /// source's.
+ pub const ADD: &str = r#"
+dir=$1 path=$2 subdir=$3 target_path=$4 target_name=$5 result=$6
+patch_dir=$7 name=$8 key=$9
+shift 9
+if test -z "$result"; then
+ # The copy keeps the name of the store path.
+ mkdir "$dir"/work
+ work=$(readlink -f "$dir"/work)/$(basename "$path" | cut -c34-)
+ cp -R "$path" "$work"
+ chmod -R u+w "$work"
+ if test -n "$patch_dir"; then
+ (
+ cd "$work"/"$subdir"
+ apply_patches "$patch_dir" "$name"
+ )
+ fi
+ if test $# -gt 0; then
+ # Not just the path, which Nix takes for the enclosing Git work tree,
+ # if any.
+ nix flake lock "$@" "path:$(url_path "$work")${subdir:+?dir=$subdir}" >&2
+ fi
+ result=$(nix-store --add-fixed --recursive sha256 "$work")
+ remove_tree "$dir"/work
+ nix-store --realise "$result" --add-root "$dir"/result --indirect >/dev/null
+ write_key "$dir"/key "$key"
+fi
+make_room_for_link "$target_path" "$target_name"
+# The source's derived state roots the store path.
+ln -fns "$result${subdir:+/$subdir}" "$target_path"
+"#;
+
+ /// generate DIR TEXT: makes DIR a flake, with TEXT as its flake.nix,
+ /// and locks it.
+ pub const GENERATE: &str = r#"
+dir=$1 text=$2
+mkdir -p "$(dirname "$dir")"
+rm -fR "$dir"
+mkdir "$dir"
+cd "$dir"
+printf %s "$text" > flake.nix
+# Not just ., which Nix takes for the enclosing Git work tree, if any.
+nix flake lock "path:$(url_path "$(pwd -P)")" >&2
+"#;
+
+ /// clean_up SENTINEL NAMES NOT_FLAKES GENERATED DERIVED PATCHED: removes
+ /// the state sources don't need anymore. Each argument after SENTINEL
+ /// is a list of source names, separated by blanks: of all sources, of
+ /// those that are neither flake nor flake-input sources, whose roots
+ /// for the garbage collector go, and of those that have a generated
+ /// flake, derived state, and patches.
+ pub const CLEAN_UP: &str = r#"
+sentinel=$1
+# The names are portable file names, so splitting them at blanks is fine.
+# shellcheck disable=SC2086
+use_state "$sentinel" $2
+# shellcheck disable=SC2086
+for name in $3; do
+ rm -fR "$sentinel"/flake-inputs/"$name"
+done
+if test -d "$sentinel"/flake-inputs &&
+ test -z "$(ls -A "$sentinel"/flake-inputs)"; then
+ rmdir "$sentinel"/flake-inputs
+fi
+remove_stale() {
+ stale_dir=$1
+ shift
+ if test -d "$stale_dir"; then
+ remove_others "$stale_dir" '' "$@"
+ if test -z "$(ls -A "$stale_dir")"; then
+ rmdir "$stale_dir"
+ fi
+ fi
+}
+# shellcheck disable=SC2086
+remove_stale "$sentinel"/flakes $4
+# shellcheck disable=SC2086
+remove_stale "$sentinel"/derived $5
+# shellcheck disable=SC2086
+remove_stale "$sentinel"/patches $6
+"#;
+}