Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ plugins {
alias(libs.plugins.diffplug.spotless) apply false
alias(libs.plugins.nodegradle.node) apply false
alias(libs.plugins.openapi.generator) apply false
alias(libs.plugins.cyclonedx) apply false
alias(libs.plugins.logchange)
}

Expand Down
12 changes: 12 additions & 0 deletions changelog/unreleased/cyclonedx-sboms-SOLR-17328.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc

title: >
Ship a CycloneDX SBOM (bom.json) in the root of the full and slim binary distributions,
covering the Java libraries, the Solr artifacts and the UI content of the webapp
type: added
authors:
- name: Piotr P. Karwasz
nick: ppkarwasz
links:
- name: SOLR-17328
url: https://issues.apache.org/jira/browse/SOLR-17328
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ commons-io = "2.22.0"
compose = "1.11.1"
cuvs-java = "26.06.0"
cuvs-lucene = "25.12.0"
cyclonedx = "3.0.2"
# @keep npm tool generating the SBOM of the OpenAPI JS client, installed by :solr:webapp:js-client

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does @keep mean? Do we need this comment? the -npm suffix seems clear.

cyclonedx-npm = "6.0.0"
decompose = "3.5.0"
diffplug-spotless = "8.7.0"
# @keep Use for dockerfile JRE version
Expand Down Expand Up @@ -203,6 +206,7 @@ xerial-snappy = "1.1.10.8"
[plugins]
benmanes-versions = { id = "com.github.ben-manes.versions", version.ref = "benmanes-versions" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
cyclonedx = { id = "org.cyclonedx.bom", version.ref = "cyclonedx" }
diffplug-spotless = { id = "com.diffplug.spotless", version.ref = "diffplug-spotless" }
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
Expand Down
502 changes: 499 additions & 3 deletions solr/packaging/build.gradle

Large diffs are not rendered by default.

331 changes: 330 additions & 1 deletion solr/packaging/gradle.lockfile

Large diffs are not rendered by default.

35 changes: 32 additions & 3 deletions solr/server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,43 @@ javadoc.enabled(false)
compileJava.enabled(false)

configurations {
libExt
// === Custom configurations used to assemble the Solr server binary distribution ===

// 1. Jetty Bootstrap JAR
// Output Path: server/start.jar
// Description: Contains the Jetty bootstrap JAR responsible for launching the Solr server.
startJar

// 2. Server Libraries
// Output Path: server/lib/
// Description: Contains core libraries required by the Solr server at runtime (mostly Jetty-related JARs).
serverLib

// 3. Extended Server Libraries
// Output Path: server/lib/ext/
// Description: Includes optional runtime libraries such as logging (SLF4J, Log4j) and metrics (Dropwizard, etc.).
libExt

// 4. Solr Core JAR
// Output Path: server/solr-webapp/webapp/WEB-INF/lib/
// Description: Contains the solr-core JAR, which includes the core functionality and indexing logic of Solr.
solrCore

// 5. Solr Web Application Libraries
// Output Path: server/solr-webapp/webapp/WEB-INF/lib/
// Description: Contains the remaining Solr modules, packaged in exploded WAR format for deployment via Jetty.
webapp

// === Runtime Configuration ===

// Combines core runtime dependencies for launching the Solr server,
// aggregating required libraries from serverLib, libExt, and solrCore.
runtimeClasspath {
extendsFrom serverLib, libExt, solrCore
}
startJar
webapp

// Internal configuration used by packaging tasks (e.g., creating distributions).
// This configuration only includes the `packagingDir` folder generated during assembly
packaging
}

Expand Down
34 changes: 34 additions & 0 deletions solr/ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* limitations under the License.
*/

import org.cyclonedx.gradle.CyclonedxDirectTask
import org.cyclonedx.model.Component
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
Expand Down Expand Up @@ -239,3 +241,35 @@ artifacts {
)
}
}

// CycloneDX SBOM of the Maven dependencies compiled into the wasmJs UI bundle,
// merged into the distribution SBOMs by :solr:packaging.

val uiSbomFile = layout.buildDirectory.file("cyclonedx/bom-ui.json").get().asFile

val cyclonedxUi = tasks.register<CyclonedxDirectTask>("cyclonedxUi") {
group = "Bill of Materials"
description = "Generates a CycloneDX BOM of the dependencies compiled into the wasmJs UI bundle"

includeConfigs.set(listOf("wasmJsRuntimeClasspath"))
projectType.set(Component.Type.LIBRARY)

// The plugin resolves the configuration leniently and without depending on it,
// so the artifacts must be present first or their hashes are missing.
inputs.files(configurations.named("wasmJsRuntimeClasspath"))
.withPropertyName("wasmJsRuntimeArtifacts")
.withNormalizer(ClasspathNormalizer::class)

jsonOutput.set(uiSbomFile)
}

