203 lines
5.3 KiB
Bash
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
|