Populate's design
Populate delivers sources to a target directory. It consists of:
- A program, which reads the sources as JSON and delivers them to the target its command line names. It knows nothing about Nix expressions: whatever produces sources as JSON can drive it.
- A Nix library,
lib, with the types of sources, which describe how to write them in Nix, andevalSource, which turns sources written so into the JSON the program reads.
krops uses both: its pkgs.populate turns populate's arguments into a
command line, and checks them during evaluation, so mistakes show early.
So there is one program for each version of populate, instead of a script
for each set of arguments, and the program can be written in any language.
This directory holds the program in Rust. Another implementation, e.g. in
Haskell, takes the same command line, prints the same messages, and passes
the same tests.
Nix side
default.nix builds the program: nix-build in this directory, or
callPackage, as krops' pkgs.populate-unwrapped. lib/default.nix is
the Nix library, taking nixpkgs' lib.
krops' pkgs.populate, in its pkgs/krops/populate.nix, is populate's Nix
API there. Given populate's arguments, { backup ? false, force ? false, prune ? false, source, target }, it checks them with krops'
pkgs/krops/checks.nix, and returns a script running the program with the
sources as JSON on file descriptor 3, so the commands populate runs keep
standard input. A target string goes to the program as it is. A target
attribute set, as mkTarget returns it, goes part by part, as options,
with --local or --remote as krops' lib's isLocalTarget decides
during evaluation, like the other functions of krops. The script puts the
tools populate runs in front of PATH, and depends on the store paths in
the sources, so building it builds them. krops' writeDeploy and its
other functions use pkgs.populate, so their arguments stay as they are.
Command line and sources
../README.md describes populate's command line, the
sources, and what populate prints, which are its interface. The program
reads the sources into spec::Sources, and parses the target with
spec::TargetParts, completing it with run::Origin, the user running
populate and the machine's name, when it runs.
Checks
The program checks the sources and the target as krops' checks.nix does
during evaluation, with the same messages:
- The target's host isn't empty, and neither it nor its user starts with
-, so ssh can't take them for options.checks.nixdoesn't check this, since krops' targets come from the configuration. - Source names are portable file names: letters, digits, and
._-, not starting with-, and neither.,.., nor.populate. - The target path is absolute, not the root directory, and contains
neither
..nor control characters. - Inputs get drawn from sources that exist, not from the source itself, and not in circles.
- The settings of a source are those
lib/types/populate.nixdefines for its type, of the kinds it defines, e.g. absolute paths for the paths offile,pass, andpipesources.checks.nixdoesn't repeat this, sinceevalSourcechecks the types.
Checks that depend on whether the target is local come on top, e.g.
refusing to copy flakes to remote targets using sudo. krops' checks.nix
does that during evaluation too, where pkgs.populate decides whether the
target is local.
Safety
Populate never changes or removes anything outside the target directory, whatever the sources and whatever it finds on the target:
- The checks above keep source names and the target path inside it.
- A symlink at the target path of a source gets replaced, never written
through, and populate's state directory,
.populate, must not be one. - Removing a tree makes only its directories writable, and removes what's below them, not what symlinks point to.
- Hard links to files of a source get broken before rsync writes to them.
- A Git work tree gets reused only if its
.gitis a directory of its own, with no other configuration thangit cloneand populate set, and Git runs without hooks and file system monitors. - Temporary files get created with
mktemp, and moved into place.
The tests' canaries, files around the target directories and in other places populate must not touch, check this after every phase.
Execution
Whether a target is local is what --local or --remote says, or else
gets decided when the program runs, like krops' lib's isLocalTarget
does: it is, if its user is LOGNAME, or null if that's empty or unset,
and its host is localhost or the machine's name, from HOSTNAME, or, if
that's empty or unset, the only line of /etc/hostname that is a label.
For a local target, populate runs commands directly.
For a remote target, populate runs commands over ssh, sharing one master
connection per target. The commands are fixed POSIX shell snippets, with
values passed as positional arguments, so nothing gets quoted into them.
The target needs a POSIX shell, coreutils, findutils, rsync, patch for
patches, Git for git sources, and Nix; NixOS lacks Git. With sudo,
the snippets run via sudo.
Populate runs rsync, Git, ssh, and Nix as programs, found in PATH.
Development
cargo testin this directory runs the program's unit tests.nix-build tests -A smokeruns the smoke test, a few scenarios covering each source type and each kind of target.nix-build testsruns all scenarios but the exhaustive ones.- krops' tests run populate's scenarios that expect refusals, and those of
the smoke test, through krops'
pkgs.populate, withtests/builder.nix.
Status
The program delivers all source types, draws inputs from sources, makes
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.
