diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8a612f4f6ec3..d9c27c9faaa2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,19 +5,19 @@ * SPDX-FileCopyrightText: 2025 Jimly Asshiddiqy * SPDX-License-Identifier: AGPL-3.0-or-later */ -@file:Suppress("UnstableApiUsage", "DEPRECATION") import com.github.spotbugs.snom.Confidence import com.github.spotbugs.snom.Effort import com.github.spotbugs.snom.SpotBugsTask import com.karumi.shot.ShotExtension -import org.gradle.internal.jvm.Jvm +import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.dsl.JvmTarget -val shotTest = System.getenv("SHOT_TEST") == "true" -val ciBuild = System.getenv("CI") == "true" -val perfAnalysis = project.hasProperty("perfAnalysis") -val instrumentedTestBuild = project.hasProperty("coverage") || shotTest || +val shotTest = providers.environmentVariable("SHOT_TEST").orNull == "true" +val ciBuild = providers.environmentVariable("CI").orNull == "true" +val perfAnalysis = providers.gradleProperty("perfAnalysis").isPresent +val coverage = providers.gradleProperty("coverage").isPresent +val instrumentedTestBuild = coverage || shotTest || gradle.startParameter.taskNames.any { it.contains("AndroidTest", ignoreCase = true) } val enableR8 = !instrumentedTestBuild @@ -39,47 +39,46 @@ plugins { // needed to make renovate run without shot, as shot requires Android SDK // https://github.com/pedrovgs/Shot/issues/300 if (System.getenv("SHOT_TEST") == "true") alias(libs.plugins.shot) - id("checkstyle") - id("pmd") + checkstyle + pmd + id("nextcloud.jacoco-report") } -apply(from = "${rootProject.projectDir}/jacoco.gradle.kts") -println("Gradle uses Java ${Jvm.current()}") +println( + "Gradle uses Java ${System.getProperty("java.version")} " + + "(${System.getProperty("java.vm.vendor")} ${System.getProperty("java.vm.version")})" +) configurations.configureEach { // via prism4j, already using annotations explicitly exclude(group = "org.jetbrains", module = "annotations-java5") - resolutionStrategy { - eachDependency { - when (requested.group) { - "org.checkerframework" if requested.name != "checker-compat-qual" -> { - useVersion(libs.versions.checker.get()) - because("https://github.com/google/ExoPlayer/issues/10007") - } - "org.jacoco" -> { - useVersion(libs.versions.jacoco.get()) - } - "commons-logging" if requested.name == "commons-logging" -> { - useTarget(libs.slfj) - } - "org.hamcrest" -> { - useVersion("2.2") - because( - "Align hamcrest on compile and runtime. 1.3 (via junit) exposes fixed-arity " + - "anyOf/allOf overloads that 2.2 (via androidx.test) removed, causing NoSuchMethodError." - ) - } + resolutionStrategy.eachDependency { + when (requested.group) { + "org.checkerframework" if requested.name != "checker-compat-qual" -> { + useVersion(libs.versions.checker.get()) + because("https://github.com/google/ExoPlayer/issues/10007") + } + "org.jacoco" -> { + useVersion(libs.versions.jacoco.get()) + } + "commons-logging" if requested.name == "commons-logging" -> { + useTarget(libs.slfj) + } + "org.hamcrest" -> { + useVersion("2.2") + because( + "Align hamcrest on compile and runtime. 1.3 (via junit) exposes fixed-arity " + + "anyOf/allOf overloads that 2.2 (via androidx.test) removed, causing NoSuchMethodError." + ) } } } } -val ndkEnv = buildMap { - file("${project.rootDir}/ndk.env").readLines().forEach { - val (key, value) = it.split("=") - put(key, value) - } +val ndkEnv = rootDir.resolve("ndk.env").readLines().associate { line -> + val (key, value) = line.split("=") + key to value } val ncTestServerUsername = providers.gradleProperty("NC_TEST_SERVER_USERNAME").get() @@ -87,6 +86,13 @@ val ncTestServerPassword = providers.gradleProperty("NC_TEST_SERVER_PASSWORD").g val ncTestServerBaseUrl = providers.gradleProperty("NC_TEST_SERVER_BASEURL").get() android { + namespace = "com.owncloud.android" + testNamespace = "$namespace.test" + + compileSdk { + version = release(37) + } + // install this NDK version and CMake to produce smaller APKs. Build will still work if not installed ndkVersion = "${ndkEnv["NDK_VERSION"]}" externalNativeBuild { @@ -96,22 +102,19 @@ android { } } - namespace = "com.owncloud.android" - testNamespace = "${namespace}.test" - - androidResources.generateLocaleConfig = true + androidResources { + generateLocaleConfig = true + additionalParameters += listOf("--preferred-density", "xxxhdpi") + } defaultConfig { - testInstrumentationRunnerArguments += mapOf( - "TEST_SERVER_URL" to ncTestServerBaseUrl.toString(), - "TEST_SERVER_USERNAME" to ncTestServerUsername.toString(), - "TEST_SERVER_PASSWORD" to ncTestServerPassword.toString(), - "disableAnalytics" to "true" - ) applicationId = "com.nextcloud.client" - minSdk = 28 - compileSdk = 37 - targetSdk = 36 + minSdk { + version = release(28) + } + targetSdk { + version = release(36) + } // mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable) // xx .xxx .xx .xx versionCode = 350020000 @@ -120,69 +123,75 @@ android { buildConfigField("boolean", "CI", ciBuild.toString()) buildConfigField("boolean", "RUNTIME_PERF_ANALYSIS", perfAnalysis.toString()) - // arguments to be passed to functional tests - testInstrumentationRunner = if (shotTest) "com.karumi.shot.ShotTestRunner" - else "com.nextcloud.client.TestRunner" - - // adapt structure from Eclipse to Gradle/Android Studio expectations; - // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure + testInstrumentationRunner = if (shotTest) { + "com.karumi.shot.ShotTestRunner" + } else { + "com.nextcloud.client.TestRunner" + } - flavorDimensions += "default" + // arguments to be passed to functional tests + testInstrumentationRunnerArguments += mapOf( + "TEST_SERVER_URL" to ncTestServerBaseUrl, + "TEST_SERVER_USERNAME" to ncTestServerUsername, + "TEST_SERVER_PASSWORD" to ncTestServerPassword, + "disableAnalytics" to "true" + ) + } - buildTypes { - release { - buildConfigField("String", "NC_TEST_SERVER_DATA_STRING", "\"\"") - ndk { - abiFilters += allAbis - } + buildTypes { + release { + buildConfigField("String", "NC_TEST_SERVER_DATA_STRING", "\"\"") + ndk { + abiFilters += allAbis } - debug { - enableUnitTestCoverage = project.hasProperty("coverage") - enableAndroidTestCoverage = project.hasProperty("coverage") - resConfigs("xxxhdpi") - optimization { - enable = enableR8 - } - ndk { - abiFilters += if (ciBuild) allAbis else localDebugAbis - } + } + debug { + enableUnitTestCoverage = coverage + enableAndroidTestCoverage = coverage + optimization { + enable = enableR8 + } + ndk { + abiFilters += if (ciBuild) allAbis else localDebugAbis } } + } - productFlavors { - // used for f-droid - register("generic") { - applicationId = "com.nextcloud.client" - dimension = "default" - } + flavorDimensions += "default" - register("gplay") { - applicationId = "com.nextcloud.client" - dimension = "default" - } + productFlavors { + // used for f-droid + register("generic") { + applicationId = "com.nextcloud.client" + dimension = "default" + } - register("huawei") { - applicationId = "com.nextcloud.client" - dimension = "default" - } + register("gplay") { + applicationId = "com.nextcloud.client" + dimension = "default" + } - register("versionDev") { - applicationId = "com.nextcloud.android.beta" - dimension = "default" - versionCode = 20220322 - versionName = "20220322" - optimization { - enable = enableR8 - } - } + register("huawei") { + applicationId = "com.nextcloud.client" + dimension = "default" + } - register("qa") { - applicationId = "com.nextcloud.android.qa" - dimension = "default" - versionCode = 1 - versionName = "1" + register("versionDev") { + applicationId = "com.nextcloud.android.beta" + dimension = "default" + versionCode = 20220322 + versionName = "20220322" + optimization { + enable = enableR8 } } + + register("qa") { + applicationId = "com.nextcloud.android.qa" + dimension = "default" + versionCode = 1 + versionName = "1" + } } testOptions { @@ -190,11 +199,9 @@ android { animationsDisabled = true } - // adapt structure from Eclipse to Gradle/Android Studio expectations; - // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure packaging.resources { - excludes.addAll(listOf("META-INF/LICENSE*", "META-INF/versions/9/OSGI-INF/MANIFEST*")) - pickFirsts.add("MANIFEST.MF") // workaround for duplicated manifest on some dependencies + excludes += setOf("META-INF/LICENSE*", "META-INF/versions/9/OSGI-INF/MANIFEST*") + pickFirsts += "MANIFEST.MF" // workaround for duplicated manifest on some dependencies } buildFeatures { @@ -214,44 +221,43 @@ android { abortOnError = true warningsAsErrors = true checkGeneratedSources = true - disable.addAll( - listOf( - "MissingTranslation", - "GradleDependency", - "VectorPath", - "IconMissingDensityFolder", - "IconDensities", - "GoogleAppIndexingWarning", - "MissingDefaultResource", - "InvalidPeriodicWorkRequestInterval", - "StringFormatInvalid", - "MissingQuantity", - "IconXmlAndPng", - "SelectedPhotoAccess", - "UnsafeIntentLaunch", - "OldTargetApi", - "AndroidGradlePluginVersion" - ) + disable += setOf( + "MissingTranslation", + "GradleDependency", + "VectorPath", + "IconMissingDensityFolder", + "IconDensities", + "GoogleAppIndexingWarning", + "MissingDefaultResource", + "InvalidPeriodicWorkRequestInterval", + "StringFormatInvalid", + "MissingQuantity", + "IconXmlAndPng", + "SelectedPhotoAccess", + "UnsafeIntentLaunch", + "OldTargetApi", + "AndroidGradlePluginVersion" ) - htmlOutput = layout.buildDirectory.file("reports/lint/lint.html").get().asFile - htmlReport = true } sourceSets { // Adds exported schema location as test app assets. - getByName("androidTest") { - assets.srcDirs(files("$projectDir/schemas")) + named("androidTest") { + assets.directories += "$projectDir/schemas" } } - } -ksp.arg("room.schemaLocation", "$projectDir/schemas") - -// Configure KSP for test variants -ksp.arg("dagger.moduleName", project.name) +ksp { + arg("room.schemaLocation", "$projectDir/schemas") + arg("dagger.moduleName", project.name) +} -kotlin.compilerOptions.jvmTarget.set(JvmTarget.JVM_21) +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } +} spotless.kotlin { target("**/*.kt") @@ -260,25 +266,26 @@ spotless.kotlin { detekt.config.setFrom("detekt.yml") -if (shotTest) configure { - showOnlyFailingTestsInReports = ciBuild - // CI environment renders some shadows slightly different from local VMs - // Add a 0.5% tolerance to account for that - tolerance = if (ciBuild) 0.1 else 0.0 +if (shotTest) { + configure { + showOnlyFailingTestsInReports = ciBuild + // CI environment renders some shadows slightly different from local VMs + // Add a 0.5% tolerance to account for that + tolerance = if (ciBuild) 0.1 else 0.0 + } } - spotbugs { ignoreFailures = true // should continue checking effort = Effort.MAX - reportLevel = Confidence.valueOf("MEDIUM") + reportLevel = Confidence.MEDIUM } tasks.register("checkstyle") { description = "Runs Checkstyle static analysis on the Java sources to enforce the project's coding style." - configFile = file("${rootProject.projectDir}/checkstyle.xml") - setConfigProperties( - "checkstyleSuppressionsPath" to file("${rootProject.rootDir}/suppressions.xml").absolutePath + configFile = rootDir.resolve("checkstyle.xml") + configProperties = mapOf( + "checkstyleSuppressionsPath" to rootDir.resolve("suppressions.xml").absolutePath ) source("src") include("**/*.java") @@ -288,7 +295,7 @@ tasks.register("checkstyle") { tasks.register("pmd") { description = "Runs PMD static analysis on the Java sources to detect common programming flaws and bad practices." - ruleSetFiles = files("${rootProject.rootDir}/ruleset.xml") + ruleSetFiles = files(rootDir.resolve("ruleset.xml")) ignoreFailures = true // should continue checking ruleSets = emptyList() @@ -297,28 +304,27 @@ tasks.register("pmd") { exclude("**/gen/**") reports { - xml.outputLocation.set(layout.buildDirectory.file("reports/pmd/pmd.xml").get().asFile) - html.outputLocation.set(layout.buildDirectory.file("reports/pmd/pmd.html").get().asFile) + xml.outputLocation = layout.buildDirectory.file("reports/pmd/pmd.xml") + html.outputLocation = layout.buildDirectory.file("reports/pmd/pmd.html") } } tasks.withType().configureEach { - val variantNameCap = name.replace("spotbugs", "") - val variantName = variantNameCap.substring(0, 1).lowercase() + variantNameCap.substring(1) + val variantNameCap = name.removePrefix("spotbugs") + val variantName = variantNameCap.replaceFirstChar { it.lowercase() } dependsOn("compile${variantNameCap}Sources") classes = fileTree( - layout.buildDirectory.get().asFile.toString() + - "/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/" + layout.buildDirectory.dir("intermediates/javac/$variantName/compile${variantNameCap}JavaWithJavac/classes") ) - excludeFilter.set(file("${project.rootDir}/scripts/analysis/spotbugs-filter.xml")) + excludeFilter = rootDir.resolve("scripts/analysis/spotbugs-filter.xml") reports.create("xml") { - required.set(true) + required = true } reports.create("html") { - required.set(true) - outputLocation.set(layout.buildDirectory.file("reports/spotbugs/spotbugs.html")) + required = true + outputLocation = layout.buildDirectory.file("reports/spotbugs/spotbugs.html") setStylesheet("fancy.xsl") } } @@ -330,13 +336,13 @@ tasks.withType().configureEach { tasks.withType().configureEach { // Run tests in parallel - maxParallelForks = maxOf(1, Runtime.getRuntime().availableProcessors().div(2)) + maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) // increased logging for tests - testLogging.events("passed", "skipped", "failed") + testLogging.events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) } -tasks.named("check").configure { +tasks.named("check") { dependsOn("checkstyle", "spotbugsGplayDebug", "pmd", "lint", "spotlessKotlinCheck", "detekt") } diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 000000000000..594e7a803e1b --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,9 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +plugins { + `kotlin-dsl` +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 000000000000..d88fec4418ac --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +rootProject.name = "build-logic" + +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + gradlePluginPortal() + } +} diff --git a/jacoco.gradle.kts b/build-logic/src/main/kotlin/nextcloud.jacoco-report.gradle.kts similarity index 53% rename from jacoco.gradle.kts rename to build-logic/src/main/kotlin/nextcloud.jacoco-report.gradle.kts index 9123d92fd8d0..ce681e0747ae 100644 --- a/jacoco.gradle.kts +++ b/build-logic/src/main/kotlin/nextcloud.jacoco-report.gradle.kts @@ -1,28 +1,16 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2025 Jimly Asshiddiqy * SPDX-License-Identifier: AGPL-3.0-or-later */ +plugins { + jacoco +} -import org.gradle.testing.jacoco.plugins.JacocoPlugin -import org.gradle.testing.jacoco.plugins.JacocoPluginExtension -import org.gradle.testing.jacoco.tasks.JacocoReport -import org.gradle.testing.jacoco.plugins.JacocoTaskExtension -import org.gradle.api.tasks.testing.Test -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.configure -import org.gradle.kotlin.dsl.getByType -import org.gradle.kotlin.dsl.named -import org.gradle.kotlin.dsl.register - - -apply() - -// Use version from version catalog -configure { - toolVersion = extensions - .getByType() +jacoco { + toolVersion = the() .named("libs") .findVersion("jacoco") .get() @@ -31,7 +19,8 @@ configure { val flavor = "gplay" val buildType = "Debug" -val variant = "${flavor.replaceFirstChar(Char::titlecase)}${buildType.replaceFirstChar(Char::titlecase)}" +val variantName = "$flavor$buildType" +val variant = variantName.replaceFirstChar(Char::titlecase) val fileFilter = listOf( // data binding @@ -54,7 +43,6 @@ val fileFilter = listOf( "**/*\$ViewBinder*.*", "**/*Component*.*", "**/*BR*.*", - "**/Manifest*.*", "**/*\$Lambda\$*.*", "**/*Companion*.*", "**/*Module*.*", @@ -83,51 +71,50 @@ val fileFilter = listOf( "**/composables/**" ) -// Configure test tasks to include Jacoco instrumentation tasks.withType().configureEach { - extensions.configure(JacocoTaskExtension::class) { + extensions.configure { isIncludeNoLocationClasses = true excludes = listOf("jdk.internal.*") } } -// Main Jacoco report task val jacocoReport = tasks.register("jacocoTest${variant}UnitTestReport") { + description = "Generates the JaCoCo coverage report for the $variantName unit tests." + group = LifecycleBasePlugin.VERIFICATION_GROUP dependsOn("test${variant}UnitTest") reports { - xml.required.set(true) - html.required.set(true) - csv.required.set(false) - - // Output locations - html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco/${name}")) - xml.outputLocation.set(layout.buildDirectory.file("reports/jacoco/${name}/jacoco.xml")) + xml.required = true + html.required = true + csv.required = false + xml.outputLocation = layout.buildDirectory.file("reports/jacoco/$name/jacoco.xml") } - // Directories - val kotlinClasses = fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/${variant}")) - val javacClasses = fileTree(layout.buildDirectory.dir("intermediates/javac/${variant}/classes")) + val intermediates = layout.buildDirectory.dir("intermediates") + val kotlinClasses = intermediates.map { it.dir("built_in_kotlinc/$variantName/compile${variant}Kotlin/classes") } + val javacClasses = intermediates.map { it.dir("javac/$variantName/compile${variant}JavaWithJavac/classes") } - classDirectories.setFrom(files(kotlinClasses, javacClasses)) + classDirectories.setFrom( + fileTree(kotlinClasses) { exclude(fileFilter) }, + fileTree(javacClasses) { exclude(fileFilter) } + ) sourceDirectories.setFrom( - files( - "src/main/java", - "src/$flavor/java", - "src/${buildType.lowercase()}/java" - ) + "src/main/java", + "src/$flavor/java", + "src/${buildType.lowercase()}/java" ) executionData.setFrom( - fileTree(layout.buildDirectory.asFile) { + fileTree(layout.buildDirectory) { include( "jacoco/test${variant}UnitTest.exec", - "outputs/unit_test_code_coverage/${variant}UnitTest/test${variant}UnitTest.exec" + "outputs/unit_test_code_coverage/${variantName}UnitTest/test${variant}UnitTest.exec" ) } ) } -// Backward compatibility alias tasks.register("jacocoTest${variant}UnitTest") { + description = "Runs the $variantName unit tests and generates their JaCoCo coverage report." + group = LifecycleBasePlugin.VERIFICATION_GROUP dependsOn("test${variant}UnitTest", jacocoReport) } diff --git a/build.gradle.kts b/build.gradle.kts index f58d97354593..8727752e4c49 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,16 +20,17 @@ plugins { } tasks.register("clean") { - delete(rootProject.layout.buildDirectory) + description = "Deletes the root project build directory." + delete(layout.buildDirectory) } tasks.register("installGitHooks") { description = "Install git hooks" - val sourceFolder = "${rootProject.projectDir}/scripts/hooks" - val destFolder = "${rootProject.projectDir}/.git/hooks" + val sourceFolder = layout.projectDirectory.dir("scripts/hooks") + val destFolder = layout.projectDirectory.dir(".git/hooks") from(sourceFolder) { include("*") } into(destFolder) - eachFile { println("${sourceFolder}/${file.path} -> ${destFolder}/${file.path}") } + eachFile { println("$sourceFolder/$path -> $destFolder/$path") } } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c0b04ea69bc9..4f36e8954bf2 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -583,7 +583,10 @@ - + + + + @@ -35566,6 +35569,11 @@ + + + + + @@ -38275,6 +38283,11 @@ + + + + + diff --git a/scripts/analysis/lint-up.rb b/scripts/analysis/lint-up.rb index 999d90848f9a..726cb1f46a91 100644 --- a/scripts/analysis/lint-up.rb +++ b/scripts/analysis/lint-up.rb @@ -19,11 +19,8 @@ # User name for git commits made by this script. TRAVIS_GIT_USERNAME = String.new("Drone CI server") -# File name and relative path of generated Lint report. Must match build.gradle file: -# lintOptions { -# htmlOutput file("[FILE_NAME].html") -# } -LINT_REPORT_FILE = String.new("app/build/reports/lint/lint.html") +# File name and relative path of the HTML report that AGP generates for lintGplayDebug. +LINT_REPORT_FILE = String.new("app/build/reports/lint-results-gplayDebug.html") # File name and relative path of previous results of this script. PREVIOUS_LINT_RESULTS_FILE=String.new("scripts/analysis/lint-results.txt") diff --git a/settings.gradle.kts b/settings.gradle.kts index 26e4b55708b4..55ea623bdc8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,8 @@ rootProject.name = "Nextcloud" pluginManagement { + includeBuild("build-logic") + resolutionStrategy.eachPlugin { if (requested.id.id == "shot") useModule("com.karumi:shot:${requested.version}") } @@ -25,7 +27,6 @@ pluginManagement { } } -@Suppress("UnstableApiUsage") dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {