diff options
-rw-r--r-- | README | 40 | ||||
-rwxr-xr-x | bin/populate | 64 | ||||
-rw-r--r-- | lib/populate.jq | 126 |
3 files changed, 230 insertions, 0 deletions
@@ -0,0 +1,40 @@ +populate - source code installer + +populate can install sources from various locations into a directory. +Currently populate knows how to install (local) files, Git repositories, and +symlinks. Following example illustrates them all. + + +Example: Install some source to /var/src +---------------------------------------- + +First we have to create a "sentinel file" that tells populate it's okay to +install (and remove!) files from our target location: + + touch /var/src/.populate + +Next we'll run populate with a source specification: + + populate root@localhost/var/src <<EOF + { + "mystuff": { + "type": "file", + "file": { + "path": "/path/to/mystuff-1.0" + } + }, + "nixos-config": { + "type": "symlink", + "symlink": { + "target": "mystuff/configuration.nix" + } + }, + "nixpkgs": { + "type": "git", + "git": { + "ref": "8bf31d7d27cae435d7c1e9e0ccb0a320b424066f", + "url": "https://github.com/NixOS/nixpkgs" + } + } + } + EOF diff --git a/bin/populate b/bin/populate new file mode 100755 index 0000000..17a8107 --- /dev/null +++ b/bin/populate @@ -0,0 +1,64 @@ +#! /bin/sh +set -efu + +self=$(readlink -f "$0") +prefix=${self%/bin/*} +libdir=$prefix/lib + +debug=false +force=false +origin_host=${HOSTNAME-cat /proc/sys/kernel/hostname} +origin_user=$LOGNAME +target_spec= + + +fail=true + +error() { + echo "error: $1" >&2 + fail=false +} + +for arg; do + case $arg in + --debug) + debug=true + ;; + --force) + force=true + ;; + -*) + error "bad argument: $arg" + ;; + *) + if test -n "$target_spec"; then + error "bad argument: $arg" + else + target_spec=$arg + fi + ;; + esac +done + +if test -z "$target_spec"; then + error 'no target specified' +fi + +if test "$fail" != true; then + exit 1 +fi + + +script=$(jq -e -r \ + --argjson use_force "$force" \ + --arg target_spec "$target_spec" \ + --arg origin_host "$origin_host" \ + --arg origin_user "$origin_user" \ + -f "$libdir/populate.jq") + + +if test "$debug" = true; then + echo "$script" +else + eval "$script" +fi diff --git a/lib/populate.jq b/lib/populate.jq new file mode 100644 index 0000000..23e16e8 --- /dev/null +++ b/lib/populate.jq @@ -0,0 +1,126 @@ +def default(value; f): if . == null then value else f end; +def default(value): default(value; .); + +($target_spec + | match("^(?:([^@]+)@)?([^:/]+)(?::([^/]*))?(/.*)?") + | { + user: .captures[0].string | default("root"), + host: .captures[1].string, + port: .captures[2].string | default(22; + if test("^[0-9]+$") then fromjson else + error(@json "bad target port: \(.)") + end), + path: .captures[3].string | default("/var/src"), + } +) + as $target | + +(($target.host == $origin_host) and ($target.user == $origin_user)) + as $is_local | + + +to_entries | +map(select(.value.type == "file")) as $file_sources | +map(select(.value.type == "git")) as $git_sources | +map(select(.value.type == "symlink")) as $symlink_sources | + +($file_sources + $symlink_sources) as $rsync_sources | + + +# Safeguard to prevent clobbering of misspelled targets. +# This script must run at target first. +def checktarget_script: + @sh "if ! test -f \($target.path)/.populate; then", + @sh " echo error: missing sentinel file: \($target.host):\($target.path)/.populate >&2", + @sh " exit 1", + @sh "fi"; + + +def git_script: + @sh "fetch_git(){(", + #@sh " export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt", + + @sh " dst_dir=\($target.path)/$1", + @sh " src_url=$2", + @sh " src_ref=$3", + + @sh " if ! test -e \"$dst_dir\"; then", + @sh " git clone \"$src_url\" \"$dst_dir\"", + @sh " fi", + + @sh " cd \"$dst_dir\"", + + @sh " if ! url=$(git config remote.origin.url); then", + @sh " git remote add origin \"$src_url\"", + @sh " elif test \"$url\" != \"$src_url\"; then", + @sh " git remote set-url origin \"$src_url\"", + @sh " fi", + + # TODO resolve src_ref to commit hash", + @sh " hash=$src_ref", + + @sh " if ! test \"$(git log --format=%H -1)\" = \"$hash\"; then", + @sh " git fetch origin", + @sh " git checkout \"$hash\" -- \"$dst_dir\"", + @sh " git checkout -f \"$hash\"", + @sh " fi", + + @sh " git clean -dxf", + @sh ")}", + ($git_sources[] | + @sh "fetch_git \(.key) \(.value.git.url) \(.value.git.ref)"); + + +def rsync_script: + @sh "srcdir=$(mktemp -dt populate.XXXXXXXX)", + @sh "chmod 0755 \"$srcdir\"", + @sh "trap cleanup EXIT", + @sh "cleanup() {", + @sh " (set +f; rm -f \"$srcdir\"/*)", + @sh " rmdir \"$srcdir\"", + @sh "}", + + ($symlink_sources[] | + @sh "ln -s \(.value.symlink.target) \"$srcdir\"/\(.key)"), + + @sh "proot \\", + ($file_sources[] | + @sh " -b \(.value.file.path):\"$srcdir\"/\(.key) \\"), + @sh " rsync \\", + @sh " -vFrlptD \\", + @sh " --delete \\", + @sh " -f \("P /*", ($rsync_sources[] | "R /\(.key)")) \\", + @sh " \"$srcdir\"/ \\", + (if $is_local then + @sh " \($target.path)" + else + # ControlPersist=no so we reuse existing control sockets but if we + # create a new one, then remove it immediately after we are done so + # this script does not hang. + @sh " -e \("ssh -o ControlPersist=no -p \($target.port)") \\", + @sh " \($target.user)@\($target.host):\($target.path)" + end); + + +def compile: + [ + @sh "set -euf", + .[] + ] + | map(select(. != null)) | join("\n"); + + +def ssh_target: + @sh "echo \(compile) \\", + @sh " | ssh \($target.user)@\($target.host) -p \($target.port) -T"; + + +[ + (if $use_force then null else checktarget_script end), + git_script +] +| (if $is_local then . else [ssh_target] end), +[ + rsync_script +] +| compile |