Skip to content

Commit

Permalink
Fix #206: only add automatic repositories when it's safe (#211)
Browse files Browse the repository at this point in the history
* Start using FAIL_ON_PROJECT_REPOS in this project
* Add helper to access (internal) Settings from Project
* Conditionally add automatic repositories for Android
* Conditionally add automatic repositories for Kotlin
* Ignore test when Gradle is incompatible
* Start documenting the versioning madness.
  • Loading branch information
TWiStErRob committed Feb 27, 2022
1 parent 8b6a2f5 commit df02568
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 16 deletions.
7 changes: 0 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ resetGradleTestWorkerIdToDefault()

subprojects {
apply { plugin("kotlin") }

repositories {
google()
mavenCentral()
// for Kotlin-DSL
maven { setUrl("https://repo.gradle.org/gradle/libs-releases-local/") }
}
}

allprojects {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ class PmdPluginTest : BaseIntgTest() {
apply plugin: 'net.twisterrob.pmd'
pmd {
toolVersion = '5.6.1'
if (GradleVersion.version("6.0.0") <= GradleVersion.current()) {
if (GradleVersion.version("6.0.0") <= GradleVersion.current().baseVersion) {
incrementalAnalysis.set(false)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class PmdTaskTest_ConfigLocation : BaseIntgTest() {
apply plugin: 'net.twisterrob.pmd'
apply plugin: 'pmd' // TODO figure out why this is needed to set toolVersion when Pmd task works anyway
pmd {
if (GradleVersion.version("6.0.0") <= GradleVersion.current()) {
if (GradleVersion.version("6.0.0") <= GradleVersion.current().baseVersion) {
incrementalAnalysis.set(false)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.twisterrob.gradle.ext

import org.gradle.api.Project
import org.gradle.api.initialization.Settings
import org.gradle.api.internal.GradleInternal

val Project.settings: Settings
get() = (gradle as GradleInternal).settings
49 changes: 49 additions & 0 deletions docs/versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
There are many versions and abstraction levels in play, so here's a summary.

## Gradle

### Project's Gradle version
There's a Gradle version that is being used to build the project.
This is the standard `gradle/wrapper/gradle-wrapper.properties`.

This version executes:
* the compilation tasks
* calls the JUnit runner when running tests
* runs the POM generation and publishing tasks

Through `id("java-gradle-plugin")` and `gradleApi()`/`gradleApiWithoutKotlin()` dependency it also becomes part of the compile-classpath of production and test code.

The runtime-classpath of production code depends on who is running the plugin's code.

In this project's tests this version is accessible with `GradleVersion.current()`, but it'll be always the same in every CI matrix split.

### Tests' Gradle versions
When running unit tests the above version is in play via `projects.test.internal` on the test's runtime-classpath.

When running integration tests, the same is true, but `GradleRunnerRule` will use a delegated version from `net.twisterrob.gradle.runner.gradleVersion`. So while the test classpath is the same as the project, the Gradle build being executed as part of the test will have the specific version from the properties or `-P` override.

This version is accessible from JUnit test code via `val gradle: GradleRunnerRule`'s `gradle.gradleVersion` property. This will contain what's in the property, if any.

### GradleVersion.current()
When accessing this, it should be done with `GradleVersion.current().baseVersion`, so that `<=` and similar comparisons work correctly.
They would otherwise break on rc/snapshot versons.

### Testception
There's some craziness happening in `TestPluginTest`, it uses `GradleRunnerRule` to set up a Gradle project that uses `GradleRunnerRule`. This applies `TestPlugin` which adds `gradleApi()` to the classpath. Since this test build is running via an outer `GradleRunnerRule`, that project's Gradle version will be added to the inner build's classpath.

## Android Gradle Plugin

### Project's AGP versions
Yes, there are multiple, each `:compat:agp-xxx` module uses its own as `compileOnly` to prevent this project from playing in the dependency resolution hell. Using this approach allows to strictly compile against Android Gradle Plugin versions without having to use reflection and other weird hacks.

The project is built in a way that's compatible with every supported version of AGP/Gradle.

In production code `AGPVersions.CLASSPATH` will return the version that exists during runtime of the plugins.

### Tests' AGP versions
JUnit tests have the latest version on their runtime classpath, so `AGPVersions.CLASSPATH` will always be the latest in tests.
For the matrix's split version, which is defined by `net.twisterrob.test.android.pluginVersion` property or `-P` override, access the `AGPVersions.UNDER_TEST` variable.

Note: `AGPVersions.UNDER_TEST` in test code will be the same as `AGPVersions.CLASSPATH` in production code.

In `"""` strings or in test resource files which describe the project under test, use `AGPVersions.CLASSPATH` if necessary, or use the `@net.twisterrob.test.android.pluginVersion@` placeholder which will be replaced during build.
1 change: 1 addition & 0 deletions plugin/base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies {
// SdkConstants.FD_INTERMEDIATES
compileOnly(libs.android.tools.common)
compileOnly(libs.annotations.jetbrains)
implementation(projects.compat.gradle)
implementation(projects.compat.agpBase)
implementation(projects.compat.agp40x)
implementation(projects.compat.agp41x)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.twisterrob.gradle.base

import net.twisterrob.gradle.ext.settings
import org.gradle.api.Project
import org.gradle.api.initialization.resolve.RepositoriesMode
import org.gradle.util.GradleVersion

fun shouldAddAutoRepositoriesTo(project: Project): Boolean {
if (GradleVersion.version("6.8") <= GradleVersion.current().baseVersion) {
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA", "UnstableApiUsage")
return when (project.settings.dependencyResolutionManagement.repositoriesMode.get()) {
RepositoriesMode.PREFER_PROJECT -> {
// Project is using defaults, or explicitly preferring these repositories.
true
}
RepositoriesMode.PREFER_SETTINGS -> {
// Automatic repositories will be ignored, don't even try.
false
}
RepositoriesMode.FAIL_ON_PROJECT_REPOS -> {
// Automatic repositories will fail the build, respect the user.
false
}
}
} else {
// Legacy behavior is the same as RepositoriesMode.PREFER_PROJECT,
// because there's no way to define in settings.gradle.
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import net.twisterrob.gradle.android.tasks.CalculateBuildTimeTask.Companion.addB
import net.twisterrob.gradle.android.tasks.CalculateVCSRevisionInfoTask
import net.twisterrob.gradle.android.tasks.CalculateVCSRevisionInfoTask.Companion.addBuildConfigFields
import net.twisterrob.gradle.base.BasePlugin
import net.twisterrob.gradle.base.shouldAddAutoRepositoriesTo
import net.twisterrob.gradle.common.AGPVersions
import net.twisterrob.gradle.kotlin.dsl.extensions
import org.gradle.api.Project
Expand Down Expand Up @@ -42,10 +43,12 @@ class AndroidBuildPlugin : BasePlugin() {

val twisterrob = android.extensions.create<AndroidBuildPluginExtension>(AndroidBuildPluginExtension.NAME)

// most of Android's stuff is distributed here, so add by default
project.repositories.google() // https://maven.google.com
// :lintVitalRelease trying to resolve :lintClassPath that has Groovy, Kotlin and some libs
project.repositories.mavenCentral() // https://repo.maven.apache.org/maven2/
if (shouldAddAutoRepositoriesTo(project)) {
// most of Android's stuff is distributed here, so add by default
project.repositories.google() // https://maven.google.com
// :lintVitalRelease trying to resolve :lintClassPath that has Groovy, Kotlin and some libs
project.repositories.mavenCentral() // https://repo.maven.apache.org/maven2/
}

when {
AGPVersions.v71x < AGPVersions.CLASSPATH -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import net.twisterrob.gradle.test.assertNoOutputLine
import net.twisterrob.gradle.test.assertSuccess
import net.twisterrob.gradle.test.move
import net.twisterrob.gradle.test.root
import org.gradle.api.artifacts.ArtifactRepositoryContainer
import org.gradle.api.internal.artifacts.dsl.DefaultRepositoryHandler
import org.gradle.util.GradleVersion
import org.hamcrest.Matchers.greaterThanOrEqualTo
import org.hamcrest.assumeThat
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -37,6 +42,60 @@ class AndroidBuildPluginIntgTest : BaseAndroidIntgTest() {
gradle.file("org.gradle.jvmargs=-Xmx256M\n", "gradle.properties")
}

@Test fun `adds automatic repositories`() {
gradle.buildFile.writeText(gradle.buildFile.readText().removeTopLevelRepositoryBlock())

@Language("gradle")
val script = """
apply plugin: 'net.twisterrob.android-app'
afterEvaluate {
println("repoNames=" + repositories.names)
}
""".trimIndent()

val result = gradle.run(script, "assembleDebug").build()

result.assertHasOutputLine("repoNames=[${GOOGLE}, ${MAVEN_CENTRAL}]")
result.assertSuccess(":assembleDebug")
}

@Test fun `does not add repositories automatically when it would fail`() {
// See https://docs.gradle.org/6.8/release-notes.html#central-declaration-of-repositories.
assumeThat(
"Feature added in Gradle 6.8",
gradle.gradleVersion.baseVersion,
greaterThanOrEqualTo(GradleVersion.version("6.8"))
)

gradle.buildFile.writeText(gradle.buildFile.readText().removeTopLevelRepositoryBlock())

@Language("gradle")
val script = """
apply plugin: 'net.twisterrob.android-app'
afterEvaluate {
println("repoNames=" + repositories.names)
}
""".trimIndent()

@Language("gradle")
val settingsGradle = """
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
""".trimIndent()
gradle.file(settingsGradle, "settings.gradle")

val result = gradle.run(script, "assembleDebug").build()

result.assertHasOutputLine("repoNames=[]")
// Build is successful without repos, because they come from settings.gradle.
result.assertSuccess(":assembleDebug")
}

@Test fun `default build setup is simple and produces default output (debug)`() {
@Language("gradle")
val script = """
Expand Down Expand Up @@ -488,4 +547,13 @@ class AndroidBuildPluginIntgTest : BaseAndroidIntgTest() {
result.assertSuccess(":javaPreCompileDebug")
result.assertNoOutputLine(""".*annotationProcessor.*""".toRegex())
}

companion object {
private const val GOOGLE: String = DefaultRepositoryHandler.GOOGLE_REPO_NAME
private const val MAVEN_CENTRAL: String = ArtifactRepositoryContainer.DEFAULT_MAVEN_CENTRAL_REPO_NAME

/** Remove default from [GradleBuildTestResources.AndroidProject.build]. */
private fun String.removeTopLevelRepositoryBlock(): String =
this.replace("""(?s)\nrepositories \{.*?\n\}""".toRegex(), "")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.android.build.gradle.BaseExtension
import net.twisterrob.gradle.android.hasAndroid
import net.twisterrob.gradle.android.hasAndroidTest
import net.twisterrob.gradle.base.BasePlugin
import net.twisterrob.gradle.base.shouldAddAutoRepositoriesTo
import org.gradle.api.Project
import org.gradle.kotlin.dsl.get

Expand All @@ -18,7 +19,9 @@ class KotlinPlugin : BasePlugin() {
// CONSIDER https://github.com/griffio/dagger2-kotlin/blob/master/README.md
project.plugins.apply("kotlin-android")
project.plugins.apply("kotlin-kapt")
project.repositories.mavenCentral()
if (shouldAddAutoRepositoriesTo(project)) {
project.repositories.mavenCentral()
}
project.dependencies.add("implementation", kotlin("stdlib-jdk7"))
if (project.plugins.hasAndroidTest()) {
project.addTestDependencies("implementation")
Expand All @@ -31,7 +34,9 @@ class KotlinPlugin : BasePlugin() {
}
} else {
project.plugins.apply("kotlin")
project.repositories.mavenCentral()
if (shouldAddAutoRepositoriesTo(project)) {
project.repositories.mavenCentral()
}
project.dependencies.add("implementation", kotlin("stdlib"))
project.addTestDependencies("testImplementation")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import net.twisterrob.gradle.test.GradleBuildTestResources
import net.twisterrob.gradle.test.GradleBuildTestResources.basedOn
import net.twisterrob.gradle.test.GradleRunnerRule
import net.twisterrob.gradle.test.GradleRunnerRuleExtension
import net.twisterrob.gradle.test.assertHasOutputLine
import net.twisterrob.gradle.test.assertSuccess
import net.twisterrob.test.compile.generateKotlinCompilationCheck
import net.twisterrob.test.compile.generateKotlinCompilationCheckTest
import org.gradle.util.GradleVersion
import org.hamcrest.Matchers.greaterThanOrEqualTo
import org.hamcrest.assumeThat
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
Expand Down Expand Up @@ -71,4 +75,32 @@ class KotlinPluginIntgTest : BaseIntgTest() {
result.assertSuccess(":compileKotlin")
result.assertSuccess(":compileTestKotlin")
}

@Test fun `does not add repositories when it would fail`() {
// See https://docs.gradle.org/6.8/release-notes.html#central-declaration-of-repositories.
assumeThat(
"Feature added in Gradle 6.8",
gradle.gradleVersion.baseVersion,
greaterThanOrEqualTo(GradleVersion.version("6.8"))
)

gradle.basedOn(GradleBuildTestResources.kotlin)

@Language("gradle")
val settings = """
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
}
"""
gradle.settingsFile.writeText(settings)

@Language("gradle")
val script = """
apply plugin: 'net.twisterrob.kotlin'
""".trimIndent()

val result = gradle.run(script, "jar").buildAndFail()

result.assertHasOutputLine(""".*Cannot resolve external dependency (.*) because no repositories are defined\.""".toRegex())
}
}
11 changes: 11 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ if (settings.extra["net.twisterrob.gradle.build.includeExamples"].toString().toB
includeBuild("docs/examples/release")
}

@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
// for Kotlin-DSL
maven { setUrl("https://repo.gradle.org/gradle/libs-releases-local/") }
}
}

plugins {
// https://docs.gradle.com/enterprise/gradle-plugin/#release_history
id("com.gradle.enterprise") version "3.8.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class TestPluginTest : BaseIntgTest() {
// make /test/build/libs/X-0.0.jar available as 'net.twisterrob.gradle:X:0.0'
url '${artifactPath.replace("\\", "\\\\")}'
// patternLayout(Action) was introduced in 5.0, layout(String, Closure) was removed in 7.0.
if (GradleVersion.current() < GradleVersion.version("5.0.0")) {
if (GradleVersion.current().baseVersion < GradleVersion.version("5.0.0")) {
layout('pattern') {
artifact '[artifact]-[revision].[ext]'
m2compatible = true
Expand Down

0 comments on commit df02568

Please sign in to comment.