ssh-workbench/app/build.gradle.kts
jima 56b875b9fc Web platform scaffold, applicationId change to com.roundingmobile.sshwb
ApplicationId changed from com.roundingmobile.sshworkbench to
com.roundingmobile.sshwb (Firebase auto-key blocked Play Console
registration). Namespace stays com.roundingmobile.sshworkbench.
Web platform in www/: Docker stack (nginx+Node.js+MariaDB), landing
page, login (OAuth+email/pw), dashboard (vault sync+session logs),
API routes, MariaDB schema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:47:01 +02:00

259 lines
8 KiB
Kotlin

import java.text.SimpleDateFormat
import java.util.Date
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
alias(libs.plugins.google.services)
alias(libs.plugins.firebase.crashlytics.plugin)
}
val keystoreProps = Properties().apply {
load(rootProject.file("../.keystore/rmt.properties").inputStream())
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
android {
namespace = "com.roundingmobile.sshworkbench"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
minSdk = 27
targetSdk = 36
versionCode = 39
versionName = "0.0.39"
applicationId = "com.roundingmobile.sshwb"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
externalNativeBuild {
cmake {
cFlags("-Wall")
}
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
buildFeatures {
buildConfig = true
compose = true
}
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
}
create("prod") {
dimension = "environment"
}
}
signingConfigs {
create("release") {
storeFile = rootProject.file("../.keystore/${keystoreProps["keystore"]}")
storePassword = keystoreProps["keystore.password"] as String
keyAlias = keystoreProps["release.alias"] as String
keyPassword = keystoreProps["alias.password"] as String
}
}
buildTypes {
debug {
signingConfig = signingConfigs.getByName("release")
}
release {
isMinifyEnabled = true
isShrinkResources = true
signingConfig = signingConfigs.getByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
packaging {
resources {
excludes += "META-INF/LICENSE*"
excludes += "META-INF/NOTICE*"
excludes += "META-INF/DEPENDENCIES"
}
}
sourceSets {
getByName("androidTest").assets.srcDirs("$projectDir/schemas")
}
}
// ssh-workbench.v<version>.<free|pro>.<dbg|rel>.YYYY-MM-DD.apk
// AGP appends "-<flavor>-<buildType>" automatically, so we encode the rest in archivesName
// and use a finalize task to rename to the exact pattern.
// Date is set to a placeholder here; the actual date is resolved at execution time in CopyApks.
val ver: String = android.defaultConfig.versionName ?: "0"
base {
archivesName = "ssh-workbench.v${ver}"
}
abstract class CopyApks : DefaultTask() {
@get:org.gradle.api.tasks.InputDirectory
@get:org.gradle.api.tasks.PathSensitive(org.gradle.api.tasks.PathSensitivity.RELATIVE)
abstract val apkDir: org.gradle.api.file.DirectoryProperty
@get:org.gradle.api.tasks.OutputDirectory
abstract val outputDir: org.gradle.api.file.DirectoryProperty
@org.gradle.api.tasks.TaskAction
fun copyRenamed() {
val outDir = outputDir.asFile.get()
outDir.listFiles()?.forEach { it.delete() }
// Date resolved at execution time — never cached
val date = SimpleDateFormat("yyyy-MM-dd").format(Date())
val pattern = Regex("""ssh-workbench\.v(.+?)-(\w+)-(debug|release)\.apk""")
// Only copy APKs built in the last 2 minutes (skip stale cached APKs)
val recentThreshold = System.currentTimeMillis() - 120_000
apkDir.asFile.get().walkTopDown().filter { it.extension == "apk" }.forEach { apk ->
if (apk.lastModified() < recentThreshold) return@forEach
val match = pattern.matchEntire(apk.name) ?: return@forEach
val (v, flavor, buildType) = match.destructured
val suffix = if (buildType == "debug") "dbg" else "rel"
val newName = "ssh-workbench.v${v}.${flavor}.${suffix}.${date}.apk"
apk.copyTo(outDir.resolve(newName), overwrite = true)
println(" $newName")
}
}
}
// Write BuildTimestamp.kt into source tree — overwritten each build with current time
tasks.register("generateBuildTimestamp") {
val targetFile = layout.projectDirectory.file("src/main/java/com/roundingmobile/sshworkbench/BuildTimestamp.kt")
outputs.file(targetFile)
outputs.upToDateWhen { false } // always run
doLast {
val time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())
targetFile.asFile.writeText(
"package com.roundingmobile.sshworkbench\n\n// Auto-generated — do not edit\nobject BuildTimestamp {\n const val TIME = \"$time\"\n}\n"
)
}
}
tasks.matching {
(it.name.startsWith("compile") && it.name.endsWith("Kotlin")) ||
(it.name.startsWith("ksp") && it.name.endsWith("Kotlin"))
}.configureEach {
dependsOn("generateBuildTimestamp")
}
tasks.register<CopyApks>("copyApks") {
description = "Copy APKs with renamed pattern to build/deploy/"
apkDir = layout.buildDirectory.dir("outputs/apk")
outputDir = layout.buildDirectory.dir("deploy")
}
tasks.matching { it.name.startsWith("assemble") && it.name != "copyApks" }.configureEach {
finalizedBy("copyApks")
}
dependencies {
// Modules
implementation(project(":lib-terminal-view"))
implementation(project(":lib-ssh"))
implementation(project(":lib-terminal-keyboard"))
implementation(project(":lib-vault-crypto"))
// AndroidX Core
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity.ktx)
// Lifecycle
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.runtime.ktx)
// Coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
// Room
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
// Security + DataStore
implementation(libs.security.crypto)
implementation(libs.datastore.preferences)
// Hilt / Dagger
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics)
implementation(libs.firebase.crashlytics)
// Compose
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.material3)
implementation(libs.compose.foundation)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material.icons)
implementation(libs.activity.compose)
implementation(libs.navigation.compose)
implementation(libs.hilt.navigation.compose)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.lifecycle.runtime.compose)
debugImplementation(libs.compose.ui.tooling)
// BouncyCastle (for key generation)
implementation(libs.bouncycastle.prov)
implementation(libs.bouncycastle.pkix)
// Biometric
implementation(libs.biometric)
// ZXing (QR code generation + scanning)
implementation(libs.zxing.core)
implementation(libs.zxing.embedded)
// Google Play Billing
implementation(libs.billing.ktx)
// ZIP (password-protected archives)
implementation(libs.zip4j)
// Testing
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.room.testing)
}