/* * QAuxiliary - An Xposed module for QQ/TIM * Copyright (C) 2019-2022 qwq233@qwq2333.top * https://github.com/cinit/QAuxiliary * * This software is non-free but opensource software: you can redistribute it * and/or modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation; either * version 3 of the License, or any later version and our eula as published * by QAuxiliary contributors. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * and eula along with this software. If not, see * * . */ import com.android.build.gradle.internal.tasks.factory.dependsOn import com.android.tools.build.apkzlib.sign.SigningExtension import com.android.tools.build.apkzlib.sign.SigningOptions import com.android.tools.build.apkzlib.zfile.ZFiles import com.android.tools.build.apkzlib.zip.AlignmentRules import com.android.tools.build.apkzlib.zip.CompressionMethod import com.android.tools.build.apkzlib.zip.ZFile import com.android.tools.build.apkzlib.zip.ZFileOptions import org.jetbrains.changelog.markdownToHTML import java.io.FileInputStream import java.security.KeyStore import java.security.cert.X509Certificate import java.util.UUID plugins { id("io.github.qauxv.application") id("com.google.devtools.ksp") version "${Version.kotlin}-${Version.ksp}" kotlin("plugin.serialization") id("com.cookpad.android.plugin.license-tools") version "1.2.0" id("org.jetbrains.changelog") version "1.3.1" } val currentBuildUuid = UUID.randomUUID().toString() println("Current build ID is $currentBuildUuid") val ccacheExecutablePath = Common.findInPath("ccache") if (ccacheExecutablePath != null) { println("Found ccache at $ccacheExecutablePath") } else { println("No ccache found.") } android { namespace = "io.github.qauxv" defaultConfig { applicationId = "io.github.qauxv" buildConfigField("String", "BUILD_UUID", "\"$currentBuildUuid\"") buildConfigField("long", "BUILD_TIMESTAMP", "${System.currentTimeMillis()}L") externalNativeBuild { cmake { arguments += listOf( "-DQAUXV_VERSION=$versionName" ) ccacheExecutablePath?.let { arguments += listOf( "-DCMAKE_C_COMPILER_LAUNCHER=$it", "-DCMAKE_CXX_COMPILER_LAUNCHER=$it", "-DNDK_CCACHE=$it", "-DANDROID_CCACHE=$it" ) } targets += "qauxv" } } } if (System.getenv("KEYSTORE_PATH") != null) { signingConfigs { create("release") { storeFile = file(System.getenv("KEYSTORE_PATH")) storePassword = System.getenv("KEYSTORE_PASSWORD") keyAlias = System.getenv("KEY_ALIAS") keyPassword = System.getenv("KEY_PASSWORD") enableV2Signing = true } } } buildTypes { getByName("release") { isShrinkResources = true isMinifyEnabled = true proguardFiles("proguard-rules.pro") if (System.getenv("KEYSTORE_PATH") != null) { signingConfig = signingConfigs.getByName("release") } kotlinOptions.suppressWarnings = true val ltoCacheFlags = listOf( "-flto=thin", "-Wl,--thinlto-cache-policy,cache_size_bytes=300m", "-Wl,--thinlto-cache-dir=${buildDir.absolutePath}/.lto-cache", ) externalNativeBuild.cmake { cFlags += ltoCacheFlags cppFlags += ltoCacheFlags } } getByName("debug") { isShrinkResources = false isMinifyEnabled = false isCrunchPngs = false proguardFiles("proguard-rules.pro") } } androidResources { additionalParameters("--allow-reserved-package-id", "--package-id", "0x39") } externalNativeBuild { cmake { path = File(projectDir, "src/main/cpp/CMakeLists.txt") version = Version.getCMakeVersion(project) } } buildFeatures { viewBinding = true } lint { checkDependencies = true } applicationVariants.all { val variantCapped = name.capitalize() tasks.findByName("merge${variantCapped}Assets")?.dependsOn(tasks.generateLicenseJson.get()) tasks.findByName("merge${variantCapped}Assets")?.dependsOn(generateEulaAndPrivacy) } } dependencies { compileOnly(projects.libs.stub) implementation(projects.libs.mmkv) ksp(projects.libs.ksp) // androidx implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.browser:browser:1.4.0") val lifecycleVersion = "2.4.1" implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") compileOnly("de.robv.android.xposed:api:82") implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.google.android.material:material:1.6.1") implementation("com.google.android.flexbox:flexbox:3.0.0") implementation("com.afollestad.material-dialogs:core:3.3.0") implementation("com.afollestad.material-dialogs:input:3.3.0") implementation("com.jaredrummler:colorpicker:1.1.0") implementation("com.github.kyuubiran:EzXHelper:1.0.3") // festival title implementation("com.github.jinatonic.confetti:confetti:1.1.2") implementation("com.github.MatteoBattilana:WeatherView:3.0.0") val appCenterSdkVersion = "4.4.5" implementation("com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}") implementation("com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0") } val adb: String = androidComponents.sdkComponents.adb.get().asFile.absolutePath val killQQ = tasks.register("killQQ") { group = "qauxv" commandLine(adb, "shell", "am", "force-stop", "com.tencent.mobileqq") isIgnoreExitValue = true } val openQQ = tasks.register("openQQ") { group = "qauxv" commandLine(adb, "shell", "am", "start", "$(pm resolve-activity --components com.tencent.mobileqq)") isIgnoreExitValue = true } tasks.register("openTroubleShooting") { group = "qauxv" commandLine( adb, "shell", "am", "start", "-e", "qa_jump_action_cmd", "io.github.qauxv.TROUBLE_SHOOTING_ACTIVITY", "com.tencent.mobileqq/.activity.JumpActivity" ) isIgnoreExitValue = true } androidComponents.onVariants { variant -> val variantCapped = variant.name.capitalize() tasks.register("checkTargetNativeLibs$variantCapped") { dependsOn(":app:externalNativeBuild$variantCapped") doLast { val targetAbi = listOf("arm64-v8a", "armeabi-v7a") val soName = "libqauxv.so" val libPath = "app/build/intermediates/cmake/debug/obj" for (abi in targetAbi) { var tmpPath = "$libPath/$abi/$soName" if ("/" != File.separator) { tmpPath = tmpPath.replace('/', File.separatorChar) } val f = File(rootProject.projectDir, tmpPath) if (!f.exists()) { throw IllegalStateException("Native library missing for the target abi: $abi. Please run gradle task ':app:externalNativeBuild$variantCapped' manually to force android gradle plugin to satisfy all required ABIs.") } } } } task("install${variantCapped}AndRestartQQ") { group = "qauxv" dependsOn(":app:install$variantCapped", killQQ) finalizedBy(openQQ) } } tasks.register("replaceIcon") { group = "qauxv" projectDir.set(project.projectDir) commitHash = Common.getGitHeadRefsSuffix(rootProject) config() }.also { tasks.preBuild.dependsOn(it) } tasks.register("cleanCxxIntermediates") { group = "qauxv" delete(file(".cxx")) }.also { tasks.clean.dependsOn(it) } tasks.register("cleanOldIcon") { group = "qauxv" val drawableDir = File(projectDir, "src/main/res/drawable") drawableDir .listFiles() ?.filter { it.isFile && it.name.startsWith("icon") } ?.forEach(::delete) delete(file("src/main/res/drawable-anydpi-v26/icon.xml")) }.also { tasks.clean.dependsOn(it) } tasks.withType().all { if (name.contains("release", true)) { kotlinOptions { freeCompilerArgs = freeCompilerArgs + listOf( "-Xno-call-assertions", "-Xno-receiver-assertions", "-Xno-param-assertions", ) } } } tasks.register("checkGitSubmodule") { group = "qauxv" val projectDir = rootProject.projectDir doLast { listOf( "libs/mmkv/MMKV/Core", "libs/stub/qq-stub", "app/src/main/cpp/dex_builder", "app/src/main/cpp/dex_builder/external/abseil" ).forEach { val submoduleDir = File(projectDir, it.replace('/', File.separatorChar)) if (!submoduleDir.exists()) { throw IllegalStateException( "submodule dir not found: $submoduleDir" + "\nPlease run 'git submodule init' and 'git submodule update' manually." ) } } } }.also { tasks.preBuild.dependsOn(it) } val synthesizeDistReleaseApksCI by tasks.registering { group = "build" // use :app:assembleRelease output apk as input dependsOn(":app:packageRelease") inputs.files(tasks.named("packageRelease").get().outputs.files) val srcApkDir = File(project.buildDir, "outputs" + File.separator + "apk" + File.separator + "release") if (srcApkDir !in tasks.named("packageRelease").get().outputs.files) { val msg = "srcApkDir should be in packageRelease outputs, srcApkDir: $srcApkDir, " + "packageRelease outputs: ${tasks.named("packageRelease").get().outputs.files.files}" throw IllegalStateException(msg) } // output name format: "QAuxv-v${defaultConfig.versionName}-${productFlavors.first().name}.apk" val outputAbiVariants = mapOf( "arm32" to arrayOf("armeabi-v7a"), "arm64" to arrayOf("arm64-v8a"), "armAll" to arrayOf("armeabi-v7a", "arm64-v8a"), "universal" to arrayOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") ) val versionName = android.defaultConfig.versionName val outputDir = File(project.buildDir, "outputs" + File.separator + "ci") // declare output files outputAbiVariants.forEach { (variant, _) -> val outputName = "QAuxv-v${versionName}-${variant}.apk" outputs.file(File(outputDir, outputName)) } val signConfig = android.signingConfigs.findByName("release") val minSdk = android.defaultConfig.minSdk!! doLast { if (signConfig == null) { logger.error("Task :app:synthesizeDistReleaseApksCI: No release signing config found, skip signing") } val requiredAbiList = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") outputDir.mkdir() val options = ZFileOptions().apply { alignmentRule = AlignmentRules.constantForSuffix(".so", 4096) noTimestamps = true autoSortFiles = true } if (!srcApkDir.exists()) { throw IllegalStateException("input apk not found: ${srcApkDir.absolutePath}") } // srcApkDir should have one apk file val srcApkFiles = srcApkDir.listFiles()?.filter { it.isFile && it.name.endsWith(".apk") } ?: emptyList() if (srcApkFiles.size != 1) { throw IllegalStateException("input apk should have one apk file, but found ${srcApkFiles.size}") } val inputApk = srcApkFiles.single() val startTime = System.currentTimeMillis() ZFile.openReadOnly(inputApk).use { srcApk -> // check whether all required abis are in the apk requiredAbiList.forEach { abi -> val path = "lib/$abi/libqauxv.so" if (srcApk.get(path) == null) { throw IllegalStateException("input apk should contain $path, but not found") } } outputAbiVariants.forEach { (variant, abis) -> val outputApk = File(outputDir, "QAuxv-v${versionName}-${variant}.apk") if (outputApk.exists()) { outputApk.delete() } ZFiles.apk(outputApk, options).use { dstApk -> if (signConfig != null) { val keyStore = KeyStore.getInstance(signConfig.storeType ?: KeyStore.getDefaultType()) FileInputStream(signConfig.storeFile!!).use { keyStore.load(it, signConfig.storePassword!!.toCharArray()) } val protParam = KeyStore.PasswordProtection(signConfig.keyPassword!!.toCharArray()) val keyEntry = keyStore.getEntry(signConfig.keyAlias!!, protParam) val privateKey = keyEntry as KeyStore.PrivateKeyEntry val signingOptions = SigningOptions.builder() .setMinSdkVersion(minSdk) .setV1SigningEnabled(minSdk < 24) .setV2SigningEnabled(true) .setKey(privateKey.privateKey) .setCertificates(privateKey.certificate as X509Certificate) .setValidation(SigningOptions.Validation.ASSUME_INVALID) .build() SigningExtension(signingOptions).register(dstApk) } // add input apk to the output apk srcApk.entries().forEach { entry -> val cdh = entry.centralDirectoryHeader val name = cdh.name val isCompressed = cdh.compressionInfoWithWait.method != CompressionMethod.STORE if (name.startsWith("lib/")) { val abi = name.substring(4).split('/').first() if (abis.contains(abi)) { dstApk.add(name, entry.open(), isCompressed) } } else { // add all other entries to the output apk dstApk.add(name, entry.open(), isCompressed) } } dstApk.update() } } } val endTime = System.currentTimeMillis() logger.info("Task :app:synthesizeDistReleaseApksCI: completed in ${endTime - startTime}ms") } } val generateEulaAndPrivacy by tasks.registering { inputs.files("${rootDir}/LICENSE.md", "${rootDir}/PRIVACY_LICENSE.md") outputs.file("${projectDir}/src/main/assets/eulaAndPrivacy.html") doFirst { val head = """ """.trimIndent() val html = inputs.files.map{ markdownToHTML(it.readText()) } outputs.files.forEach { it.writeText( buildString { append("") append(head) html.forEach(::append) append("") } ) } } }