feat: integrate N7Cockpit remote mode control
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
[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.'
|
||||
Reference in New Issue
Block a user