Skip to content

Commit 4758dd2

Browse files
authored
Merge pull request #623 from graalvm/feature/maven-plugin-improve-sbom
Improve Native Image SBOM Generation
2 parents c430529 + 0b57feb commit 4758dd2

File tree

15 files changed

+1508
-24
lines changed

15 files changed

+1508
-24
lines changed

common/utils/src/main/java/org/graalvm/buildtools/utils/NativeImageUtils.java

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import static org.graalvm.buildtools.utils.SharedConstants.GRAALVM_EXE_EXTENSION;
5757

5858
public class NativeImageUtils {
59+
public static final String ORACLE_GRAALVM_IDENTIFIER = "Oracle GraalVM";
5960

6061
private static final Pattern requiredVersionPattern = Pattern.compile("^([0-9]+)(?:\\.([0-9]+)?)?(?:\\.([0-9]+)?)?$");
6162

gradle/libs.versions.toml

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ groovy = "3.0.11"
1919
jetty = "11.0.11"
2020
plexusUtils = "4.0.0"
2121
plexusXml = "4.0.2"
22+
cyclonedxMaven = "2.8.1"
23+
pluginExecutorMaven = "2.4.0"
2224

2325
[libraries]
2426
# Local projects
@@ -61,3 +63,6 @@ jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty
6163
plexus-utils = { module = "org.codehaus.plexus:plexus-utils", version.ref = "plexusUtils" }
6264

6365
plexus-xml = { module = "org.codehaus.plexus:plexus-xml", version.ref = "plexusXml" }
66+
67+
cyclonedx-maven-plugin = { module = "org.cyclonedx:cyclonedx-maven-plugin", version.ref="cyclonedxMaven" }
68+
plugin-executor-maven = { module = "org.twdata.maven:mojo-executor", version.ref="pluginExecutorMaven" }

native-maven-plugin/build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ dependencies {
6565
implementation(libs.jvmReachabilityMetadata)
6666
implementation(libs.plexus.utils)
6767
implementation(libs.plexus.xml)
68+
implementation(libs.cyclonedx.maven.plugin)
69+
implementation(libs.plugin.executor.maven)
6870

6971
compileOnly(libs.maven.pluginApi)
7072
compileOnly(libs.maven.core)
@@ -178,3 +180,4 @@ tasks.withType<Checkstyle>().configureEach {
178180
// generated code
179181
exclude("**/RuntimeMetadata*")
180182
}
183+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
42+
package org.graalvm.buildtools.maven
43+
44+
import com.fasterxml.jackson.databind.node.ObjectNode
45+
import org.graalvm.buildtools.maven.sbom.SBOMGenerator
46+
import org.graalvm.buildtools.utils.NativeImageUtils
47+
import spock.lang.Requires
48+
import com.fasterxml.jackson.databind.ObjectMapper
49+
50+
class SBOMFunctionalTest extends AbstractGraalVMMavenFunctionalTest {
51+
private static boolean EE() {
52+
NativeCompileNoForkMojo.isOracleGraalVM(null)
53+
}
54+
55+
private static boolean CE() {
56+
!EE()
57+
}
58+
59+
private static boolean jdkVersionSupportsAugmentedSBOM() {
60+
NativeImageUtils.getMajorJDKVersion(NativeCompileNoForkMojo.getVersionInformation(null)) >= SBOMGenerator.requiredNativeImageVersion
61+
}
62+
63+
private static boolean unsupportedJDKVersion() {
64+
!jdkVersionSupportsAugmentedSBOM()
65+
}
66+
67+
private static boolean supportedAugmentedSBOMVersion() {
68+
EE() && jdkVersionSupportsAugmentedSBOM()
69+
}
70+
71+
@Requires({ supportedAugmentedSBOMVersion() })
72+
def "sbom is created when buildArg '--enable-sbom=export,embed' is used"() {
73+
withSample 'java-application'
74+
75+
when:
76+
/* The 'native-sbom' profile sets the '--enable-sbom' argument. */
77+
mvn '-Pnative-sbom', '-DquickBuild', '-DskipTests', 'package', 'exec:exec@native'
78+
79+
def sbom = file("target/example-app.sbom.json")
80+
81+
then:
82+
buildSucceeded
83+
outputContainsPattern".*CycloneDX SBOM with \\d+ component\\(s\\) is embedded in binary \\(.*?\\) and exported as JSON \\(see build artifacts\\)\\."
84+
outputDoesNotContain "Use '--enable-sbom' to assemble a Software Bill of Materials (SBOM)"
85+
validateSbom sbom
86+
!file(String.format("target/%s", SBOMGenerator.SBOM_FILENAME)).exists()
87+
outputContains "Hello, native!"
88+
}
89+
90+
/**
91+
* If user sets {@link NativeCompileNoForkMojo#AUGMENTED_SBOM_PARAM_NAME} to true then an SBOM should be generated
92+
* with default SBOM arguments even if user did not explicitly specify '--enable-sbom' as a buildArg.
93+
*/
94+
@Requires({ supportedAugmentedSBOMVersion() })
95+
def "sbom is created when only the augmented sbom parameter is used (but not the '--enable-sbom' buildArg)"() {
96+
withSample 'java-application'
97+
98+
when:
99+
mvn '-Pnative-augmentedSBOM-only', '-DquickBuild', '-DskipTests', 'package', 'exec:exec@native'
100+
101+
def sbom = file("target/example-app.sbom.json")
102+
103+
then:
104+
buildSucceeded
105+
outputContainsPattern".*CycloneDX SBOM with \\d+ component\\(s\\) is embedded in binary \\(.*?\\)."
106+
outputDoesNotContain "Use '--enable-sbom' to assemble a Software Bill of Materials (SBOM)"
107+
validateSbom sbom
108+
!file(String.format("target/%s", SBOMGenerator.SBOM_FILENAME)).exists()
109+
outputContains "Hello, native!"
110+
}
111+
112+
@Requires({ CE() })
113+
def "error is thrown when augmented sbom parameter is used with CE"() {
114+
withSample 'java-application'
115+
116+
when:
117+
mvn '-Pnative-augmentedSBOM-only', '-DquickBuild', '-DskipTests', 'package'
118+
119+
then:
120+
buildFailed
121+
}
122+
123+
@Requires({ EE() && unsupportedJDKVersion() })
124+
def "error is thrown when augmented sbom parameter is used with EE but not with an unsupported JDK version"() {
125+
withSample 'java-application'
126+
127+
when:
128+
mvn '-Pnative-augmentedSBOM-only', '-DquickBuild', '-DskipTests', 'package'
129+
130+
then:
131+
buildFailed
132+
}
133+
134+
/**
135+
* Validates the SBOM produced from 'java-application'.
136+
* @param sbom path to the SBOM.
137+
* @return true if validation succeeded.
138+
*/
139+
private static boolean validateSbom(File sbom) {
140+
try {
141+
if (!sbom.exists()) {
142+
println "SBOM not found: ${sbom}"
143+
return false
144+
}
145+
146+
def mapper = new ObjectMapper()
147+
def rootNode = mapper.readTree(sbom)
148+
149+
// Check root fields
150+
assert rootNode.has('bomFormat')
151+
assert rootNode.get('bomFormat').asText() == 'CycloneDX'
152+
assert rootNode.has('specVersion')
153+
assert rootNode.has('serialNumber')
154+
assert rootNode.has('version')
155+
assert rootNode.has('metadata')
156+
assert rootNode.has('components')
157+
assert rootNode.has('dependencies')
158+
159+
// Check metadata/component
160+
def metadataComponent = rootNode.path('metadata').path('component')
161+
assert metadataComponent.has('group')
162+
assert metadataComponent.get('group').asText() == 'org.graalvm.buildtools.examples'
163+
assert metadataComponent.has('name')
164+
assert metadataComponent.get('name').asText() == 'maven'
165+
166+
// Check that components and dependencies are non-empty
167+
assert !rootNode.get('components').isEmpty()
168+
assert !rootNode.get('dependencies').isEmpty()
169+
170+
// Check that the main component has no dependencies
171+
def mainComponentId = metadataComponent.get('bom-ref').asText()
172+
def mainComponentDependency = rootNode.get('dependencies').find { it.get('ref').asText() == mainComponentId } as ObjectNode
173+
assert mainComponentDependency.get('dependsOn').isEmpty()
174+
175+
// Check that the main component is not found in "components"
176+
assert !rootNode.get('components').any { it.get('bom-ref').asText() == mainComponentId }
177+
178+
return true
179+
} catch (AssertionError | Exception e) {
180+
println "SBOM validation failed: ${e.message}"
181+
return false
182+
}
183+
}
184+
}

native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java

+27-21
Original file line numberDiff line numberDiff line change
@@ -49,44 +49,33 @@
4949
import org.apache.maven.plugins.annotations.Component;
5050
import org.apache.maven.plugins.annotations.Parameter;
5151
import org.apache.maven.toolchain.ToolchainManager;
52+
import org.codehaus.plexus.logging.Logger;
5253
import org.graalvm.buildtools.maven.config.ExcludeConfigConfiguration;
5354
import org.graalvm.buildtools.utils.NativeImageConfigurationUtils;
5455
import org.graalvm.buildtools.utils.NativeImageUtils;
5556
import org.graalvm.buildtools.utils.SharedConstants;
5657

5758
import javax.inject.Inject;
58-
import java.io.File;
59-
import java.io.InputStream;
60-
import java.io.BufferedReader;
61-
import java.io.InputStreamReader;
62-
import java.io.IOException;
59+
import java.io.*;
6360
import java.net.URI;
6461
import java.nio.charset.StandardCharsets;
6562
import java.nio.file.FileSystem;
66-
import java.nio.file.FileSystems;
67-
import java.nio.file.FileSystemAlreadyExistsException;
68-
import java.nio.file.Path;
69-
import java.nio.file.Paths;
70-
import java.nio.file.Files;
71-
import java.util.ArrayList;
72-
import java.util.Arrays;
73-
import java.util.HashSet;
74-
import java.util.List;
75-
import java.util.Collections;
76-
import java.util.Map;
77-
import java.util.Optional;
78-
import java.util.Set;
63+
import java.nio.file.*;
64+
import java.util.*;
7965
import java.util.regex.Pattern;
8066
import java.util.stream.Collectors;
8167
import java.util.stream.Stream;
8268

69+
import static org.graalvm.buildtools.utils.NativeImageUtils.ORACLE_GRAALVM_IDENTIFIER;
70+
8371
/**
8472
* @author Sebastien Deleuze
8573
*/
8674
public abstract class AbstractNativeImageMojo extends AbstractNativeMojo {
8775
protected static final String NATIVE_IMAGE_META_INF = "META-INF/native-image";
8876
protected static final String NATIVE_IMAGE_PROPERTIES_FILENAME = "native-image.properties";
8977
protected static final String NATIVE_IMAGE_DRY_RUN = "nativeDryRun";
78+
private static String nativeImageVersionInformation = null;
9079

9180
@Parameter(defaultValue = "${plugin}", readonly = true) // Maven 3 only
9281
protected PluginDescriptor plugin;
@@ -447,6 +436,24 @@ protected void checkRequiredVersionIfNeeded() throws MojoExecutionException {
447436
if (requiredVersion == null) {
448437
return;
449438
}
439+
NativeImageUtils.checkVersion(requiredVersion, getVersionInformation(logger));
440+
}
441+
442+
static protected boolean isOracleGraalVM(Logger logger) throws MojoExecutionException {
443+
return getVersionInformation(logger).contains(ORACLE_GRAALVM_IDENTIFIER);
444+
}
445+
446+
/**
447+
* Returns the output of calling "native-image --version".
448+
* @param logger a logger, that may be null, to print warnings or useful information.
449+
* @return the output as a string joined by "\n".
450+
* @throws MojoExecutionException when any errors occurred.
451+
*/
452+
static protected String getVersionInformation(Logger logger) throws MojoExecutionException {
453+
if (nativeImageVersionInformation != null) {
454+
return nativeImageVersionInformation;
455+
}
456+
450457
Path nativeImageExecutable = NativeImageConfigurationUtils.getNativeImage(logger);
451458
try {
452459
ProcessBuilder processBuilder = new ProcessBuilder(nativeImageExecutable.toString());
@@ -457,12 +464,11 @@ protected void checkRequiredVersionIfNeeded() throws MojoExecutionException {
457464
throw new MojoExecutionException("Execution of " + commandString + " returned non-zero result");
458465
}
459466
InputStream inputStream = versionCheckProcess.getInputStream();
460-
String versionToCheck = new BufferedReader(
467+
nativeImageVersionInformation = new BufferedReader(
461468
new InputStreamReader(inputStream, StandardCharsets.UTF_8))
462469
.lines()
463470
.collect(Collectors.joining("\n"));
464-
NativeImageUtils.checkVersion(requiredVersion, versionToCheck);
465-
471+
return nativeImageVersionInformation;
466472
} catch (IOException | InterruptedException e) {
467473
throw new MojoExecutionException("Checking GraalVM version with " + nativeImageExecutable + " failed", e);
468474
}

native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeMojo.java

+7
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.apache.maven.artifact.Artifact;
4545
import org.apache.maven.execution.MavenSession;
4646
import org.apache.maven.plugin.AbstractMojo;
47+
import org.apache.maven.plugin.BuildPluginManager;
4748
import org.apache.maven.plugin.descriptor.PluginDescriptor;
4849
import org.apache.maven.plugins.annotations.Component;
4950
import org.apache.maven.plugins.annotations.Parameter;
@@ -122,6 +123,12 @@ public abstract class AbstractNativeMojo extends AbstractMojo {
122123
@Component
123124
protected MavenSession mavenSession;
124125

126+
@Component
127+
protected MavenProject mavenProject;
128+
129+
@Component
130+
protected BuildPluginManager pluginManager;
131+
125132
@Component
126133
protected RepositorySystem repositorySystem;
127134

0 commit comments

Comments
 (0)