diff --git a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt index cd38491d10f..b8cae982544 100644 --- a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt +++ b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt @@ -17,6 +17,7 @@ import kotlin.script.experimental.dependencies.ExternalDependenciesResolver import kotlin.script.experimental.dependencies.RepositoryCoordinates import kotlin.script.experimental.dependencies.impl.* import kotlin.script.experimental.dependencies.maven.impl.AetherResolveSession +import kotlin.script.experimental.dependencies.maven.impl.ResolutionKind import kotlin.script.experimental.dependencies.maven.impl.mavenCentral @Deprecated( @@ -58,15 +59,20 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { return try { val dependencyScopes = options.dependencyScopes ?: listOf(JavaScopes.COMPILE, JavaScopes.RUNTIME) - val transitive = options.transitive ?: true + val kind = when (options.partialResolution) { + true -> ResolutionKind.TRANSITIVE_PARTIAL + false, null -> when(options.transitive) { + true, null -> ResolutionKind.TRANSITIVE + false -> ResolutionKind.NON_TRANSITIVE + } + } val classifier = options.classifier val extension = options.extension - val deps = AetherResolveSession( + AetherResolveSession( null, remoteRepositories() ).resolve( - artifactId, dependencyScopes.joinToString(","), transitive, null, classifier, extension + artifactId, dependencyScopes.joinToString(","), kind, null, classifier, extension ) - ResultWithDiagnostics.Success(deps.map { it.file }) } catch (e: RepositoryException) { makeResolveFailureResult(e, sourceCodeLocation) } diff --git a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/impl/aether.kt b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/impl/aether.kt index 6069bf7204a..c43224e8987 100644 --- a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/impl/aether.kt +++ b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/impl/aether.kt @@ -39,10 +39,23 @@ import org.eclipse.aether.util.repository.AuthenticationBuilder import org.eclipse.aether.util.repository.DefaultMirrorSelector import org.eclipse.aether.util.repository.DefaultProxySelector import java.io.File -import java.util.* +import kotlin.script.experimental.api.ResultWithDiagnostics +import kotlin.script.experimental.api.ScriptDiagnostic +import kotlin.script.experimental.api.asSuccess val mavenCentral: RemoteRepository = RemoteRepository.Builder("maven central", "default", "https://repo.maven.apache.org/maven2/").build() +internal enum class ResolutionKind { + NON_TRANSITIVE, + TRANSITIVE, + + // Partial resolution is successful in case if dependency tree was built, + // but may return non-complete list of dependencies - i.e. while requesting sources, some libraries may lack sources artifacts. + // Resolution errors will be attached as reports. + // Also, might be slightly slower than usual transitive resolution. + TRANSITIVE_PARTIAL +} + internal class AetherResolveSession( localRepoDirectory: File? = null, remoteRepos: List = listOf(mavenCentral) @@ -133,22 +146,63 @@ internal class AetherResolveSession( fun resolve( root: Artifact, scope: String, - transitive: Boolean, + kind: ResolutionKind, filter: DependencyFilter?, classifier: String? = null, extension: String? = null, - ): List { - return if (transitive) resolveDependencies(root, scope, filter, classifier, extension) - else resolveArtifact(root) + ): ResultWithDiagnostics> { + if (kind == ResolutionKind.NON_TRANSITIVE) return resolveArtifact(root).asSuccess() + + val requests = resolveTree(root, scope, filter, classifier, extension) + + @Suppress("KotlinConstantConditions") + return when (kind) { + ResolutionKind.TRANSITIVE -> resolveDependencies(requests) { + repositorySystem.resolveArtifacts( + repositorySystemSession, + requests + ).toFiles().asSuccess() + } + + ResolutionKind.TRANSITIVE_PARTIAL -> resolveDependencies(requests) { + val reports = mutableListOf() + val results = mutableListOf() + for (req in requests) { + try { + results.add( + repositorySystem.resolveArtifact( + repositorySystemSession, + req + ).artifact.file + ) + } catch (e: ArtifactResolutionException) { + reports.add( + ScriptDiagnostic( + ScriptDiagnostic.unspecifiedError, + e.message.orEmpty(), + ScriptDiagnostic.Severity.WARNING, + exception = e + ) + ) + } + } + + ResultWithDiagnostics.Success(results, reports) + } + + ResolutionKind.NON_TRANSITIVE -> { + error("This statement is not reachable") + } + } } - private fun resolveDependencies( + private fun resolveTree( root: Artifact, scope: String, filter: DependencyFilter?, classifier: String?, extension: String?, - ): List { + ): Collection { return fetch( request(Dependency(root, scope)), { req -> @@ -163,8 +217,7 @@ internal class AetherResolveSession( ) ) - val requests = requestsBuilder.requests - repositorySystem.resolveArtifacts(repositorySystemSession, requests) + requestsBuilder.requests }, { req, ex -> DependencyCollectionException( @@ -176,7 +229,25 @@ internal class AetherResolveSession( ) } - private fun resolveArtifact(artifact: Artifact): List { + private fun Collection.toFiles() = map { it.artifact.file } + + private fun resolveDependencies( + requests: Collection, + resolveAction: (Collection) -> ResultWithDiagnostics> + ): ResultWithDiagnostics> { + return fetch( + requests, + resolveAction + ) { _, ex -> + DependencyCollectionException( + null, + ex.message, + ex + ) + } + } + + private fun resolveArtifact(artifact: Artifact): List { val request = ArtifactRequest() request.artifact = artifact for (repo in remotes) { @@ -187,7 +258,7 @@ internal class AetherResolveSession( request, { req -> listOf(repositorySystem.resolveArtifact(repositorySystemSession, req)) }, { req, ex -> ArtifactResolutionException(listOf(ArtifactResult(req)), ex.message, IllegalArgumentException(ex)) } - ) + ).toFiles() } private fun request(root: Dependency): CollectRequest { @@ -199,25 +270,19 @@ internal class AetherResolveSession( return request } - private fun fetch( + private fun fetch( request: RequestT, - fetchBody: (RequestT) -> Collection, + fetchBody: (RequestT) -> ResultT, wrapException: (RequestT, Exception) -> Exception - ): List { - val deps: MutableList = LinkedList() - try { - var results: Collection + ): ResultT { + return try { synchronized(this) { - results = fetchBody(request) - } - for (res in results) { - deps.add(res.artifact) + fetchBody(request) } // @checkstyle IllegalCatch (1 line) } catch (ex: Exception) { throw wrapException(request, ex) } - return deps } private fun getMirrorSelector(): DefaultMirrorSelector { diff --git a/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt b/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt index 01c6546d973..8d44bfbddfc 100644 --- a/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt +++ b/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt @@ -183,6 +183,24 @@ class MavenResolverTest : ResolversTestBase() { ) } + @Ignore("ignored because spark is a very heavy dependency") + fun ignore_testPartialResolution() { + val resolver = MavenDependenciesResolver() + val options = buildOptions( + DependenciesResolverOptionsName.PARTIAL_RESOLUTION to "true", + DependenciesResolverOptionsName.CLASSIFIER to "sources", + DependenciesResolverOptionsName.EXTENSION to "jar", + ) + + val result = runBlocking { + resolver.resolve("org.jetbrains.kotlinx.spark:kotlin-spark-api_3.3.0_2.13:1.2.1", options) + } + + result as ResultWithDiagnostics.Success + assertTrue(result.reports.isNotEmpty()) + assertTrue(result.value.isNotEmpty()) + } + // Ignored - tests with custom repos often break the CI due to the caching issues // TODO: find a way to enable it back @Ignore diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/resolverNamedOptions.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/resolverNamedOptions.kt index 731e0812e61..60cfa180c2f 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/resolverNamedOptions.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/resolverNamedOptions.kt @@ -23,6 +23,7 @@ operator fun MutableMap.set(key: DependenciesResolverOptionsName */ enum class DependenciesResolverOptionsName(optionName: String? = null) { TRANSITIVE, + PARTIAL_RESOLUTION, SCOPE, USERNAME, PASSWORD, @@ -37,6 +38,13 @@ enum class DependenciesResolverOptionsName(optionName: String? = null) { val ExternalDependenciesResolver.Options.transitive get() = flag(DependenciesResolverOptionsName.TRANSITIVE) +/** + * Enables partial resolution of transitive dependencies. + * When this flag is enabled, resolver ignores [transitive] flag. + */ +val ExternalDependenciesResolver.Options.partialResolution + get() = flag(DependenciesResolverOptionsName.PARTIAL_RESOLUTION) + val ExternalDependenciesResolver.Options.dependencyScopes get() = value(DependenciesResolverOptionsName.SCOPE)?.split(",")