#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: HYPERTWIST_REMOTE_WINDOWS_PASSWORD=... \ scripts/run-hypertwist-remote-unreal-build.sh Options: --target Optional Unreal build target. Defaults to UnrealHyperTwistEditor. --platform Optional Unreal platform. Defaults to Win64. --configuration Optional Unreal configuration. Defaults to Development. --worktree-root Optional Windows worktree root. Defaults to C:\HyperTwist_worktrees\phase10validate. --project-path Optional Windows .uproject path. Defaults beneath the worktree root. --build-batch-path Optional Build.bat path. Defaults to the UE 5.7 installed-engine path. --max-parallel-actions 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 Retry the remote build when UnrealBuildTool hits a transient Windows source-file lock. Defaults to 1. --source-lock-retry-delay-seconds Delay between transient source-lock retries. Defaults to 3 seconds. --extra-arg 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