summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README40
-rwxr-xr-xbin/populate64
-rw-r--r--lib/populate.jq126
3 files changed, 230 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..b1573aa
--- /dev/null
+++ b/README
@@ -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