V14
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Windows build for the DragonTCP Lite APK (no Gradle, no Android Studio).
|
||||
|
||||
.DESCRIPTION
|
||||
Port of build_apk.sh. Compiles the Kotlin TUN adapter and the Java UI/service,
|
||||
dexes them with d8, packages resources with aapt, aligns with zipalign and
|
||||
signs with apksigner.
|
||||
|
||||
Run it from the directory that holds AndroidManifest.xml. The Go core lives
|
||||
in <repo>/core; the repo root is located by walking up from this script until
|
||||
a core\go.mod is found, so the script keeps working if the tree is nested.
|
||||
|
||||
Everything else is auto-detected where possible:
|
||||
* Android SDK - ANDROID_SDK_ROOT / ANDROID_HOME, or -SdkRoot
|
||||
* build-tools - newest installed version that has aapt/d8/apksigner/zipalign
|
||||
* platform - android-35 if present, else newest installed
|
||||
* JDK - JAVA_HOME, then javac on PATH, then C:\Program Files\Java\*
|
||||
* Kotlin - KOTLIN_HOME, or downloaded once into .\.tools\kotlinc-<version>
|
||||
* native lib - built via <repo>\build_core.ps1 if the .so is missing
|
||||
|
||||
.EXAMPLE
|
||||
.\build_apk.ps1
|
||||
.\build_apk.ps1 -BuildCore # rebuild the Go .so first
|
||||
.\build_apk.ps1 -BuildTools 35.0.0 -Platform android-35
|
||||
.\build_apk.ps1 -Keystore C:\keys\rel.jks -KsPass secret -KeyAlias rel -KeyPass secret
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$SdkRoot,
|
||||
[string]$BuildTools,
|
||||
[string]$Platform,
|
||||
[string]$JavaHome,
|
||||
[string]$KotlinHome,
|
||||
[string]$KotlinVersion = '2.1.21',
|
||||
[string]$RepoRoot,
|
||||
[string]$Keystore,
|
||||
[string]$KsPass = 'dragontcp',
|
||||
[string]$KeyAlias = 'dragontcp',
|
||||
[string]$KeyPass = 'dragontcp',
|
||||
# Rebuild the Go native library before packaging.
|
||||
[switch]$BuildCore,
|
||||
# Fail instead of downloading the Kotlin compiler.
|
||||
[switch]$NoDownload
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = $PSScriptRoot
|
||||
$ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest is ~10x faster without it
|
||||
|
||||
function Step { param([string]$m) Write-Host "[apk] $m" -ForegroundColor Cyan }
|
||||
function Die { param([string]$m) throw $m }
|
||||
|
||||
function Invoke-Tool {
|
||||
param([string]$Exe, [string[]]$ToolArgs, [string]$What)
|
||||
# These tools write harmless warnings to stderr (the JVM's sun.misc.Unsafe
|
||||
# notice, apksigner's native-access notice). Under $ErrorActionPreference =
|
||||
# 'Stop' with a merged stream those become terminating errors, so judge
|
||||
# success by the exit code alone.
|
||||
$saved = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try { & $Exe @ToolArgs } finally { $ErrorActionPreference = $saved }
|
||||
if ($LASTEXITCODE -ne 0) { Die "$What failed (exit $LASTEXITCODE): $Exe" }
|
||||
}
|
||||
|
||||
if (-not (Test-Path (Join-Path $Root 'AndroidManifest.xml'))) {
|
||||
Die "No AndroidManifest.xml next to this script. Run it from the Android app directory."
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ repo root
|
||||
if (-not $RepoRoot) {
|
||||
# Walk up collecting every ancestor that looks like a checkout. A tree can
|
||||
# contain more than one copy of core\, so prefer the one that also carries
|
||||
# build_core.ps1 and only fall back to the nearest bare core\go.mod.
|
||||
$fallback = $null
|
||||
$probe = $Root
|
||||
for ($i = 0; $i -lt 8 -and $probe; $i++) {
|
||||
if (Test-Path (Join-Path $probe 'core\go.mod')) {
|
||||
if (Test-Path (Join-Path $probe 'build_core.ps1')) { $RepoRoot = $probe; break }
|
||||
if (-not $fallback) { $fallback = $probe }
|
||||
}
|
||||
$parent = Split-Path $probe -Parent
|
||||
if ($parent -eq $probe) { break }
|
||||
$probe = $parent
|
||||
}
|
||||
if (-not $RepoRoot) { $RepoRoot = $fallback }
|
||||
}
|
||||
if ($RepoRoot) { $RepoRoot = (Resolve-Path $RepoRoot).Path }
|
||||
|
||||
# ---------------------------------------------------------------- Android SDK
|
||||
if (-not $SdkRoot) {
|
||||
foreach ($c in $env:ANDROID_SDK_ROOT, $env:ANDROID_HOME,
|
||||
"$env:LOCALAPPDATA\Android\Sdk", 'C:\Android\Sdk') {
|
||||
if ($c -and (Test-Path $c)) { $SdkRoot = $c; break }
|
||||
}
|
||||
}
|
||||
if (-not $SdkRoot -or -not (Test-Path $SdkRoot)) {
|
||||
Die "Android SDK not found. Set ANDROID_SDK_ROOT (or pass -SdkRoot 'D:\Android\Sdk')."
|
||||
}
|
||||
$SdkRoot = (Resolve-Path $SdkRoot).Path
|
||||
|
||||
$needTools = @{ aapt = 'aapt.exe'; d8 = 'd8.bat'; apksigner = 'apksigner.bat'; zipalign = 'zipalign.exe' }
|
||||
|
||||
function Test-BuildTools {
|
||||
param([string]$Dir)
|
||||
foreach ($f in $needTools.Values) { if (-not (Test-Path (Join-Path $Dir $f))) { return $false } }
|
||||
return $true
|
||||
}
|
||||
|
||||
$btRoot = Join-Path $SdkRoot 'build-tools'
|
||||
if (-not (Test-Path $btRoot)) { Die "No build-tools under $SdkRoot. Install them via the SDK Manager." }
|
||||
if ($BuildTools) {
|
||||
$BT = Join-Path $btRoot $BuildTools
|
||||
if (-not (Test-BuildTools $BT)) { Die "build-tools $BuildTools is missing one of: $($needTools.Values -join ', ')" }
|
||||
} else {
|
||||
$candidates = Get-ChildItem $btRoot -Directory | Sort-Object {
|
||||
try { [version]$_.Name } catch { [version]'0.0.0' }
|
||||
} -Descending
|
||||
$BT = $null
|
||||
foreach ($c in $candidates) { if (Test-BuildTools $c.FullName) { $BT = $c.FullName; $BuildTools = $c.Name; break } }
|
||||
if (-not $BT) { Die "No usable build-tools found under $btRoot (need $($needTools.Values -join ', '))." }
|
||||
}
|
||||
|
||||
$platRoot = Join-Path $SdkRoot 'platforms'
|
||||
if ($Platform) {
|
||||
$AJ = Join-Path $platRoot "$Platform\android.jar"
|
||||
} else {
|
||||
$preferred = Join-Path $platRoot 'android-35\android.jar'
|
||||
if (Test-Path $preferred) {
|
||||
$Platform = 'android-35'; $AJ = $preferred
|
||||
} else {
|
||||
$p = Get-ChildItem $platRoot -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { Test-Path (Join-Path $_.FullName 'android.jar') } |
|
||||
Sort-Object { [int]($_.Name -replace '\D', '') } -Descending |
|
||||
Select-Object -First 1
|
||||
if (-not $p) { Die "No android.jar found under $platRoot. Install a platform via the SDK Manager." }
|
||||
$Platform = $p.Name; $AJ = Join-Path $p.FullName 'android.jar'
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path $AJ)) { Die "Android platform jar not found: $AJ" }
|
||||
|
||||
# ----------------------------------------------------------------------- JDK
|
||||
if (-not $JavaHome) {
|
||||
if ($env:JAVA_HOME -and (Test-Path "$env:JAVA_HOME\bin\javac.exe")) {
|
||||
$JavaHome = $env:JAVA_HOME
|
||||
} else {
|
||||
$jc = Get-Command javac.exe -ErrorAction SilentlyContinue
|
||||
# javapath\javac.exe is a shim: it can compile but has no jar/keytool next to it.
|
||||
if ($jc -and (Test-Path (Join-Path (Split-Path $jc.Source) 'jar.exe'))) {
|
||||
$JavaHome = Split-Path (Split-Path $jc.Source)
|
||||
} else {
|
||||
$j = Get-ChildItem 'C:\Program Files\Java', 'C:\Program Files\Eclipse Adoptium',
|
||||
'C:\Program Files\Microsoft', 'C:\Program Files\Android\Android Studio' `
|
||||
-Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { Test-Path (Join-Path $_.FullName 'bin\javac.exe') } |
|
||||
Sort-Object Name -Descending | Select-Object -First 1
|
||||
if ($j) { $JavaHome = $j.FullName }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $JavaHome -or -not (Test-Path "$JavaHome\bin\javac.exe")) {
|
||||
Die "No JDK found. Install a JDK (17 or 21 recommended) and set JAVA_HOME, or pass -JavaHome."
|
||||
}
|
||||
$JavaHome = (Resolve-Path $JavaHome).Path
|
||||
$javaExe = "$JavaHome\bin\java.exe"
|
||||
$javac = "$JavaHome\bin\javac.exe"
|
||||
$jar = "$JavaHome\bin\jar.exe"
|
||||
$keytool = "$JavaHome\bin\keytool.exe"
|
||||
foreach ($t in $javaExe, $javac, $jar, $keytool) { if (-not (Test-Path $t)) { Die "Missing $t (a JRE is not enough - a full JDK is required)." } }
|
||||
# apksigner.bat and d8.bat resolve java through JAVA_HOME.
|
||||
$env:JAVA_HOME = $JavaHome
|
||||
|
||||
# -------------------------------------------------------------------- Kotlin
|
||||
function Resolve-KotlinHome {
|
||||
param([string]$Dir)
|
||||
if (-not $Dir) { return $null }
|
||||
if (Test-Path (Join-Path $Dir 'bin\kotlinc.bat')) { return (Resolve-Path $Dir).Path }
|
||||
# Accept the parent of an extracted kotlin-compiler zip too.
|
||||
$inner = Join-Path $Dir 'kotlinc'
|
||||
if (Test-Path (Join-Path $inner 'bin\kotlinc.bat')) { return (Resolve-Path $inner).Path }
|
||||
return $null
|
||||
}
|
||||
|
||||
$toolsDir = Join-Path $Root '.tools'
|
||||
if (-not $KotlinHome) {
|
||||
foreach ($c in $env:KOTLIN_HOME, (Join-Path $toolsDir "kotlinc-$KotlinVersion")) {
|
||||
$r = Resolve-KotlinHome $c
|
||||
if ($r) { $KotlinHome = $r; break }
|
||||
}
|
||||
} else {
|
||||
$KotlinHome = Resolve-KotlinHome $KotlinHome
|
||||
if (-not $KotlinHome) { Die "No bin\kotlinc.bat under the -KotlinHome you passed." }
|
||||
}
|
||||
|
||||
if (-not $KotlinHome) {
|
||||
if ($NoDownload) { Die "Kotlin compiler not found. Set KOTLIN_HOME or drop -NoDownload." }
|
||||
$dest = Join-Path $toolsDir "kotlinc-$KotlinVersion"
|
||||
$zip = Join-Path $toolsDir "kotlin-compiler-$KotlinVersion.zip"
|
||||
$url = "https://github.com/JetBrains/kotlin/releases/download/v$KotlinVersion/kotlin-compiler-$KotlinVersion.zip"
|
||||
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
|
||||
Step "Downloading Kotlin $KotlinVersion (~85 MB, one time)..."
|
||||
try {
|
||||
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
|
||||
} catch {
|
||||
Die "Kotlin download failed: $($_.Exception.Message)`nDownload $url manually, extract it, and pass -KotlinHome <dir>\kotlinc."
|
||||
}
|
||||
Step 'Extracting Kotlin...'
|
||||
if (Test-Path $dest) { Remove-Item $dest -Recurse -Force }
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $dest)
|
||||
Remove-Item $zip -Force
|
||||
$KotlinHome = Resolve-KotlinHome $dest
|
||||
if (-not $KotlinHome) { Die "Extracted Kotlin to $dest but found no bin\kotlinc.bat." }
|
||||
}
|
||||
|
||||
# kotlinc.bat is deliberately NOT used: cmd.exe treats ';' as an argument
|
||||
# delimiter, which shreds any -classpath we hand it. Drive the compiler jar directly.
|
||||
$kotlinCompilerJar = Join-Path $KotlinHome 'lib\kotlin-compiler.jar'
|
||||
if (-not (Test-Path $kotlinCompilerJar)) { Die "Missing $kotlinCompilerJar." }
|
||||
$coro = Join-Path $KotlinHome 'lib\kotlinx-coroutines-core-jvm.jar'
|
||||
$stdlib = @('kotlin-stdlib.jar', 'kotlin-stdlib-jdk7.jar', 'kotlin-stdlib-jdk8.jar') |
|
||||
ForEach-Object { Join-Path $KotlinHome "lib\$_" } | Where-Object { Test-Path $_ }
|
||||
if (-not (Test-Path $coro)) { Die "Missing $coro - use a Kotlin distribution that bundles kotlinx-coroutines-core-jvm.jar." }
|
||||
if (-not $stdlib) { Die "No kotlin-stdlib jars under $KotlinHome\lib." }
|
||||
|
||||
# ----------------------------------------------------------------- native lib
|
||||
$libDir = Join-Path $Root 'lib\arm64-v8a'
|
||||
$so = Join-Path $libDir 'libdragontcp_client.so'
|
||||
if ($BuildCore -or -not (Test-Path $so)) {
|
||||
$why = if ($BuildCore) { '-BuildCore was requested' } else { 'libdragontcp_client.so is missing' }
|
||||
if (-not $RepoRoot) { Die "$why but no core\go.mod was found above $Root. Pass -RepoRoot." }
|
||||
$coreScript = Join-Path $RepoRoot 'build_core.ps1'
|
||||
if (-not (Test-Path $coreScript)) { Die "$why but $coreScript was not found. Pass -RepoRoot." }
|
||||
Step 'Native core (Go)...'
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $coreScript -ClientOnly -AndroidLibDir $libDir
|
||||
if ($LASTEXITCODE -ne 0) { Die 'build_core.ps1 failed.' }
|
||||
}
|
||||
if (-not (Test-Path $so)) { Die "Missing $so - build the Go core first." }
|
||||
|
||||
Write-Host "app dir : $Root"
|
||||
Write-Host "repo root : $(if ($RepoRoot) { $RepoRoot } else { '<not found>' })"
|
||||
Write-Host "SDK : $SdkRoot"
|
||||
Write-Host "build-tools: $BuildTools"
|
||||
Write-Host "platform : $Platform"
|
||||
Write-Host "JDK : $JavaHome"
|
||||
Write-Host "Kotlin : $KotlinHome"
|
||||
|
||||
# -------------------------------------------------------------------- build
|
||||
$B = Join-Path $Root 'build'
|
||||
if (Test-Path $B) { Remove-Item $B -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path "$B\kclasses", "$B\jclasses", "$B\dex" | Out-Null
|
||||
|
||||
Step 'Resources...'
|
||||
Invoke-Tool (Join-Path $BT 'aapt.exe') @(
|
||||
'package', '-f',
|
||||
'-M', (Join-Path $Root 'AndroidManifest.xml'),
|
||||
'-S', (Join-Path $Root 'res'),
|
||||
'-A', (Join-Path $Root 'assets'),
|
||||
'-I', $AJ,
|
||||
'-F', "$B\resources.ap_"
|
||||
) 'aapt'
|
||||
|
||||
Step 'Kotlin TUN adapter...'
|
||||
$ktSources = Get-ChildItem "$Root\src" -Recurse -Filter *.kt | ForEach-Object { $_.FullName }
|
||||
if (-not $ktSources) { Die "No .kt sources under $Root\src." }
|
||||
$CP = (@($AJ, $coro) + $stdlib) -join ';'
|
||||
Invoke-Tool $javaExe (@(
|
||||
'-Xmx2g', '-Dfile.encoding=UTF-8', '-Djava.awt.headless=true',
|
||||
'-cp', $kotlinCompilerJar, 'org.jetbrains.kotlin.cli.jvm.K2JVMCompiler',
|
||||
'-nowarn', '-no-stdlib', '-jvm-target', '1.8',
|
||||
'-kotlin-home', $KotlinHome,
|
||||
'-classpath', $CP, '-d', "$B\kclasses"
|
||||
) + $ktSources) 'kotlinc'
|
||||
|
||||
Step 'Java UI/service...'
|
||||
$javaSources = Get-ChildItem "$Root\src" -Recurse -Filter *.java | ForEach-Object { $_.FullName }
|
||||
if (-not $javaSources) { Die "No .java sources under $Root\src." }
|
||||
$JCP = (@($AJ, $coro, "$B\kclasses") + $stdlib) -join ';'
|
||||
# --release 8 keeps the API surface at Java 8; -bootclasspath is rejected by modern javac.
|
||||
Invoke-Tool $javac (@('-nowarn', '--release', '8', '-classpath', $JCP, '-d', "$B\jclasses") + $javaSources) 'javac'
|
||||
|
||||
Invoke-Tool $jar @('cf', "$B\kclasses.jar", '-C', "$B\kclasses", '.') 'jar (kotlin)'
|
||||
Invoke-Tool $jar @('cf', "$B\jclasses.jar", '-C', "$B\jclasses", '.') 'jar (java)'
|
||||
|
||||
Step 'DEX...'
|
||||
Invoke-Tool (Join-Path $BT 'd8.bat') (@(
|
||||
'--lib', $AJ, '--min-api', '29', '--output', "$B\dex",
|
||||
"$B\kclasses.jar", "$B\jclasses.jar", $coro
|
||||
) + $stdlib) 'd8'
|
||||
|
||||
Step 'Packaging...'
|
||||
$unsigned = "$B\DragonTCP-Hybrid-unsigned.apk"
|
||||
Copy-Item "$B\resources.ap_" $unsigned -Force
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$zipFile = [System.IO.Compression.ZipFile]::Open($unsigned, 'Update')
|
||||
try {
|
||||
$level = [System.IO.Compression.CompressionLevel]::Optimal
|
||||
foreach ($dex in Get-ChildItem "$B\dex" -Filter 'classes*.dex') {
|
||||
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipFile, $dex.FullName, $dex.Name, $level) | Out-Null
|
||||
}
|
||||
$libRoot = Join-Path $Root 'lib'
|
||||
foreach ($f in Get-ChildItem $libRoot -Recurse -File) {
|
||||
$entry = 'lib/' + $f.FullName.Substring($libRoot.Length + 1).Replace('\', '/')
|
||||
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipFile, $f.FullName, $entry, $level) | Out-Null
|
||||
}
|
||||
} finally {
|
||||
$zipFile.Dispose()
|
||||
}
|
||||
|
||||
Step 'Aligning...'
|
||||
$aligned = "$B\DragonTCP-Hybrid-aligned.apk"
|
||||
Invoke-Tool (Join-Path $BT 'zipalign.exe') @('-p', '-f', '4', $unsigned, $aligned) 'zipalign'
|
||||
|
||||
# ------------------------------------------------------------------- signing
|
||||
if (-not $Keystore) { $Keystore = Join-Path $Root 'dragontcp-lite-debug.jks' }
|
||||
if (-not (Test-Path $Keystore)) {
|
||||
Step 'Generating debug keystore...'
|
||||
Invoke-Tool $keytool @(
|
||||
'-genkeypair', '-keystore', $Keystore, '-storepass', $KsPass, '-keypass', $KeyPass,
|
||||
'-alias', $KeyAlias, '-keyalg', 'RSA', '-keysize', '2048', '-validity', '10000',
|
||||
'-dname', 'CN=DragonTCP Lite,O=DragonTCP,C=US'
|
||||
) 'keytool'
|
||||
}
|
||||
|
||||
Step 'Signing...'
|
||||
$out = "$B\DragonTCP-Hybrid-arm64.apk"
|
||||
Invoke-Tool (Join-Path $BT 'apksigner.bat') @(
|
||||
'sign', '--ks', $Keystore, '--ks-pass', "pass:$KsPass", '--key-pass', "pass:$KeyPass",
|
||||
'--out', $out, $aligned
|
||||
) 'apksigner sign'
|
||||
Invoke-Tool (Join-Path $BT 'apksigner.bat') @('verify', '--verbose', $out) 'apksigner verify'
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "APK: $out" -ForegroundColor Green
|
||||
Write-Host ("Size: {0:N1} MB" -f ((Get-Item $out).Length / 1MB))
|
||||
Write-Host "Install with: adb install -r `"$out`""
|
||||
Reference in New Issue
Block a user