com.apollographql.apollo3.gradle.internal.DefaultApolloExtension.kt Maven / Gradle / Ivy
package com.apollographql.apollo3.gradle.internal
import com.apollographql.apollo3.annotations.ApolloExperimental
import com.apollographql.apollo3.compiler.OperationIdGenerator
import com.apollographql.apollo3.compiler.OperationOutputGenerator
import com.apollographql.apollo3.compiler.PackageNameGenerator
import com.apollographql.apollo3.compiler.capitalizeFirstLetter
import com.apollographql.apollo3.gradle.api.AndroidProject
import com.apollographql.apollo3.gradle.api.ApolloAttributes
import com.apollographql.apollo3.gradle.api.ApolloExtension
import com.apollographql.apollo3.gradle.api.Service
import com.apollographql.apollo3.gradle.api.androidExtension
import com.apollographql.apollo3.gradle.api.isKotlinMultiplatform
import com.apollographql.apollo3.gradle.api.javaConvention
import com.apollographql.apollo3.gradle.api.kotlinMultiplatformExtension
import com.apollographql.apollo3.gradle.api.kotlinProjectExtension
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.attributes.Usage
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.provider.Property
import org.gradle.api.tasks.TaskProvider
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import java.io.File
import java.util.concurrent.Callable
abstract class DefaultApolloExtension(
private val project: Project,
private val defaultService: DefaultService,
) : ApolloExtension, Service by defaultService {
private val services = mutableListOf()
private val checkVersionsTask: TaskProvider
private val apolloConfiguration: Configuration
private val rootProvider: TaskProvider
private var registerDefaultService = true
// Called when the plugin is applied
init {
require(GradleVersion.current() >= GradleVersion.version(MIN_GRADLE_VERSION)) {
"apollo-android requires Gradle version $MIN_GRADLE_VERSION or greater"
}
apolloConfiguration = project.configurations.create(ModelNames.apolloConfiguration()) {
it.isCanBeConsumed = false
it.isCanBeResolved = false
it.attributes {
it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage::class.java, USAGE_APOLLO_METADATA))
}
}
checkVersionsTask = registerCheckVersionsTask()
/**
* An aggregate task to easily generate all models
*/
rootProvider = project.tasks.register(ModelNames.generateApolloSources()) {
it.group = TASK_GROUP
it.description = "Generate Apollo models for all services"
}
/**
* A simple task to be used from the command line to ease the schema download
*/
project.tasks.register(ModelNames.downloadApolloSchema(), ApolloDownloadSchemaTask::class.java) { task ->
task.group = TASK_GROUP
}
/**
* A simple task to be used from the command line to ease the schema upload
*/
project.tasks.register(ModelNames.pushApolloSchema(), ApolloPushSchemaTask::class.java) { task ->
task.group = TASK_GROUP
}
/**
* A simple task to be used from the command line to ease schema conversion
*/
project.tasks.register(ModelNames.convertApolloSchema(), ApolloConvertSchemaTask::class.java) { task ->
task.group = TASK_GROUP
}
project.afterEvaluate {
if (registerDefaultService) {
registerService(defaultService)
} else {
@Suppress("DEPRECATION")
check(defaultService.graphqlSourceDirectorySet.isEmpty
&& defaultService.schemaFile.isPresent.not()
&& defaultService.schemaFiles.isEmpty
&& defaultService.alwaysGenerateTypesMatching.isPresent.not()
&& defaultService.customScalarsMapping.isPresent.not()
&& defaultService.customTypeMapping.isPresent.not()
&& defaultService.excludes.isPresent.not()
&& defaultService.includes.isPresent.not()
&& defaultService.failOnWarnings.isPresent.not()
&& defaultService.generateApolloMetadata.isPresent.not()
&& defaultService.generateAsInternal.isPresent.not()
&& defaultService.codegenModels.isPresent.not()
&& defaultService.generateFragmentImplementations.isPresent.not()
) {
"""
Configuring the default service is ignored if you specify other services, remove your configuration from the root of the apollo {} block:
apollo {
// remove everything at the top level
// add individual services
service("service1") {
// ...
}
service("service2") {
// ...
}
}
""".trimIndent()
}
}
maybeLinkSqlite()
}
}
private fun maybeLinkSqlite() {
val doLink = when (linkSqlite.orNull) {
false -> return // explicit opt-out
true -> true // explicit opt-in
null -> { // default: automatic detection
project.configurations.any {
it.dependencies.any {
// Try to detect if a native version of apollo-normalized-cache-sqlite is in the classpath
it.name.contains("apollo-normalized-cache-sqlite")
&& !it.name.contains("jvm")
&& !it.name.contains("android")
}
}
}
}
if (doLink) {
linkSqlite(project)
}
}
/**
* Call from users to explicitly register a service or by the plugin to register the implicit service
*/
override fun service(name: String, action: Action) {
registerDefaultService = false
val service = project.objects.newInstance(DefaultService::class.java, project, name)
action.execute(service)
registerService(service)
}
// Gradle will consider the task never UP-TO-DATE if we pass a lambda to doLast()
@Suppress("ObjectLiteralToLambda")
private fun registerCheckVersionsTask(): TaskProvider {
return project.tasks.register(ModelNames.checkApolloVersions()) {
val outputFile = BuildDirLayout.versionCheck(project)
it.inputs.property("allVersions", Callable {
val allDeps = (
getDeps(project.rootProject.buildscript.configurations) +
getDeps(project.buildscript.configurations) +
getDeps(project.configurations)
)
allDeps.mapNotNull { it.version }.distinct().sorted()
})
it.outputs.file(outputFile)
it.doLast(object : Action {
override fun execute(t: Task) {
val allVersions = it.inputs.properties["allVersions"] as List<*>
check(allVersions.size <= 1) {
"Apollo: All apollo versions should be the same. Found:\n$allVersions"
}
val version = allVersions.firstOrNull()
outputFile.get().asFile.parentFile.mkdirs()
outputFile.get().asFile.writeText("All versions are consistent: $version")
}
})
}
}
private fun registerService(service: DefaultService) {
check(services.find { it.name == service.name } == null) {
"There is already a service named $name, please use another name"
}
services.add(service)
val producerConfigurationName = ModelNames.producerConfiguration(service)
project.configurations.create(producerConfigurationName) {
it.isCanBeConsumed = true
it.isCanBeResolved = false
/**
* Expose transitive dependencies to downstream consumers
*/
it.extendsFrom(apolloConfiguration)
it.attributes {
it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage::class.java, USAGE_APOLLO_METADATA))
it.attribute(ApolloAttributes.APOLLO_SERVICE_ATTRIBUTE, project.objects.named(ApolloAttributes.Service::class.java, service.name))
}
}
val consumerConfiguration = project.configurations.create(ModelNames.consumerConfiguration(service)) {
it.isCanBeResolved = true
it.isCanBeConsumed = false
it.extendsFrom(apolloConfiguration)
it.attributes {
it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage::class.java, USAGE_APOLLO_METADATA))
it.attribute(ApolloAttributes.APOLLO_SERVICE_ATTRIBUTE, project.objects.named(ApolloAttributes.Service::class.java, service.name))
}
}
val codegenProvider = registerCodeGenTask(project, service, consumerConfiguration)
project.afterEvaluate {
if (shouldGenerateMetadata(service)) {
project.artifacts {
it.add(producerConfigurationName, codegenProvider.flatMap { it.metadataOutputFile })
}
}
}
codegenProvider.configure {
it.dependsOn(checkVersionsTask)
it.dependsOn(consumerConfiguration)
}
val checkApolloDuplicates = maybeRegisterCheckDuplicates(project.rootProject, service)
// Add project dependency on root project to this project, with our new configurations
project.rootProject.dependencies.apply {
add(
ModelNames.duplicatesConsumerConfiguration(service),
project(mapOf("path" to project.path))
)
}
codegenProvider.configure {
it.finalizedBy(checkApolloDuplicates)
}
if (service.operationOutputAction != null) {
val operationOutputConnection = Service.OperationOutputConnection(
task = codegenProvider,
operationOutputFile = codegenProvider.flatMap { it.operationOutputFile }
)
service.operationOutputAction!!.execute(operationOutputConnection)
}
if (service.outputDirAction == null) {
service.outputDirAction = defaultOutputDirAction
}
service.outputDirAction!!.execute(
DefaultDirectoryConnection(
project = project,
task = codegenProvider,
outputDir = codegenProvider.flatMap { it.outputDir }
)
)
if (service.testDirAction == null) {
service.testDirAction = defaultTestDirAction
}
service.testDirAction!!.execute(
DefaultDirectoryConnection(
project = project,
task = codegenProvider,
outputDir = codegenProvider.flatMap { it.testDir }
)
)
rootProvider.configure {
it.dependsOn(codegenProvider)
}
registerDownloadSchemaTasks(service)
maybeRegisterRegisterOperationsTasks(project, service, codegenProvider)
}
private fun maybeRegisterRegisterOperationsTasks(project: Project, service: DefaultService, codegenProvider: TaskProvider) {
val registerOperationsConfig = service.registerOperationsConfig
if (registerOperationsConfig != null) {
project.tasks.register(ModelNames.registerApolloOperations(service), ApolloRegisterOperationsTask::class.java) { task ->
task.group = TASK_GROUP
task.graph.set(registerOperationsConfig.graph)
task.graphVariant.set(registerOperationsConfig.graphVariant)
task.key.set(registerOperationsConfig.key)
task.operationOutput.set(codegenProvider.flatMap { it.operationOutputFile })
}
}
}
/**
* Generate metadata
* - if the user opted in
* - or if this project belongs to a multi-module build
* The last case is needed to check for potential duplicate types
*/
private fun shouldGenerateMetadata(service: DefaultService): Boolean {
return service.generateApolloMetadata.getOrElse(false)
|| apolloConfiguration.dependencies.isNotEmpty()
}
/**
* The default wiring.
*/
private val defaultOutputDirAction = Action { connection ->
when {
project.kotlinMultiplatformExtension != null -> {
connection.connectToKotlinSourceSet("commonMain")
}
project.androidExtension != null -> {
connection.connectToAndroidSourceSet("main")
}
project.kotlinProjectExtension != null -> {
connection.connectToKotlinSourceSet("main")
}
project.javaConvention != null -> {
connection.connectToJavaSourceSet("main")
}
else -> throw IllegalStateException("Cannot find a Java/Kotlin extension, please apply the kotlin or java plugin")
}
}
private val defaultTestDirAction = Action { connection ->
when {
project.kotlinMultiplatformExtension != null -> {
connection.connectToKotlinSourceSet("commonTest")
}
project.androidExtension != null -> {
connection.connectToAndroidSourceSet("test")
connection.connectToAndroidSourceSet("androidTest")
}
project.kotlinProjectExtension != null -> {
connection.connectToKotlinSourceSet("test")
}
project.javaConvention != null -> {
connection.connectToJavaSourceSet("test")
}
else -> throw IllegalStateException("Cannot find a Java/Kotlin extension, please apply the kotlin or java plugin")
}
}
private fun maybeRegisterCheckDuplicates(rootProject: Project, service: Service): TaskProvider {
val taskName = ModelNames.checkApolloDuplicates(service)
return try {
@Suppress("UNCHECKED_CAST")
rootProject.tasks.named(taskName) as TaskProvider
} catch (e: Exception) {
val configuration = rootProject.configurations.create(ModelNames.duplicatesConsumerConfiguration(service)) {
it.isCanBeResolved = true
it.isCanBeConsumed = false
it.attributes {
it.attribute(Usage.USAGE_ATTRIBUTE, rootProject.objects.named(Usage::class.java, USAGE_APOLLO_METADATA))
it.attribute(ApolloAttributes.APOLLO_SERVICE_ATTRIBUTE, rootProject.objects.named(ApolloAttributes.Service::class.java, service.name))
}
}
rootProject.tasks.register(taskName, ApolloCheckDuplicatesTask::class.java) {
it.outputFile.set(BuildDirLayout.duplicatesCheck(rootProject, service))
it.metadataFiles.from(configuration)
}
}
}
private fun Project.hasJavaPlugin() = project.extensions.findByName("java") != null
private fun Project.hasKotlinPlugin() = project.extensions.findByName("kotlin") != null
private fun registerCodeGenTask(
project: Project,
service: DefaultService,
consumerConfiguration: Configuration,
): TaskProvider {
return project.tasks.register(ModelNames.generateApolloSources(service), ApolloGenerateSourcesTask::class.java) { task ->
task.group = TASK_GROUP
task.description = "Generate Apollo models for ${service.name} GraphQL queries"
if (service.graphqlSourceDirectorySet.isReallyEmpty) {
val sourceFolder = service.sourceFolder.getOrElse("")
val dir = File(project.projectDir, "src/${mainSourceSet(project)}/graphql/$sourceFolder")
service.graphqlSourceDirectorySet.srcDir(dir)
}
service.graphqlSourceDirectorySet.include(service.includes.getOrElse(listOf("**/*.graphql", "**/*.gql")))
service.graphqlSourceDirectorySet.exclude(service.excludes.getOrElse(emptyList()))
task.graphqlFiles.setFrom(service.graphqlSourceDirectorySet)
// Since this is stored as a list of string, the order matter hence the sorting
task.rootFolders.set(project.provider { service.graphqlSourceDirectorySet.srcDirs.map { it.relativeTo(project.projectDir).path }.sorted() })
// 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.operationOutputGenerator = service.operationOutputGenerator.getOrElse(
OperationOutputGenerator.Default(
service.operationIdGenerator.orElse(OperationIdGenerator.Sha256).get()
)
)
if (project.hasKotlinPlugin()) {
checkKotlinPluginVersion(project)
}
val generateKotlinModels: Boolean
when {
service.generateKotlinModels.isPresent -> {
generateKotlinModels = service.generateKotlinModels.get()
if (generateKotlinModels) {
check(project.hasKotlinPlugin()) {
"Apollo: generateKotlinModels.set(true) requires to apply a Kotlin plugin"
}
} else {
check(project.hasJavaPlugin()) {
"Apollo: generateKotlinModels.set(false) requires to apply the Java plugin"
}
}
}
project.hasKotlinPlugin() -> {
generateKotlinModels = true
}
project.hasJavaPlugin() -> {
generateKotlinModels = false
}
else -> {
error("Apollo: No Java or Kotlin plugin found")
}
}
task.useSemanticNaming.set(service.useSemanticNaming)
task.generateKotlinModels.set(generateKotlinModels)
task.warnOnDeprecatedUsages.set(service.warnOnDeprecatedUsages)
task.failOnWarnings.set(service.failOnWarnings)
@Suppress("DEPRECATION")
task.customScalarsMapping.set(service.customScalarsMapping.orElse(service.customTypeMapping))
task.outputDir.apply {
set(service.outputDir.orElse(BuildDirLayout.outputDir(project, service)).get())
disallowChanges()
}
task.testDir.apply {
set(service.testDir.orElse(BuildDirLayout.testDir(project, service)).get())
disallowChanges()
}
task.debugDir.apply {
set(service.debugDir)
disallowChanges()
}
if (service.generateOperationOutput.getOrElse(false)) {
task.operationOutputFile.apply {
set(service.operationOutputFile.orElse(BuildDirLayout.operationOutput(project, service)))
disallowChanges()
}
}
if (shouldGenerateMetadata(service)) {
task.metadataOutputFile.apply {
set(BuildDirLayout.metadata(project, service))
disallowChanges()
}
}
task.metadataFiles.from(consumerConfiguration)
check(!(service.packageName.isPresent && service.packageNameGenerator.isPresent)) {
"Apollo: it is an error to specify both 'packageName' and 'packageNameGenerator' " +
"(either directly or indirectly through useVersion2Compat())"
}
var packageNameGenerator = service.packageNameGenerator.orNull
if (packageNameGenerator == null) {
packageNameGenerator = PackageNameGenerator.Flat(service.packageName.orNull ?: error("""
|Apollo: specify 'packageName':
|apollo {
| packageName.set("com.example")
|
| // Alternatively, if you're migrating from 2.x, you can keep the 2.x
| // behaviour with `packageNamesFromFilePaths()`:
| packageNamesFromFilePaths()
|}
""".trimMargin()))
}
task.packageNameGenerator = packageNameGenerator
task.generateAsInternal.set(service.generateAsInternal)
task.generateFilterNotNull.set(project.isKotlinMultiplatform)
task.alwaysGenerateTypesMatching.set(service.alwaysGenerateTypesMatching)
task.projectName.set(project.name)
task.generateFragmentImplementations.set(service.generateFragmentImplementations)
task.generateQueryDocument.set(service.generateQueryDocument)
task.generateSchema.set(service.generateSchema)
task.codegenModels.set(service.codegenModels)
task.flattenModels.set(service.flattenModels)
@OptIn(ApolloExperimental::class)
task.generateTestBuilders.set(service.generateTestBuilders)
task.sealedClassesForEnumsMatching.set(service.sealedClassesForEnumsMatching)
task.generateOptionalOperationVariables.set(service.generateOptionalOperationVariables)
task.languageVersion.set(service.languageVersion)
}
}
private fun lazySchemaFileForDownload(service: DefaultService, schemaFile: RegularFileProperty): String {
if (schemaFile.isPresent) {
return schemaFile.get().asFile.absolutePath
}
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().absolutePath
}
private fun registerDownloadSchemaTasks(service: DefaultService) {
val introspection = service.introspection
if (introspection != null) {
project.tasks.register(ModelNames.downloadApolloSchemaIntrospection(service), ApolloDownloadSchemaTask::class.java) { task ->
task.group = TASK_GROUP
task.endpoint.set(introspection.endpointUrl)
task.header = introspection.headers.get().map { "${it.key}: ${it.value}" }
task.schema.set(project.provider { lazySchemaFileForDownload(service, introspection.schemaFile) })
}
}
val registry = service.registry
if (registry != null) {
project.tasks.register(ModelNames.downloadApolloSchemaIntrospection(service), ApolloDownloadSchemaTask::class.java) { task ->
task.group = TASK_GROUP
task.graph.set(registry.graph)
task.key.set(registry.key)
task.graphVariant.set(registry.graphVariant)
task.schema.set(project.provider { lazySchemaFileForDownload(service, registry.schemaFile) })
}
}
}
override fun createAllAndroidVariantServices(
sourceFolder: String,
nameSuffix: String,
action: Action,
) {
/**
* The android plugin will call us back when the variants are ready but before `afterEvaluate`,
* disable the default service
*/
registerDefaultService = false
check(!File(sourceFolder).isRooted && !sourceFolder.startsWith("../..")) {
"""
Apollo: using 'sourceFolder = "$sourceFolder"' makes no sense with Android variants as the same generated models will be used in all variants.
""".trimIndent()
}
AndroidProject.onEachVariant(project, true) { variant ->
val name = "${variant.name}${nameSuffix.capitalizeFirstLetter()}"
service(name) { service ->
action.execute(service)
check(!service.sourceFolder.isPresent) {
"Apollo: service.sourceFolder is not used when calling createAllAndroidVariantServices. Use the parameter instead"
}
variant.sourceSets.forEach { sourceProvider ->
service.srcDir("src/${sourceProvider.name}/graphql/$sourceFolder")
}
(service as DefaultService).outputDirAction = Action { connection ->
connection.connectToAndroidVariant(variant)
}
}
}
}
override fun createAllKotlinSourceSetServices(sourceFolder: String, nameSuffix: String, action: Action) {
registerDefaultService = false
check(!File(sourceFolder).isRooted && !sourceFolder.startsWith("../..")) {
"""Apollo: using 'sourceFolder = "$sourceFolder"' makes no sense with Kotlin source sets as the same generated models will be used in all source sets.
""".trimMargin()
}
createAllKotlinSourceSetServices(this, project, sourceFolder, nameSuffix, action)
}
abstract override val linkSqlite: Property
companion object {
private const val TASK_GROUP = "apollo"
const val MIN_GRADLE_VERSION = "5.6"
private const val USAGE_APOLLO_METADATA = "apollo-metadata"
private data class Dep(val name: String, val version: String?)
private fun getDeps(configurations: ConfigurationContainer): List {
return configurations.flatMap { configuration ->
configuration.dependencies
.filter {
it.group == "com.apollographql.apollo3"
}.map { dependency ->
Dep(dependency.name, dependency.version)
}
}
}
// Don't use `graphqlSourceDirectorySet.isEmpty` here, it doesn't work for some reason
private val SourceDirectorySet.isReallyEmpty
get() = sourceDirectories.isEmpty
private fun mainSourceSet(project: Project): String {
val kotlinExtension = project.extensions.findByName("kotlin")
return when (kotlinExtension) {
is KotlinMultiplatformExtension -> "commonMain"
else -> "main"
}
}
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") }.toList()
}.toSet()
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy