Skip to content

Commit b446f42

Browse files
authored
[gradle-plugin] Manage our classloaders manually (#6305)
* Manage our classloaders manually * share the warning consumer
1 parent c64bb51 commit b446f42

File tree

11 files changed

+393
-174
lines changed

11 files changed

+393
-174
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package com.apollographql.apollo.compiler
2+
3+
import com.apollographql.apollo.annotations.ApolloInternal
4+
import com.apollographql.apollo.compiler.codegen.SchemaAndOperationsLayout
5+
import com.apollographql.apollo.compiler.codegen.writeTo
6+
import com.apollographql.apollo.compiler.internal.GradleCompilerPluginLogger
7+
import com.apollographql.apollo.compiler.operationoutput.OperationDescriptor
8+
import com.apollographql.apollo.compiler.operationoutput.OperationId
9+
import com.apollographql.apollo.compiler.operationoutput.OperationOutput
10+
import java.io.File
11+
import java.util.ServiceLoader
12+
import java.util.function.Consumer
13+
14+
/**
15+
* EntryPoints contains code that is called using reflection from the Gradle plugin.
16+
* This is so that the classloader can be isolated, and we can use our preferred version of
17+
* Kotlin and other dependencies without risking conflicts.
18+
*/
19+
@Suppress("UNUSED") // Used from reflection
20+
@ApolloInternal
21+
class EntryPoints {
22+
fun buildCodegenSchema(
23+
arguments: Map<String, Any?>,
24+
logLevel: Int,
25+
warnIfNotFound: Boolean = false,
26+
normalizedSchemaFiles: List<Any>,
27+
warning: Consumer<String>,
28+
codegenSchemaOptionsFile: File,
29+
codegenSchemaFile: File,
30+
) {
31+
val plugin = apolloCompilerPlugin(
32+
arguments,
33+
logLevel,
34+
warnIfNotFound
35+
)
36+
37+
ApolloCompiler.buildCodegenSchema(
38+
schemaFiles = normalizedSchemaFiles.toInputFiles(),
39+
logger = warning.toLogger(),
40+
codegenSchemaOptions = codegenSchemaOptionsFile.toCodegenSchemaOptions(),
41+
foreignSchemas = plugin?.foreignSchemas().orEmpty()
42+
).writeTo(codegenSchemaFile)
43+
}
44+
45+
fun buildIr(
46+
arguments: Map<String, Any?>,
47+
logLevel: Int,
48+
graphqlFiles: List<Any>,
49+
codegenSchemaFiles: List<Any>,
50+
upstreamIrOperations: List<Any>,
51+
irOptionsFile: File,
52+
warning: Consumer<String>,
53+
irOperationsFile: File,
54+
) {
55+
val plugin = apolloCompilerPlugin(arguments, logLevel)
56+
57+
val upstream = upstreamIrOperations.toInputFiles().map { it.file.toIrOperations() }
58+
ApolloCompiler.buildIrOperations(
59+
executableFiles = graphqlFiles.toInputFiles(),
60+
codegenSchema = codegenSchemaFiles.toInputFiles().map { it.file }.findCodegenSchemaFile().toCodegenSchema(),
61+
upstreamCodegenModels = upstream.map { it.codegenModels },
62+
upstreamFragmentDefinitions = upstream.flatMap { it.fragmentDefinitions },
63+
documentTransform = plugin?.documentTransform(),
64+
options = irOptionsFile.toIrOptions(),
65+
logger = warning.toLogger(),
66+
).writeTo(irOperationsFile)
67+
}
68+
69+
fun buildSourcesFromIr(
70+
arguments: Map<String, Any?>,
71+
logLevel: Int,
72+
warnIfNotFound: Boolean = false,
73+
codegenSchemaFiles: List<Any>,
74+
upstreamMetadata: List<Any>,
75+
irOperations: File,
76+
downstreamUsedCoordinates: Map<String, Map<String, Set<String>>>,
77+
codegenOptions: File,
78+
operationManifestFile: File?,
79+
outputDir: File,
80+
metadataOutputFile: File?
81+
) {
82+
val plugin = apolloCompilerPlugin(
83+
arguments,
84+
logLevel,
85+
warnIfNotFound
86+
)
87+
val codegenSchemaFile = codegenSchemaFiles.toInputFiles().map { it.file }.findCodegenSchemaFile()
88+
89+
val codegenSchema = codegenSchemaFile.toCodegenSchema()
90+
91+
val upstreamCodegenMetadata = upstreamMetadata.toInputFiles().map { it.file.toCodegenMetadata() }
92+
ApolloCompiler.buildSchemaAndOperationsSourcesFromIr(
93+
codegenSchema = codegenSchema,
94+
irOperations = irOperations.toIrOperations(),
95+
downstreamUsedCoordinates = downstreamUsedCoordinates.toUsedCoordinates(),
96+
upstreamCodegenMetadata = upstreamCodegenMetadata,
97+
codegenOptions = codegenOptions.toCodegenOptions(),
98+
layout = plugin?.layout(codegenSchema),
99+
irOperationsTransform = plugin?.irOperationsTransform(),
100+
javaOutputTransform = plugin?.javaOutputTransform(),
101+
kotlinOutputTransform = plugin?.kotlinOutputTransform(),
102+
operationManifestFile = operationManifestFile,
103+
operationOutputGenerator = plugin?.toOperationOutputGenerator(),
104+
).writeTo(outputDir, true, metadataOutputFile)
105+
106+
if (upstreamCodegenMetadata.isEmpty()) {
107+
plugin?.schemaListener()?.onSchema(codegenSchema.schema, outputDir)
108+
}
109+
}
110+
111+
fun buildSources(
112+
arguments: Map<String, Any?>,
113+
logLevel: Int,
114+
warnIfNotFound: Boolean = false,
115+
schemaFiles: List<Any>,
116+
graphqlFiles: List<Any>,
117+
codegenSchemaOptions: File,
118+
codegenOptions: File,
119+
irOptions: File,
120+
warning: Consumer<String>,
121+
operationManifestFile: File?,
122+
outputDir: File
123+
) {
124+
val plugin = apolloCompilerPlugin(
125+
arguments,
126+
logLevel,
127+
warnIfNotFound
128+
)
129+
130+
val codegenSchema = ApolloCompiler.buildCodegenSchema(
131+
schemaFiles = schemaFiles.toInputFiles(),
132+
codegenSchemaOptions = codegenSchemaOptions.toCodegenSchemaOptions(),
133+
foreignSchemas = plugin?.foreignSchemas().orEmpty(),
134+
logger = warning.toLogger()
135+
)
136+
137+
ApolloCompiler.buildSchemaAndOperationsSources(
138+
codegenSchema,
139+
executableFiles = graphqlFiles.toInputFiles(),
140+
codegenOptions = codegenOptions.toCodegenOptions(),
141+
irOptions = irOptions.toIrOptions(),
142+
logger = warning.toLogger(),
143+
layoutFactory = object : LayoutFactory {
144+
override fun create(codegenSchema: CodegenSchema): SchemaAndOperationsLayout? {
145+
return plugin?.layout(codegenSchema)
146+
}
147+
},
148+
operationOutputGenerator = plugin?.toOperationOutputGenerator(),
149+
irOperationsTransform = plugin?.irOperationsTransform(),
150+
javaOutputTransform = plugin?.javaOutputTransform(),
151+
kotlinOutputTransform = plugin?.kotlinOutputTransform(),
152+
documentTransform = plugin?.documentTransform(),
153+
operationManifestFile = operationManifestFile,
154+
).writeTo(outputDir, true, null)
155+
156+
plugin?.schemaListener()?.onSchema(codegenSchema.schema, outputDir)
157+
}
158+
}
159+
160+
internal fun ApolloCompilerPlugin.toOperationOutputGenerator(): OperationOutputGenerator {
161+
return object : OperationOutputGenerator {
162+
override fun generate(operationDescriptorList: Collection<OperationDescriptor>): OperationOutput {
163+
var operationIds = operationIds(operationDescriptorList.toList())
164+
if (operationIds == null) {
165+
operationIds = operationDescriptorList.map { OperationId(OperationIdGenerator.Sha256.apply(it.source, it.name), it.name) }
166+
}
167+
return operationDescriptorList.associateBy { descriptor ->
168+
val operationId = operationIds.firstOrNull { it.name == descriptor.name } ?: error("No id found for operation ${descriptor.name}")
169+
operationId.id
170+
}
171+
}
172+
}
173+
}
174+
175+
internal fun Consumer<String>.toLogger(): ApolloCompiler.Logger {
176+
return object : ApolloCompiler.Logger {
177+
override fun warning(message: String) {
178+
accept(message)
179+
}
180+
}
181+
}
182+
183+
@ApolloInternal
184+
fun Iterable<File>.findCodegenSchemaFile(): File {
185+
return firstOrNull {
186+
it.length() > 0
187+
} ?: error("Cannot find CodegenSchema in $this")
188+
}
189+
190+
internal fun apolloCompilerPlugin(
191+
arguments: Map<String, Any?>,
192+
logLevel: Int,
193+
warnIfNotFound: Boolean = false,
194+
): ApolloCompilerPlugin? {
195+
val plugins = ServiceLoader.load(ApolloCompilerPlugin::class.java, ApolloCompilerPlugin::class.java.classLoader).toList()
196+
197+
if (plugins.size > 1) {
198+
error("Apollo: only a single compiler plugin is allowed")
199+
}
200+
201+
val plugin = plugins.singleOrNull()
202+
if (plugin != null) {
203+
error("Apollo: use ApolloCompilerPluginProvider instead of ApolloCompilerPlugin directly. ApolloCompilerPluginProvider allows arguments and logging")
204+
}
205+
206+
val pluginProviders = ServiceLoader.load(ApolloCompilerPluginProvider::class.java, ApolloCompilerPlugin::class.java.classLoader).toList()
207+
208+
if (pluginProviders.size > 1) {
209+
error("Apollo: only a single compiler plugin provider is allowed")
210+
}
211+
212+
if (pluginProviders.isEmpty() && warnIfNotFound) {
213+
println("Apollo: a compiler plugin was added with `Service.plugin()` but could not be loaded by the ServiceLoader. Check your META-INF/services/com.apollographql.apollo.compiler.ApolloCompilerPluginProvider file.")
214+
}
215+
216+
val provider = pluginProviders.singleOrNull()
217+
if (provider != null) {
218+
return provider.create(
219+
ApolloCompilerPluginEnvironment(
220+
arguments,
221+
GradleCompilerPluginLogger(logLevel)
222+
)
223+
)
224+
}
225+
226+
return plugins.singleOrNull()
227+
}
228+
229+
230+
internal fun List<Any>.toInputFiles(): List<InputFile> = buildList {
231+
val iterator = this@toInputFiles.iterator()
232+
while (iterator.hasNext()) {
233+
add(InputFile(normalizedPath = iterator.next() as String, file = iterator.next() as File))
234+
}
235+
}

libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo/gradle/api/Service.kt

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -728,17 +728,6 @@ interface Service {
728728
*/
729729
val decapitalizeFields: Property<Boolean>
730730

731-
/**
732-
* Whether to use process isolation. By default, the Apollo tasks use classloader isolation, which may leak and increase memory usage over time.
733-
* By using process isolation, the tasks are run in a separate process and the memory is reclaimed when the process stops. This comes at a price
734-
* of more forking time.
735-
*
736-
* See https://github.com/apollographql/apollo-kotlin/issues/6266
737-
* See https://github.com/gradle/gradle/issues/18313
738-
*/
739-
@ApolloExperimental
740-
val useProcessIsolation: Property<Boolean>
741-
742731
@Deprecated("Not supported any more, use dependsOn() instead", level = DeprecationLevel.ERROR)
743732
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v4_0_0)
744733
fun usedCoordinates(file: File)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.apollographql.apollo.gradle.internal
2+
3+
import org.gradle.api.file.FileCollection
4+
import org.gradle.api.services.BuildService
5+
import org.gradle.api.services.BuildServiceParameters
6+
import java.net.URL
7+
import java.net.URLClassLoader
8+
9+
abstract class ApolloBuildService: BuildService<BuildServiceParameters.None>, AutoCloseable {
10+
private val classloaders = mutableMapOf<Array<URL>, ClassLoader>()
11+
12+
fun classloader(classpath: FileCollection): ClassLoader {
13+
val urls = classpath.map { it.toURI().toURL() }.toTypedArray()
14+
return classloaders.getOrPut(urls) {
15+
URLClassLoader(
16+
urls,
17+
ClassLoader.getPlatformClassLoader()
18+
)
19+
}
20+
}
21+
22+
override fun close() {
23+
classloaders.clear()
24+
}
25+
}

libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo/gradle/internal/ApolloGenerateCodegenSchemaTask.kt

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package com.apollographql.apollo.gradle.internal
22

3-
import com.apollographql.apollo.compiler.ApolloCompiler
4-
import com.apollographql.apollo.compiler.toCodegenSchemaOptions
5-
import com.apollographql.apollo.compiler.writeTo
63
import org.gradle.api.file.ConfigurableFileCollection
4+
import org.gradle.api.file.FileCollection
75
import org.gradle.api.file.RegularFileProperty
6+
import org.gradle.api.provider.Property
87
import org.gradle.api.tasks.CacheableTask
98
import org.gradle.api.tasks.InputFile
109
import org.gradle.api.tasks.InputFiles
@@ -15,9 +14,8 @@ import org.gradle.api.tasks.PathSensitivity
1514
import org.gradle.api.tasks.TaskAction
1615
import org.gradle.workers.WorkAction
1716
import org.gradle.workers.WorkParameters
18-
import org.gradle.workers.WorkerExecutor
1917
import java.io.File
20-
import javax.inject.Inject
18+
import java.util.function.Consumer
2119

2220
@CacheableTask
2321
abstract class ApolloGenerateCodegenSchemaTask : ApolloTaskWithClasspath() {
@@ -64,6 +62,8 @@ abstract class ApolloGenerateCodegenSchemaTask : ApolloTaskWithClasspath() {
6462
it.hasPlugin = hasPlugin.get()
6563
it.arguments = arguments.get()
6664
it.logLevel = logLevel.get().ordinal
65+
it.apolloBuildService.set(apolloBuildService)
66+
it.classpath = classpath
6767
}
6868
}
6969
}
@@ -72,30 +72,32 @@ abstract class ApolloGenerateCodegenSchemaTask : ApolloTaskWithClasspath() {
7272
private abstract class GenerateCodegenSchema : WorkAction<GenerateCodegenSchemaParameters> {
7373
override fun execute() {
7474
with(parameters) {
75-
val plugin = apolloCompilerPlugin(
76-
arguments,
77-
logLevel,
78-
hasPlugin
79-
)
80-
81-
val normalizedSchemaFiles = (schemaFiles.takeIf { it.isNotEmpty() }?: fallbackSchemaFiles).toInputFiles()
82-
83-
ApolloCompiler.buildCodegenSchema(
84-
schemaFiles = normalizedSchemaFiles,
85-
logger = logger(),
86-
codegenSchemaOptions = codegenSchemaOptionsFile.get().asFile.toCodegenSchemaOptions(),
87-
foreignSchemas = plugin?.foreignSchemas().orEmpty()
88-
).writeTo(codegenSchemaFile.get().asFile)
75+
runInIsolation(apolloBuildService.get(), classpath) {
76+
it.javaClass.declaredMethods.single { it.name == "buildCodegenSchema" }
77+
.invoke(
78+
it,
79+
arguments,
80+
logLevel,
81+
hasPlugin,
82+
(schemaFiles.takeIf { it.isNotEmpty() }?: fallbackSchemaFiles),
83+
warningMessageConsumer,
84+
codegenSchemaOptionsFile.get().asFile,
85+
codegenSchemaFile.get().asFile
86+
)
87+
}
8988
}
9089
}
9190
}
91+
9292
private interface GenerateCodegenSchemaParameters : WorkParameters {
9393
var codegenSchemaFile: RegularFileProperty
94-
var schemaFiles: List<Pair<String, File>>
95-
var fallbackSchemaFiles: List<Pair<String, File>>
94+
var schemaFiles: List<Any>
95+
var fallbackSchemaFiles: List<Any>
9696
val codegenSchemaOptionsFile: RegularFileProperty
9797
var hasPlugin: Boolean
9898
var arguments: Map<String, Any?>
9999
var logLevel: Int
100+
val apolloBuildService: Property<ApolloBuildService>
101+
var classpath: FileCollection
100102
}
101103

0 commit comments

Comments
 (0)