Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix plugin not working if module has a lot of dependencies #50

Merged
merged 5 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,59 +1,89 @@
package com.dropbox.focus

import java.io.Serializable
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.SelfResolvingDependency
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault

@DisableCachingByDefault(because = "Not worth caching")
public abstract class CreateFocusSettingsTask : DefaultTask() {

@get:Input
protected abstract val projectDeps: SetProperty<DependentProjectInfo>

@get:Input
protected abstract val projectPath: Property<String>

@get:OutputFile
public abstract val settingsFile: RegularFileProperty

@get:OutputFile
public abstract val modulesToDirMapFile: RegularFileProperty

/**
* Project info required for focus.settings.gradle generation.
*/
protected data class DependentProjectInfo(
public val path: String,
public val projectDir: String,
): Serializable

init {
outputs.upToDateWhen { false }
}

@TaskAction
public fun createFocusSettings() {
val dependencies = project.collectDependencies().sortedBy { it.path }
val dependencies = projectDeps.get().sortedBy { it.path }

// generate CSV mapping from module name to its absolute path
modulesToDirMapFile.get().asFile.writer().use { writer ->

dependencies
.forEach { dep ->
val projectPath = dep.path
val projectDir = dep.projectDir
writer.appendLine("$projectPath,$projectDir")
}
}

settingsFile.get().asFile.writer().use { writer ->
writer.write("// ${project.path} specific settings\n")
writer.write("// ${projectPath.get()} specific settings\n")
writer.appendLine("//")
writer.appendLine("// This file is autogenerated by the focus task. Changes will be overwritten.")
writer.appendLine()

// Add the includes statements
dependencies.forEach { dep ->
writer.appendLine("include(\"${dep.path}\")")
}

writer.appendLine()

// Add overrides for projects with a root that's different from the gradle path
dependencies
.forEach { dep ->
val gradleProjectPath = dep.path.substring(1).replace(":", "/")
if (project.rootDir.resolve(gradleProjectPath) != dep.projectDir) {
val gradleProjectDir = dep.projectDir.path.replace("\\", "\\\\")
// language=groovy
writer.appendLine("""project("${dep.path}").projectDir = new File('$gradleProjectDir')""")
// language=groovy
writer.append(
"""
File f = new File("${modulesToDirMapFile.get().asFile.absolutePath}")
if (f.exists()) {
f.eachLine { line ->
var values = line.split(",")
var module = values[0]
var path = values[1]
include(module)
project(module).projectDir = new File(path)
}
}
""".trimIndent()
)
}
}

private fun Project.collectDependencies(): Set<Project> {
val result = mutableSetOf<Project>()
private fun Project.collectDependencies(): Set<DependentProjectInfo> {
val result = mutableSetOf<DependentProjectInfo>()
fun addDependent(project: Project) {
val configuredProject = this.evaluationDependsOn(project.path)
if (result.add(configuredProject)) {
val dep = DependentProjectInfo(project.path, project.projectDir.absolutePath)
if (result.add(dep)) {
configuredProject.configurations.forEach { config ->
config.dependencies
.filterIsInstance<ProjectDependency>()
Expand All @@ -71,7 +101,11 @@ public abstract class CreateFocusSettingsTask : DefaultTask() {
public operator fun invoke(subExtension: FocusSubExtension): CreateFocusSettingsTask.() -> Unit = {
group = FOCUS_TASK_GROUP
settingsFile.set(subExtension.focusSettingsFile)
notCompatibleWithConfigurationCache("This reads configurations from the project at action-time.")
modulesToDirMapFile.set(subExtension.moduleToDirMapFile)
projectPath.set(project.path)
// Collecting dependencies info in the configuration phase.
// Gradle disallows accessing project in execution phase.
projectDeps.set(project.collectDependencies())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this end up forcing dependency resolution at configuration time? When I originally wrote this it was explicitly excluded from configuration caching and deferred dependency resolution to action-time to avoid exploding the configuration phase of a project due to dependency resolution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, yeah. I can't figure out how to support configuration cache and resolve dependencies at execution phase.
I tried accessing ConfigurationContainer but for some reason it's empty.

To reproduce this issue I add gradle.properties:

org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn

I reverted configuration cache changes.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ public abstract class FocusSubExtension @Inject constructor(
layout.buildDirectory.file("focus.settings.gradle")
)

public val moduleToDirMapFile: RegularFileProperty = objects.fileProperty().convention(
layout.buildDirectory.file("moduleToDirMap.csv")
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.dropbox.focus

import com.google.common.truth.Truth.assertThat
import java.io.File
import java.time.temporal.Temporal
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.junit.Before
Expand All @@ -22,7 +21,24 @@ class FocusPluginTest {
}

@Test
fun configurationCache() {
fun configurationCache_focus() {
val fixtureRoot = File("src/test/projects/configuration-cache-compatible")

gradleRunner
.withArguments("--configuration-cache", "focus")
.withProjectDir(fixtureRoot)
.build()

val result = gradleRunner
.withArguments("--configuration-cache", "focus")
.withProjectDir(fixtureRoot)
.build()

assertThat(result.output).contains("Reusing configuration cache.")
}

@Test
fun configurationCache_clear() {
val fixtureRoot = File("src/test/projects/configuration-cache-compatible")

gradleRunner
Expand Down Expand Up @@ -51,17 +67,31 @@ class FocusPluginTest {
}

@Test
fun singleQuotePath() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of replacing this test, can you add a new one? The single-quote test was intentionally introduced as part of resolving #25.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I restored the original test with some changes, take a look

val fixtureRoot = File("src/test/projects/single-quote-path")
fun happyPath_CsvCreated() {
val fixtureRoot = File("src/test/projects/happy-path")

gradleRunner
.withArguments(":module:focus")
.runFixture(fixtureRoot) { build() }

val focusFileContent = File("src/test/projects/single-quote-path/build/notnowhere/build/focus.settings.gradle").readText()
val csvFileContents = File("src/test/projects/happy-path/build/notnowhere/build/moduleToDirMap.csv").readText()
val absoluteFilePath = fixtureRoot.resolve("build/notnowhere").absolutePath.replace("\\", "\\\\")
// language=csv
assertThat(csvFileContents).contains(""":module,$absoluteFilePath""")
}

@Test
fun happyPath_CsvRead() {
val fixtureRoot = File("src/test/projects/happy-path")

gradleRunner
.withArguments(":module:focus")
.runFixture(fixtureRoot) { build() }

val csvFilePath = File("src/test/projects/happy-path/build/notnowhere/build/moduleToDirMap.csv").absolutePath
val focusFileContent = File("src/test/projects/happy-path/build/notnowhere/build/focus.settings.gradle").readText()
// language=groovy
assertThat(focusFileContent).contains("""project(":module").projectDir = new File('$absoluteFilePath')""")
assertThat(focusFileContent).contains("""File f = new File("$csvFilePath")""")
}

private fun GradleRunner.runFixture(
Expand Down