127 lines
7.5 KiB
PowerShell
127 lines
7.5 KiB
PowerShell
param(
|
|
[Parameter(Mandatory=$true)][string]$ProjectPath,
|
|
[ValidateSet('debug','release')][string]$Mode='debug',
|
|
[string]$ApiUrl='http://127.0.0.1:9996/api',
|
|
[string]$FlutterPath='',
|
|
[string]$OutputPath='',
|
|
[string]$BuildName='',
|
|
[string]$BuildNumber='',
|
|
[ValidateRange(2,8)][int]$GradleHeapGB=4,
|
|
[switch]$RunTests,
|
|
[switch]$CheckOnly,
|
|
[switch]$InstallOnly,
|
|
[string]$ApkPath=''
|
|
)
|
|
$ErrorActionPreference='Stop'
|
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
$OutputEncoding = [Console]::OutputEncoding
|
|
$buildLock=$null
|
|
$transcribing=$false
|
|
$previousGradleOpts=$env:GRADLE_OPTS
|
|
function Invoke-Checked([string]$Executable, [string[]]$Arguments) {
|
|
$ErrorActionPreference='Continue'
|
|
& $Executable @Arguments 2>&1 | ForEach-Object { Write-Output "$_" }
|
|
if ($LASTEXITCODE -ne 0) { throw "Command failed (exit $LASTEXITCODE): $Executable" }
|
|
}
|
|
function Read-Property([string]$Name) {
|
|
$propertyFile=Join-Path $ProjectPath 'android/local.properties'
|
|
if (Test-Path -LiteralPath $propertyFile) {
|
|
$line=Get-Content -LiteralPath $propertyFile | Where-Object { $_.StartsWith($Name+'=') } | Select-Object -First 1
|
|
if ($line) { return $line.Substring($Name.Length+1).Replace('\\','\').Replace('\:',':') }
|
|
}
|
|
return ''
|
|
}
|
|
try {
|
|
$ProjectPath=(Resolve-Path -LiteralPath $ProjectPath).Path
|
|
if (!(Test-Path -LiteralPath (Join-Path $ProjectPath 'pubspec.yaml'))) { throw 'Select the Flutter project folder containing pubspec.yaml.' }
|
|
Set-Location -LiteralPath $ProjectPath
|
|
if (!$OutputPath) { $OutputPath=Join-Path $ProjectPath 'packages' }
|
|
$OutputPath=[IO.Path]::GetFullPath($OutputPath)
|
|
$null=New-Item -ItemType Directory -Force -Path $OutputPath
|
|
$jobPath=Join-Path $OutputPath ((Get-Date -Format 'yyyyMMdd-HHmmss-fff')+'-'+$Mode)
|
|
$null=New-Item -ItemType Directory -Path $jobPath
|
|
Start-Transcript -LiteralPath (Join-Path $jobPath 'build.log') -Force | Out-Null
|
|
$transcribing=$true
|
|
Write-Output "PACKAGER_OUTPUT=$jobPath"
|
|
$lockDirectory=Join-Path $ProjectPath '.dart_tool'
|
|
$null=New-Item -ItemType Directory -Force -Path $lockDirectory
|
|
$buildLock=[IO.File]::Open((Join-Path $lockDirectory 'yd-packager.lock'),[IO.FileMode]::OpenOrCreate,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None)
|
|
$sdkPath=Read-Property 'sdk.dir'
|
|
if (!$sdkPath) { $sdkPath=$env:ANDROID_HOME }
|
|
if (!$sdkPath) { $sdkPath=$env:ANDROID_SDK_ROOT }
|
|
$adb=if ($sdkPath) { Join-Path $sdkPath 'platform-tools/adb.exe' } else { '' }
|
|
if (!$adb -or !(Test-Path -LiteralPath $adb)) { $command=Get-Command adb.exe -ErrorAction SilentlyContinue; if ($command) { $adb=$command.Source } }
|
|
if ($InstallOnly) {
|
|
if (!$adb -or !(Test-Path -LiteralPath $adb)) { throw 'ADB not found. Install Android SDK platform-tools.' }
|
|
if (!(Test-Path -LiteralPath $ApkPath) -or [IO.Path]::GetExtension($ApkPath) -ne '.apk') { throw 'A valid APK file is required.' }
|
|
$deviceLines=& $adb devices
|
|
$devices=@($deviceLines | Where-Object { $_ -match '^\S+\s+device$' } | ForEach-Object { ($_ -split '\s+')[0] })
|
|
if ($devices.Count -ne 1) { throw 'Connect exactly one authorized Android device. Check USB debugging authorization.' }
|
|
Invoke-Checked $adb @('-s',$devices[0],'install','-r',$ApkPath)
|
|
Write-Output 'PACKAGER_INSTALL_OK'
|
|
exit 0
|
|
}
|
|
[Uri]$endpoint=$null
|
|
if (![Uri]::TryCreate($ApiUrl,[UriKind]::Absolute,[ref]$endpoint) -or $endpoint.Scheme -notin @('http','https') -or !$endpoint.Host -or $endpoint.UserInfo -or $endpoint.Query -or $endpoint.Fragment) {
|
|
throw 'API URL must be an absolute HTTP/HTTPS address, without credentials, query or fragment.'
|
|
}
|
|
$ApiUrl=$ApiUrl.TrimEnd('/')
|
|
if ($endpoint.AbsolutePath -eq '/') {
|
|
$ApiUrl += '/api'
|
|
Write-Output "API root URL normalized to: $ApiUrl"
|
|
}
|
|
if ($Mode -eq 'release' -and $endpoint.Scheme -ne 'https') { throw 'Release requires HTTPS. Use Debug for HTTP testing.' }
|
|
if ($BuildName -and $BuildName -notmatch '^\d+\.\d+\.\d+(?:[.-][A-Za-z0-9.-]+)?$') { throw 'Invalid version name. Example: 1.0.0' }
|
|
if ($BuildNumber -and ($BuildNumber -notmatch '^[1-9]\d{0,8}$')) { throw 'Version code must be a positive integer (max 9 digits).' }
|
|
if (!$FlutterPath) {
|
|
$flutterSdk=Read-Property 'flutter.sdk'
|
|
if ($flutterSdk) { $FlutterPath=Join-Path $flutterSdk 'bin/flutter.bat' }
|
|
}
|
|
if (!$FlutterPath) { $command=Get-Command flutter.bat -ErrorAction SilentlyContinue; if ($command) { $FlutterPath=$command.Source } }
|
|
if (!$FlutterPath -or !(Test-Path -LiteralPath $FlutterPath)) { throw 'Flutter was not found. Select flutter.bat in the tool.' }
|
|
if (!$sdkPath -or !(Test-Path -LiteralPath $sdkPath)) { throw 'Android SDK not found. Configure android/local.properties or ANDROID_HOME.' }
|
|
Write-Output "Project: $ProjectPath"
|
|
Write-Output "Flutter: $FlutterPath"
|
|
Write-Output "Android SDK: $sdkPath"
|
|
Write-Output "API: $ApiUrl"
|
|
Write-Output 'NOTE: Existing in-app API settings override the compiled default URL.'
|
|
if ($Mode -eq 'release') { Write-Output 'WARNING: Current project uses a debug signing key for Release. Not for production publishing.' }
|
|
Invoke-Checked $FlutterPath @('--version')
|
|
if ($CheckOnly) { Write-Output 'PACKAGER_CHECK_OK'; exit 0 }
|
|
if ($RunTests) {
|
|
Invoke-Checked $FlutterPath @('analyze')
|
|
Invoke-Checked $FlutterPath @('test')
|
|
}
|
|
# User-level gradle.properties overrides project properties. An explicit
|
|
# JVM system property wins for this child process, without changing globals.
|
|
$heapOption='-Dorg.gradle.jvmargs="-Xmx'+$GradleHeapGB+'g -XX:MaxMetaspaceSize=1g -XX:ReservedCodeCacheSize=256m -Dfile.encoding=UTF-8"'
|
|
$env:GRADLE_OPTS=($previousGradleOpts+' '+$heapOption).Trim()
|
|
Write-Output "Gradle build heap: ${GradleHeapGB}GB; metaspace: 1GB (process-local override)."
|
|
# A fresh Gradle daemon avoids accumulated heap pressure on this workstation.
|
|
$gradle=Join-Path $ProjectPath 'android/gradlew.bat'
|
|
if (Test-Path -LiteralPath $gradle) {
|
|
Push-Location (Join-Path $ProjectPath 'android')
|
|
try { Invoke-Checked $gradle @('--stop') } finally { Pop-Location }
|
|
}
|
|
$arguments=@('build','apk',('--'+$Mode),('--dart-define=API_BASE_URL='+$ApiUrl))
|
|
if ($BuildName) { $arguments+=('--build-name='+$BuildName) }
|
|
if ($BuildNumber) { $arguments+=('--build-number='+$BuildNumber) }
|
|
Invoke-Checked $FlutterPath $arguments
|
|
$sourceApk=Join-Path $ProjectPath ('build/app/outputs/flutter-apk/app-'+$Mode+'.apk')
|
|
if (!(Test-Path -LiteralPath $sourceApk)) { throw 'Build completed without the expected APK.' }
|
|
$targetApk=Join-Path $jobPath ('YD-Pattern-Scoring-'+$Mode+'.apk')
|
|
Copy-Item -LiteralPath $sourceApk -Destination $targetApk
|
|
$hash=(Get-FileHash -LiteralPath $targetApk -Algorithm SHA256).Hash
|
|
[ordered]@{ builtAt=(Get-Date).ToString('o'); mode=$Mode; apiBaseUrl=$ApiUrl; gradleHeapGB=$GradleHeapGB; versionName=$BuildName; versionCode=$BuildNumber; apk=$targetApk; sha256=$hash; signing='Project Gradle signing configuration (currently debug key)' } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $jobPath 'build-info.json') -Encoding UTF8
|
|
Write-Output "SHA256=$hash"
|
|
Write-Output "PACKAGER_APK=$targetApk"
|
|
exit 0
|
|
} catch {
|
|
Write-Output ('PACKAGER_ERROR='+$_.Exception.Message)
|
|
exit 1
|
|
} finally {
|
|
$env:GRADLE_OPTS=$previousGradleOpts
|
|
if ($buildLock) { $buildLock.Dispose() }
|
|
if ($transcribing) { Stop-Transcript | Out-Null }
|
|
}
|