hypertwist/scripts/run-hypertwist-remote-windows-file-sync.sh
2026-07-23 08:42:30 +00:00

206 lines
5.6 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
HYPERTWIST_REMOTE_WINDOWS_PASSWORD=... \
scripts/run-hypertwist-remote-windows-file-sync.sh \
--file UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp
Options:
--file <path>
Relative repo path to copy into the remote Windows worktree. May be repeated.
--remote-root <path>
Optional Windows worktree root. Defaults to C:\HyperTwist_worktrees\phase10validate.
--dry-run
Print the copy plan without transferring files.
Environment:
HYPERTWIST_REMOTE_WINDOWS_PASSWORD Required password for the reverse-SSH Windows user.
HYPERTWIST_REMOTE_TUNNEL_PORT Optional, defaults to 22022.
HYPERTWIST_REMOTE_WINDOWS_USER Optional, defaults to "anthracite ace".
HYPERTWIST_REMOTE_TUNNEL_HOST Optional, defaults to "localhost".
EOF
}
if ! command -v sshpass >/dev/null 2>&1; then
echo "sshpass is required but was not found on PATH." >&2
exit 1
fi
if ! command -v sha256sum >/dev/null 2>&1; then
echo "sha256sum is required but was not found on PATH." >&2
exit 1
fi
if ! command -v scp >/dev/null 2>&1; then
echo "scp is required but was not found on PATH." >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required but was not found on PATH." >&2
exit 1
fi
if [[ -z "${HYPERTWIST_REMOTE_WINDOWS_PASSWORD:-}" ]]; then
echo "Set HYPERTWIST_REMOTE_WINDOWS_PASSWORD before using this wrapper." >&2
exit 1
fi
remote_root='C:\HyperTwist_worktrees\phase10validate'
dry_run="false"
files=()
while [[ $# -gt 0 ]]; do
case "$1" in
--file)
files+=("${2:-}")
shift 2
;;
--remote-root)
remote_root="${2:-}"
shift 2
;;
--dry-run)
dry_run="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 1
;;
esac
done
if [[ ${#files[@]} -eq 0 ]]; then
echo "Provide at least one --file." >&2
exit 1
fi
remote_port="${HYPERTWIST_REMOTE_TUNNEL_PORT:-22022}"
remote_user="${HYPERTWIST_REMOTE_WINDOWS_USER:-anthracite ace}"
remote_host="${HYPERTWIST_REMOTE_TUNNEL_HOST:-localhost}"
encode_powershell_command() {
local script_text="$1"
python3 - <<'PY' "$script_text"
import base64
import sys
script = sys.argv[1]
sys.stdout.write(base64.b64encode(script.encode("utf-16le")).decode("ascii"))
PY
}
ps_single_quote() {
printf "'%s'" "${1//\'/\'\'}"
}
ssh_base=(
sshpass -p "${HYPERTWIST_REMOTE_WINDOWS_PASSWORD}"
ssh
-o StrictHostKeyChecking=no
-o PreferredAuthentications=password
-o PubkeyAuthentication=no
-p "${remote_port}"
-l "${remote_user}"
"${remote_host}"
)
scp_base=(
sshpass -p "${HYPERTWIST_REMOTE_WINDOWS_PASSWORD}"
scp
-q
-o StrictHostKeyChecking=no
-o PreferredAuthentications=password
-o PubkeyAuthentication=no
-P "${remote_port}"
)
for relative_path in "${files[@]}"; do
if [[ "$relative_path" = /* ]]; then
echo "Use repo-relative paths only: $relative_path" >&2
exit 1
fi
if [[ ! -f "$relative_path" ]]; then
echo "File not found: $relative_path" >&2
exit 1
fi
windows_relative_path="${relative_path//\//\\}"
windows_dest_path="${remote_root}\\${windows_relative_path}"
local_hash="$(sha256sum "$relative_path" | awk '{print tolower($1)}')"
windows_incoming_path="${windows_dest_path}.hypertwist-incoming-${local_hash:0:12}"
if [[ "$dry_run" == "true" ]]; then
printf '%s -> %s (%s)\n' "$relative_path" "$windows_dest_path" "$local_hash"
continue
fi
remote_prepare_script="$(
cat <<EOF
\$ProgressPreference = 'SilentlyContinue'
\$ErrorActionPreference = 'Stop'
\$Path = $(ps_single_quote "$windows_dest_path")
\$IncomingPath = $(ps_single_quote "$windows_incoming_path")
\$dir = [System.IO.Path]::GetDirectoryName(\$Path)
if (\$dir) {
[System.IO.Directory]::CreateDirectory(\$dir) | Out-Null
}
if (Test-Path -LiteralPath \$IncomingPath) {
Remove-Item -LiteralPath \$IncomingPath -Force
}
EOF
)"
encoded_remote_prepare_script="$(encode_powershell_command "$remote_prepare_script")"
"${ssh_base[@]}" "powershell -NoProfile -EncodedCommand ${encoded_remote_prepare_script}" >/dev/null
incoming_scp_path="${windows_incoming_path//\\//}"
"${scp_base[@]}" \
"$relative_path" \
"${remote_user}@${remote_host}:${incoming_scp_path}"
remote_finalize_script="$(
cat <<EOF
\$ErrorActionPreference = 'Stop'
\$Path = $(ps_single_quote "$windows_dest_path")
\$IncomingPath = $(ps_single_quote "$windows_incoming_path")
\$ExpectedHash = '${local_hash}'
if (-not (Test-Path -LiteralPath \$IncomingPath -PathType Leaf)) {
throw "Incoming upload was not found at '\$IncomingPath'."
}
\$IncomingHash = (Get-FileHash -LiteralPath \$IncomingPath -Algorithm SHA256).Hash.ToLowerInvariant()
if (\$IncomingHash -ne \$ExpectedHash) {
Remove-Item -LiteralPath \$IncomingPath -Force
throw "Incoming hash mismatch: expected \$ExpectedHash but received \$IncomingHash."
}
Move-Item -LiteralPath \$IncomingPath -Destination \$Path -Force
Write-Output ('REMOTE_HASH=' + (Get-FileHash -LiteralPath \$Path -Algorithm SHA256).Hash.ToLowerInvariant())
EOF
)"
encoded_remote_finalize_script="$(encode_powershell_command "$remote_finalize_script")"
remote_hash="$(
"${ssh_base[@]}" "powershell -NoProfile -EncodedCommand ${encoded_remote_finalize_script}" \
| tr -d '\r' \
| sed -n 's/^REMOTE_HASH=//p'
)"
if [[ "$remote_hash" != "$local_hash" ]]; then
echo "Hash mismatch for $relative_path" >&2
echo " local : $local_hash" >&2
echo " remote: $remote_hash" >&2
exit 1
fi
printf 'Synced %s -> %s (%s)\n' "$relative_path" "$windows_dest_path" "$local_hash"
done