build(android): build and sign a release, without the password touching anything

build-apk.ps1 -Release builds the Release configuration and signs with the
user's keystore. Not through Delphi's Provisioning page, for three reasons:
the .dproj is versioned, paclient --apksign puts the password ON THE COMMAND
LINE where any process on the machine reads it, and tooling belongs in the
repository rather than hidden in an IDE setting. apksigner called directly
accepts --ks-pass env:.

The password is asked for with Read-Host -AsSecureString - nothing echoed,
nothing left in console history - handed over through an environment
variable, and removed in a finally even when the script fails.

It prints the certificate it signed with. A release signed by accident with
the debug key installs and runs: nothing tells it apart in use, and one finds
out only when the first update is refused.

It does not install the release. Android refuses a different signature on an
existing package, and uninstalling would wipe the local cache and the
device's settings - a deliberate act, not a script's side effect.

Keystores are gitignored. The debug path is unchanged and still builds.
This commit is contained in:
r-zakarya
2026-08-22 01:05:42 +01:00
parent 6d8f36b5c7
commit 3e0bc7bd84
4 changed files with 120 additions and 19 deletions
+6
View File
@@ -89,3 +89,9 @@ android-client/PMA.LocalConfig.inc
android-client/theme/theme.RES android-client/theme/theme.RES
android-client/theme/theme.res android-client/theme/theme.res
android-client/web/*.res android-client/web/*.res
# Magasins de cles de signature. Le perdre interdit toute mise a jour de
# l'application deja installee, Android refusant une signature differente :
# il se sauvegarde ailleurs, il ne se versionne pas.
*.jks
*.keystore
+31 -11
View File
@@ -561,24 +561,44 @@ tombstones ni génération d'uuid.
devenaient des membres inconnus. Invisible tant qu'on ne construit qu'en devenaient des membres inconnus. Invisible tant qu'on ne construit qu'en
débogage — ce qui a été le cas jusqu'ici. débogage — ce qui a été le cas jusqu'ici.
### Reste : la signature ### La signature — outillée le 2026-08-22
Un magasin de clés dédié, jamais la clé de débogage — celle-ci est partagée par `build-apk.ps1` sait construire et signer une release. **Pas par Delphi** :
toutes les installations de Delphi, donc n'authentifie rien. Le mot de passe du
magasin appartient à l'utilisateur ; il n'a sa place ni ici ni dans le dépôt. ```
.ndroid-clientuild-apk.ps1 -Release `
-Keystore C:\cles\pmserver-release.jks -Alias pmserver
```
Le magasin se crée une fois, hors du dépôt :
``` ```
keytool -genkeypair -v -keystore pmserver-release.jks -alias pmserver -keyalg RSA -keysize 4096 -validity 10000 keytool -genkeypair -v -keystore pmserver-release.jks -alias pmserver -keyalg RSA -keysize 4096 -validity 10000
``` ```
Puis, dans Delphi : *Project > Options > Provisioning*, configuration **Release**, **Le mot de passe ne touche rien de durable.** Il est demandé par
pointer le fichier `.jks` et l'alias. Le magasin se range **hors du dépôt** et `Read-Host -AsSecureString` — rien ne s'affiche, rien ne reste dans
se sauvegarde ailleurs : le perdre interdit toute mise à jour de l'application l'historique de la console —, passé à `apksigner` par une variable
déjà installée, Android refusant une signature différente. d'environnement, et retiré dans un `finally` même si le script échoue.
Une release se construit ensuite comme le débogage, en changeant la Trois raisons de ne pas passer par *Project > Options > Provisioning* :
configuration ; `android-client/build-apk.ps1` ne connaît que Debug pour le `.dproj` est versionné, `paclient --apksign` met le mot de passe **sur la
l'instant, et ne saura signer que le jour où le magasin existe. ligne de commande** où n'importe quel processus de la machine le lit, et
l'outillage doit rester lisible dans le dépôt plutôt que caché dans un
réglage d'IDE. `apksigner` appelé en direct accepte `--ks-pass env:`.
Le script **affiche le certificat utilisé** après signature. Une release signée
par mégarde avec la clé de débogage s'installe et fonctionne : rien ne la
distingue à l'usage, et on ne s'en aperçoit qu'au refus de la première mise à
jour.
Il **n'installe pas** la release : Android refuse une signature différente sur
un paquet déjà présent, et désinstaller effacerait le cache local et les
réglages de l'appareil. À faire sciemment, pas en effet de bord d'un script.
`*.jks` et `*.keystore` sont ignorés par git. **Le magasin se sauvegarde
ailleurs** : le perdre interdit toute mise à jour de l'application déjà
installée.
--- ---
+46 -5
View File
@@ -7,8 +7,22 @@
# d'avant l'edition, et le pont echouait pour une raison qui n'existait plus. # d'avant l'edition, et le pont echouait pour une raison qui n'existait plus.
# #
# -NoInstall compile et empaquette sans toucher au telephone. # -NoInstall compile et empaquette sans toucher au telephone.
# -Release configuration Release, signee avec VOTRE magasin de cles.
#
# LA RELEASE NE S'INSTALLE PAS PAR-DESSUS LE DEBOGAGE. Android refuse une
# signature differente sur un paquet deja installe ; il faut desinstaller
# d'abord, ce qui EFFACE le cache local et les reglages de l'appareil. Le
# script ne le fait donc pas a votre place - il produit l'APK et s'arrete.
#
# .\android-client\build-apk.ps1 -Release `
# -Keystore C:\cles\pmserver-release.jks -Alias pmserver
param([switch]$NoInstall) param(
[switch]$NoInstall,
[switch]$Release,
[string]$Keystore,
[string]$Alias = 'pmserver'
)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path $PSScriptRoot -Parent
@@ -17,9 +31,20 @@ $sdk = 'C:\Users\Public\Documents\Embarcadero\Studio\23.0\CatalogRepository'
$adb = "$sdk\AndroidSDK-2525-23.0.51511.6924\platform-tools\Adb.exe" $adb = "$sdk\AndroidSDK-2525-23.0.51511.6924\platform-tools\Adb.exe"
$strip = "$sdk\AndroidNDK-21-23.0.51511.6924\android-ndk-r21\toolchains\" + $strip = "$sdk\AndroidNDK-21-23.0.51511.6924\android-ndk-r21\toolchains\" +
'aarch64-linux-android-4.9\prebuilt\windows-x86_64\aarch64-linux-android\bin\strip.exe' 'aarch64-linux-android-4.9\prebuilt\windows-x86_64\aarch64-linux-android\bin\strip.exe'
$out = "$root\Android64\Debug" $cfg = if ($Release) { 'Release' } else { 'Debug' }
$out = "$root\Android64\$cfg"
$stage = "$out\pmserverApk" $stage = "$out\pmserverApk"
if ($Release) {
if (-not $Keystore) { throw 'Chemin du magasin de cles requis : -Keystore' }
if (-not (Test-Path $Keystore)) { throw "Magasin introuvable : $Keystore" }
# LU ICI ET NULLE PART AILLEURS. Read-Host -AsSecureString n'affiche rien et
# ne laisse pas la frappe dans l'historique de la console.
$sec = Read-Host "Mot de passe du magasin $([IO.Path]::GetFileName($Keystore))" -AsSecureString
$env:PM_KS_PASS = [Runtime.InteropServices.Marshal]::PtrToStringBSTR(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
}
# 1. La page HTML -> ressource liee dans l'executable. # 1. La page HTML -> ressource liee dans l'executable.
Push-Location "$PSScriptRoot\web" Push-Location "$PSScriptRoot\web"
& "$bin\brcc32.exe" web.rc -foweb.res | Out-Null & "$bin\brcc32.exe" web.rc -foweb.res | Out-Null
@@ -27,7 +52,7 @@ Pop-Location
# 2. Compilation Delphi. # 2. Compilation Delphi.
$msbuild = "`"$bin\rsvars.bat`" && msbuild `"$root\pmserverApk.dproj`"" $msbuild = "`"$bin\rsvars.bat`" && msbuild `"$root\pmserverApk.dproj`""
$log = cmd /c "$msbuild /p:Platform=Android64 /p:Config=Debug /t:Build /v:minimal /nologo" 2>&1 $log = cmd /c "$msbuild /p:Platform=Android64 /p:Config=$cfg /t:Build /v:minimal /nologo" 2>&1
$err = $log | Select-String -Pattern 'error' $err = $log | Select-String -Pattern 'error'
if ($err) { $err | ForEach-Object { $_.Line }; throw 'compilation echouee' } if ($err) { $err | ForEach-Object { $_.Line }; throw 'compilation echouee' }
$log | Select-String -Pattern 'lines,' | ForEach-Object { $_.Line.Trim() } $log | Select-String -Pattern 'lines,' | ForEach-Object { $_.Line.Trim() }
@@ -48,13 +73,29 @@ Copy-Item "$out\pmserverApk.classes\*.dex" "$stage\classes\" -Force
# manifeste resout vers un identifiant reel. # manifeste resout vers un identifiant reel.
# #
# On ne masque donc que CETTE erreur, nommement, et tout autre message remonte. # On ne masque donc que CETTE erreur, nommement, et tout autre message remonte.
$deploy = cmd /c "$msbuild /p:Platform=Android64 /p:Config=Debug /t:Deploy /v:minimal /nologo" 2>&1 $deploy = cmd /c "$msbuild /p:Platform=Android64 /p:Config=$cfg /t:Deploy /v:minimal /nologo" 2>&1
$deploy | Select-String -Pattern 'error' | $deploy | Select-String -Pattern 'error' |
Where-Object { $_.Line -notmatch 'network_security_config|Unable to execute.*Aapt\.exe' } | Where-Object { $_.Line -notmatch 'network_security_config|Unable to execute.*Aapt\.exe' } |
ForEach-Object { $_.Line } ForEach-Object { $_.Line }
# 4. aapt + zipalign + apksigner, et remise en place de ce que Deploy efface. # 4. aapt + zipalign + apksigner, et remise en place de ce que Deploy efface.
& "$PSScriptRoot\repack-apk.ps1" | Select-Object -Last 1 try {
if ($Release) {
& "$PSScriptRoot\repack-apk.ps1" -Config Release -Keystore $Keystore -Alias $Alias
} else {
& "$PSScriptRoot\repack-apk.ps1" | Select-Object -Last 1
}
} finally {
# Le mot de passe ne survit pas au script, meme si celui-ci echoue.
Remove-Item Env:PM_KS_PASS -ErrorAction SilentlyContinue
}
if ($Release) {
"APK signe : $stage\bin\pmserverApk.apk"
'Non installe : desinstaller d''abord la version de debogage effacerait le' +
' coffre local et les reglages.'
return
}
if ($NoInstall) { return } if ($NoInstall) { return }
+37 -3
View File
@@ -8,6 +8,21 @@
# remet ce qu'il a efface et on refait aapt / zipalign / signature. # remet ce qu'il a efface et on refait aapt / zipalign / signature.
# #
# A lancer depuis la racine du depot, apres un Deploy reussi. # A lancer depuis la racine du depot, apres un Deploy reussi.
#
# -Config Release empaquette la sortie Release au lieu de Debug
# -Keystore <chemin> -Alias signe avec CETTE cle au lieu de celle de debogage
#
# LE MOT DE PASSE NE PASSE PAS PAR LA LIGNE DE COMMANDE. paclient --apksign
# l'y met en clair, ou n'importe quel processus de la machine peut le lire.
# apksigner sait lire une variable d'environnement ; on l'appelle donc en
# direct pour une vraie cle, et on ne garde paclient que pour la cle de
# debogage, dont le mot de passe est public et vaut "android".
param(
[ValidateSet('Debug', 'Release')] [string]$Config = 'Debug',
[string]$Keystore,
[string]$Alias
)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@@ -15,7 +30,7 @@ $sdk = 'C:\Users\Public\Documents\Embarcadero\Studio\23.0\CatalogRepository\And
$bds = 'C:\Program Files (x86)\Embarcadero\Studio\23.0' $bds = 'C:\Program Files (x86)\Embarcadero\Studio\23.0'
$jdk = 'C:\Program Files\Eclipse Adoptium\jdk-11.0.15.10-hotspot' $jdk = 'C:\Program Files\Eclipse Adoptium\jdk-11.0.15.10-hotspot'
$pac = "$bds\bin\paclient.exe" $pac = "$bds\bin\paclient.exe"
$p = '.\Android64\Debug\pmserverApk' $p = ".\Android64\$Config\pmserverApk"
# 1. Nos icones par-dessus celles de Delphi : # 1. Nos icones par-dessus celles de Delphi :
# les proprietes Android_LauncherIcon* et Android_AdaptiveIcon* du .dproj # les proprietes Android_LauncherIcon* et Android_AdaptiveIcon* du .dproj
@@ -46,8 +61,27 @@ Remove-Item "$p\bin\pmserverApk.apk" -Force -ErrorAction SilentlyContinue
& $pac -u8 --zipalign="$sdk\build-tools\33.0.2\ZipAlign.exe,$p\bin\pmserverApk-unaligned.apk,$p\bin\pmserverApk.apk,4" & $pac -u8 --zipalign="$sdk\build-tools\33.0.2\ZipAlign.exe,$p\bin\pmserverApk-unaligned.apk,$p\bin\pmserverApk.apk,4"
if ($LASTEXITCODE -ne 0) { throw "zipalign a echoue" } if ($LASTEXITCODE -ne 0) { throw "zipalign a echoue" }
& $pac -u8 --apksign="$jdk\bin\java.exe,$sdk\build-tools\33.0.2\lib\apksigner.jar,$p\bin\pmserverApk.apk,androiddebugkey,$env:APPDATA\Embarcadero\BDS\23.0\debug.keystore,android,android" if ($Keystore) {
if ($LASTEXITCODE -ne 0) { throw "signature echouee" } # LA CLE DE L'UTILISATEUR. Le mot de passe arrive par PM_KS_PASS, pose par
# l'appelant juste avant et retire juste apres : il ne figure ni dans le
# depot, ni dans le .dproj, ni dans une ligne de commande.
if (-not $env:PM_KS_PASS) { throw 'PM_KS_PASS absent - mot de passe du magasin' }
& "$jdk\bin\java.exe" -jar "$sdk\build-tools\33.0.2\lib\apksigner.jar" sign `
--ks "$Keystore" --ks-key-alias "$Alias" `
--ks-pass env:PM_KS_PASS --key-pass env:PM_KS_PASS `
"$p\bin\pmserverApk.apk"
if ($LASTEXITCODE -ne 0) { throw "signature echouee" }
# DIRE AVEC QUOI ON A SIGNE. Une release signee par megarde avec la cle de
# debogage s'installe et fonctionne : rien ne la distingue a l'usage, et on
# ne s'en apercoit qu'au refus de la premiere mise a jour.
& "$jdk\bin\java.exe" -jar "$sdk\build-tools\33.0.2\lib\apksigner.jar" `
verify --print-certs "$p\bin\pmserverApk.apk" |
Select-String -Pattern 'Signer #1 certificate DN|SHA-256 digest' |
ForEach-Object { $_.Line.Trim() }
} else {
& $pac -u8 --apksign="$jdk\bin\java.exe,$sdk\build-tools\33.0.2\lib\apksigner.jar,$p\bin\pmserverApk.apk,androiddebugkey,$env:APPDATA\Embarcadero\BDS\23.0\debug.keystore,android,android"
if ($LASTEXITCODE -ne 0) { throw "signature echouee" }
}
$apk = Get-Item "$p\bin\pmserverApk.apk" $apk = Get-Item "$p\bin\pmserverApk.apk"
"APK {0:N1} Mo" -f ($apk.Length / 1MB) "APK {0:N1} Mo" -f ($apk.Length / 1MB)