diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloDownloadSchemaTask.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloDownloadSchemaTask.kt index 2c41d92c736..07de7b0054f 100644 --- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloDownloadSchemaTask.kt +++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloDownloadSchemaTask.kt @@ -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 @@ -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 - @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 diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateCodegenSchemaTask.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateCodegenSchemaTask.kt index 9fb28e4567b..92083631ecd 100644 --- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateCodegenSchemaTask.kt +++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateCodegenSchemaTask.kt @@ -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 @@ -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) diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateSourcesTask.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateSourcesTask.kt index 945bad0b6be..786fe5d70d5 100644 --- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateSourcesTask.kt +++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateSourcesTask.kt @@ -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 @@ -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(), diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt index ebd15e7dc9c..48d567db300 100644 --- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt +++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt @@ -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 @@ -57,7 +56,7 @@ abstract class DefaultApolloExtension( internal fun getServiceInfos(project: Project): List = services.map { service -> DefaultServiceInfo( name = service.name, - schemaFiles = service.lazySchemaFiles(project), + schemaFiles = service.schemaFilesSnapshot(project), graphqlSrcDirs = service.graphqlSourceDirectorySet.srcDirs, upstreamProjects = service.upstreamDependencies.filterIsInstance().map { it.name }.toSet(), endpointUrl = service.introspection?.endpointUrl?.orNull, @@ -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) } @@ -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)) @@ -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? = null @@ -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}" } } @@ -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) @@ -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 { - 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 } diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultService.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultService.kt index 9409251c9f6..1423ec540ef 100644 --- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultService.kt +++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultService.kt @@ -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 @@ -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 { + 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() +} \ No newline at end of file diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/project.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/project.kt new file mode 100644 index 00000000000..8e99d6dfc93 --- /dev/null +++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/project.kt @@ -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" + } +} diff --git a/libraries/apollo-gradle-plugin/src/test-java11/kotlin/com/apollographql/apollo3/gradle/test/ConfigurationCacheTests.kt b/libraries/apollo-gradle-plugin/src/test-java11/kotlin/com/apollographql/apollo3/gradle/test/ConfigurationCacheTests.kt index f5ca216a25c..576ab171caf 100644 --- a/libraries/apollo-gradle-plugin/src/test-java11/kotlin/com/apollographql/apollo3/gradle/test/ConfigurationCacheTests.kt +++ b/libraries/apollo-gradle-plugin/src/test-java11/kotlin/com/apollographql/apollo3/gradle/test/ConfigurationCacheTests.kt @@ -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.")) + } } diff --git a/libraries/apollo-gradle-plugin/src/test-java11/kotlin/com/apollographql/apollo3/gradle/test/LazyTests.kt b/libraries/apollo-gradle-plugin/src/test-java11/kotlin/com/apollographql/apollo3/gradle/test/LazyTests.kt index 305e351d7f6..79c917c3907 100644 --- a/libraries/apollo-gradle-plugin/src/test-java11/kotlin/com/apollographql/apollo3/gradle/test/LazyTests.kt +++ b/libraries/apollo-gradle-plugin/src/test-java11/kotlin/com/apollographql/apollo3/gradle/test/LazyTests.kt @@ -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 { @@ -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) + } + } } diff --git a/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/build.gradle.kts b/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/build.gradle.kts new file mode 100644 index 00000000000..934d658aa69 --- /dev/null +++ b/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.apollo) +} + +apollo { + service("service") { + packageName.set("com.example") + } +} diff --git a/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/settings.gradle.kts b/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/settings.gradle.kts new file mode 100644 index 00000000000..18b257a15c4 --- /dev/null +++ b/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/settings.gradle.kts @@ -0,0 +1,3 @@ +apply(from = "../../../../gradle/test.settings.gradle.kts") + +include(":root", ":leaf") diff --git a/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/src/main/graphql/operation.graphql b/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/src/main/graphql/operation.graphql new file mode 100644 index 00000000000..ac48d4cf6a5 --- /dev/null +++ b/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/src/main/graphql/operation.graphql @@ -0,0 +1,3 @@ +query GetRandom { + random +} \ No newline at end of file diff --git a/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/src/main/graphql/schema.graphqls b/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/src/main/graphql/schema.graphqls new file mode 100644 index 00000000000..cfa6a5058bc --- /dev/null +++ b/libraries/apollo-gradle-plugin/testProjects/configuration-cache2/src/main/graphql/schema.graphqls @@ -0,0 +1,3 @@ +type Query { + random: String +} \ No newline at end of file diff --git a/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/build.gradle.kts b/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/build.gradle.kts new file mode 100644 index 00000000000..a1f25e93aab --- /dev/null +++ b/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/build.gradle.kts @@ -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 }) + } +} diff --git a/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/settings.gradle.kts b/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/settings.gradle.kts new file mode 100644 index 00000000000..c29a26fab83 --- /dev/null +++ b/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/settings.gradle.kts @@ -0,0 +1,2 @@ +apply(from = "../../../../gradle/test.settings.gradle.kts") + diff --git a/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/src/main/graphql/operation.graphql b/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/src/main/graphql/operation.graphql new file mode 100644 index 00000000000..ac48d4cf6a5 --- /dev/null +++ b/libraries/apollo-gradle-plugin/testProjects/lazy-schema-file/src/main/graphql/operation.graphql @@ -0,0 +1,3 @@ +query GetRandom { + random +} \ No newline at end of file