From 05534c806db2883e28f9905bffae9bd30c73b838 Mon Sep 17 00:00:00 2001 From: tv Date: Fri, 15 Dec 2017 00:12:49 +0100 Subject: replace jq-generated sh by direct sh --- README | 1 - bin/populate | 302 +++++++++++++++++++++++++++++++++++++++++++------------- lib/populate.jq | 137 ------------------------- 3 files changed, 236 insertions(+), 204 deletions(-) delete mode 100644 lib/populate.jq 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 -- cgit v1.2.3