<# .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 /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- * native lib - built via \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 { # Compile against the newest installed platform: targetSdk 36 must be built # against android-36 or aapt rejects the manifest. $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 \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 -AndroidLibRoot (Join-Path $Root 'lib') 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 { '' })" 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, # From targetSdk 30 Android refuses to install an APK whose resources.arsc # is compressed, so tell aapt to store it instead. '-0', 'arsc', '-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', '24', '--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`""