val uiSbom = configurations.create("uiSbom") {
isCanBeConsumed = true
isCanBeResolved = false
}

artifacts {
add("uiSbom", uiSbomFile) {
builtBy(cyclonedxUi)
}
}
76 changes: 75 additions & 1 deletion solr/webapp/js-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import com.github.gradle.node.npm.task.NpmTask
import com.github.gradle.node.npm.task.NpxTask
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

// Builds the OpenAPI-generated JS client (from :solr:api) into a single bundled
// file, for :solr:webapp to include in the war. This is the only place in the
Expand Down Expand Up @@ -49,9 +51,29 @@ val syncJSClientSourceCode = tasks.register<Sync>("syncJSClientSourceCode") {

into(jsClientWorkspace)

// Keep the node modules, so that they don't need to be re-downloaded
// Keep the outputs of "npm install", so that they don't need to be regenerated
preserve {
include("node_modules/**")
include("package-lock.json")
}

// The OpenAPI generator wrongly declares the @babel/cli build tool as a runtime
// dependency; move it to devDependencies, so that the SBOM of the bundle
// (generated with --omit dev) only lists what browserify actually bundles.
doLast {
val packageJson = File(jsClientWorkspace, "package.json")
@Suppress("UNCHECKED_CAST")
val json = JsonSlurper().parse(packageJson) as MutableMap<String, Any?>

@Suppress("UNCHECKED_CAST")
val dependencies = json["dependencies"] as? MutableMap<String, String>
dependencies?.remove("@babel/cli")?.let { babelCliVersion ->
@Suppress("UNCHECKED_CAST")
val devDependencies =
json.getOrPut("devDependencies") { mutableMapOf<String, String>() } as MutableMap<String, String>
devDependencies["@babel/cli"] = babelCliVersion
}
packageJson.writeText(JsonOutput.prettyPrint(JsonOutput.toJson(json)))
}
}

Expand All @@ -64,6 +86,7 @@ val jsClientDownloadDeps = tasks.register<NpmTask>("jsClientDownloadDeps") {
inputs.dir("$jsClientWorkspace/src")
inputs.file("$jsClientWorkspace/package.json")
outputs.dir("$jsClientWorkspace/node_modules")
outputs.file("$jsClientWorkspace/package-lock.json")
}

val jsClientBuild = tasks.register<NpmTask>("jsClientBuild") {
Expand Down Expand Up @@ -117,3 +140,54 @@ artifacts {
builtBy(finalizeJsBundleDir)
}
}

// CycloneDX SBOM of the bundle, merged into the distribution SBOMs by :solr:packaging

val jsClientSbomFile = layout.buildDirectory.file("cyclonedx/bom-js-client.json").get().asFile

val downloadCyclonedxNpm = tasks.register<NpmTask>("downloadCyclonedxNpm") {
args.set(listOf("install", "@cyclonedx/cyclonedx-npm@${libs.versions.cyclonedx.npm.get()}"))

inputs.property("cyclonedx-npm version", libs.versions.cyclonedx.npm.get())
outputs.dir(project.extra["nodeProjectDir"].toString() + "/node_modules/@cyclonedx/cyclonedx-npm")
}

val generateJsClientSbom = tasks.register<NpxTask>("generateJsClientSbom") {
dependsOn(downloadCyclonedxNpm)
// Needs the package-lock.json and node_modules produced by the install
dependsOn(jsClientDownloadDeps)

// The full package spec, since the bare "cyclonedx-npm" command name resolves to an
// unrelated npm package. Runs from the node project dir, where downloadCyclonedxNpm
// installed the pinned version, and points at the workspace manifest instead.
command.set("@cyclonedx/cyclonedx-npm@${libs.versions.cyclonedx.npm.get()}")
args.set(
listOf(
// Only the packages bundled into the shipped file, not the build tooling
"--omit", "dev",
// Match the spec version emitted by the CycloneDX Gradle plugin in :solr:packaging
"--spec-version", "1.6",
"--output-reproducible",
"--output-format", "JSON",
"--output-file", jsClientSbomFile.absolutePath,
"$jsClientWorkspace/package.json",
),
)
workingDir.set(File(project.extra["nodeProjectDir"].toString()))

inputs.file("$jsClientWorkspace/package.json")
inputs.file("$jsClientWorkspace/package-lock.json")
inputs.property("cyclonedx-npm version", libs.versions.cyclonedx.npm.get())
outputs.file(jsClientSbomFile)
}

val jsClientSbom = configurations.create("jsClientSbom") {
isCanBeConsumed = true
isCanBeResolved = false
}

artifacts {
add("jsClientSbom", jsClientSbomFile) {
builtBy(generateJsClientSbom)
}
}
Loading