hypertwist/scripts/run-hypertwist-remote-windows-file-sync.sh
2026-06-24 11:03:34 +00:00

203 lines
5.3 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 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
}
stream_file_as_base64() {
local file_path="$1"
python3 - <<'PY' "$file_path"
import base64
import pathlib
import sys
file_path = pathlib.Path(sys.argv[1])
sys.stdout.write(base64.b64encode(file_path.read_bytes()).decode("ascii"))
PY
}
measure_base64_length() {
local file_path="$1"
python3 - <<'PY' "$file_path"
import pathlib
import sys
size = pathlib.Path(sys.argv[1]).stat().st_size
sys.stdout.write(str(((size + 2) // 3) * 4))
PY
}
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)}')"
expected_base64_length="$(measure_base64_length "$relative_path")"
if [[ "$dry_run" == "true" ]]; then
printf '%s -> %s (%s)\n' "$relative_path" "$windows_dest_path" "$local_hash"
continue
fi
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}"
)
remote_write_script="$(
cat <<EOF
\$ProgressPreference = 'SilentlyContinue'
\$ErrorActionPreference = 'Stop'
\$Path='${windows_dest_path}'
\$ExpectedBase64Length = ${expected_base64_length}
\$builder = New-Object System.Text.StringBuilder
\$reader = [Console]::In
while (\$builder.Length -lt \$ExpectedBase64Length) {
\$remaining = \$ExpectedBase64Length - \$builder.Length
\$chunkSize = [Math]::Min(4096, \$remaining)
\$buffer = New-Object char[] \$chunkSize
\$read = \$reader.Read(\$buffer, 0, \$chunkSize)
if (\$read -le 0) {
break
}
[void]\$builder.Append(\$buffer, 0, \$read)
}
\$content = \$builder.ToString()
if (\$content.Length -ne \$ExpectedBase64Length) {
throw ('Expected ' + \$ExpectedBase64Length + ' base64 characters but received ' + \$content.Length + '.')
}
\$bytes = [Convert]::FromBase64String(\$content)
\$dir = [System.IO.Path]::GetDirectoryName(\$Path)
if (\$dir) {
[System.IO.Directory]::CreateDirectory(\$dir) | Out-Null
}
[System.IO.File]::WriteAllBytes(\$Path, \$bytes)
Write-Output ('REMOTE_HASH=' + (Get-FileHash -LiteralPath \$Path -Algorithm SHA256).Hash.ToLowerInvariant())
EOF
)"
encoded_remote_write_script="$(encode_powershell_command "$remote_write_script")"
remote_hash="$(
stream_file_as_base64 "$relative_path" \
| "${ssh_base[@]}" "powershell -NoProfile -EncodedCommand ${encoded_remote_write_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