1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.build.importMaven
18 
19 import androidx.build.importMaven.ArtifactResolver.resolveArtifacts
20 import androidx.build.importMaven.KmpConfig.SUPPORTED_KONAN_TARGETS
21 import java.net.URI
22 import org.apache.logging.log4j.kotlin.logger
23 import org.gradle.api.Named
24 import org.gradle.api.Project
25 import org.gradle.api.artifacts.Configuration
26 import org.gradle.api.artifacts.Dependency
27 import org.gradle.api.artifacts.component.ModuleComponentIdentifier
28 import org.gradle.api.artifacts.dsl.RepositoryHandler
29 import org.gradle.api.artifacts.result.ResolvedArtifactResult
30 import org.gradle.api.attributes.Attribute
31 import org.gradle.api.attributes.AttributeContainer
32 import org.gradle.api.attributes.Category
33 import org.gradle.api.attributes.LibraryElements
34 import org.gradle.api.attributes.Usage
35 import org.gradle.api.attributes.java.TargetJvmEnvironment
36 import org.gradle.api.attributes.java.TargetJvmVersion
37 import org.gradle.api.attributes.plugin.GradlePluginApiVersion
38 import org.gradle.api.internal.artifacts.verification.exceptions.DependencyVerificationException
39 import org.gradle.util.GradleVersion
40 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
41 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
42 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
43 import org.jetbrains.kotlin.gradle.targets.js.KotlinJsCompilerAttribute
44 import org.jetbrains.kotlin.gradle.targets.js.KotlinWasmTargetAttribute
45 import org.jetbrains.kotlin.konan.target.KonanTarget
46 
47 /**
48  * Provides functionality to resolve and download artifacts.
49  * see: [resolveArtifacts]
50  * see: [LocalMavenRepoDownloader]
51  * see: [MavenRepositoryProxy]
52  */
53 internal object ArtifactResolver {
54     internal val jetbrainsRepositories = listOf(
55         "https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev/",
56         "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev",
57         "https://maven.pkg.jetbrains.space/public/p/compose/dev",
58         "https://maven.pkg.jetbrains.space/kotlin/p/dokka/test"
59     )
60 
61     internal val gradlePluginPortalRepo = "https://plugins.gradle.org/m2/"
62 
63     internal fun createAndroidXRepo(
64         buildId: Int
65     ) = "https://androidx.dev/snapshots/builds/$buildId/artifacts/repository"
66 
67     internal fun createMetalavaRepo(
68         buildId: Int
69     ) = "https://androidx.dev/metalava/builds/$buildId/artifacts/repo/m2repository"
70 
71     /**
72      * Resolves given set of [artifacts].
73      *
74      * @param artifacts List of artifacts to resolve.
75      * @param additionalRepositories List of repositories in addition to mavenCentral and google
76      * @param localRepositories List of local repositories. If an artifact is found here, it won't
77      *        be downloaded.
78      * @param explicitlyFetchInheritedDependencies If set to true, each discovered dependency will
79      *        be fetched again. For instance:
80      *        artifact1:v1
81      *          artifact2:v2
82      *            artifact3:v1
83      *          artifact3:v3
84      *       If this flag is `false`, we'll only fetch artifact1:v1, artifact2:v2, artifact3:v3.
85      *       If this flag is `true`, we'll fetch `artifact3:v1` as well (because artifact2:v2
86      *       declares a dependency on it even though it is overridden by the dependency of
87      *       artifact1:v1
88      * @param downloadObserver An observer that will be notified each time a file is downloaded from
89      *        a remote repository.
90      */
91     fun resolveArtifacts(
92         artifacts: List<String>,
93         additionalRepositories: List<String> = emptyList(),
94         localRepositories: List<String> = emptyList(),
95         explicitlyFetchInheritedDependencies: Boolean = false,
96         downloadObserver: DownloadObserver?,
97     ): ArtifactsResolutionResult {
98         return SingleUseArtifactResolver(
99             project = ProjectService.createProject(),
100             artifacts = artifacts,
101             additionalPriorityRepositories = additionalRepositories,
102             localRepositories = localRepositories,
103             explicitlyFetchInheritedDependencies = explicitlyFetchInheritedDependencies,
104             downloadObserver = downloadObserver
105         ).resolveArtifacts()
106     }
107 
108     /**
109      * see docs for [ArtifactResolver.resolveArtifacts]
110      */
111     private class SingleUseArtifactResolver(
112         private val project: Project,
113         private val artifacts: List<String>,
114         private val additionalPriorityRepositories: List<String>,
115         private val localRepositories: List<String>,
116         private val explicitlyFetchInheritedDependencies: Boolean,
117         private val downloadObserver: DownloadObserver?,
118     ) {
119         private val logger = logger("ArtifactResolver")
120         fun resolveArtifacts(): ArtifactsResolutionResult {
121             logger.info {
122                 """--------------------------------------------------------------------------------
123 Resolving artifacts:
124 ${artifacts.joinToString(separator = "\n - ", prefix = " - ")}
125 Local repositories:
126 ${localRepositories.joinToString(separator = "\n - ", prefix = " - ")}
127 High priority repositories:
128 ${
129     if (additionalPriorityRepositories.isEmpty())
130         " - None"
131     else
132         additionalPriorityRepositories.joinToString(separator = "\n - ", prefix = " - ")
133 }
134 --------------------------------------------------------------------------------"""
135             }
136             return withProxyServer(
137                 downloadObserver = downloadObserver
138             ) {
139                 logger.trace {
140                     "Initialized proxy servers"
141                 }
142                 var dependenciesPassedVerification = true
143 
144                 project.dependencies.apply {
145                     components.all(CustomMetadataRules::class.java)
146                     attributesSchema.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE)
147                         .compatibilityRules.add(JarAndAarAreCompatible::class.java)
148                 }
149                 val completedComponentIds = mutableSetOf<String>()
150                 val pendingComponentIds = mutableSetOf<String>().also {
151                     it.addAll(artifacts)
152                 }
153                 val allResolvedArtifacts = mutableSetOf<ResolvedArtifactResult>()
154                 do {
155                     val dependencies = pendingComponentIds.map {
156                         project.dependencies.create(it)
157                     }
158                     val resolvedArtifacts = createConfigurationsAndResolve(dependencies)
159                     if (!resolvedArtifacts.dependenciesPassedVerification) {
160                         dependenciesPassedVerification = false
161                     }
162                     allResolvedArtifacts.addAll(resolvedArtifacts.artifacts)
163                     completedComponentIds.addAll(pendingComponentIds)
164                     pendingComponentIds.clear()
165                     val newComponentIds = resolvedArtifacts.artifacts.mapNotNull {
166                         (it.id.componentIdentifier as? ModuleComponentIdentifier)?.toString()
167                     }.filter {
168                         !completedComponentIds.contains(it) && pendingComponentIds.add(it)
169                     }
170                     logger.trace {
171                         "New component ids:\n${newComponentIds.joinToString("\n")}"
172                     }
173                     pendingComponentIds.addAll(newComponentIds)
174                 } while (explicitlyFetchInheritedDependencies && pendingComponentIds.isNotEmpty())
175                 ArtifactsResolutionResult(
176                     allResolvedArtifacts.toList(),
177                     dependenciesPassedVerification
178                 )
179             }.also { result ->
180                 val artifacts = result.artifacts
181                 logger.trace {
182                     "Resolved files: ${artifacts.size}"
183                 }
184                 check(artifacts.isNotEmpty()) {
185                     "Didn't resolve any artifacts from $artifacts. Try --verbose for more " +
186                       "information"
187                 }
188                 artifacts.forEach { artifact ->
189                     logger.trace {
190                         artifact.id.toString()
191                     }
192                 }
193             }
194         }
195 
196         /**
197          * Creates configurations with the given list of dependencies and resolves them.
198          */
199         private fun createConfigurationsAndResolve(
200             dependencies: List<Dependency>
201         ): ArtifactsResolutionResult {
202             val configurations = dependencies.flatMap { dep ->
203                 buildList {
204                     addAll(createApiConfigurations(dep))
205                     addAll(createRuntimeConfigurations(dep))
206                     addAll(createGradlePluginConfigurations(dep))
207                     addAll(createKmpConfigurations(dep))
208                 }
209             }
210             val resolutionList = configurations.map { configuration ->
211                 resolveArtifacts(configuration)
212             }
213             val artifacts = resolutionList.flatMap { resolution ->
214                 resolution.artifacts
215             }
216             val dependenciesPassedVerification = resolutionList.all { resolution ->
217                 resolution.dependenciesPassedVerification
218             }
219             return ArtifactsResolutionResult(artifacts, dependenciesPassedVerification)
220         }
221 
222         /**
223          * Resolves the given configuration.
224          * @param configuration The configuration to resolve
225          */
226         private fun resolveArtifacts(
227             configuration: Configuration,
228         ): ArtifactsResolutionResult {
229             val artifacts = configuration.incoming.artifactView {
230                 // We need to be lenient because we are requesting files that might not exist.
231                 // For example source.jar or .asc.
232                 it.lenient(true)
233             }.artifacts.artifacts.toList()
234             return ArtifactsResolutionResult(artifacts.toList(), dependenciesPassedVerification = false)
235         }
236 
237         /**
238          * Creates proxy servers for remote repositories, adds them to the project and invokes
239          * the block. Once the block is complete, all proxy servers will be closed.
240          */
241         private fun <T> withProxyServer(
242             downloadObserver: DownloadObserver? = null,
243             block: () -> T
244         ): T {
245             val repoUrls = additionalPriorityRepositories + listOf(
246                 RepositoryHandler.GOOGLE_URL,
247                 RepositoryHandler.MAVEN_CENTRAL_URL,
248                 gradlePluginPortalRepo
249             )
250             return MavenRepositoryProxy.startAll(
251                 repositoryUrls = repoUrls,
252                 downloadObserver = downloadObserver
253             ) { repoUris ->
254                 project.repositories.clear()
255                 // add local repositories first, they are not tracked
256                 localRepositories.map { localRepo ->
257                     project.repositories.maven {
258                         it.url = URI(localRepo)
259                     }
260                 }
261                 repoUris.map { mavenUri ->
262                     project.repositories.maven {
263                         it.url = mavenUri
264                         it.isAllowInsecureProtocol = true
265                     }
266                 }
267                 block()
268             }
269         }
270 
271         private fun createConfiguration(
272             vararg dependencies: Dependency,
273             configure: Configuration.() -> Unit
274         ): Configuration {
275             val configuration = project.configurations.detachedConfiguration(*dependencies)
276             configuration.configure()
277             return configuration
278         }
279 
280         /**
281          * Creates a configuration that has the same attributes as java runtime configuration
282          */
283         private fun createRuntimeConfigurations(
284             vararg dependencies: Dependency
285         ): List<Configuration> {
286             return listOf(
287                 LibraryElements.JAR to TargetJvmEnvironment.STANDARD_JVM,
288                 LibraryElements.JAR to TargetJvmEnvironment.ANDROID,
289                 "aar" to TargetJvmEnvironment.ANDROID,
290             ).map { (libraryElement, jvmEnvironment) ->
291                 createConfiguration(*dependencies) {
292                     attributes.apply {
293                         attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, libraryElement)
294                         attribute(Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME)
295                         attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
296                         attribute(
297                             TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
298                             jvmEnvironment
299                         )
300                     }
301                 }
302             }
303         }
304 
305         @Suppress("UnstableApiUsage")
306         private fun createGradlePluginConfigurations(
307             vararg dependencies: Dependency
308         ): List<Configuration> {
309             return listOf(
310                 GradleVersion.current().baseVersion,
311                 GradleVersion.current()
312             ).map { version ->
313                 // taken from DefaultScriptHandler in gradle
314                 createConfiguration(*dependencies) {
315                     attributes.apply {
316                         attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
317                         attribute(
318                             TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
319                             TargetJvmEnvironment.STANDARD_JVM
320                         )
321 
322                         attribute(Usage.USAGE_ATTRIBUTE, Usage.JAVA_API)
323 
324                         attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, LibraryElements.JAR)
325 
326                         attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
327                         attribute(
328                             GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
329                             version.version
330                         )
331                     }
332                 }
333             }
334         }
335 
336         /**
337          * Creates a configuration that has the same attributes as java api configuration
338          */
339         private fun createApiConfigurations(
340             vararg dependencies: Dependency
341         ): List<Configuration> {
342             return listOf(
343                 LibraryElements.JAR,
344                 "aar"
345             ).map { libraryElement ->
346                 createConfiguration(*dependencies) {
347                     attributes.apply {
348                         attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, libraryElement)
349                         attribute(Usage.USAGE_ATTRIBUTE, Usage.JAVA_API)
350                         attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
351                     }
352                 }
353             }
354         }
355 
356         /**
357          * Creates configuration that resembles the ones created by KMP.
358          * Note that, the configurations built by KMP depends on flags etc so to account for all of
359          * them, we create all variations with different attribute values.
360          */
361         private fun createKmpConfigurations(
362             vararg dependencies: Dependency,
363         ): List<Configuration> {
364             val konanTargetConfigurations = SUPPORTED_KONAN_TARGETS.flatMap { konanTarget ->
365                 KOTlIN_USAGES.map { kotlinUsage ->
366                     createKonanTargetConfiguration(
367                         dependencies = dependencies,
368                         konanTarget = konanTarget,
369                         kotlinUsage = kotlinUsage
370                     )
371                 }
372             }
373             // jvm and android configurations
374             val jvmAndAndroid = KOTlIN_USAGES.flatMap { kotlinUsage ->
375                 listOf(
376                     "jvm",
377                     TargetJvmEnvironment.ANDROID
378                 ).map { targetJvm ->
379                     createConfiguration(*dependencies) {
380                         attributes.apply {
381                             attribute(Usage.USAGE_ATTRIBUTE, kotlinUsage)
382                             attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
383                             attribute(
384                                 TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
385                                 targetJvm
386                             )
387                         }
388                     }
389                 }
390             }
391 
392             val wasmJs = KOTlIN_USAGES.map { kotlinUsage ->
393                 createConfiguration(*dependencies) {
394                     attributes.apply {
395                         attribute(KotlinPlatformType.attribute, KotlinPlatformType.wasm)
396                         attribute(Usage.USAGE_ATTRIBUTE, kotlinUsage)
397                         attribute(
398                             KotlinWasmTargetAttribute.wasmTargetAttribute,
399                             KotlinWasmTargetAttribute.js
400                         )
401                         attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
402                         attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, "non-jvm")
403                     }
404                 }
405             }
406 
407             val js =
408                 KOTlIN_USAGES.map { kotlinUsage ->
409                     createConfiguration(*dependencies) {
410                         attributes.apply {
411                             attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
412                             attribute(
413                                 TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
414                                 "non-jvm"
415                             )
416                             attribute(Usage.USAGE_ATTRIBUTE, kotlinUsage)
417                             attribute(
418                                 KotlinJsCompilerAttribute.jsCompilerAttribute,
419                                 KotlinJsCompilerAttribute.ir
420                             )
421                             attribute(KotlinPlatformType.attribute, KotlinPlatformType.js)
422                         }
423                     }
424                 }
425 
426             val commonArtifacts = KOTlIN_USAGES.map { kotlinUsage ->
427                 createConfiguration(*dependencies) {
428                     attributes.apply {
429                         attribute(Usage.USAGE_ATTRIBUTE, kotlinUsage)
430                         attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
431                         attribute(KotlinPlatformType.attribute, KotlinPlatformType.common)
432                     }
433                 }
434             }
435             return jvmAndAndroid + wasmJs + js + konanTargetConfigurations + commonArtifacts
436         }
437 
438         private fun createKonanTargetConfiguration(
439             vararg dependencies: Dependency,
440             konanTarget: KonanTarget,
441             kotlinUsage: String
442         ): Configuration {
443             return createConfiguration(*dependencies) {
444                 attributes.apply {
445                     attribute(KotlinPlatformType.attribute, KotlinPlatformType.native)
446                     attribute(Usage.USAGE_ATTRIBUTE, kotlinUsage)
447                     attribute(KotlinNativeTarget.konanTargetAttribute, konanTarget.name)
448                     attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
449                     attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, "non-jvm")
450                 }
451             }
452         }
453 
454         private fun <T : Named> AttributeContainer.attribute(
455             key: Attribute<T>,
456             value: String
457         ) = attribute(
458             key, project.objects.named(
459                 key.type,
460                 value
461             )
462         )
463 
464         companion object {
465             /**
466              * Kotlin usage attributes that we want to pull.
467              */
468             private val KOTlIN_USAGES = listOf(
469                 KotlinUsages.KOTLIN_API,
470                 KotlinUsages.KOTLIN_METADATA,
471                 KotlinUsages.KOTLIN_CINTEROP,
472                 KotlinUsages.KOTLIN_RUNTIME,
473                 KotlinUsages.KOTLIN_SOURCES
474             )
475         }
476     }
477 }
478