diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c7ab900f49..c7b66b4afe 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,25 @@ If you are reading this in the browser, then you can quickly jump to specific ve ## 5.0.0 (under development) +### Support for CycloneDX Maven Plugin + +The `tycho-sbom` plugin can be added as a dependency to the CycloneDX plugin, +in order to handle the PURL creation of p2 artifacts: + +``` + + org.cyclonedx + cyclonedx-maven-plugin + + + org.eclipse.tycho + tycho-sbom + ${tycho-version} + + + +``` + ### Support for parallel execution of product assembly / archiving The mojos `materialize-products` and `archive-products` now support a new `` parameter diff --git a/pom.xml b/pom.xml index bcd72968fb..f53b628f5e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ tycho-surefire diff --git a/tycho-core/src/main/java/org/eclipse/tycho/core/maven/TychoReactorReader.java b/tycho-core/src/main/java/org/eclipse/tycho/core/maven/TychoReactorReader.java index d53be3b151..3469cedce2 100644 --- a/tycho-core/src/main/java/org/eclipse/tycho/core/maven/TychoReactorReader.java +++ b/tycho-core/src/main/java/org/eclipse/tycho/core/maven/TychoReactorReader.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Christoph Läubrich and others. + * Copyright (c) 2023, 2024 Christoph Läubrich and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -67,7 +67,7 @@ public File findArtifact(Artifact artifact) { }).orElse(null); } - private Optional getTychoReactorProject(Artifact artifact) { + public Optional getTychoReactorProject(Artifact artifact) { if (isTychoReactorArtifact(artifact)) { String projectKey = ArtifactUtils.key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()); @@ -81,13 +81,20 @@ private Optional getTychoReactorProject(Artifact artifact) { return Optional.empty(); } - private boolean isTychoReactorArtifact(Artifact artifact) { + public boolean isTychoReactorArtifact(Artifact artifact) { if (artifact.getClassifier() == null || artifact.getClassifier().isBlank()) { - return PackagingType.TYCHO_PACKAGING_TYPES.contains(artifact.getProperty("type", "")); + return PackagingType.TYCHO_PACKAGING_TYPES.contains(getPackagingType(artifact)); } return false; } + public String getPackagingType(Artifact artifact) { + if (artifact != null) { + return artifact.getProperty("type", ""); + } + return null; + } + @Override public List findVersions(Artifact artifact) { return getTychoReactorProject(artifact).map(project -> List.of(artifact.getVersion())).orElse(List.of()); diff --git a/tycho-its/pom.xml b/tycho-its/pom.xml index 8ea65cd6e8..e5fd9535d3 100644 --- a/tycho-its/pom.xml +++ b/tycho-its/pom.xml @@ -1,6 +1,6 @@ + + org.cyclonedx + cyclonedx-core-java + 8.0.3 + diff --git a/tycho-its/projects/sbom/.mvn/extensions.xml b/tycho-its/projects/sbom/.mvn/extensions.xml new file mode 100644 index 0000000000..ff2889110d --- /dev/null +++ b/tycho-its/projects/sbom/.mvn/extensions.xml @@ -0,0 +1,8 @@ + + + + org.eclipse.tycho + tycho-build + ${tycho-version} + + diff --git a/tycho-its/projects/sbom/.mvn/maven.config b/tycho-its/projects/sbom/.mvn/maven.config new file mode 100644 index 0000000000..babb6c469f --- /dev/null +++ b/tycho-its/projects/sbom/.mvn/maven.config @@ -0,0 +1 @@ +-Dtycho-version=5.0.0-SNAPSHOT diff --git a/tycho-its/projects/sbom/example.feature/build.properties b/tycho-its/projects/sbom/example.feature/build.properties new file mode 100644 index 0000000000..64f93a9f0b --- /dev/null +++ b/tycho-its/projects/sbom/example.feature/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/tycho-its/projects/sbom/example.feature/feature.xml b/tycho-its/projects/sbom/example.feature/feature.xml new file mode 100644 index 0000000000..653f3f980f --- /dev/null +++ b/tycho-its/projects/sbom/example.feature/feature.xml @@ -0,0 +1,23 @@ + + + + + [Enter Feature Description here.] + + + + [Enter Copyright Description here.] + + + + [Enter License Description here.] + + + + + diff --git a/tycho-its/projects/sbom/example.plugin/META-INF/MANIFEST.MF b/tycho-its/projects/sbom/example.plugin/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..49a9b4e514 --- /dev/null +++ b/tycho-its/projects/sbom/example.plugin/META-INF/MANIFEST.MF @@ -0,0 +1,11 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Plugin with SBOM +Bundle-SymbolicName: example.plugin +Bundle-Version: 1.0.0.qualifier +Require-Bundle: org.eclipse.core.databinding;bundle-version="1.13.100", + org.eclipse.core.databinding.beans;bundle-version="1.10.100", + org.eclipse.core.databinding.observable;bundle-version="1.13.100", + org.eclipse.core.databinding.property;bundle-version="1.10.100" +Automatic-Module-Name: example.plugin +Bundle-RequiredExecutionEnvironment: JavaSE-17 diff --git a/tycho-its/projects/sbom/example.plugin/build.properties b/tycho-its/projects/sbom/example.plugin/build.properties new file mode 100644 index 0000000000..34d2e4d2da --- /dev/null +++ b/tycho-its/projects/sbom/example.plugin/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/tycho-its/projects/sbom/pom.xml b/tycho-its/projects/sbom/pom.xml new file mode 100644 index 0000000000..6247a9b633 --- /dev/null +++ b/tycho-its/projects/sbom/pom.xml @@ -0,0 +1,86 @@ + + 4.0.0 + tycho-demo + sbom + 1.0.0-SNAPSHOT + pom + + + 5.0.0-SNAPSHOT + https://www.example.p2.repo/ + + + + example.feature + example.plugin + product + repository + + + + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + ../target-definition.target + + + + linux + gtk + x86_64 + + + + + + org.eclipse.tycho + tycho-packaging-plugin + ${tycho-version} + + today + + + + org.cyclonedx + cyclonedx-maven-plugin + 2.7.9 + + true + + + + + makeBom + + + + + + org.eclipse.tycho + tycho-sbom + ${tycho-version} + + + + + + + + org.eclipse.tycho + tycho-p2-director-plugin + ${tycho-version} + + + + + diff --git a/tycho-its/projects/sbom/product/example.product b/tycho-its/projects/sbom/product/example.product new file mode 100644 index 0000000000..251a1e1fb4 --- /dev/null +++ b/tycho-its/projects/sbom/product/example.product @@ -0,0 +1,29 @@ + + + + + + + + + + -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts + + + + + + + + + + + + + + + + + + + diff --git a/tycho-its/projects/sbom/repository/category.xml b/tycho-its/projects/sbom/repository/category.xml new file mode 100644 index 0000000000..11a78fe996 --- /dev/null +++ b/tycho-its/projects/sbom/repository/category.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tycho-its/projects/sbom/target-definition.target b/tycho-its/projects/sbom/target-definition.target new file mode 100644 index 0000000000..938d5b5f7b --- /dev/null +++ b/tycho-its/projects/sbom/target-definition.target @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tycho-its/src/test/java/org/eclipse/tycho/test/reactor/BomCreationTest.java b/tycho-its/src/test/java/org/eclipse/tycho/test/reactor/BomCreationTest.java new file mode 100644 index 0000000000..7ebd6df798 --- /dev/null +++ b/tycho-its/src/test/java/org/eclipse/tycho/test/reactor/BomCreationTest.java @@ -0,0 +1,267 @@ +/******************************************************************************* + * Copyright (c) 2024 Patrick Ziegler and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.test.reactor; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.maven.it.Verifier; +import org.cyclonedx.exception.ParseException; +import org.cyclonedx.model.Bom; +import org.cyclonedx.model.Dependency; +import org.cyclonedx.parsers.Parser; +import org.cyclonedx.parsers.XmlParser; +import org.eclipse.tycho.test.AbstractTychoIntegrationTest; +import org.junit.Before; +import org.junit.Test; + +public class BomCreationTest extends AbstractTychoIntegrationTest { + private static Verifier verifier; + + @Before + public void setUp() throws Exception { + if (verifier == null) { + verifier = getVerifier("sbom", false); + verifier.executeGoal("verify"); + verifyErrorFreeLog(verifier); + } + } + + @Test + public void verifyBundle() throws Exception { + String bomPath = getBomPath("example.plugin"); + verifier.verifyFilePresent(bomPath); + + Bom bom = getBom(bomPath); + List dependencies = bom.getDependencies(); + verifyBundleDependencies(dependencies); + verifyCommonDependencies(dependencies); + assertEquals(dependencies.size(), 7); + } + + @Test + public void verifyFeature() throws Exception { + String bomPath = getBomPath("example.feature"); + verifier.verifyFilePresent(bomPath); + + Bom bom = getBom(bomPath); + List dependencies = bom.getDependencies(); + verifyFeatureDependencies(dependencies); + verifyBundleDependencies(dependencies); + verifyCommonDependencies(dependencies); + assertEquals(dependencies.size(), 8); + } + + @Test + public void verifyRepository() throws Exception { + String bomPath = getBomPath("repository"); + verifier.verifyFilePresent(bomPath); + + Bom bom = getBom(bomPath); + List dependencies = bom.getDependencies(); + verifyRepositoryDependencies(dependencies); + verifyBundleDependencies(dependencies); + verifyCommonDependencies(dependencies); + // dependency to example.plugin is already satisfied by repository + // verifyFeatureDependencies(dependencies); + Dependency feature = getDependency(dependencies, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + assertNull(feature.getDependencies()); + assertEquals(dependencies.size(), 9); + } + + @Test + public void verifyProduct() throws Exception { + String bomPath = getBomPath("product"); + verifier.verifyFilePresent(bomPath); + + Bom bom = getBom(bomPath); + List dependencies = bom.getDependencies(); + verifyProductDependencies(dependencies); + // All dependencies are already satisfied by the product + // verifyBundleDependencies(dependencies); + // verifyFeatureDependencies(dependencies); + // verifyRepositoryDependencies(dependencies); + Dependency dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.beans@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.observable@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.property@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.common@3.18.200.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.osgi@3.18.600.v20231110-1900?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.launcher@1.6.600.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.launcher.gtk.linux.x86_64@1.2.800.v20231003-1442?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.executable@3.8.2300.v20231106-1826?classifier=org.eclipse.update.feature&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.executable_root.gtk.linux.x86_64@3.8.2300.v20231106-1826?classifier=binary&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:maven/p2.p2.installable.unit/org.eclipse.equinox.executable_root.gtk.linux.x86_64@3.8.2300.v20231106-1826?type=p2-installable-unit"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + assertNull(dependency.getDependencies()); + // + assertEquals(dependencies.size(), 14); + } + + private void verifyBundleDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.core.databinding@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.core.databinding.beans@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.core.databinding.observable@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.core.databinding.property@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertEquals(dependency.getDependencies().size(), 4); + } + + private void verifyFeatureDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + verifyDependency(dependency, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + assertEquals(dependency.getDependencies().size(), 1); + } + + private void verifyRepositoryDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:maven/tycho-demo/repository.eclipse-repository@1.0.0-SNAPSHOT?type=eclipse-repository"); + verifyDependency(dependencies, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + verifyDependency(dependency, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + assertEquals(dependency.getDependencies().size(), 2); + } + + private void verifyCommonDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.equinox.common@3.18.200.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.osgi@3.18.600.v20231110-1900?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertEquals(dependency.getDependencies().size(), 2); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.beans@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.observable@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.property@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.common@3.18.200.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.osgi@3.18.600.v20231110-1900?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + } + + private void verifyProductDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:maven/tycho-demo/example@1.0.0-SNAPSHOT?type=eclipse-repository"); + verifyDependency(dependencies, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + verifyDependency(dependency, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.beans@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.observable@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.property@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.equinox.common@3.18.200.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.osgi@3.18.600.v20231110-1900?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertEquals(dependency.getDependencies().size(), 8); + } + + private void verifyDependency(Dependency parent, String ref) { + verifyDependency(parent.getDependencies(), ref); + } + + private void verifyDependency(List dependencies, String ref) { + if (dependencies.stream().noneMatch(dependency -> match(dependency, ref))) { + fail("No dependency found matching: " + ref); + } + } + + private Dependency getDependency(List dependencies, String ref) { + return dependencies.stream().filter(dependency -> match(dependency, ref)).findFirst().orElseThrow(); + } + + private boolean match(Dependency dependency, String ref) { + return URLDecoder.decode(dependency.getRef(), StandardCharsets.UTF_8).equals(ref); + } + + private String getBomPath(String projectName) { + return projectName + "/target/bom.xml"; + } + + private Bom getBom(String bomPath) throws ParseException { + Parser parser = new XmlParser(); + File bom = new File(verifier.getBasedir(), bomPath); + return parser.parse(bom); + } +} diff --git a/tycho-sbom/.settings/org.eclipse.jdt.core.prefs b/tycho-sbom/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..cf2cd4590a --- /dev/null +++ b/tycho-sbom/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/tycho-sbom/pom.xml b/tycho-sbom/pom.xml new file mode 100644 index 0000000000..4ec09d123a --- /dev/null +++ b/tycho-sbom/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + org.eclipse.tycho + tycho + 5.0.0-SNAPSHOT + + tycho-sbom + Tycho SBOM model extension + + + + org.eclipse.tycho + tycho-core + ${project.version} + + + org.eclipse.tycho + tycho-api + ${project.version} + + + + org.apache.maven + maven-plugin-api + + + + org.cyclonedx + cyclonedx-maven-plugin + 2.7.10 + + + + + + + org.codehaus.plexus + plexus-component-metadata + + + + generate-metadata + + + + + + + \ No newline at end of file diff --git a/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoModelConverter.java b/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoModelConverter.java new file mode 100644 index 0000000000..4cd9ca814a --- /dev/null +++ b/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoModelConverter.java @@ -0,0 +1,302 @@ +/******************************************************************************* + * Copyright (c) 2024 Patrick Ziegler and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.sbom; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import javax.inject.Inject; + +import org.apache.maven.RepositoryUtils; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.LegacySupport; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.component.annotations.Component; +import org.cyclonedx.maven.DefaultModelConverter; +import org.cyclonedx.maven.ModelConverter; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.equinox.p2.core.ProvisionException; +import org.eclipse.equinox.p2.metadata.IArtifactKey; +import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository; +import org.eclipse.tycho.ArtifactKey; +import org.eclipse.tycho.DefaultArtifactKey; +import org.eclipse.tycho.MavenRepositoryLocation; +import org.eclipse.tycho.ReactorProject; +import org.eclipse.tycho.core.TargetPlatformConfiguration; +import org.eclipse.tycho.core.TychoProjectManager; +import org.eclipse.tycho.core.maven.TychoReactorReader; +import org.eclipse.tycho.core.osgitools.DefaultReactorProject; +import org.eclipse.tycho.core.resolver.target.ArtifactTypeHelper; +import org.eclipse.tycho.p2maven.repository.P2RepositoryManager; +import org.eclipse.tycho.targetplatform.TargetDefinition.InstallableUnitLocation; +import org.eclipse.tycho.targetplatform.TargetDefinition.Location; +import org.eclipse.tycho.targetplatform.TargetDefinition.Repository; +import org.eclipse.tycho.targetplatform.TargetDefinitionFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Custom implementation of the CycloneDX model converter with support for both + * Maven and p2 artifacts. The generated PURL is usually of the form: + * + *
+ * pkg:/p2/<id>@<version>?classifier=<classifier>&location=<download-url>
+ * 
+ * + * This converter can be used with the {@code cyclonedx-maven-plugin} by adding + * it as a dependency as follows: + * + *
+ * <plugin>
+ *   <groupId>org.cyclonedx</groupId>
+ *   <artifactId>cyclonedx-maven-plugin</artifactId>
+ *   <dependencies>
+ *     <dependency>
+ *       <groupId>org.eclipse.tycho.extras</groupId>
+ *       <artifactId>tycho-sbom</artifactId>
+ *     </dependency>
+ *   </dependencies>
+ * </plugin>
+ * 
+ */ +@Component(role = ModelConverter.class) +public class TychoModelConverter extends DefaultModelConverter { + private static final Logger LOG = LoggerFactory.getLogger(TychoModelConverter.class); + + @Inject + private P2RepositoryManager repositoryManager; + + @Inject + private TychoProjectManager projectManager; + + @Inject + private TychoReactorReader reactorReader; + + @Inject + private LegacySupport legacySupport; + + @Override + public String generatePackageUrl(org.apache.maven.artifact.Artifact mavenArtifact) { + Artifact artifact = RepositoryUtils.toArtifact(mavenArtifact); + return generatePackageUrl(artifact, true, true, () -> super.generatePackageUrl(mavenArtifact)); + } + + @Override + public String generatePackageUrl(Artifact artifact) { + return generatePackageUrl(RepositoryUtils.toArtifact(artifact)); + } + + @Override + public String generateVersionlessPackageUrl(org.apache.maven.artifact.Artifact mavenArtifact) { + Artifact artifact = RepositoryUtils.toArtifact(mavenArtifact); + return generatePackageUrl(artifact, false, true, () -> super.generateVersionlessPackageUrl(mavenArtifact)); + } + + @Override + public String generateVersionlessPackageUrl(Artifact artifact) { + return generateVersionlessPackageUrl(artifact); + } + + @Override + public String generateClassifierlessPackageUrl(Artifact artifact) { + return generatePackageUrl(artifact, true, false, () -> super.generateClassifierlessPackageUrl(artifact)); + } + + /** + * Calculates the package URL for the given artifact. For OSGi artifacts, the + * {@code p2} type is chosen, otherwise {@code maven}. + * + * @param artifact One of the artifacts available in the current reactor + * build. + * @param withVersion Whether the version should be included in the PURL. For + * OSGi bundles, the {@code -SNAPSHOT} is replaced by the + * actual build qualifier. + * @param withClassifier Whether the classifier should be included in the PURL. + * @param fallback The method for generating the Maven PURL, in case the + * artifact is not an OSGi bundle. + * @return + */ + private String generatePackageUrl(Artifact artifact, boolean withVersion, boolean withClassifier, + Supplier fallback) { + if (reactorReader.isTychoReactorArtifact(artifact)) { + ArtifactKey artifactKey = getQualifiedArtifactKey(artifact); + IArtifactKey p2artifactKey = ArtifactTypeHelper.toP2ArtifactKey(artifactKey); + boolean isReactorProject = reactorReader.getTychoReactorProject(artifact).isPresent(); + if (p2artifactKey != null) { + String p2purl = generateP2PackageUrl(p2artifactKey, withVersion, withClassifier, isReactorProject); + if (p2purl != null) { + return p2purl; + } + } + } + return fallback.get(); + } + + /** + * Creates the actual package URL for the p2 artifact key, using the following + * format: + *
scheme:type/namespace/name@version?qualifiers#subpath
+ * The scheme and type of the artifact are always {@code pkg} and {@code p2}, + * respectively. The namespace is optional and not used. The name and version + * corresponds to {@link IArtifactKey#getId()} and + * {@link ArtifactKey#getVersion()}. The qualifier contains the optional key + * {@code classifier} with value {@link IArtifactKey#getClassifier()}and the + * mandatory key {@code location} with the percentage-encoded repository URL + * containing this artifact. + * + * @param p2artifactKey One of the p2 artifacts available in the current + * reactor build. + * @param withVersion Whether the version should be included in the PURL. + * For OSGi bundles, the {@code -SNAPSHOT} is replaced + * by the actual build qualifier. + * @param withClassifier Whether the classifier should be included in the + * PURL. + * @param isReactorProject if {@code true}, then the given artifact key + * corresponds to a reactor project. + * @return + * @see here + */ + /* package */ String generateP2PackageUrl(IArtifactKey p2artifactKey, boolean withVersion, boolean withClassifier, + boolean isReactorProject) { + String location = getRepositoryLocation(p2artifactKey, isReactorProject); + if (location == null) { + LOG.warn("Unknown p2 repository for artifact: " + p2artifactKey.getId()); + return null; + } + String encodedLocation = URLEncoder.encode(location, StandardCharsets.UTF_8); + StringBuilder builder = new StringBuilder(); + builder.append("pkg:p2/"); + builder.append(p2artifactKey.getId()); + if (withVersion) { + builder.append('@'); + builder.append(p2artifactKey.getVersion()); + } + builder.append('?'); + if (withClassifier) { + builder.append("classifier="); + builder.append(p2artifactKey.getClassifier()); + builder.append('&'); + } + builder.append("location="); + builder.append(encodedLocation); + return builder.toString(); + } + + /** + * Calculates the first repository from which the given artifact can be + * downloaded. If the artifact is part of the local reactor build, the URL + * specified in the {@code tycho.sbom.url} property is returned (as it has not + * yet been deployed to any update site). If the artifact is not available on + * any of the repositories, {@code null} is returned. + * + * @param p2artifactKey The P2 coordinates of the artifact. Used to check + * whether the artifact is available on one of the p2 + * update sites. + * @param isReactorProject if {@code true}, then the given artifact key + * corresponds to a reactor project. + * @return The base URl of the repository or {@code null}, if the artifact isn't + * hosted on any known repository. + */ + private final String getRepositoryLocation(IArtifactKey p2artifactKey, boolean isReactorProject) { + MavenSession currentSession = legacySupport.getSession(); + if (currentSession == null) { + LOG.error("Maven session couldn't be found."); + return null; + } + + MavenProject currentProject = currentSession.getCurrentProject(); + + // Artifacts from the current reactor build haven't been deployed yet. Use a + // build property to determine which repository they are published to. + if (isReactorProject) { + String defaultUrl = currentProject.getProperties().getProperty("tycho.sbom.url"); + if (defaultUrl == null) { + LOG.error("'tycho.sbom.url' property not set."); + } + return defaultUrl; + } + + // Iterate over all p2 repository and return the first one containing the + // artifact. Note that the location might be arbitrary, if the artifact is + // contained by multiple repositories. + for (Repository repository : getTargetRepositories(currentProject)) { + String id = repository.getId(); + URI location = URI.create(repository.getLocation()); + MavenRepositoryLocation mavenRepository = new MavenRepositoryLocation(id, location); + try { + IArtifactRepository artifactRepository = repositoryManager.getArtifactRepository(mavenRepository); + if (artifactRepository.contains(p2artifactKey)) { + return repository.getLocation(); + } + } catch (ProvisionException e) { + LOG.error(e.getMessage(), e); + } + } + return null; + } + + /** + * Returns the Eclipse/OSGi {@link ArtifactKey} of the given artifact. For + * reactor projects, the (optional) {@code -SNAPSHOT} of suffix of the version + * string is replaced by its expanded version. + * + * @param artifact A Maven artifact with valid GAV + * @return The Eclipse/OSGi artifact key of the given artifact. + */ + private ArtifactKey getQualifiedArtifactKey(Artifact artifact) { + String expandedVersion = artifact.getVersion(); + + MavenProject mavenProject = reactorReader.getTychoReactorProject(artifact).orElse(null); + if (mavenProject != null) { + ReactorProject reactorProject = DefaultReactorProject.adapt(mavenProject); + if (reactorProject != null) { + expandedVersion = reactorProject.getExpandedVersion(); + } else { + LOG.error(mavenProject + " is not a Tycho project."); + } + } + + String type = reactorReader.getPackagingType(artifact); + return new DefaultArtifactKey(type, artifact.getArtifactId(), expandedVersion); + } + + /** + * Returns a list of all target repositories which are accessible by the given + * Maven project. All IUs required by this proejct should be accessible via one + * of those repositories. + * + * @param currentProject The current project of the reactor build, for which the + * SBOM is generated. + * @return An unmodifiable list of all target repositories. + */ + private List getTargetRepositories(MavenProject currentProject) { + TargetPlatformConfiguration targetConfiguration = projectManager.getTargetPlatformConfiguration(currentProject); + List p2repositories = new ArrayList<>(); + + for (TargetDefinitionFile targetFile : targetConfiguration.getTargets()) { + for (Location location : targetFile.getLocations()) { + if (location instanceof InstallableUnitLocation iuLocation) { + p2repositories.addAll(iuLocation.getRepositories()); + } + } + } + + return Collections.unmodifiableList(p2repositories); + } +} diff --git a/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoProjectDependenciesConverter.java b/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoProjectDependenciesConverter.java new file mode 100644 index 0000000000..28cd2c73ca --- /dev/null +++ b/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoProjectDependenciesConverter.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * Copyright (c) 2024 Patrick Ziegler and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.sbom; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.inject.Inject; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.LegacySupport; +import org.apache.maven.project.MavenProject; +import org.cyclonedx.maven.DefaultProjectDependenciesConverter; +import org.cyclonedx.maven.ProjectDependenciesConverter; +import org.cyclonedx.model.Component; +import org.cyclonedx.model.Dependency; +import org.cyclonedx.model.Metadata; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.equinox.p2.metadata.IArtifactKey; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.tycho.p2.tools.P2DependencyTreeGenerator; +import org.eclipse.tycho.p2.tools.P2DependencyTreeGenerator.DependencyTreeNode; +import org.eclipse.tycho.p2maven.MavenProjectDependencyProcessor; +import org.eclipse.tycho.p2maven.MavenProjectDependencyProcessor.ProjectDependencyClosure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@org.codehaus.plexus.component.annotations.Component(role = ProjectDependenciesConverter.class) +public class TychoProjectDependenciesConverter extends DefaultProjectDependenciesConverter { + private static final Logger LOG = LoggerFactory.getLogger(TychoProjectDependenciesConverter.class); + + @Inject + private LegacySupport legacySupport; + + @Inject + private TychoModelConverter modelConverter; + + @Inject + private P2DependencyTreeGenerator dependencyGenerator; + + @Inject + private MavenProjectDependencyProcessor dependencyProcessor; + + @Override + public void cleanupBomDependencies(Metadata metadata, Map components, + Map dependencies) { + final MavenSession mavenSession = legacySupport.getSession(); + final MavenProject currentProject = mavenSession.getCurrentProject(); + // + try { + Set unmapped = new HashSet<>(); + List rootNodes = dependencyGenerator.buildDependencyTree(currentProject, unmapped); + // Something doesn't seem right... + if (rootNodes.isEmpty() && unmapped.isEmpty()) { + LOG.info("Project " + currentProject + " doesn't seem to be a Tycho project. Skip..."); + return; + } + // Synchronize with dependency tree node + TreeSet newDependencies = new TreeSet<>(Comparator.comparing(Dependency::getRef)); + for (DependencyTreeNode rootNode : rootNodes) { + convertToDependency(rootNode, newDependencies); + } + for (IInstallableUnit iu : unmapped) { + for (String bomRef : getBomRepresentation(iu)) { + newDependencies.add(new Dependency(bomRef)); + } + } + for (Dependency dependency : newDependencies) { + dependencies.put(dependency.getRef(), dependency); + } + } catch (CoreException e) { + LOG.error(e.getMessage()); + } + } + + private void convertToDependency(DependencyTreeNode node, Set dependencies) { + for (String bomRef : getBomRepresentation(node.getInstallableUnit())) { + Dependency dependency = new Dependency(bomRef); + for (DependencyTreeNode childNode : node.getChildren()) { + for (String childBomRef : getBomRepresentation(childNode.getInstallableUnit())) { + dependency.addDependency(new Dependency(childBomRef)); + } + convertToDependency(childNode, dependencies); + } + dependencies.add(dependency); + } + } + + /** + * Calculates the BOM representation of the give {@link IInstallableUnit}. We + * need to distinguish between IUs that are part of the current reactor build + * and IUs external IUs, in order to properly handle e.g. the version qualifier. + * For reactor IUs, the BOM is calculated using the Maven artifact, otherwise + * via the {@link IArtifactKey}s. If no BOM representation can be calculated, an + * empty list is returned. + * + * @param iu The installable unit for which to generate + * @return A {@code mutable} list of all bom representation. matching the given + * IU. + */ + private List getBomRepresentation(IInstallableUnit iu) { + final MavenSession mavenSession = legacySupport.getSession(); + final List reactorProjects = mavenSession.getAllProjects(); + // mutable! + List bomRefs = new ArrayList<>(); + // (I) IU describes local reactor project + try { + ProjectDependencyClosure dependencyClosure = dependencyProcessor + .computeProjectDependencyClosure(reactorProjects, mavenSession); + MavenProject iuProject = dependencyClosure.getProject(iu).orElse(null); + if (iuProject != null) { + String bomRef = modelConverter.generatePackageUrl(iuProject.getArtifact()); + if (bomRef == null) { + LOG.error("Unable to calculate BOM for: " + iuProject); + return bomRefs; + } + bomRefs.add(bomRef); + return bomRefs; + } + } catch (CoreException e) { + LOG.error(e.getMessage(), e); + return Collections.emptyList(); + } + // (II) IU describes external artifact + for (IArtifactKey p2artifactKey : iu.getArtifacts()) { + String bomRef = modelConverter.generateP2PackageUrl(p2artifactKey, true, true, false); + if (bomRef == null) { + LOG.error("Unable to calculate BOM for: " + p2artifactKey); + continue; + } + bomRefs.add(bomRef); + } + return bomRefs; // mutable! + } +}