Skip to content

Commit 770ee75

Browse files
oheger-boschsschuberth
authored andcommitted
feat(Tycho): Improve handling of wrapped artifacts
If a wrapped artifact cannot be matched against the target platform, still try to resolve it via the standard Maven resolution mechanism. This seems to be necessary for fat or uber jars for which Tycho sometimes operate on submodules. Signed-off-by: Oliver Heger <oliver.heger@bosch.com>
1 parent 4af9208 commit 770ee75

File tree

4 files changed

+185
-23
lines changed

4 files changed

+185
-23
lines changed

plugins/package-managers/maven/src/main/kotlin/tycho/TargetHandler.kt

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,19 +115,72 @@ internal class TargetHandler(
115115
* Maven dependencies. There are also options to change the default resolution mechanism for such dependencies.
116116
* If this is done, Tycho sometimes creates alternative identifiers for the dependencies. This function attempts
117117
* to find the original Maven coordinates for affected artifacts. They are needed to retrieve the correct metadata.
118-
* Result is *null* if no matching Maven dependency is found.
118+
* There are cases where no unique mapping can be found, especially if Tycho automatically wraps a Maven artifact to
119+
* an OSGi bundle. In such cases, there may be a number of potential candidates for the original Maven artifact
120+
* which are returned as a [List]. The caller should then try all of these candidates to find the correct one. If
121+
* no mapping can be found, result is an empty [List].
119122
*/
120-
fun mapToMavenDependency(tychoArtifact: Artifact): Artifact? {
123+
fun mapToMavenDependency(tychoArtifact: Artifact): List<Artifact> {
121124
// Strip the "wrapped." prefix that might have been added by Tycho's automatic bundle wrapping mechanism
122125
// (missingManifest="generate") to find the original Maven artifact.
123126
val unwrappedArtifactId = tychoArtifact.artifactId.removePrefix("wrapped.")
124127

125-
return mavenDependencies[unwrappedArtifactId]?.also { dep ->
128+
return mavenDependencies[unwrappedArtifactId]?.let { dep ->
126129
logger.info {
127130
"Mapping Tycho artifact '${tychoArtifact.groupId}:${tychoArtifact.artifactId}' to Maven " +
128131
"dependency '${dep.groupId}:${dep.artifactId}'."
129132
}
133+
134+
listOf(dep)
135+
} ?: if (unwrappedArtifactId != tychoArtifact.artifactId) {
136+
logger.info {
137+
"Handling a wrapped Tycho artifact '${tychoArtifact.artifactId}' by enumerating all candidates."
138+
}
139+
140+
createCandidatesForWrappedArtifact(tychoArtifact, unwrappedArtifactId)
141+
} else {
142+
emptyList()
143+
}
144+
}
145+
146+
/**
147+
* Generate potential candidates for the original Maven artifact for the given [tychoArtifact] which has been
148+
* wrapped by Tycho's automatic bundle wrapping mechanism (missingManifest="generate"). For such wrapped artifacts,
149+
* Tycho generates an artifact ID by concatenating the group ID and artifact ID of the original Maven dependency
150+
* using a dot as separator and adding a "wrapped." prefix. This transformation cannot be reversed in a unique way
151+
* if the original Maven artifactId contains dots. Therefore, this function generates a list of all combinations
152+
* of group and artifact IDs resulting in the given [unwrappedArtifactId].
153+
*/
154+
private fun createCandidatesForWrappedArtifact(
155+
tychoArtifact: Artifact,
156+
unwrappedArtifactId: String
157+
): List<Artifact> {
158+
val candidates = mutableListOf<Artifact>()
159+
160+
tailrec fun generateCandidates(groupId: String, artifactId: String) {
161+
val newGroupId = groupId.substringBeforeLast('.')
162+
if (newGroupId != groupId) {
163+
val newArtifactComponent = groupId.substringAfterLast('.')
164+
val newArtifactId = if (artifactId.isEmpty()) {
165+
newArtifactComponent
166+
} else {
167+
"$newArtifactComponent.$artifactId"
168+
}
169+
170+
candidates += DefaultArtifact(
171+
newGroupId,
172+
newArtifactId,
173+
tychoArtifact.classifier,
174+
"jar", // Always set to "jar", the Tycho artifact may have "pom" instead.
175+
tychoArtifact.version
176+
)
177+
178+
generateCandidates(newGroupId, newArtifactId)
179+
}
130180
}
181+
182+
generateCandidates(unwrappedArtifactId, "")
183+
return candidates
131184
}
132185
}
133186

plugins/package-managers/maven/src/main/kotlin/tycho/Tycho.kt

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import java.io.File
2323
import java.util.concurrent.atomic.AtomicReference
2424
import java.util.jar.Manifest
2525

26+
import org.apache.logging.log4j.kotlin.KotlinLogger
2627
import org.apache.logging.log4j.kotlin.logger
2728
import org.apache.maven.AbstractMavenLifecycleParticipant
2829
import org.apache.maven.cli.MavenCli
@@ -199,7 +200,8 @@ class Tycho(override val descriptor: PluginDescriptor = TychoFactory.descriptor)
199200
mavenSupport.defaultPackageResolverFun(),
200201
repositoryHelper,
201202
resolver,
202-
targetHandler
203+
targetHandler,
204+
logger
203205
)
204206
val dependencyHandler = MavenDependencyHandler(descriptor.displayName, projectType, mavenProjects, resolverFun)
205207
return DependencyGraphBuilder(dependencyHandler)
@@ -372,21 +374,52 @@ internal fun tychoPackageResolverFun(
372374
delegate: PackageResolverFun,
373375
repositoryHelper: LocalRepositoryHelper,
374376
resolver: P2ArtifactResolver,
375-
targetHandler: TargetHandler
377+
targetHandler: TargetHandler,
378+
logger: KotlinLogger
376379
): PackageResolverFun =
377380
{ dependency ->
378381
runCatching {
379382
delegate(dependency)
380383
}.recoverCatching { exception ->
381-
targetHandler.mapToMavenDependency(dependency.artifact)?.let { artifact ->
384+
targetHandler.mapToMavenDependency(dependency.artifact)
385+
.takeIf { it.isNotEmpty() }?.let { artifacts ->
386+
resolveMavenArtifacts(dependency, delegate, artifacts, logger)
387+
} ?: createPackageFromLocalArtifact(dependency.artifact, repositoryHelper, resolver)
388+
?: throw exception
389+
}.getOrThrow()
390+
}
391+
392+
/**
393+
* Resolve a [dependency] via the standard Maven resolution process accessible through the given [resolver] function.
394+
* Try the given [candidates] as potential Maven artifacts corresponding to the given dependency and return the first
395+
* successfully resolved [Package]. Throw an exception if none of the candidates could be resolved.
396+
*/
397+
private fun resolveMavenArtifacts(
398+
dependency: DependencyNode,
399+
resolver: PackageResolverFun,
400+
candidates: List<Artifact>,
401+
logger: KotlinLogger
402+
): Package =
403+
checkNotNull(
404+
candidates.firstNotNullOfOrNull { artifact ->
405+
runCatching {
406+
logger.debug {
407+
"Trying to resolve Maven artifact candidate " +
408+
"'${artifact.groupId}:${artifact.artifactId}:${artifact.version}'."
409+
}
410+
382411
val mappedDependency = DefaultDependencyNode(artifact).apply {
383412
repositories = dependency.repositories
384413
}
385414

386-
delegate(mappedDependency)
387-
} ?: createPackageFromLocalArtifact(dependency.artifact, repositoryHelper, resolver)
388-
?: throw exception
389-
}.getOrThrow()
415+
resolver(mappedDependency)
416+
}.onFailure { exception ->
417+
logger.debug(exception) { "Failed to resolve Maven artifact candidate." }
418+
}.getOrNull()
419+
}
420+
) {
421+
"Failed to resolve ${candidates.size} candidates for dependency " +
422+
"'${dependency.artifact.identifier()}'."
390423
}
391424

