hypertwist/scripts/run-hypertwist-remote-unreal-build.sh
2026-06-30 11:47:41 +00:00

272 lines
7.7 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
HYPERTWIST_REMOTE_WINDOWS_PASSWORD=... \
scripts/run-hypertwist-remote-unreal-build.sh
Options:
--target <name>
Optional Unreal build target. Defaults to UnrealHyperTwistEditor.
--platform <name>
Optional Unreal platform. Defaults to Win64.
--configuration <name>
Optional Unreal configuration. Defaults to Development.
--worktree-root <path>
Optional Windows worktree root. Defaults to C:\HyperTwist_worktrees\phase10validate.
--project-path <path>
Optional Windows .uproject path. Defaults beneath the worktree root.
--build-batch-path <path>
Optional Build.bat path. Defaults to the UE 5.7 installed-engine path.
--max-parallel-actions <count>
Optional MaxParallelActions override. Defaults to 2 for the remote lane.
--allow-uba
Allow UBA instead of forcing the verified remote-lane -NoUBA posture.
--source-lock-retries <count>
Retry the remote build when UnrealBuildTool hits a transient Windows
source-file lock. Defaults to 1.
--source-lock-retry-delay-seconds <count>
Delay between transient source-lock retries. Defaults to 3 seconds.
--extra-arg <value>
Append an extra Build.bat argument. May be repeated.
--dry-run
Print the generated remote PowerShell payload instead of executing it.
Environment:
HYPERTWIST_REMOTE_WINDOWS_PASSWORD Required by the underlying tunnel wrapper.
HYPERTWIST_REMOTE_WINDOWS_WORKTREE_ROOT Optional default for --worktree-root.
HYPERTWIST_REMOTE_WINDOWS_PROJECT_PATH Optional default for --project-path.
HYPERTWIST_REMOTE_WINDOWS_BUILD_BAT Optional default for --build-batch-path.
HYPERTWIST_REMOTE_WINDOWS_MAX_PARALLEL_ACTIONS
Optional default for --max-parallel-actions.
HYPERTWIST_REMOTE_WINDOWS_SOURCE_LOCK_RETRIES
Optional default for --source-lock-retries.
HYPERTWIST_REMOTE_WINDOWS_SOURCE_LOCK_RETRY_DELAY_SECONDS
Optional default for --source-lock-retry-delay-seconds.
EOF
}
target_name="UnrealHyperTwistEditor"
platform_name="Win64"
configuration_name="Development"
worktree_root="${HYPERTWIST_REMOTE_WINDOWS_WORKTREE_ROOT:-C:\\HyperTwist_worktrees\\phase10validate}"
project_path=""
build_batch_path="${HYPERTWIST_REMOTE_WINDOWS_BUILD_BAT:-C:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Build\\BatchFiles\\Build.bat}"
max_parallel_actions="${HYPERTWIST_REMOTE_WINDOWS_MAX_PARALLEL_ACTIONS:-2}"
source_lock_retries="${HYPERTWIST_REMOTE_WINDOWS_SOURCE_LOCK_RETRIES:-1}"
source_lock_retry_delay_seconds="${HYPERTWIST_REMOTE_WINDOWS_SOURCE_LOCK_RETRY_DELAY_SECONDS:-3}"
allow_uba="false"
dry_run="false"
extra_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
target_name="${2:-}"
shift 2
;;
--platform)
platform_name="${2:-}"
shift 2
;;
--configuration)
configuration_name="${2:-}"
shift 2
;;
--worktree-root)
worktree_root="${2:-}"
shift 2
;;
--project-path)
project_path="${2:-}"
shift 2
;;
--build-batch-path)
build_batch_path="${2:-}"
shift 2
;;
--max-parallel-actions)
max_parallel_actions="${2:-}"
shift 2
;;
--allow-uba)
allow_uba="true"
shift
;;
--source-lock-retries)
source_lock_retries="${2:-}"
shift 2
;;
--source-lock-retry-delay-seconds)
source_lock_retry_delay_seconds="${2:-}"
shift 2
;;
--extra-arg)
extra_args+=("${2:-}")
shift 2
;;
--dry-run)
dry_run="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 1
;;
esac
done
if [[ -z "$project_path" ]]; then
project_path="${HYPERTWIST_REMOTE_WINDOWS_PROJECT_PATH:-${worktree_root}\\UnrealHyperTwist\\UnrealHyperTwist.uproject}"
fi
if [[ -z "$target_name" || -z "$platform_name" || -z "$configuration_name" || -z "$project_path" || -z "$build_batch_path" ]]; then
echo "Target, platform, configuration, project path, and Build.bat path must all be populated." >&2
exit 1
fi
if [[ -n "$max_parallel_actions" && ! "$max_parallel_actions" =~ ^[0-9]+$ ]]; then
echo "--max-parallel-actions must be numeric." >&2
exit 1
fi
if [[ -n "$source_lock_retries" && ! "$source_lock_retries" =~ ^[0-9]+$ ]]; then
echo "--source-lock-retries must be numeric." >&2
exit 1
fi
if [[ -n "$source_lock_retry_delay_seconds" && ! "$source_lock_retry_delay_seconds" =~ ^[0-9]+$ ]]; then
echo "--source-lock-retry-delay-seconds must be numeric." >&2
exit 1
fi
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
remote_ps_wrapper="${script_dir}/run-hypertwist-remote-windows-powershell.sh"
if [[ ! -x "$remote_ps_wrapper" ]]; then
echo "Expected executable wrapper at $remote_ps_wrapper" >&2
exit 1
fi
powershell_payload="$(
python3 - <<'PY' \
"$build_batch_path" \
"$target_name" \
"$platform_name" \
"$configuration_name" \
"$project_path" \
"$max_parallel_actions" \
"$allow_uba" \
"${extra_args[@]}"
import sys
build_batch_path, target_name, platform_name, configuration_name, project_path, max_parallel_actions, allow_uba, *extra_args = sys.argv[1:]
def ps_single_quote(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
lines = [
f"$BuildBatchPath = {ps_single_quote(build_batch_path)}",
f"$TargetName = {ps_single_quote(target_name)}",
f"$PlatformName = {ps_single_quote(platform_name)}",
f"$ConfigurationName = {ps_single_quote(configuration_name)}",
f"$ProjectPath = {ps_single_quote(project_path)}",
f"$MaxParallelActions = {max_parallel_actions}",
f"$AllowUba = ${'true' if allow_uba == 'true' else 'false'}",
"$ExtraArgs = @(",
]
for value in extra_args:
lines.append(f" {ps_single_quote(value)}")
lines.extend([
")",
"",
"if (-not (Test-Path -LiteralPath $BuildBatchPath)) {",
" throw \"Build.bat was not found at '$BuildBatchPath'.\"",
"}",
"",
"if (-not (Test-Path -LiteralPath $ProjectPath)) {",
" throw \"Project file was not found at '$ProjectPath'.\"",
"}",
"",
"$BuildArgs = @(",
" $TargetName",
" $PlatformName",
" $ConfigurationName",
" $ProjectPath",
" '-WaitMutex'",
" '-NoHotReloadFromIDE'",
")",
"",
"if (-not $AllowUba) {",
" $BuildArgs += '-NoUBA'",
"}",
"",
"if ($MaxParallelActions -gt 0) {",
" $BuildArgs += \"-MaxParallelActions=$MaxParallelActions\"",
"}",
"",
"if ($ExtraArgs.Count -gt 0) {",
" $BuildArgs += $ExtraArgs",
"}",
"",
"& $BuildBatchPath @BuildArgs",
"exit $LASTEXITCODE",
])
print("\n".join(lines))
PY
)"
if [[ "$dry_run" == "true" ]]; then
printf '%s\n' "$powershell_payload"
exit 0
fi
source_lock_marker="because it is being used by another process"
attempt=1
max_attempts=$((source_lock_retries + 1))
build_output_path="$(mktemp)"
trap 'rm -f "$build_output_path"' EXIT
while :; do
set +e
"$remote_ps_wrapper" --command "$powershell_payload" 2>&1 | tee "$build_output_path"
build_exit="${PIPESTATUS[0]}"
set -e
if [[ "$build_exit" -eq 0 ]]; then
exit 0
fi
if ! grep -Fq "$source_lock_marker" "$build_output_path"; then
exit "$build_exit"
fi
if (( attempt >= max_attempts )); then
echo "Remote Unreal build exhausted ${max_attempts} attempt(s) after transient source-file lock detection." >&2
exit "$build_exit"
fi
attempt=$((attempt + 1))
echo "Retrying remote Unreal build after transient source-file lock (${attempt}/${max_attempts}) in ${source_lock_retry_delay_seconds}s..." >&2
sleep "$source_lock_retry_delay_seconds"
done