Self-hosted APK distribution
Memby is handed out from a NAS or web server rather than a store, so:
- UpdateChecker gains a static-manifest source alongside Gitea. A .json
update URL is read as a manifest ({version, apkUrl, notes}); anything
else is still treated as a Gitea host. apkUrl may be relative and is
resolved against the manifest's own URL.
- Release signing config reads memby.keystore from local.properties or
the environment. Without it the build still succeeds but warns loudly:
an unsigned APK will not install, and a changed key forces every user
to uninstall before they can update.
- release.ps1 bumps the version, runs tests, builds a signed APK and
assembles dist/out/ (landing page, latest.json, versioned APK) ready to
copy onto the NAS.
- Keystores and dist/out are gitignored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2ce405c540
commit
08360b75e4
@@ -1,3 +1,5 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
@@ -13,6 +15,17 @@ val embyServerUrl: String = (project.findProperty("memby.serverUrl") as String?)
|
||||
// becomes a thin renderer; blank keeps the direct-to-Emby path above.
|
||||
val membyGatewayUrl: String = (project.findProperty("memby.gatewayUrl") as String?).orEmpty().trim()
|
||||
|
||||
/** Reads a key from local.properties, which is gitignored and holds machine secrets. */
|
||||
val localProperties = Properties().apply {
|
||||
val file = rootProject.file("local.properties")
|
||||
if (file.exists()) {
|
||||
file.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun localProperty(key: String): String? =
|
||||
localProperties.getProperty(key)?.trim()?.takeIf { it.isNotEmpty() }
|
||||
|
||||
android {
|
||||
namespace = "com.ponzischeme89.memby"
|
||||
compileSdk = 35
|
||||
@@ -34,6 +47,25 @@ android {
|
||||
buildConfigField("String", "MEMBY_GATEWAY_URL", "\"${membyGatewayUrl.replace("\"", "\\\"")}\"")
|
||||
}
|
||||
|
||||
// Release signing. Android identifies an app by (applicationId, signing key), so
|
||||
// every update must be signed with the SAME key — a different one is rejected as a
|
||||
// different app and forces users to uninstall first. Keep the keystore and these
|
||||
// credentials off the repo: set them in local.properties (gitignored) or the
|
||||
// environment. Without them, `assembleRelease` still builds but stays unsigned.
|
||||
val keystorePath = localProperty("memby.keystore") ?: System.getenv("MEMBY_KEYSTORE")
|
||||
val hasKeystore = !keystorePath.isNullOrBlank() && file(keystorePath).exists()
|
||||
|
||||
signingConfigs {
|
||||
if (hasKeystore) {
|
||||
create("release") {
|
||||
storeFile = file(keystorePath!!)
|
||||
storePassword = localProperty("memby.keystorePassword") ?: System.getenv("MEMBY_KEYSTORE_PASSWORD")
|
||||
keyAlias = localProperty("memby.keyAlias") ?: System.getenv("MEMBY_KEY_ALIAS") ?: "memby"
|
||||
keyPassword = localProperty("memby.keyPassword") ?: System.getenv("MEMBY_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
@@ -41,6 +73,15 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
if (hasKeystore) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
} else {
|
||||
logger.warn(
|
||||
"Memby: no release keystore configured (memby.keystore). " +
|
||||
"assembleRelease will produce an UNSIGNED apk that cannot be installed. " +
|
||||
"See README > Distributing builds.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user