summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README1
-rwxr-xr-xbin/populate302
-rw-r--r--lib/populate.jq137
3 files changed, 236 insertions, 204 deletions
diff --git a/README b/README
index 6aeca5e..b1573aa 100644
--- a/README
+++ b/README
@@ -20,7 +20,6 @@ Next we'll run populate with a source specification:
"mystuff": {
"type": "file",
"file": {
- "exclude": [ ".git" "TODO*" ],
"path": "/path/to/mystuff-1.0"
}
},
diff --git a/bin/populate b/bin/populate
index fdc4c5d..ab7dce5 100755
--- a/bin/populate
+++ b/bin/populate
@@ -1,75 +1,245 @@
#! /bin/sh
set -efu
-self=$(readlink -f "$0")
-basename=${0##*/}
-prefix=${self%/bin/*}
-libdir=$prefix/lib
+main() {(
+ self=$(readlink -f "$0")
+ basename=${0##*/}
-debug=false
-force=false
-origin_host=${HOSTNAME-cat /proc/sys/kernel/hostname}
-origin_user=$LOGNAME
-ssh_cmd=ssh
-target_spec=
+ debug=false
+ force=false
+ origin_host=${HOSTNAME-cat /proc/sys/kernel/hostname}
+ origin_user=$LOGNAME
+ target_spec=
-fail=true
+ fail=true
-error() {
- echo "$basename: error: $1" >&2
- fail=false
-}
+ error() {
+ echo "$basename: error: $1" >&2
+ fail=false
+ }
-for arg; do
- case $arg in
- --debug)
- debug=true
- ;;
- --force)
- force=true
- ;;
- --ssh=*)
- ssh_cmd=${arg#--ssh=}
- ;;
- -*)
- error "bad argument: $arg"
- ;;
- *)
- if test -n "$target_spec"; then
+ for arg; do
+ case $arg in
+ --force)
+ force=true
+ ;;
+ -*)
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 11
-fi
-
-
-script=$(jq -e -r \
- --argjson use_force "$force" \
- --arg ssh_cmd "$ssh_cmd" \
- --arg target_spec "$target_spec" \
- --arg origin_host "$origin_host" \
- --arg origin_user "$origin_user" \
- -f "$libdir/populate.jq")
-
-if test -z "$script"; then
- error 'no script produced'
- exit 12
-fi
-
-
-if test "$debug" = true; then
- echo "$script"
-else
- eval "$script"
-fi
+ ;;
+ *)
+ 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 11
+ fi
+
+ target=$(
+ echo "$target_spec" | jq -R '
+ def default(value; f): if . == null then value else f end;
+ def default(value): default(value; .);
+
+ 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"),
+ }
+ '
+ )
+
+ target_host=$(echo $target | jq -r .host)
+ target_path=$(echo $target | jq -r .path)
+ target_port=$(echo $target | jq -r .port)
+ target_user=$(echo $target | jq -r .user)
+
+ if test "$force" = true; then
+ force_target
+ else
+ check_target
+ fi
+
+ jq -c 'to_entries | group_by(.value.type) | flatten[]' |
+ while read -r source; do
+ key=$(echo "$source" | jq -r .key)
+ type=$(echo "$source" | jq -r .value.type)
+ case $type in
+ file)
+ file_path=$(echo "$source" | jq -r .value.file.path)
+ populate_file "$key" "$file_path"
+ ;;
+ git)
+ git_url=$(echo "$source" | jq -r .value.git.url)
+ git_ref=$(echo "$source" | jq -r .value.git.ref)
+ populate_git "$key" "$git_url" "$git_ref"
+ ;;
+ symlink)
+ symlink_target=$(echo "$source" | jq -r .value.symlink.target)
+ populate_symlink "$key" "$symlink_target"
+ ;;
+ *)
+ echo "Warning: ignoring $source" >&2
+ ;;
+ esac
+ done
+)}
+
+# Safeguard to prevent clobbering of misspelled targets.
+# This function has to be called first.
+check_target() {
+ {
+ echo target_host=$(quote "$target_host")
+ echo target_path=$(quote "$target_path")
+ echo 'sentinel_file=$target_path/.populate'
+ echo 'if ! test -f "$sentinel_file"; then'
+ echo ' echo "error: missing sentinel file: $target_host:$sentinel_file" >&2'
+ echo ' exit 1'
+ echo 'fi'
+ } \
+ |
+ target_shell
+}
+
+force_target() {
+ {
+ echo target_path=$(quote "$target_path")
+ echo 'sentinel_file=$target_path/.populate'
+ echo 'mkdir -vp "$target_path"'
+ echo 'touch "$sentinel_file"'
+ } \
+ |
+ target_shell
+}
+
+is_local_target() {
+ test "$target_host" = "$origin_host" &&
+ test "$target_user" = "$origin_user"
+}
+
+populate_file() {(
+ print_info populate_file "$@"
+
+ file_name=$1
+ file_path=$2
+
+ if is_local_target; then
+ file_target=$target_path/$file_name
+ else
+ file_target=$target_user@$target_host:$target_path/$file_name
+ fi
+
+ rsync \
+ -vFrlptD \
+ --delete-excluded \
+ "$file_path"/ \
+ -e "ssh -o ControlPersist=no -p $target_port" \
+ "$file_target"
+)}
+
+populate_git() {(
+ print_info populate_git "$@"
+
+ git_name=$1
+ git_url=$2
+ git_ref=$3
+
+ git_work_tree=$target_path/$git_name
+
+ {
+ echo set -efu
+
+ echo git_name=$(quote "$git_name")
+ echo git_url=$(quote "$git_url")
+ echo git_ref=$(quote "$git_ref")
+
+ echo git_work_tree=$(quote "$git_work_tree")
+
+ echo 'if ! test -e "$git_work_tree"; then'
+ echo ' git clone "$git_url" "$git_work_tree"'
+ echo 'fi'
+
+ echo 'cd $git_work_tree'
+
+ echo 'if ! url=$(git config remote.origin.url); then'
+ echo ' git remote add origin "$git_url"'
+ echo 'elif test "$url" != "$git_url"; then'
+ echo ' git remote set-url origin "$git_url"'
+ echo 'fi'
+
+ # TODO resolve git_ref to commit hash
+ echo 'hash=$git_ref'
+
+ echo 'if ! test "$(git log --format=%H -1)" = "$hash"; then'
+ echo ' if ! git log -1 "$hash" >/dev/null 2>&1; then'
+ echo ' git fetch origin'
+ echo ' fi'
+ echo ' git checkout "$hash" -- "$git_work_tree"'
+ echo ' git -c advice.detachedHead=false checkout -f "$hash"'
+ echo 'fi'
+
+ echo 'git clean -dfx'
+
+ } \
+ |
+ target_shell
+)}
+
+populate_symlink() {(
+ print_info populate_symlink "$@"
+
+ symlink_name=$1
+ symlink_source=$2
+
+ symlink_target=$target_path/$symlink_name
+
+ {
+ # TODO rm -fR instead of ln -f?
+ echo ln -fns $(quote "$symlink_source" "$symlink_target")
+ } \
+ |
+ target_shell
+)}
+
+print_info() {
+ printf '\e[1;33m* %s\e[m\n' "$*" >&2
+}
+
+quote() {
+ printf %s "$1" | sed 's/./\\&/g'
+ while test $# -gt 1; do
+ printf ' '
+ shift
+ printf %s "$1" | sed 's/./\\&/g'
+ done
+ echo
+}
+
+target_shell() {
+ if is_local_target; then
+ /bin/sh
+ else
+ ssh "$target_host" \
+ -l "$target_user" \
+ -o ControlPersist=no \
+ -p "$target_port" \
+ -T \
+ /bin/sh
+ fi
+}
+
+main "$@"
diff --git a/lib/populate.jq b/lib/populate.jq
deleted file mode 100644
index d7cf964..0000000
--- a/lib/populate.jq
+++ /dev/null
@@ -1,137 +0,0 @@
-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 forcetarget_script:
- @sh "mkdir -vp \($target.path)",
- @sh "touch \($target.path)/.populate";
-
-
-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 " if ! git log -1 \"$hash\" >/dev/null 2>&1; then",
- @sh " git fetch origin",
- @sh " fi",
- @sh " git checkout \"$hash\" -- \"$dst_dir\"",
- @sh " git checkout -f \"$hash\"",
- @sh " fi",
-
- @sh " git clean -dxf",
- @sh ")}",
- ($git_sources[] |
- @sh "echo fetch_git \(.key) \(.value.git.url) \(.value.git.ref)",
- @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-excluded \\",
- @sh " -f \("P /*", ($rsync_sources[] | "R /\(.key)")) \\",
- ($file_sources[] | .key as $key | .value.file.exclude | select(. != null)[] |
- @sh " -f \("- /\($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_cmd) -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_cmd) \($target.user)@\($target.host) -p \($target.port) \\",
- @sh " -T \("nix-shell -p git --run /bin/sh")";
-
-
-[
- (if $use_force then forcetarget_script else checktarget_script end),
- git_script
-]
-| (if $is_local then . else [ssh_target] end),
-[
- rsync_script
-]
-| compile