392425
/**

plugins/package-managers/maven/src/test/kotlin/tycho/TargetHandlerTest.kt

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ import io.kotest.core.spec.style.WordSpec
2424
import io.kotest.engine.spec.tempdir
2525
import io.kotest.matchers.collections.beEmpty
2626
import io.kotest.matchers.collections.containExactlyInAnyOrder
27-
import io.kotest.matchers.nulls.beNull
28-
import io.kotest.matchers.nulls.shouldNotBeNull
27+
import io.kotest.matchers.collections.shouldBeSingleton
2928
import io.kotest.matchers.should
3029
import io.kotest.matchers.shouldBe
3130

@@ -55,23 +54,40 @@ class TargetHandlerTest : WordSpec({
5554
}
5655

5756
"mapToMavenDependency()" should {
58-
"return null if an artifact cannot be mapped to a Maven dependency" {
57+
"return an empty list if an artifact cannot be mapped to a Maven dependency" {
5958
val tychoArtifact = DefaultArtifact("groupId", "artifactId", "ext", "version")
6059
val targetHandler = TargetHandler.create(tempdir())
6160

62-
targetHandler.mapToMavenDependency(tychoArtifact) should beNull()
61+
targetHandler.mapToMavenDependency(tychoArtifact) should beEmpty()
6362
}
6463

6564
"return the correct Maven dependency if an artifact can be mapped" {
6665
val tychoArtifact = DefaultArtifact("p2.eclipse.plugin", "ch.qos.logback.logback-classic", "jar", "1.5.6")
6766
val targetHandler = createTargetHandlerWithTargetFiles()
6867

69-
targetHandler.mapToMavenDependency(tychoArtifact).shouldNotBeNull {
70-
groupId shouldBe "ch.qos.logback"
71-
artifactId shouldBe "logback-classic"
72-
version shouldBe "1.5.6"
68+
targetHandler.mapToMavenDependency(tychoArtifact).shouldBeSingleton {
69+
it.groupId shouldBe "ch.qos.logback"
70+
it.artifactId shouldBe "logback-classic"
71+
it.version shouldBe "1.5.6"
7372
}
7473
}
74+
75+
"return the potential candidates for a wrapped artifact" {
76+
val tychoArtifact = DefaultArtifact(
77+
"p2.eclipse.plugin",
78+
"wrapped.org.apache.commons.commons-lang3",
79+
"pom",
80+
"3.12.0"
81+
)
82+
val expectedCandidates = listOf(
83+
DefaultArtifact("org.apache.commons", "commons-lang3", "jar", "3.12.0"),
84+
DefaultArtifact("org.apache", "commons.commons-lang3", "jar", "3.12.0"),
85+
DefaultArtifact("org", "apache.commons.commons-lang3", "jar", "3.12.0")
86+
)
87+
val targetHandler = createTargetHandlerWithTargetFiles()
88+
89+
targetHandler.mapToMavenDependency(tychoArtifact) should containExactlyInAnyOrder(expectedCandidates)
90+
}
7591
}
7692

7793
"featureIds" should {

plugins/package-managers/maven/src/test/kotlin/tycho/TychoTest.kt

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ class TychoTest : WordSpec({
314314
pkg
315315
}
316316

317-
val resolver = tychoPackageResolverFun(delegate, mockk(), mockk(), mockk())
317+
val resolver = tychoPackageResolverFun(delegate, mockk(), mockk(), mockk(), mockk(relaxed = true))
318318

319319
resolver(dependency) shouldBe pkg
320320
}
@@ -404,13 +404,73 @@ class TychoTest : WordSpec({
404404
}
405405

406406
val targetHandler = mockk<TargetHandler> {
407-
every { mapToMavenDependency(originalArtifact) } returns mappedArtifact
407+
every { mapToMavenDependency(originalArtifact) } returns listOf(mappedArtifact)
408408
}
409409

410-
val resolver = tychoPackageResolverFun(delegate, mockk(), mockk(), targetHandler)
410+
val resolver = tychoPackageResolverFun(delegate, mockk(), mockk(), targetHandler, mockk(relaxed = true))
411411

412412
resolver(dependency) shouldBe pkg
413413
}
414+
415+
"test all candidates for a wrapped artifact" {
416+
val originalArtifact = mockk<Artifact>()
417+
val mappedArtifact1 = mockk<Artifact>()
418+
val mappedArtifact2 = mockk<Artifact>()
419+
val mappedArtifact3 = mockk<Artifact>()
420+
val mappedArtifact4 = mockk<Artifact>()
421+
val dependency = DefaultDependencyNode(originalArtifact)
422+
423+
var delegateCount = 0
424+
val pkg = mockk<Package>()
425+
val delegate: PackageResolverFun = { node ->
426+
delegateCount++
427+
if (node.artifact != mappedArtifact3) {
428+
throw IOException("Test exception: Unresolvable dependency.")
429+
}
430+
431+
pkg
432+
}
433+
434+
val targetHandler = mockk<TargetHandler> {
435+
every { mapToMavenDependency(originalArtifact) } returns listOf(
436+
mappedArtifact1,
437+
mappedArtifact2,
438+
mappedArtifact3,
439+
mappedArtifact4
440+
)
441+
}
442+
443+
val resolver = tychoPackageResolverFun(delegate, mockk(), mockk(), targetHandler, mockk(relaxed = true))
444+
445+
resolver(dependency) shouldBe pkg
446+
delegateCount shouldBe 4
447+
}
448+
449+
"throw if none of the candidates for a wrapped artifact can be resolved" {
450+
val originalArtifact = mockk<Artifact>(relaxed = true)
451+
val mappedArtifact1 = mockk<Artifact>()
452+
val mappedArtifact2 = mockk<Artifact>()
453+
val mappedArtifact3 = mockk<Artifact>()
454+
val dependency = DefaultDependencyNode(originalArtifact)
455+
456+
val delegate: PackageResolverFun = {
457+
throw IOException("Test exception: Unresolvable dependency.")
458+
}
459+
460+
val targetHandler = mockk<TargetHandler> {
461+
every { mapToMavenDependency(originalArtifact) } returns listOf(
462+
mappedArtifact1,
463+
mappedArtifact2,
464+
mappedArtifact3
465+
)
466+
}
467+
468+
val resolver = tychoPackageResolverFun(delegate, mockk(), mockk(), targetHandler, mockk(relaxed = true))
469+
470+
shouldThrow<IllegalStateException> {
471+
resolver(dependency)
472+
}
473+
}
414474
}
415475

416476
"createPackageFromManifest()" should {
@@ -656,11 +716,11 @@ private fun createResolverFunWithRepositoryHelper(block: LocalRepositoryHelper.(
656716
val helper = mockk<LocalRepositoryHelper>(block = block)
657717
val resolver = createResolverMock()
658718
val targetHandler = mockk<TargetHandler> {
659-
every { mapToMavenDependency(any()) } returns null
719+
every { mapToMavenDependency(any()) } returns emptyList()
660720
}
661721

662722
val delegateResolverFun: PackageResolverFun = { throw resolveException }
663-
return tychoPackageResolverFun(delegateResolverFun, helper, resolver, targetHandler)
723+
return tychoPackageResolverFun(delegateResolverFun, helper, resolver, targetHandler, mockk(relaxed = true))
664724
}
665725

666726
/**

0 commit comments

Comments
 (0)