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

[🐘gradle-plugin] configuration cache and lazy properties for schema files #5580

Merged
merged 3 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
import java.io.File
Expand Down Expand Up @@ -50,8 +49,13 @@ abstract class ApolloDownloadSchemaTask : DefaultTask() {
@get:Option(option = "schema", description = "path where the schema will be downloaded, relative to the root project directory")
abstract val schema: Property<String>

@get:OutputFile
@get:Optional
/**
* This is not declared as an output as it triggers this Gradle error else:
* "Reason: Task ':root:generateServiceApolloCodegenSchema' uses this output of task ':root:downloadServiceApolloSchemaFromIntrospection' without declaring an explicit or implicit dependency."
*
* Since it's unlikely that users want to download the schema every time, just set it as an internal property.
*/
@get:Internal
abstract val outputFile: RegularFileProperty

@get:Internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ abstract class ApolloGenerateCodegenSchemaTask : DefaultTask() {
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val schemaFiles: ConfigurableFileCollection

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val fallbackSchemaFiles: ConfigurableFileCollection

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val upstreamSchemaFiles: ConfigurableFileCollection
Expand All @@ -47,7 +51,7 @@ abstract class ApolloGenerateCodegenSchemaTask : DefaultTask() {
}

ApolloCompiler.buildCodegenSchema(
schemaFiles = schemaFiles.files,
schemaFiles = schemaFiles.files.takeIf { it.isNotEmpty() } ?: fallbackSchemaFiles.files,
logger = logger(),
codegenSchemaOptions = codegenSchemaOptionsFile.get().asFile.toCodegenSchemaOptions(),
).writeTo(codegenSchemaFile.get().asFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ abstract class ApolloGenerateSourcesTask : ApolloGenerateSourcesBaseTask() {
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val schemaFiles: ConfigurableFileCollection

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val fallbackSchemaFiles: ConfigurableFileCollection

@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val codegenSchemaOptionsFile: RegularFileProperty
Expand All @@ -31,7 +35,7 @@ abstract class ApolloGenerateSourcesTask : ApolloGenerateSourcesBaseTask() {
@TaskAction
fun taskAction() {
ApolloCompiler.buildSchemaAndOperationsSources(
schemaFiles = schemaFiles.files,
schemaFiles = schemaFiles.files.takeIf { it.isNotEmpty() } ?: fallbackSchemaFiles.files,
executableFiles = graphqlFiles.files,
codegenSchemaOptions = codegenSchemaOptionsFile.get().asFile.toCodegenSchemaOptions(),
codegenOptions = codegenOptionsFile.get().asFile.toCodegenOptions(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import org.gradle.api.attributes.Usage
import org.gradle.api.component.AdhocComponentWithVariants
import org.gradle.api.component.SoftwareComponentFactory
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.provider.Property
import org.gradle.api.tasks.TaskProvider
Expand Down Expand Up @@ -57,7 +56,7 @@ abstract class DefaultApolloExtension(
internal fun getServiceInfos(project: Project): List<ApolloGradleToolingModel.ServiceInfo> = services.map { service ->
DefaultServiceInfo(
name = service.name,
schemaFiles = service.lazySchemaFiles(project),
schemaFiles = service.schemaFilesSnapshot(project),
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
graphqlSrcDirs = service.graphqlSourceDirectorySet.srcDirs,
upstreamProjects = service.upstreamDependencies.filterIsInstance<ProjectDependency>().map { it.name }.toSet(),
endpointUrl = service.introspection?.endpointUrl?.orNull,
Expand Down Expand Up @@ -362,7 +361,7 @@ abstract class DefaultApolloExtension(

if (service.graphqlSourceDirectorySet.isReallyEmpty) {
val sourceFolder = service.sourceFolder.getOrElse("")
val dir = File(project.projectDir, "src/${mainSourceSet(project)}/graphql/$sourceFolder")
val dir = File(project.projectDir, "src/${project.mainSourceSet()}/graphql/$sourceFolder")

service.graphqlSourceDirectorySet.srcDir(dir)
}
Expand Down Expand Up @@ -737,9 +736,8 @@ abstract class DefaultApolloExtension(
task.group = TASK_GROUP
task.description = "Generate Apollo schema for service '${service.name}'"

// This has to be lazy in case the schema is not written yet during configuration
// See the `graphql files can be generated by another task` test
task.schemaFiles.from(project.provider { service.lazySchemaFiles(project) })
task.schemaFiles.from(service.schemaFiles(project))
task.fallbackSchemaFiles.from(service.fallbackSchemaFiles(project))
task.upstreamSchemaFiles.from(schemaConsumerConfiguration)
task.codegenSchemaOptionsFile.set(optionsTaskProvider.flatMap { it.codegenSchemaOptionsFile })
task.codegenSchemaFile.set(BuildDirLayout.codegenSchema(project, service))
Expand Down Expand Up @@ -828,37 +826,13 @@ abstract class DefaultApolloExtension(

configureBaseCodegenTask(project, task, optionsTaskProvider, service)

// This has to be lazy in case the schema is not written yet during configuration
// See the `graphql files can be generated by another task` test
task.schemaFiles.from(project.provider { service.lazySchemaFiles(project) })
task.schemaFiles.from(service.schemaFiles(project))
task.fallbackSchemaFiles.from(service.fallbackSchemaFiles(project))
task.codegenSchemaOptionsFile.set(optionsTaskProvider.map { it.codegenSchemaOptionsFile.get() })
task.irOptionsFile.set(optionsTaskProvider.map { it.irOptionsFile.get() })
}
}

/**
* XXX: this returns an absolute path, which might be an issue for the build cache.
* I don't think this is much of an issue because tasks like ApolloDownloadSchemaTask don't have any
* outputs and are therefore never up-to-date so the build cache will not help much.
*
* If that ever becomes an issue, making the path relative to the project root might be a good idea.
*/
private fun lazySchemaFileForDownload(service: DefaultService, schemaFile: RegularFileProperty): File {
if (schemaFile.isPresent) {
return schemaFile.get().asFile
}

val candidates = service.lazySchemaFiles(project)
check(candidates.isNotEmpty()) {
"No schema files found. Specify introspection.schemaFile or registry.schemaFile"
}
check(candidates.size == 1) {
"Multiple schema files found:\n${candidates.joinToString("\n")}\n\nSpecify introspection.schemaFile or registry.schemaFile"
}

return candidates.single()
}

private fun registerDownloadSchemaTasks(service: DefaultService) {
val introspection = service.introspection
var taskProvider: TaskProvider<ApolloDownloadSchemaTask>? = null
Expand All @@ -868,7 +842,7 @@ abstract class DefaultApolloExtension(
taskProvider = project.tasks.register(ModelNames.downloadApolloSchemaIntrospection(service), ApolloDownloadSchemaTask::class.java) { task ->

task.group = TASK_GROUP
task.outputFile.set(lazySchemaFileForDownload(service, introspection.schemaFile))
task.outputFile.set(service.guessSchemaFile(project, introspection.schemaFile))
task.endpoint.set(introspection.endpointUrl)
task.header = introspection.headers.get().map { "${it.key}: ${it.value}" }
}
Expand All @@ -879,7 +853,7 @@ abstract class DefaultApolloExtension(
taskProvider = project.tasks.register(ModelNames.downloadApolloSchemaRegistry(service), ApolloDownloadSchemaTask::class.java) { task ->

task.group = TASK_GROUP
task.outputFile.set(lazySchemaFileForDownload(service, registry.schemaFile))
task.outputFile.set(service.guessSchemaFile(project, registry.schemaFile))
task.graph.set(registry.graph)
task.key.set(registry.key)
task.graphVariant.set(registry.graphVariant)
Expand Down Expand Up @@ -981,38 +955,6 @@ abstract class DefaultApolloExtension(
private val SourceDirectorySet.isReallyEmpty
get() = sourceDirectories.isEmpty

private fun mainSourceSet(project: Project): String {
return when (project.extensions.findByName("kotlin")) {
is KotlinMultiplatformExtension -> "commonMain"
else -> "main"
}
}

/**
* May return an empty set
*/
fun DefaultService.lazySchemaFiles(project: Project): Set<File> {
val files = if (schemaFile.isPresent) {
check(schemaFiles.isEmpty) {
"Specifying both schemaFile and schemaFiles is an error"
}
project.files(schemaFile)
} else {
schemaFiles
}

if (!files.isEmpty) {
return files.files
}

return graphqlSourceDirectorySet.srcDirs.flatMap { srcDir ->
srcDir.walkTopDown().filter {
it.extension in listOf("json", "sdl", "graphqls")
&& !it.name.startsWith("used") // Avoid detecting the used coordinates as a schema
}.toList()
}.toSet()
}

internal fun Project.hasJavaPlugin() = project.extensions.findByName("java") != null
internal fun Project.hasKotlinPlugin() = project.extensions.findByName("kotlin") != null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import com.apollographql.apollo3.gradle.api.Service
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import org.gradle.api.file.ConfigurableFileTree
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import java.io.File
import javax.inject.Inject

Expand Down Expand Up @@ -199,3 +202,70 @@ abstract class DefaultService @Inject constructor(val project: Project, override
internal fun isSchemaModule(): Boolean = upstreamDependencies.isEmpty()
}

internal fun DefaultService.fallbackFiles(project: Project, block: (ConfigurableFileTree) -> Unit): FileCollection {
val fileCollection = project.files()

graphqlSourceDirectorySet.srcDirs.forEach { directory ->
fileCollection.from(project.fileTree(directory, block))
}

return fileCollection
}

internal fun DefaultService.schemaFiles(project: Project): FileCollection {
val fileCollection = project.files()

if (schemaFile.isPresent) {
fileCollection.from(schemaFile)
} else {
fileCollection.from(schemaFiles)
}

return fileCollection
}

/**
* ConfigurableFileCollections have no way to check for absent vs empty
* [schemaFiles] can be empty at configuration time because the task responsible
* to create the file did not run yet but still be set (because it will ultimately
* create the file)
*
* The only workaround I found is to pass both to the task and defer the decision
* which one to choose to execution time.
*
* See https://github.com/gradle/gradle/issues/21752
*/
internal fun DefaultService.fallbackSchemaFiles(project: Project): FileCollection {
return fallbackFiles(project) { configurableFileTree ->
configurableFileTree.include(listOf("**/*.graphqls", "**/*.json", "**/*.sdl"))
}
}

/**
* Returns a snapshot of the schema files. Some of the schema files might be missing if generated
* from another task
*/
internal fun DefaultService.schemaFilesSnapshot(project: Project): Set<File> {
return schemaFiles(project).files.takeIf { it.isNotEmpty() } ?: fallbackSchemaFiles(project).files
}

/**
* Tries to guess where the schema file is.
* This can fail when:
* - there are several schema files.
* - the schema file is not written yet (because it needs to be written by another task)
*/
internal fun DefaultService.guessSchemaFile(project: Project, schemaFile: RegularFileProperty): File {
if (schemaFile.isPresent) {
return schemaFile.get().asFile
}
val candidates = schemaFilesSnapshot(project)
check(candidates.isNotEmpty()) {
"No schema files found. Specify introspection.schemaFile or registry.schemaFile"
}
check(candidates.size == 1) {
"Multiple schema files found:\n${candidates.joinToString("\n")}\n\nSpecify introspection.schemaFile or registry.schemaFile"
}

return candidates.single()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.apollographql.apollo3.gradle.internal

import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

internal fun Project.mainSourceSet(): String {
return when (project.extensions.findByName("kotlin")) {
is KotlinMultiplatformExtension -> "commonMain"
else -> "main"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,22 @@ class ConfigurationCacheTests {
)
assert(result.output.contains("Reusing configuration cache."))
}

@Test
fun schemaCanBeRenamed() = withTestProject("configuration-cache2") { dir ->
TestUtils.executeGradle(
dir,
"--configuration-cache",
"generateApolloSources"
)

dir.resolve("src/main/graphql/schema.graphqls")
.renameTo(dir.resolve("src/main/graphql/schema2.graphqls"))
val result = TestUtils.executeGradle(
dir,
"--configuration-cache",
"generateApolloSources",
)
assert(result.output.contains("Reusing configuration cache."))
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.apollographql.apollo3.gradle.test

import util.TestUtils
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Assert
import org.junit.Test
import util.TestUtils
import util.TestUtils.withTestProject


class LazyTests {
Expand Down Expand Up @@ -64,4 +65,13 @@ apollo {
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":installTask")?.outcome)
}
}

@Test
fun `schema file can be generated by another task`() {
withTestProject("lazy-schema-file") { dir ->
val result = TestUtils.executeTask("generateApolloSources", dir)
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":generateApolloSources")!!.outcome)
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":generateSchema")?.outcome)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.apollo)
}

apollo {
service("service") {
packageName.set("com.example")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
apply(from = "../../../../gradle/test.settings.gradle.kts")

include(":root", ":leaf")
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query GetRandom {
random
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type Query {
random: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.apollo)
}

abstract class GenerateSchemaTask: DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty

@TaskAction
fun taskAction() {
println("generating schema")
outputFile.asFile.get().writeText("type Query { random: Int }")
}
}
val installTask = tasks.register("generateSchema", GenerateSchemaTask::class.java) {
outputFile.set(project.file("build/schema.graphqls"))
}

apollo {
service("service") {
packageName.set("com.example")
schemaFile.set(installTask.flatMap { it.outputFile })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
apply(from = "../../../../gradle/test.settings.gradle.kts")