[CmdletBinding()] param( [string]$ConfigPath = '', [ValidateSet('debug', 'release')] [string]$Variant = 'debug', [switch]$AllowHardware ) $ErrorActionPreference = 'Stop' $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path if (-not $ConfigPath) { $ConfigPath = Join-Path $projectRoot 'config\migration.psd1' } if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path $projectRoot $ConfigPath } if (-not (Test-Path -LiteralPath $ConfigPath)) { throw "Migration config does not exist: $ConfigPath" } $config = Import-PowerShellDataFile -LiteralPath $ConfigPath $required = @( 'ServerAUrl', 'AndroidServerAUrl', 'VehicleId', 'HardwareClientId', 'ExecutionTarget', 'TokenFile' ) foreach ($key in $required) { if (-not $config.ContainsKey($key) -or -not ([string]$config[$key]).Trim()) { throw "Migration config is missing: $key" } } $serverAUrl = ([string]$config.ServerAUrl).TrimEnd('/') $androidServerAUrl = ([string]$config.AndroidServerAUrl).TrimEnd('/') $vehicleId = ([string]$config.VehicleId).Trim() $hardwareClientId = ([string]$config.HardwareClientId).Trim() $executionTarget = ([string]$config.ExecutionTarget).Trim().ToLowerInvariant() if ($executionTarget -notin @('dry_run', 'simulated_hardware', 'hardware')) { throw "Unsupported ExecutionTarget: $executionTarget" } if ($executionTarget -eq 'hardware' -and -not $AllowHardware) { throw 'Safety interlock: hardware builds require the explicit -AllowHardware switch.' } $tokenPath = [string]$config.TokenFile if (-not [System.IO.Path]::IsPathRooted($tokenPath)) { $tokenPath = Join-Path $projectRoot $tokenPath } if (-not (Test-Path -LiteralPath $tokenPath)) { throw "Device token file does not exist: $tokenPath" } $deviceToken = (Get-Content -LiteralPath $tokenPath -Raw).Trim() if ($deviceToken.Length -lt 32) { throw 'The device token must contain at least 32 characters.' } $javaHomeCandidates = @(@( [string]$config.JavaHome, $env:JAVA_HOME, 'C:\Program Files\Android\Android Studio\jbr' ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) }) if (-not $javaHomeCandidates) { throw 'JDK 17 was not found. Set JavaHome in config or JAVA_HOME.' } $javaHome = $javaHomeCandidates[0] $javaExe = Join-Path $javaHome 'bin\java.exe' if (-not (Test-Path -LiteralPath $javaExe)) { throw "java.exe does not exist under JavaHome: $javaHome" } $savedErrorAction = $ErrorActionPreference $ErrorActionPreference = 'Continue' $javaVersion = (& $javaExe -version 2>&1 | Out-String) $ErrorActionPreference = $savedErrorAction $javaMajor = if ($javaVersion -match 'version "(?\d+)') { [int]$Matches.major } else { 0 } if ($javaMajor -lt 17) { throw "JDK 17 or newer is required. Detected: $($javaVersion.Trim())" } $sdkCandidates = @(@( [string]$config.AndroidSdk, $env:ANDROID_SDK_ROOT, $env:ANDROID_HOME, (Join-Path $env:LOCALAPPDATA 'Android\Sdk') ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) }) if (-not $sdkCandidates) { throw 'Android SDK was not found. Set AndroidSdk, ANDROID_SDK_ROOT, or ANDROID_HOME.' } $androidSdk = $sdkCandidates[0] if (-not (Test-Path -LiteralPath (Join-Path $androidSdk 'platforms\android-35'))) { throw "Android SDK platform 35 is missing under: $androidSdk" } $headers = @{ Authorization = "Bearer $deviceToken" } $catalogUri = "$serverAUrl/api/v1/vehicles/$vehicleId/modes" try { $catalog = Invoke-RestMethod -Uri $catalogUri -Headers $headers -TimeoutSec 10 } catch { throw "Server A preflight failed at $catalogUri : $($_.Exception.Message)" } $presetCount = @($catalog.modes | Where-Object { $_.origin -eq 'preset' }).Count if ($presetCount -lt 1) { throw 'Server A returned no repository preset modes.' } $oldJavaHome = $env:JAVA_HOME $oldAndroidSdkRoot = $env:ANDROID_SDK_ROOT $oldAndroidHome = $env:ANDROID_HOME $oldBaseUrl = $env:COGNITIVE_AGENT_BASE_URL $oldDeviceToken = $env:COGNITIVE_AGENT_DEVICE_API_TOKEN try { $env:JAVA_HOME = $javaHome $env:ANDROID_SDK_ROOT = $androidSdk $env:ANDROID_HOME = $androidSdk $env:COGNITIVE_AGENT_BASE_URL = $androidServerAUrl $env:COGNITIVE_AGENT_DEVICE_API_TOKEN = $deviceToken $gradle = ([string]$config.GradleExecutable).Trim() if (-not $gradle) { $gradle = Join-Path $projectRoot 'gradlew.bat' } if (-not [System.IO.Path]::IsPathRooted($gradle)) { $gradle = Join-Path $projectRoot $gradle } if (-not (Test-Path -LiteralPath $gradle)) { throw "Gradle executable does not exist: $gradle" } $variantTask = if ($Variant -eq 'release') { 'assembleRelease' } else { 'assembleDebug' } $testTask = if ($Variant -eq 'release') { $null } else { 'testDebugUnitTest' } $arguments = @( '--no-daemon', "-PorchestratorVehicleId=$vehicleId", "-PorchestratorHardwareClientId=$hardwareClientId", "-PorchestratorExecutionTarget=$executionTarget" ) if ($testTask) { $arguments += $testTask } $arguments += $variantTask Push-Location $projectRoot try { & $gradle @arguments if ($LASTEXITCODE -ne 0) { throw 'Gradle build failed.' } } finally { Pop-Location } } finally { $env:JAVA_HOME = $oldJavaHome $env:ANDROID_SDK_ROOT = $oldAndroidSdkRoot $env:ANDROID_HOME = $oldAndroidHome $env:COGNITIVE_AGENT_BASE_URL = $oldBaseUrl $env:COGNITIVE_AGENT_DEVICE_API_TOKEN = $oldDeviceToken $deviceToken = $null } $sourceApk = Join-Path $projectRoot "app\build\outputs\apk\$Variant\app-$Variant.apk" if (-not (Test-Path -LiteralPath $sourceApk)) { throw "Built APK was not found: $sourceApk" } $distDirectory = Join-Path $projectRoot 'dist' New-Item -ItemType Directory -Force -Path $distDirectory | Out-Null $destinationApk = Join-Path $distDirectory "N7Cockpit-remote-$Variant.apk" Copy-Item -LiteralPath $sourceApk -Destination $destinationApk -Force $apkHash = (Get-FileHash -LiteralPath $destinationApk -Algorithm SHA256).Hash $buildInfo = [ordered]@{ generated_at = (Get-Date).ToUniversalTime().ToString('o') variant = $Variant package = if ($Variant -eq 'debug') { 'com.seeker.n7cockpit.remote' } else { 'com.seeker.n7cockpit' } server_a_url_from_server_b = $serverAUrl android_server_a_url = $androidServerAUrl vehicle_id = $vehicleId hardware_client_id = $hardwareClientId execution_target = $executionTarget preset_count = $presetCount apk_sha256 = $apkHash } $buildInfo | ConvertTo-Json | Set-Content -LiteralPath ( Join-Path $distDirectory 'build-info.json' ) -Encoding UTF8 Write-Host '' Write-Host 'BUILD PASSED' Write-Host "APK: $destinationApk" Write-Host "SHA256: $apkHash" Write-Host "Execution target: $executionTarget" Write-Host 'The token is embedded in the APK. Keep the APK private and rotate the token if leaked.'