193 lines
6.8 KiB
PowerShell
193 lines
6.8 KiB
PowerShell
[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 "(?<major>\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.'
|