This is an automated email from the ASF dual-hosted git repository.

jdaugherty pushed a commit to branch 8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git


The following commit(s) were added to refs/heads/8.0.x by this push:
     new fa30af085b Fix sbom exemptions for cli split
fa30af085b is described below

commit fa30af085bd009620dd9a41655e99a0cd2069693
Author: James Daugherty <[email protected]>
AuthorDate: Mon Jul 20 06:48:25 2026 -0500

    Fix sbom exemptions for cli split
---
 .../org/apache/grails/buildsrc/SbomPlugin.groovy   | 26 ++++++++++++++--------
 .../apache/grails/buildsrc/SbomPluginSpec.groovy   | 26 ++++++++++++++++++++++
 2 files changed, 43 insertions(+), 9 deletions(-)

diff --git 
a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
 
b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
index 2931b26e06..531425fe7d 100644
--- 
a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
+++ 
b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
@@ -150,6 +150,10 @@ class SbomPlugin implements Plugin<Project> {
             'grails-data-hibernate5-dbmigration': [
                     'pkg:maven/javax.xml.bind/[email protected]?type=jar': 
'CDDL-1.1', // api export
             ],
+            'grails-data-hibernate5-dbmigration-cli': [
+                    
'pkg:maven/org.hibernate.common/[email protected]?type=jar':
 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in 
hibernate 7
+                    
'pkg:maven/org.hibernate/[email protected]?type=jar'          
   : 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license 
in hibernate 7
+            ],
             'grails-data-hibernate7-dbmigration-core': [
                     'pkg:maven/javax.xml.bind/[email protected]?type=jar': 
'CDDL-1.1', // api export
             ],
@@ -278,6 +282,7 @@ class SbomPlugin implements Plugin<Project> {
                 def projectName = project.name
                 def projectPath = project.path
                 Provider<RegularFile> bomOutput = jsonOutput
+                Provider<String> sbomComponent = componentName
                 Provider<Boolean> isReproducibleBuildProvider = 
project.provider { lookupProperty(project, 'isReproducibleBuild') as boolean }
                 Provider<ZonedDateTime> buildDateProvider = project.provider { 
lookupProperty(project, 'buildDate') as ZonedDateTime }
                 doLast {
@@ -299,7 +304,7 @@ class SbomPlugin implements Plugin<Project> {
                         comps.each { c ->
                             // .licenses => choose a license that is 
compatible with ASF policy if multiple licensed
                             if (c instanceof Map && c.licenses instanceof List 
&& !(c.licenses as List).empty) {
-                                def chosen = pickLicense(logger, projectName, 
c['bom-ref'] as String, c.licenses as List)
+                                def chosen = pickLicense(logger, projectName, 
sbomComponent.get(), c['bom-ref'] as String, c.licenses as List)
                                 if (chosen != null) {
                                     c.licenses = [chosen]
                                 }
@@ -445,14 +450,15 @@ class SbomPlugin implements Plugin<Project> {
      *
      * @param logger the logger to use for logging
      * @param projectName the name of the project (captured at configuration 
time)
+     * @param sbomComponent the artifact the SBOM describes (the primary or 
cli companion coordinate)
      * @param bomRef the bom reference for the dependency
      * @param licenseChoices the list of license choices
      * @return the chosen license
      */
     @CompileDynamic
-    private static Object pickLicense(org.gradle.api.logging.Logger logger, 
String projectName, String bomRef, List licenseChoices) {
+    static Object pickLicense(org.gradle.api.logging.Logger logger, String 
projectName, String sbomComponent, String bomRef, List licenseChoices) {
         if (!bomRef) {
-            throw new GradleException("No bomRef found for a dependency of 
${projectName}, cannot pick license")
+            throw new GradleException("No bomRef found for a dependency of 
${sbomComponent}, cannot pick license")
         }
 
         logger.info('Picking license for {} from {} choices', bomRef, 
licenseChoices.size())
@@ -464,14 +470,14 @@ class SbomPlugin implements Plugin<Project> {
 
             def licenseBlock = LICENSES[licenseId]
             if (!licenseBlock) {
-                throw new GradleException("Cannot find license information for 
id ${licenseId} to use for bomRef ${bomRef} in project ${projectName}")
+                throw new GradleException("Cannot find license information for 
id ${licenseId} to use for bomRef ${bomRef} in ${sbomComponent}")
             }
 
             return licenseBlock
         }
 
         if (!(licenseChoices instanceof List) || licenseChoices.isEmpty()) {
-            throw new GradleException("No License was found for dependency: 
${bomRef} in project ${projectName}")
+            throw new GradleException("No License was found for dependency: 
${bomRef} in ${sbomComponent}")
         }
 
         def licenseIds = licenseChoices.findAll { it instanceof Map && 
it.license instanceof Map && it.license.id }
@@ -483,13 +489,15 @@ class SbomPlugin implements Plugin<Project> {
         def defaultLicense = licenseChoices[0] // pick the first one found
         def defaultLicenseId = defaultLicense.license.id as String
         if (defaultLicenseId == null) {
-            throw new GradleException("Could not determine License id for 
dependency: ${bomRef} in project ${projectName} for value ${defaultLicense}")
+            throw new GradleException("Could not determine License id for 
dependency: ${bomRef} in ${sbomComponent} for value ${defaultLicense}")
         }
         if (!(defaultLicenseId in PREFERRED_LICENSES)) {
-            def projectLicenseExemptions = LICENSE_EXCEPTIONS[projectName] ?: 
[:]
-            def permittedLicense = projectLicenseExemptions.get(bomRef) == 
defaultLicenseId
+            // an exemption may be declared against the project or the 
specific SBOM component (e.g. a
+            // cli companion whose runtime classpath differs from the primary 
jar)
+            def exemptions = (LICENSE_EXCEPTIONS[projectName] ?: [:]) + 
(LICENSE_EXCEPTIONS[sbomComponent] ?: [:])
+            def permittedLicense = exemptions.get(bomRef) == defaultLicenseId
             if (!permittedLicense) {
-                throw new GradleException("Unpermitted License found for bom 
dependency: ${bomRef} in project ${projectName} : ${defaultLicenseId}")
+                throw new GradleException("Unpermitted License found for bom 
dependency: ${bomRef} in ${sbomComponent} : ${defaultLicenseId}")
             }
         }
 
diff --git 
a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/SbomPluginSpec.groovy
 
b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/SbomPluginSpec.groovy
index 295d59c763..4786a254a9 100644
--- 
a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/SbomPluginSpec.groovy
+++ 
b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/SbomPluginSpec.groovy
@@ -18,13 +18,22 @@
  */
 package org.apache.grails.buildsrc
 
+import org.gradle.api.GradleException
 import org.gradle.api.Project
+import org.gradle.api.logging.Logging
 import org.gradle.api.provider.Property
 import org.gradle.testfixtures.ProjectBuilder
 import spock.lang.Specification
 
 class SbomPluginSpec extends Specification {
 
+    private static final def LOGGER = Logging.getLogger(SbomPluginSpec)
+    private static final String HIBERNATE_COMMONS = 
'pkg:maven/org.hibernate.common/[email protected]?type=jar'
+
+    private static List lgplChoice() {
+        [[license: [id: 'LGPL-2.1-only']]]
+    }
+
     static class FakeCliArtifactExtension {
         final Property<String> artifactId
         FakeCliArtifactExtension(Property<String> artifactId) { 
this.artifactId = artifactId }
@@ -71,4 +80,21 @@ class SbomPluginSpec extends Specification {
         then: "the sbom file name tracks the custom name, not the 
<project>-cli default"
         location.get().asFile.name == 'grails-core-custom-cli-8.0.0-sbom.json'
     }
+
+    void "a category-X license is permitted when exempted for the sbom 
component (cli companion)"() {
+        expect: "the hibernate 5 LGPL exemption keyed to the dbmigration cli 
companion is honoured"
+        SbomPlugin.pickLicense(LOGGER, 'grails-data-hibernate5-dbmigration',
+                'grails-data-hibernate5-dbmigration-cli', HIBERNATE_COMMONS, 
lgplChoice()) != null
+    }
+
+    void "an unexempted category-X license fails naming the sbom component, 
not just the project"() {
+        when: "the same LGPL dependency appears in an sbom component with no 
matching exemption"
+        SbomPlugin.pickLicense(LOGGER, 'grails-data-hibernate5-dbmigration',
+                'grails-data-hibernate5-dbmigration', HIBERNATE_COMMONS, 
lgplChoice())
+
+        then: "the failure identifies the offending sbom component"
+        GradleException e = thrown(GradleException)
+        e.message.contains('grails-data-hibernate5-dbmigration')
+        e.message.contains('LGPL-2.1-only')
+    }
 }

Reply via email to