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 " git fetch origin", @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