Skip to content

Commit 826652a

Browse files
oheger-boschsschuberth
authored andcommitted
feat(Maven): Obtain package metadata for Tycho OSGi artifacts
The normal mechanism to resolve package metadata does not work for Tycho OSGi artifacts. Such artifacts do not have a POM, but may contain metadata in their manifest. Therefore, try to locate these artifacts in the local Maven repository (where they should have been downloaded during the Tycho build) and read their manifests. Signed-off-by: Oliver Heger <[email protected]>
1 parent 5dae200 commit 826652a

File tree

3 files changed

+394
-6
lines changed

3 files changed

+394
-6
lines changed

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

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,51 @@ package org.ossreviewtoolkit.plugins.packagemanagers.maven
2222
import java.io.File
2323
import java.nio.file.Path
2424
import java.util.concurrent.atomic.AtomicReference
25+
import java.util.jar.JarInputStream
26+
import java.util.jar.Manifest
2527

2628
import kotlin.io.path.invariantSeparatorsPathString
2729

2830
import org.apache.logging.log4j.kotlin.logger
31+
import org.apache.logging.log4j.kotlin.loggerOf
2932
import org.apache.maven.AbstractMavenLifecycleParticipant
3033
import org.apache.maven.cli.MavenCli
3134
import org.apache.maven.execution.MavenSession
35+
import org.apache.maven.model.Scm
3236
import org.apache.maven.project.MavenProject
37+
import org.apache.maven.repository.RepositorySystem
3338

3439
import org.codehaus.plexus.PlexusContainer
3540
import org.codehaus.plexus.classworlds.ClassWorld
3641

42+
import org.eclipse.aether.artifact.Artifact
3743
import org.eclipse.aether.graph.DependencyNode
3844

3945
import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory
4046
import org.ossreviewtoolkit.analyzer.PackageManager
4147
import org.ossreviewtoolkit.analyzer.PackageManagerResult
4248
import org.ossreviewtoolkit.analyzer.ProjectResults
4349
import org.ossreviewtoolkit.model.DependencyGraph
50+
import org.ossreviewtoolkit.model.Identifier
4451
import org.ossreviewtoolkit.model.Issue
52+
import org.ossreviewtoolkit.model.Package
4553
import org.ossreviewtoolkit.model.ProjectAnalyzerResult
54+
import org.ossreviewtoolkit.model.RemoteArtifact
4655
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration
4756
import org.ossreviewtoolkit.model.config.RepositoryConfiguration
4857
import org.ossreviewtoolkit.model.createAndLogIssue
4958
import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder
5059
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.LocalProjectWorkspaceReader
5160
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.MavenDependencyHandler
5261
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.MavenSupport
62+
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.PackageResolverFun
5363
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.createIssuesForAutoGeneratedPoms
5464
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.identifier
5565
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.internalId
5666
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.isTychoProject
5767
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.parseDependencyTree
68+
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.parseScm
69+
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.processDeclaredLicenses
5870
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.toOrtProject
5971
import org.ossreviewtoolkit.utils.ort.createOrtTempFile
6072

@@ -151,8 +163,8 @@ class Tycho(
151163
mavenSupport: MavenSupport,
152164
mavenProjects: Map<String, MavenProject>
153165
): DependencyGraphBuilder<DependencyNode> {
154-
val dependencyHandler =
155-
MavenDependencyHandler(managerName, projectType, mavenProjects, mavenSupport.defaultPackageResolverFun())
166+
val resolverFun = tychoPackageResolverFun(mavenSupport.defaultPackageResolverFun())
167+
val dependencyHandler = MavenDependencyHandler(managerName, projectType, mavenProjects, resolverFun)
156168
return DependencyGraphBuilder(dependencyHandler)
157169
}
158170

