fix: add ability to use custom Gradle dist version in tooling API

This commit is contained in:
Akash Yadav
2023-09-03 14:11:33 +05:30
parent 32f4441935
commit 4290de353d
13 changed files with 269 additions and 127 deletions
@@ -46,9 +46,7 @@ class ModuleProjectTest {
@Test
fun test() {
val (server, project) = ToolingApiTestLauncher().launchServer()
val result = server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString))
.get()
val (_, project, result) = ToolingApiTestLauncher().launchServer()
assertThat(result?.isSuccessful).isTrue()
Lookup.getDefault().register(BuildService.KEY_PROJECT_PROXY, project)
@@ -20,6 +20,8 @@ package com.itsaky.androidide.tooling.impl
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiClient
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.tooling.api.messages.GradleDistributionParams
import com.itsaky.androidide.tooling.api.messages.GradleDistributionType
import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams
import com.itsaky.androidide.tooling.api.messages.TaskExecutionMessage
import com.itsaky.androidide.tooling.api.messages.result.BuildCancellationRequestResult
@@ -86,7 +88,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
log.debug("Got initialize request", params)
val stopWatch = StopWatch("Connection to project")
this.connector = GradleConnector.newConnector().forProjectDirectory(File(params.directory))
setupConnectorForGradleInstallation(this.connector!!, params.gradleInstallation)
setupConnectorForGradleInstallation(this.connector!!, params.gradleDistribution)
stopWatch.lap("Connector created")
if (this.connector == null) {
@@ -155,7 +157,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
}
this.connector!!.forProjectDirectory(project.getMetadata().get().projectDir)
setupConnectorForGradleInstallation(this.connector!!, message.gradleInstallation)
setupConnectorForGradleInstallation(this.connector!!, message.gradleDistribution)
val connection = this.connector!!.connect()
val builder = connection.newBuild()
@@ -188,20 +190,29 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
private fun setupConnectorForGradleInstallation(
connector: GradleConnector,
gradleDistribution: String?
params: GradleDistributionParams
) {
if (gradleDistribution.isNullOrBlank()) {
log.info("Using Gradle wrapper for build...")
return
}
val file = File(gradleDistribution)
if (!file.exists() || !file.isDirectory) {
log.error("Specified Gradle installation does not exist:", gradleDistribution)
return
}
when (params.type) {
GradleDistributionType.GRADLE_WRAPPER -> {
log.info("Using Gradle wrapper for build...")
}
log.info("Using Gradle installation:", file.canonicalPath)
connector.useInstallation(file)
GradleDistributionType.GRADLE_INSTALLATION -> {
val file = File(params.value)
if (!file.exists() || !file.isDirectory) {
log.error("Specified Gradle installation does not exist:", params)
return
}
log.info("Using Gradle installation:", file.canonicalPath)
connector.useInstallation(file)
}
GradleDistributionType.GRADLE_VERSION -> {
log.info("Using Gradle version '${params.value}'")
connector.useGradleVersion(params.value)
}
}
}
private fun notifyBuildFailure(tasks: List<String>) {
@@ -49,8 +49,8 @@ class MultiModuleAndroidProjectTest {
@Test
fun `test simple multi module project initialization`() {
val (server, project) = ToolingApiTestLauncher().launchServer()
server.initializeTestProject()
val (server, project, result) = ToolingApiTestLauncher().launchServer()
assertThat(result?.isSuccessful).isTrue()
doAssertions(project, server)
}
@@ -200,10 +200,9 @@ class MultiModuleAndroidProjectTest {
for ((agpVersion, gradleVersion) in versions) {
client.agpVersion = agpVersion
client.gradleVersion = gradleVersion
val (server, project) = ToolingApiTestLauncher().launchServer(client = client)
server.initializeTestProject()
val (server, project, result) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(result?.isSuccessful).isTrue()
doAssertions(project = project, server = server)
FileProvider.testProjectRoot().resolve(MultiVersionTestClient.buildFile).deleteExisting()
}
}
}
@@ -211,36 +210,38 @@ class MultiModuleAndroidProjectTest {
@Test
@Throws(CIOnlyException::class)
fun `test CI-only latest tested AGP version warning`() {
val log = CollectingLogger()
val agpVersion = "8.1.1"
val client = MultiVersionTestClient(agpVersion = agpVersion, gradleVersion = "8.2", log = log)
val (server, _) = ToolingApiTestLauncher().launchServer(client = client, log = log)
val result = server.initializeTestProject()
val output = log.toString()
ciOnlyTest {
val log = CollectingLogger()
val agpVersion = "8.1.1"
val client = MultiVersionTestClient(agpVersion = agpVersion, gradleVersion = "8.1", log = log)
val (_, _, result) = ToolingApiTestLauncher().launchServer(client = client, log = log)
val output = log.toString()
if (result?.isSuccessful != true) {
// print the output if the initialization fails
println(output)
if (result?.isSuccessful != true) {
// print the output if the initialization fails
println(output)
}
assertThat(result?.isSuccessful).isTrue()
assertThat(output).contains(
"You are using Android Gradle Plugin version that has not been tested with AndroidIDE.")
}
assertThat(result?.isSuccessful).isTrue()
assertThat(output).contains(
"You are using Android Gradle Plugin version that has not been tested with AndroidIDE.")
}
@Test
@Throws(CIOnlyException::class)
fun `test CI-only minimum AGP version failure`() {
val log = CollectingLogger()
val agpVersion = "7.1.0"
val client = MultiVersionTestClient(agpVersion = agpVersion, log = log)
val (server, _) = ToolingApiTestLauncher().launchServer(client = client, log = log)
val result = server.initializeTestProject()
assertThat(result?.isSuccessful).isFalse()
ciOnlyTest {
val log = CollectingLogger()
val agpVersion = "7.1.0"
val client = MultiVersionTestClient(agpVersion = agpVersion, gradleVersion = "7.2", log = log)
val (_, _, result) = ToolingApiTestLauncher().launchServer(client = client, log = log)
assertThat(result?.isSuccessful).isFalse()
val output = log.toString()
assertThat(output).contains(
"Android Gradle Plugin version $agpVersion is not supported by AndroidIDE.")
val output = log.toString()
assertThat(output).contains(
"Android Gradle Plugin version $agpVersion is not supported by AndroidIDE.")
}
}
private fun ciOnlyTest(test: () -> Unit) {
@@ -264,11 +265,7 @@ class MultiModuleAndroidProjectTest {
return System.getenv("TEST_TOOLING_API_IMPL").let { it == "true" }
}
private fun IToolingApiServer.initializeTestProject(): InitializeResult? {
return initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString)).get()
}
internal class CollectingLogger() : ILogger(CollectingLogger::class.simpleName) {
internal class CollectingLogger : ILogger(CollectingLogger::class.simpleName) {
private val string = StringBuilder()
@@ -21,9 +21,12 @@ import com.itsaky.androidide.models.LogLine
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiClient
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.tooling.api.messages.GradleDistributionParams
import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams
import com.itsaky.androidide.tooling.api.messages.result.BuildInfo
import com.itsaky.androidide.tooling.api.messages.result.BuildResult
import com.itsaky.androidide.tooling.api.messages.result.GradleWrapperCheckResult
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
import com.itsaky.androidide.tooling.api.util.ToolingApiLauncher
import com.itsaky.androidide.tooling.events.ProgressEvent
import com.itsaky.androidide.utils.FileProvider
@@ -60,10 +63,16 @@ class ToolingApiTestLauncher {
"jdk.compiler" to "com.sun.tools.javac.tree",
"jdk.compiler" to "com.sun.tools.javac.util")
@JvmOverloads
fun launchServer(
client: IToolingApiClient = MultiVersionTestClient(),
projectDir: Path = FileProvider.testProjectRoot(),
client: MultiVersionTestClient = MultiVersionTestClient(),
initParams: InitializeProjectParams = InitializeProjectParams(
projectDir.pathString,
GradleDistributionParams.forVersion(client.gradleVersion)
),
log: ILogger = ILogger.newInstance("BuildOutputLogger")
): Pair<IToolingApiServer, IProject> {
): Triple<IToolingApiServer, IProject, InitializeResult?> {
val builder = ProcessBuilder(createProcessCmd(FileProvider.implModule()
.resolve("build/libs/tooling-api-all.jar").pathString))
val androidHome = findAndroidHome()
@@ -79,7 +88,10 @@ class ToolingApiTestLauncher {
launcher.startListening()
return launcher.remoteProxy as IToolingApiServer to launcher.remoteProxy as IProject
val server = launcher.remoteProxy as IToolingApiServer
val project = launcher.remoteProxy as IProject
val result = server.initialize(initParams).get()
return Triple(server, project, result)
}
private fun createProcessCmd(jar: String): List<String> {
@@ -94,7 +106,7 @@ class ToolingApiTestLauncher {
cmd[0] = java.absolutePath
}
}
// cmd.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000")
for (open in opens) {
cmd.add("--add-opens=${open.key}/${open.value}=ALL-UNNAMED")
}
@@ -123,11 +135,8 @@ class ToolingApiTestLauncher {
const val buildFile = "build.gradle"
const val buildFileIn = "$buildFile.in"
const val gradlewProps = "gradle/wrapper/gradle-wrapper.properties"
const val gradlewPropsIn = "$gradlewProps.in"
const val DEFAULT_AGP_VERSION = "7.2.0"
const val DEFAULT_GRADLE_VERSION = "7.5.1"
const val DEFAULT_GRADLE_VERSION = "7.3.3"
const val GENERATED_FILE_WARNING =
"DO NOT EDIT - Automatically generated file"
@@ -150,22 +159,18 @@ class ToolingApiTestLauncher {
projectDir.resolve(buildFileIn)
.replaceContents(dest = projectDir.resolve(buildFile),
candidate = "@@TOOLING_API_TEST_AGP_VERSION@@" to this.agpVersion)
projectDir.resolve(gradlewPropsIn)
.replaceContents(comment = "#", dest = projectDir.resolve(gradlewProps),
candidate = "@@GRADLE_VERSION@@" to this.gradleVersion)
}
override fun onBuildSuccessful(result: BuildResult) {
onBuildResult(result)
}
override fun onBuildFailed(result: BuildResult) {
onBuildResult(result)
}
private fun onBuildResult(result: BuildResult) {
projectDir.resolve(buildFile).deleteIfExists()
projectDir.resolve(gradlewProps).deleteIfExists()
}
override fun onProgressEvent(event: ProgressEvent) {}
@@ -0,0 +1,38 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.tooling.api.messages
import java.io.Serializable
/**
* Initialization params for Android project/modules.
*
* @property variantSelections The map of module paths to the name of the variants which should
* be fetched/initialized.
*/
data class AndroidInitializationParams(val variantSelections: Map<String, String>) : Serializable {
companion object {
/**
* Default initialization params. This initializes the Android modules with default values.
*/
@JvmStatic
val DEFAULT = AndroidInitializationParams(emptyMap())
}
}
@@ -0,0 +1,77 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.tooling.api.messages
import java.io.Serializable
/**
* Parameters for specifying the Gradle distribution which should be used during project initialization.
*
* @property type The type of Gradle distribution.
* @property value The value for the distribution type.
* @author Akash Yadav
*/
data class GradleDistributionParams(val type: GradleDistributionType, val value: String) :
Serializable {
companion object {
/**
* [GradleDistributionParams] with type [GRADLE_WRAPPER][GradleDistributionType.GRADLE_WRAPPER].
*/
@JvmStatic
val WRAPPER = GradleDistributionParams(GradleDistributionType.GRADLE_WRAPPER, "")
/**
* Creates [GradleDistributionParams] for the given [distribution path][path].
*/
@JvmStatic
fun forInstallationDir(path: String): GradleDistributionParams {
return GradleDistributionParams(GradleDistributionType.GRADLE_INSTALLATION, path)
}
/**
* Creates [GradleDistributionParams] for the given [Gradle version][version].
*/
@JvmStatic
fun forVersion(version: String): GradleDistributionParams {
return GradleDistributionParams(GradleDistributionType.GRADLE_VERSION, version)
}
}
}
/**
* Type of Gradle distributions for project initialization.
*/
enum class GradleDistributionType {
/**
* Initialize the project using the distribution specified in `gradle-wrapper.properties`.
*/
GRADLE_WRAPPER,
/**
* Initialize the project using a specific Gradle version.
*/
GRADLE_VERSION,
/**
* Initialize the project using a specific Gradle distribution path.
*/
GRADLE_INSTALLATION
}
@@ -23,30 +23,13 @@ import java.io.Serializable
* Message sent from client to server to initialize the tooling API client in the given directory.
*
* @property directory The absolute path to the root directory of the project to initialize.
* @property gradleInstallation The installation path of the Gradle distribution to use.
* @property gradleDistribution The parameters for the Gradle distribution to use.
* @property androidParams The [AndroidInitializationParams] for initializing the Android module projects.
* @author Akash Yadav
*/
data class InitializeProjectParams @JvmOverloads constructor(
val directory: String,
val gradleInstallation: String = "",
val gradleDistribution: GradleDistributionParams = GradleDistributionParams.WRAPPER,
val androidParams: AndroidInitializationParams = AndroidInitializationParams.DEFAULT
) : Serializable
/**
* Initialization params for Android project/modules.
*
* @property variantSelections The map of module paths to the name of the variants which should
* be fetched/initialized.
*/
data class AndroidInitializationParams(val variantSelections: Map<String, String>) : Serializable {
companion object {
/**
* Default initialization params. This initializes the Android modules with default values.
*/
@JvmStatic
val DEFAULT = AndroidInitializationParams(emptyMap())
}
}
@@ -24,5 +24,5 @@ package com.itsaky.androidide.tooling.api.messages
data class TaskExecutionMessage(
val projectPath: String?,
val tasks: List<String>,
val gradleInstallation: String = ""
val gradleDistribution: GradleDistributionParams = GradleDistributionParams.WRAPPER
)