Some checks failed
deploy-site / deploy (push) Failing after 3m26s
The manual install path still needed 7-Zip, which the target machine does not have, so "installable without a certificate" was not yet true. The channel now also publishes the app as byte-split zip parts, and the install script joins them and extracts with the built-in Windows Expand-Archive after verifying every part and the joined archive. Version 1.3.1 republishes the channel from a single build, because a version's artifacts can only match one build and published volumes are never overwritten.
411 lines
16 KiB
Groovy
411 lines
16 KiB
Groovy
apply plugin: "com.android.application"
|
|
apply plugin: "org.jetbrains.kotlin.android"
|
|
apply plugin: "com.facebook.react"
|
|
|
|
abstract class VerifyD3roMobileVariantConfiguration extends DefaultTask {
|
|
private static final String GOOGLE_TEST_ADMOB_PUBLISHER_ID = "ca-app-pub-3940256099942544"
|
|
private static final java.util.regex.Pattern STRICT_SEMVER = java.util.regex.Pattern.compile(
|
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
|
|
)
|
|
|
|
@org.gradle.api.tasks.Input
|
|
abstract org.gradle.api.provider.Property<String> getVariantName()
|
|
|
|
@org.gradle.api.tasks.Input
|
|
@org.gradle.api.tasks.Optional
|
|
abstract org.gradle.api.provider.Property<String> getVersionNameSetting()
|
|
|
|
@org.gradle.api.tasks.Input
|
|
@org.gradle.api.tasks.Optional
|
|
abstract org.gradle.api.provider.Property<String> getVersionCodeSetting()
|
|
|
|
@org.gradle.api.tasks.Input
|
|
abstract org.gradle.api.provider.ListProperty<String> getMissingReleaseSettings()
|
|
|
|
@org.gradle.api.tasks.Input
|
|
@org.gradle.api.tasks.Optional
|
|
abstract org.gradle.api.provider.Property<String> getFirebaseConfigPath()
|
|
|
|
@org.gradle.api.tasks.Input
|
|
@org.gradle.api.tasks.Optional
|
|
abstract org.gradle.api.provider.Property<String> getReleaseStoreFilePath()
|
|
|
|
@org.gradle.api.tasks.Input
|
|
@org.gradle.api.tasks.Optional
|
|
abstract org.gradle.api.provider.Property<String> getProductionAdMobAppId()
|
|
|
|
@org.gradle.api.tasks.Input
|
|
@org.gradle.api.tasks.Optional
|
|
abstract org.gradle.api.provider.Property<String> getProductionBannerUnitId()
|
|
|
|
@org.gradle.api.tasks.Input
|
|
@org.gradle.api.tasks.Optional
|
|
abstract org.gradle.api.provider.Property<String> getProductionRewardedUnitId()
|
|
|
|
@org.gradle.api.tasks.TaskAction
|
|
void verifyConfiguration() {
|
|
def versionName = versionNameSetting.orNull
|
|
def versionCode = versionCodeSetting.orNull
|
|
|
|
if (versionName == null || versionName.trim().isEmpty()) {
|
|
throw new GradleException("D3RO_VERSION_NAME is required for release and e2e builds")
|
|
}
|
|
if (versionName.length() > 100 || !STRICT_SEMVER.matcher(versionName).matches()) {
|
|
throw new GradleException("D3RO_VERSION_NAME must be a strict semantic version")
|
|
}
|
|
if (versionCode == null || !(versionCode ==~ /[1-9]\d{0,9}/)) {
|
|
throw new GradleException("D3RO_VERSION_CODE must be a positive decimal integer")
|
|
}
|
|
if (versionCode.toLong() > 2100000000L) {
|
|
throw new GradleException("D3RO_VERSION_CODE exceeds the Android maximum")
|
|
}
|
|
|
|
if (variantName.get() != "release") {
|
|
return
|
|
}
|
|
|
|
if (!new File(firebaseConfigPath.get()).isFile()) {
|
|
throw new GradleException(
|
|
"Release build is blocked. Add android/app/google-services.json for com.d3ro.voice"
|
|
)
|
|
}
|
|
def missingSettings = missingReleaseSettings.get()
|
|
if (!missingSettings.isEmpty()) {
|
|
throw new GradleException(
|
|
"Release build is blocked. Configure: ${missingSettings.join(', ')}"
|
|
)
|
|
}
|
|
if (!new File(releaseStoreFilePath.get()).isFile()) {
|
|
throw new GradleException("D3RO_RELEASE_STORE_FILE must point to an existing keystore")
|
|
}
|
|
def adMobAppId = productionAdMobAppId.get()
|
|
if (!(adMobAppId ==~ /ca-app-pub-\d+~\d+/)) {
|
|
throw new GradleException("D3RO_ADMOB_APP_ID has an invalid format")
|
|
}
|
|
if (adMobAppId.startsWith(GOOGLE_TEST_ADMOB_PUBLISHER_ID + "~")) {
|
|
throw new GradleException(
|
|
"D3RO_ADMOB_APP_ID must not use Google's official test publisher"
|
|
)
|
|
}
|
|
[
|
|
D3RO_ADMOB_BANNER_UNIT_ID: productionBannerUnitId.get(),
|
|
D3RO_ADMOB_REWARDED_UNIT_ID: productionRewardedUnitId.get(),
|
|
].each { name, value ->
|
|
if (!(value ==~ /ca-app-pub-\d+\/\d+/)) {
|
|
throw new GradleException("${name} has an invalid format")
|
|
}
|
|
if (value.startsWith(GOOGLE_TEST_ADMOB_PUBLISHER_ID + "/")) {
|
|
throw new GradleException(
|
|
"${name} must not use Google's official test publisher"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def firebaseConfigFile = file("google-services.json")
|
|
if (firebaseConfigFile.isFile()) {
|
|
apply plugin: "com.google.gms.google-services"
|
|
}
|
|
|
|
def secureSetting = { String name ->
|
|
def gradleValue = providers.gradleProperty(name)
|
|
if (gradleValue.present) {
|
|
return gradleValue.get()
|
|
}
|
|
def environmentValue = providers.environmentVariable(name)
|
|
return environmentValue.present ? environmentValue.get() : null
|
|
}
|
|
|
|
def releaseStoreFilePath = secureSetting("D3RO_RELEASE_STORE_FILE")
|
|
def releaseStorePassword = secureSetting("D3RO_RELEASE_STORE_PASSWORD")
|
|
def releaseKeyAlias = secureSetting("D3RO_RELEASE_KEY_ALIAS")
|
|
def releaseKeyPassword = secureSetting("D3RO_RELEASE_KEY_PASSWORD")
|
|
def releaseSigningReady = [
|
|
releaseStoreFilePath,
|
|
releaseStorePassword,
|
|
releaseKeyAlias,
|
|
releaseKeyPassword,
|
|
].every { it != null && !it.trim().isEmpty() }
|
|
|
|
def productionAdMobAppId = secureSetting("D3RO_ADMOB_APP_ID")
|
|
def productionBannerUnitId = secureSetting("D3RO_ADMOB_BANNER_UNIT_ID")
|
|
def productionRewardedUnitId = secureSetting("D3RO_ADMOB_REWARDED_UNIT_ID")
|
|
def configuredVersionName = secureSetting("D3RO_VERSION_NAME")
|
|
def configuredVersionCode = secureSetting("D3RO_VERSION_CODE")
|
|
def e2eSupabaseUrlOverride = secureSetting("D3RO_E2E_SUPABASE_URL") ?: ""
|
|
def e2eSupabaseAnonKeyOverride = secureSetting("D3RO_E2E_SUPABASE_ANON_KEY") ?: ""
|
|
def e2eSupabaseOverridePresent = !e2eSupabaseUrlOverride.isEmpty() || !e2eSupabaseAnonKeyOverride.isEmpty()
|
|
def e2eSupabaseOverrideValid = e2eSupabaseUrlOverride == "http://10.0.2.2:55321" &&
|
|
e2eSupabaseAnonKeyOverride ==~ /sb_publishable_[A-Za-z0-9_-]{20,}/
|
|
if (e2eSupabaseOverridePresent && !e2eSupabaseOverrideValid) {
|
|
throw new GradleException(
|
|
"Local mobile E2E Supabase override must use the exact emulator endpoint and a publishable key"
|
|
)
|
|
}
|
|
def strictSemver = ~/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
|
|
def configuredVersionCodeValue = configuredVersionCode != null &&
|
|
configuredVersionCode ==~ /[1-9]\d{0,9}/ ? configuredVersionCode.toLong() : null
|
|
def versionSettingsValid = configuredVersionName != null &&
|
|
configuredVersionName.length() <= 100 &&
|
|
configuredVersionName ==~ strictSemver &&
|
|
configuredVersionCodeValue != null &&
|
|
configuredVersionCodeValue <= 2100000000L
|
|
def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.1"
|
|
def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031001
|
|
|
|
def requiredReleaseSettings = [
|
|
D3RO_RELEASE_STORE_FILE: releaseStoreFilePath,
|
|
D3RO_RELEASE_STORE_PASSWORD: releaseStorePassword,
|
|
D3RO_RELEASE_KEY_ALIAS: releaseKeyAlias,
|
|
D3RO_RELEASE_KEY_PASSWORD: releaseKeyPassword,
|
|
D3RO_ADMOB_APP_ID: productionAdMobAppId,
|
|
D3RO_ADMOB_BANNER_UNIT_ID: productionBannerUnitId,
|
|
D3RO_ADMOB_REWARDED_UNIT_ID: productionRewardedUnitId,
|
|
]
|
|
|
|
def missingReleaseSettingNames = requiredReleaseSettings.findAll {
|
|
it.value == null || it.value.trim().isEmpty()
|
|
}.keySet().toList().sort()
|
|
def releaseStoreAbsolutePath = releaseStoreFilePath == null || releaseStoreFilePath.trim().isEmpty()
|
|
? null
|
|
: file(releaseStoreFilePath).absolutePath
|
|
def releaseAdMobAppIdValue = productionAdMobAppId
|
|
def releaseBannerUnitIdValue = productionBannerUnitId
|
|
def releaseRewardedUnitIdValue = productionRewardedUnitId
|
|
|
|
def configureVersionGate = { VerifyD3roMobileVariantConfiguration gate ->
|
|
if (configuredVersionName != null) {
|
|
gate.versionNameSetting.set(configuredVersionName)
|
|
}
|
|
if (configuredVersionCode != null) {
|
|
gate.versionCodeSetting.set(configuredVersionCode)
|
|
}
|
|
gate.missingReleaseSettings.set([])
|
|
}
|
|
|
|
def verifyE2eBuildConfiguration = tasks.register(
|
|
"verifyE2eBuildConfiguration",
|
|
VerifyD3roMobileVariantConfiguration
|
|
) {
|
|
group = "verification"
|
|
description = "Fails closed unless the e2e version contract is configured."
|
|
variantName.set("e2e")
|
|
configureVersionGate(delegate)
|
|
}
|
|
|
|
def verifyReleaseBuildConfiguration = tasks.register(
|
|
"verifyReleaseBuildConfiguration",
|
|
VerifyD3roMobileVariantConfiguration
|
|
) {
|
|
group = "verification"
|
|
description = "Fails closed unless all production release settings are configured."
|
|
variantName.set("release")
|
|
configureVersionGate(delegate)
|
|
missingReleaseSettings.set(missingReleaseSettingNames)
|
|
firebaseConfigPath.set(firebaseConfigFile.absolutePath)
|
|
if (releaseStoreAbsolutePath != null) {
|
|
releaseStoreFilePath.set(releaseStoreAbsolutePath)
|
|
}
|
|
if (releaseAdMobAppIdValue != null) {
|
|
productionAdMobAppId.set(releaseAdMobAppIdValue)
|
|
}
|
|
if (releaseBannerUnitIdValue != null) {
|
|
productionBannerUnitId.set(releaseBannerUnitIdValue)
|
|
}
|
|
if (releaseRewardedUnitIdValue != null) {
|
|
productionRewardedUnitId.set(releaseRewardedUnitIdValue)
|
|
}
|
|
}
|
|
|
|
// Wire gates to actual Android variant lifecycle tasks. Aggregate invocations
|
|
// such as `assemble` and `build` cannot bypass these dependencies merely because
|
|
// their requested task names omit the build type.
|
|
tasks.configureEach { candidate ->
|
|
if (candidate.name == "preE2eBuild") {
|
|
candidate.dependsOn(verifyE2eBuildConfiguration)
|
|
} else if (candidate.name == "preReleaseBuild") {
|
|
candidate.dependsOn(verifyReleaseBuildConfiguration)
|
|
}
|
|
}
|
|
|
|
def debugAdMobAppId = "ca-app-pub-3940256099942544~3347511713"
|
|
def debugBannerUnitId = "ca-app-pub-3940256099942544/6300978111"
|
|
def debugRewardedUnitId = "ca-app-pub-3940256099942544/5224354917"
|
|
def whisperModelFile = file("src/main/assets/models/ggml-tiny.bin")
|
|
def whisperModelSize = 77691713L
|
|
def whisperModelSha256 = "be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21"
|
|
|
|
def verifyWhisperModel = tasks.register("verifyWhisperModel") {
|
|
inputs.file(whisperModelFile)
|
|
doLast {
|
|
if (!whisperModelFile.isFile()) {
|
|
throw new GradleException("Missing bundled Whisper model: ${whisperModelFile}")
|
|
}
|
|
if (whisperModelFile.length() != whisperModelSize) {
|
|
throw new GradleException("Bundled Whisper model has an invalid size")
|
|
}
|
|
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
|
whisperModelFile.withInputStream { stream ->
|
|
byte[] buffer = new byte[1024 * 1024]
|
|
int read
|
|
while ((read = stream.read(buffer)) > 0) {
|
|
digest.update(buffer, 0, read)
|
|
}
|
|
}
|
|
def actualSha256 = digest.digest().collect {
|
|
String.format("%02x", it & 0xff)
|
|
}.join()
|
|
if (actualSha256 != whisperModelSha256) {
|
|
throw new GradleException("Bundled Whisper model checksum verification failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.named("preBuild").configure {
|
|
dependsOn(verifyWhisperModel)
|
|
}
|
|
|
|
def asBuildConfigString = { String value ->
|
|
return "\"${(value ?: '').replace('\\', '\\\\').replace('\"', '\\\"')}\""
|
|
}
|
|
|
|
react {
|
|
autolinkLibrariesWithApp()
|
|
// Only the developer debug variant relies on Metro. The signed `e2e`
|
|
// variant below must embed its Hermes bundle for clean-install emulator
|
|
// testing and CI cold starts.
|
|
debuggableVariants = ["debug"]
|
|
}
|
|
|
|
def enableProguardInReleaseBuilds = false
|
|
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
|
|
|
android {
|
|
ndkVersion rootProject.ext.ndkVersion
|
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
|
compileSdk rootProject.ext.compileSdkVersion
|
|
|
|
namespace "com.d3ro.voice"
|
|
|
|
buildFeatures {
|
|
buildConfig true
|
|
}
|
|
|
|
androidResources {
|
|
noCompress += ["bin"]
|
|
}
|
|
|
|
defaultConfig {
|
|
applicationId "com.d3ro.voice"
|
|
minSdkVersion rootProject.ext.minSdkVersion
|
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
versionCode resolvedVersionCode
|
|
versionName resolvedVersionName
|
|
manifestPlaceholders = [
|
|
usesCleartextTraffic: "false",
|
|
d3roAdMobAppId: debugAdMobAppId,
|
|
]
|
|
buildConfigField "boolean", "FIREBASE_CONFIGURED", firebaseConfigFile.isFile().toString()
|
|
buildConfigField "boolean", "E2E_TEST_BUILD", "false"
|
|
buildConfigField "String", "SUPABASE_URL_OVERRIDE", asBuildConfigString("")
|
|
buildConfigField "String", "SUPABASE_ANON_KEY_OVERRIDE", asBuildConfigString("")
|
|
}
|
|
|
|
signingConfigs {
|
|
debug {
|
|
storeFile file('debug.keystore')
|
|
storePassword 'android'
|
|
keyAlias 'androiddebugkey'
|
|
keyPassword 'android'
|
|
}
|
|
if (releaseSigningReady) {
|
|
release {
|
|
storeFile file(releaseStoreFilePath)
|
|
storePassword releaseStorePassword
|
|
keyAlias releaseKeyAlias
|
|
keyPassword releaseKeyPassword
|
|
v1SigningEnabled true
|
|
v2SigningEnabled true
|
|
enableV3Signing = true
|
|
enableV4Signing = true
|
|
}
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
debug {
|
|
signingConfig signingConfigs.debug
|
|
manifestPlaceholders = [
|
|
usesCleartextTraffic: "true",
|
|
d3roAdMobAppId: debugAdMobAppId,
|
|
]
|
|
buildConfigField "String", "ADMOB_BANNER_UNIT_ID", asBuildConfigString(debugBannerUnitId)
|
|
buildConfigField "String", "ADMOB_REWARDED_UNIT_ID", asBuildConfigString(debugRewardedUnitId)
|
|
ndk {
|
|
abiFilters "arm64-v8a", "x86_64"
|
|
}
|
|
}
|
|
e2e {
|
|
initWith(buildTypes.debug)
|
|
debuggable false
|
|
signingConfig signingConfigs.debug
|
|
// A non-debuggable E2E app is compiled as RelWithDebInfo. Keep
|
|
// native dependency variants on the same NDEBUG/props ABI.
|
|
matchingFallbacks = ["release"]
|
|
manifestPlaceholders = [
|
|
usesCleartextTraffic: e2eSupabaseOverrideValid ? "true" : "false",
|
|
d3roAdMobAppId: debugAdMobAppId,
|
|
]
|
|
buildConfigField "boolean", "E2E_TEST_BUILD", "true"
|
|
buildConfigField "String", "SUPABASE_URL_OVERRIDE", asBuildConfigString(e2eSupabaseUrlOverride)
|
|
buildConfigField "String", "SUPABASE_ANON_KEY_OVERRIDE", asBuildConfigString(e2eSupabaseAnonKeyOverride)
|
|
buildConfigField "String", "ADMOB_BANNER_UNIT_ID", asBuildConfigString(debugBannerUnitId)
|
|
buildConfigField "String", "ADMOB_REWARDED_UNIT_ID", asBuildConfigString(debugRewardedUnitId)
|
|
ndk {
|
|
abiFilters.clear()
|
|
abiFilters.add("arm64-v8a")
|
|
abiFilters.add("x86_64")
|
|
}
|
|
}
|
|
release {
|
|
if (releaseSigningReady) {
|
|
signingConfig signingConfigs.release
|
|
}
|
|
manifestPlaceholders = [
|
|
usesCleartextTraffic: "false",
|
|
d3roAdMobAppId: productionAdMobAppId ?: debugAdMobAppId,
|
|
]
|
|
buildConfigField "String", "ADMOB_BANNER_UNIT_ID", asBuildConfigString(productionBannerUnitId)
|
|
buildConfigField "String", "ADMOB_REWARDED_UNIT_ID", asBuildConfigString(productionRewardedUnitId)
|
|
ndk {
|
|
abiFilters "arm64-v8a"
|
|
}
|
|
minifyEnabled enableProguardInReleaseBuilds
|
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
androidComponents {
|
|
onVariants(selector().withBuildType("release")) { variant ->
|
|
variant.packaging.jniLibs.excludes.add("**/x86_64/**")
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
implementation("com.facebook.react:react-android")
|
|
implementation platform("com.google.firebase:firebase-bom:34.17.0")
|
|
implementation("com.google.firebase:firebase-messaging")
|
|
|
|
if (hermesEnabled.toBoolean()) {
|
|
implementation("com.facebook.react:hermes-android")
|
|
} else {
|
|
implementation jscFlavor
|
|
}
|
|
|
|
androidTestImplementation("androidx.test:runner:1.7.0")
|
|
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
|
}
|