@@ -329,3 +341,104 @@ internal class TychoProjectsCollector : AbstractMavenLifecycleParticipant() {
329341
logger.info { "Found ${builtProjects.size} projects during build." }
330342
}
331343
}
344+
345+
private val tychoLogger = loggerOf(Tycho::class.java)
346+
347+
/**
348+
* Return a [PackageResolverFun] that can handle special OSGi artifacts detected during a Tycho analysis.
349+
* OSGi bundles retrieved from a P2 repository cannot be handled by the default resolver function, which is not aware
350+
* of the special repository layout. Also, they do not have a POM with metadata, but may contain some metadata in
351+
* their OSGi manifest.
352+
* This function first delegates to the given [delegate] function to handle normal packages in the usual way. If this
353+
* fails, it checks whether the dependency can be found in the p2 cache of the local Maven repository pointed to by
354+
* [localRepositoryRoot]. (Since the Tycho project has been built, the cache should be populated.) If this is the case,
355+
* it tries to obtain metadata from the OSGi manifest of the bundle. This approach is partly inspired by
356+
* https://github.com/OpenNTF/p2-layout-provider.
357+
*/
358+
internal fun tychoPackageResolverFun(
359+
delegate: PackageResolverFun,
360+
localRepositoryRoot: File = RepositorySystem.defaultUserLocalRepository
361+
): PackageResolverFun =
362+
{ dependency ->
363+
runCatching {
364+
delegate(dependency)
365+
}.recoverCatching { exception ->
366+
createPackageFromLocalArtifact(dependency.artifact, localRepositoryRoot)
367+
?: throw exception
368+
}.getOrThrow()
369+
}
370+
371+
/**
372+
* Create a [Package] for the given [artifact] obtaining metadata from the given [manifest]. This function is used for
373+
* OSGi artifacts for which no POM is available.
374+
*/
375+
internal fun createPackageFromManifest(artifact: Artifact, manifest: Manifest): Package =
376+
with(manifest.mainAttributes) {
377+
val declaredLicenses = setOfNotNull(getValue("Bundle-License"))
378+
val declaredLicensesProcessed = processDeclaredLicenses(declaredLicenses)
379+
val authors = setOfNotNull(getValue("Bundle-Vendor"))
380+
381+
val vcs = parseScm(getValue("Eclipse-SourceReferences").toScm(), artifact.identifier())
382+
383+
Package(
384+
id = Identifier(
385+
type = "Maven",
386+
namespace = artifact.groupId,
387+
name = artifact.artifactId,
388+
version = artifact.version
389+
),
390+
authors = authors,
391+
declaredLicenses = declaredLicenses,
392+
declaredLicensesProcessed = declaredLicensesProcessed,
393+
description = getValue("Bundle-Description").orEmpty(),
394+
homepageUrl = "",
395+
binaryArtifact = RemoteArtifact.EMPTY,
396+
sourceArtifact = RemoteArtifact.EMPTY,
397+
vcs = vcs,
398+
vcsProcessed = vcs
399+
)
400+
}
401+
402+
/**
403+
* Try to find an OSGi artifact in the local Maven repository at [localRepositoryRoot] that matches the given
404+
* [artifact]. If found, create a [Package] from the artifact's OSGi manifest.
405+
*/
406+
private fun createPackageFromLocalArtifact(artifact: Artifact, localRepositoryRoot: File): Package? {
407+
val artifactFile = localRepositoryRoot.resolve("p2/osgi/bundle")
408+
.resolve(artifact.artifactId)
409+
.resolve(artifact.version)
410+
.resolve("${artifact.artifactId}-${artifact.version}.jar")
411+
412+
tychoLogger.debug {
413+
"Looking up package metadata for '${artifact.identifier()}' in local repository '$artifactFile'."
414+
}
415+
416+
if (!artifactFile.isFile) return null
417+
418+
tychoLogger.info {
419+
"Reading metadata for '${artifact.identifier()}' from local repository '$artifactFile'."
420+
}
421+
422+
return JarInputStream(artifactFile.inputStream()).use { jar ->
423+
createPackageFromManifest(artifact, jar.manifest ?: Manifest())
424+
}
425+
}
426+
427+
/**
428+
* Parse this string with a source references manifest header to a [Scm] structure.
429+
* See https://wiki.eclipse.org/PDE/UI/SourceReferences.
430+
*/
431+
private fun String?.toScm(): Scm? =
432+
this?.split(',')?.firstOrNull()
433+
?.let { scmInfo ->
434+
val fields = scmInfo.split(';')
435+
val properties = fields.drop(1).filter { "=" in it }.associate { field ->
436+
val (key, value) = field.split('=')
437+
key to value.removeSurrounding("\"")
438+
}
439+
440+
Scm().apply {
441+
connection = fields[0]
442+
tag = properties["tag"]
443+
}
444+
}

plugins/package-managers/maven/src/main/kotlin/utils/MavenParsers.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,10 @@ internal fun parseLicenses(mavenProject: MavenProject): Set<String> =
157157
listOfNotNull(license.name, license.url, license.comments).firstOrNull { it.isNotBlank() }
158158
}
159159

160-
internal fun parseVcsInfo(project: MavenProject): VcsInfo {
161-
val scm = getOriginalScm(project)
160+
internal fun parseVcsInfo(project: MavenProject): VcsInfo =
161+
parseScm(getOriginalScm(project), project.artifact?.toString().orEmpty())
162+
163+
internal fun parseScm(scm: Scm?, artifactId: String): VcsInfo {
162164
val connection = scm?.connection
163165
if (connection.isNullOrEmpty()) return VcsInfo.EMPTY
164166

@@ -183,14 +185,14 @@ internal fun parseVcsInfo(project: MavenProject): VcsInfo {
183185
// It is a common mistake to omit the "scm:[provider]:" prefix. Add fall-backs for nevertheless
184186
// clear cases.
185187
logger.info {
186-
"Maven SCM connection '$connection' of project ${project.artifact} lacks the required " +
188+
"Maven SCM connection '$connection' of project $artifactId lacks the required " +
187189
"'scm' prefix."
188190
}
189191

190192
VcsInfo(type = VcsType.GIT, url = connection, revision = tag)
191193
} else {
192194
logger.info {
193-
"Ignoring Maven SCM connection '$connection' of project ${project.artifact} due to an " +
195+
"Ignoring Maven SCM connection '$connection' of project $artifactId due to an " +
194196
"unexpected format."
195197
}
196198

0 commit comments

Comments
 (0)