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

janhoy pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new edeb8e85b86 SOLR-18032 Docker gradle build support multi-platform 
(#3964)
edeb8e85b86 is described below

commit edeb8e85b86d8049c31e4a61b301d356f8a6e6a4
Author: Jan Høydahl <[email protected]>
AuthorDate: Mon Dec 22 12:42:36 2025 +0100

    SOLR-18032 Docker gradle build support multi-platform (#3964)
    
    (cherry picked from commit 894fcbe15ccbd21127195855e2c959d92c2fa7c2)
---
 dev-docs/gradle-help/docker.txt |  53 ++++++++++++++-
 solr/docker/README.md           |  23 +++++++
 solr/docker/build.gradle        | 146 +++++++++++++++++++++++++++++++++++-----
 3 files changed, 204 insertions(+), 18 deletions(-)

diff --git a/dev-docs/gradle-help/docker.txt b/dev-docs/gradle-help/docker.txt
index bd53b6a864d..05070e63152 100644
--- a/dev-docs/gradle-help/docker.txt
+++ b/dev-docs/gradle-help/docker.txt
@@ -26,18 +26,32 @@ Solr Distribution: (Either Full or Slim, the solr binary 
distribution to build t
    EnvVar: SOLR_DOCKER_DIST
    Gradle Property: -Psolr.docker.dist
 
+Docker Platforms: (Comma-separated list of target platforms)
+   Default: None (builds for current architecture only)
+   EnvVar: SOLR_DOCKER_PLATFORM
+   Gradle Property: -Psolr.docker.platform
+   Examples:
+     - "linux/amd64" (single platform - can be used with dockerBuild)
+     - "linux/arm64,linux/amd64" (multi-platform - only supported by 
dockerPush)
+
+   Note: Multi-platform builds require Docker Buildx and are ONLY supported
+   by the dockerPush task. The dockerBuild task does not support 
multi-platform builds because
+   Docker cannot load multi-platform images into the local daemon. Use 
dockerPush to build and push
+   multi-platform images in a single step. You may need QEMU installed to run 
multi-platform
+   builds.
+
 Tagging and Pushing
 -------
 
 To tag the docker image, run the following command.
 This will also ensure that the docker image has been built as per the inputs 
detailed above.
 
-gradlew dockerTag
+./gradlew dockerTag
 
 And to push the image with the given tag, run the following command.
 Gradle will ensure that the docker image is built and tagged as the inputs 
describe before being pushed.
 
-gradlew dockerPush
+./gradlew dockerPush
 
 The docker image tag can be customized via the following options, all accepted 
via both Environment Variables and Gradle Properties.
 
@@ -61,13 +75,46 @@ Docker Image Name: (Use this to explicitly set a whole 
image name. If given, the
    EnvVar: SOLR_DOCKER_IMAGE_NAME
    Gradle Property: -Psolr.docker.imageName
 
+Multi-Platform Builds
+-------
+
+To build a multi-platform Docker image, use the SOLR_DOCKER_PLATFORM 
environment variable or Gradle property.
+
+IMPORTANT: Multi-platform builds (with multiple platforms) are ONLY supported 
by dockerPush, not dockerBuild.
+This is because Docker cannot load multi-platform images into the local daemon.
+
+Prerequisites:
+   - Docker Buildx must be installed (included with Docker Desktop and recent 
Docker Engine versions)
+   - A buildx builder must be configured. If you don't have one, create it:
+     docker buildx create --name solr-builder --use
+   - For cross-platform builds, QEMU may be required
+
+Build and push for multiple platforms:
+   SOLR_DOCKER_PLATFORM=linux/arm64,linux/amd64 ./gradlew dockerPush
+
+Using Gradle property:
+   ./gradlew dockerPush -Psolr.docker.platform=linux/arm64,linux/amd64
+
+The dockerPush task will build and push the multi-platform image in a single 
step using Docker Buildx.
+The task will validate that a suitable builder exists and that it supports all 
requested platforms before building.
+
+Single-Platform Builds with Explicit Platform
+--------------------------------------------------
+
+You can build for a specific platform and load it into your local Docker 
daemon:
+
+Build for a specific platform:
+   SOLR_DOCKER_PLATFORM=linux/arm64 ./gradlew dockerBuild
+
+This uses Docker Buildx with --load to make the image available locally for 
testing.
+
 Testing
 -------
 
 To test the docker image, run the following command.
 This will also ensure that the docker image has been built as per the inputs 
detailed above in the "Building" section.
 
-gradlew testDocker
+./gradlew testDocker
 
 If a docker image build parameters were used during building, then the same 
inputs must be used while testing.
 Otherwise a new docker image will be built for the tests to run with.
diff --git a/solr/docker/README.md b/solr/docker/README.md
index 7a3b7674bd0..93f953515b2 100644
--- a/solr/docker/README.md
+++ b/solr/docker/README.md
@@ -50,6 +50,29 @@ When building the image, Solr accepts arguments for 
customization. Currently onl
 docker build --build-arg BASE_IMAGE=custom/jdk:17-slim -f 
solr-X.Y.Z/docker/Dockerfile 
https://www.apache.org/dyn/closer.lua/solr/X.Y.Z/solr-X.Y.Z.tgz
 ```
 
+Multi-Platform Builds
+----
+
+To build multi-platform Docker images from the binary distribution, use Docker 
Buildx.
+
+**Prerequisites**:
+- Docker Buildx must be installed (included with Docker Desktop and recent 
Docker Engine)
+- A buildx builder must be configured. Create one if needed:
+  ```bash
+  docker buildx create --name solr-builder --use
+  ```
+- For cross-platform builds, QEMU is be required
+
+**Important**: When building for multiple platforms, you cannot use `--load` 
to load the image into your local Docker daemon. You must use `--push` to push 
directly to a registry.
+
+```bash
+# Build and push for multiple platforms
+docker buildx build --platform linux/amd64,linux/arm64 -f 
solr-X.Y.Z/docker/Dockerfile --tag myrepo/solr:X.Y.Z --push - < solr-X.Y.Z.tgz
+
+# Build for a single specific platform (can use --load for local testing)
+docker buildx build --platform linux/arm64 -f solr-X.Y.Z/docker/Dockerfile 
--tag myrepo/solr:X.Y.Z-arm64 --load - < solr-X.Y.Z.tgz
+```
+
 Official Image Management
 ----
 
diff --git a/solr/docker/build.gradle b/solr/docker/build.gradle
index 0efa18dc309..50170ce8ae9 100644
--- a/solr/docker/build.gradle
+++ b/solr/docker/build.gradle
@@ -34,6 +34,19 @@ def dockerImageRepo = "${ -> 
propertyOrEnvOrDefault("solr.docker.imageRepo", "SO
 def dockerImageTag = "${ -> propertyOrEnvOrDefault("solr.docker.imageTag", 
"SOLR_DOCKER_IMAGE_TAG", project.version + dockerImageDistSuffix) }"
 def dockerImageName = "${ -> propertyOrEnvOrDefault("solr.docker.imageName", 
"SOLR_DOCKER_IMAGE_NAME", 
"${dockerImageRepo}:${dockerImageTag}${dockerImageTagSuffix}") }"
 def baseDockerImage = "${ -> propertyOrEnvOrDefault("solr.docker.baseImage", 
"SOLR_DOCKER_BASE_IMAGE", 'eclipse-temurin:17-jre-jammy') }"
+def dockerImagePlatforms = "${ -> 
propertyOrEnvOrDefault("solr.docker.platform", "SOLR_DOCKER_PLATFORM", '') }"
+def isMultiPlatform = { -> !dockerImagePlatforms.isEmpty() && 
dockerImagePlatforms.contains(',') }
+def validatePlatformFormat = { platformValue ->
+  if (!platformValue.isEmpty()) {
+    def platformPattern = 
~/^linux\/[\w-]+(\/[\w-]+)?(,\s*linux\/[\w-]+(\/[\w-]+)?)*$/
+    if (!platformValue.matches(platformPattern)) {
+      throw new GradleException("Invalid platform format: ${platformValue}\n" +
+              "Platform must be in format 'linux/arch' or 'linux/arch/variant' 
(comma-separated for multiple).\n" +
+              "Examples: 'linux/amd64', 'linux/arm64', 'linux/arm/v7', 
'linux/amd64,linux/arm64'")
+    }
+  }
+}
+def getSolrTgzConfiguration = { -> isImageSlim() ? configurations.solrSlimTgz 
: configurations.solrFullTgz }
 def officialDockerImageName = {String dist -> "${ -> 
propertyOrEnvOrDefault("solr.docker.imageName", "SOLR_DOCKER_IMAGE_NAME", 
"${dockerImageRepo}-official:${dockerImageTag}${distToSuffix(dist)}${dockerImageTagSuffix}")
 }" }
 
 def releaseGpgFingerprint = "${ -> 
propertyOrDefault('signing.gnupg.keyName',propertyOrDefault('signing.keyId',''))
 }"
@@ -134,6 +147,32 @@ def checksum = { file ->
   return new DigestUtils(DigestUtils.sha512Digest).digestAsHex(file).trim()
 }
 
+// Helper method to get Docker Buildx builder info, throws exception if not 
available
+def getBuildxBuilderInfo = {
+  def builderCheckOutput = new ByteArrayOutputStream()
+  def builderCheckResult = exec {
+    ignoreExitValue = true
+    standardOutput = builderCheckOutput
+    errorOutput = new ByteArrayOutputStream()
+    commandLine "docker", "buildx", "inspect"
+  }
+
+  if (builderCheckResult.exitValue != 0) {
+    throw new GradleException("Docker Buildx is not available or no builder is 
configured.\n" +
+            "Please ensure Docker Buildx is installed and create a builder:\n" 
+
+            "  docker buildx create --name solr-builder --use\n" +
+            "Or use an existing builder:\n" +
+            "  docker buildx use <builder-name>")
+  }
+
+  return builderCheckOutput.toString()
+}
+
+// Helper method to validate Docker Buildx builder is available
+def validateBuildxBuilder = {
+  getBuildxBuilderInfo()
+}
+
 
 task assemblePackaging(type: Sync) {
   description = 'Assemble docker scripts and Dockerfile for Solr Packaging'
@@ -158,21 +197,51 @@ task dockerBuild() {
 
   // Ensure that the docker image is rebuilt on build-arg changes or changes 
in the docker context
   inputs.properties([
-          baseDockerImage: baseDockerImage
+          baseDockerImage: baseDockerImage,
+          dockerImagePlatforms: dockerImagePlatforms
   ])
-  var solrTgzConfiguration = isImageSlim() ? configurations.solrSlimTgz : 
configurations.solrFullTgz
+  var solrTgzConfiguration = getSolrTgzConfiguration()
   inputs.files(solrTgzConfiguration)
   inputs.property("isSlimImage", isImageSlim())
+  inputs.property("isMultiPlatform", isMultiPlatform())
   dependsOn(solrTgzConfiguration)
 
   doLast {
-    exec {
-      standardInput = solrTgzConfiguration.singleFile.newDataInputStream()
-      commandLine "docker", "build",
-              "-f", "solr-${ -> project.version 
}${dockerImageDistSuffix}/docker/Dockerfile",
-              "--iidfile", imageIdFile,
-              "--build-arg", "BASE_IMAGE=${ -> 
inputs.properties.baseDockerImage}",
-              "-"
+    def platformValue = 
inputs.properties.dockerImagePlatforms.toString().trim()
+    validatePlatformFormat(platformValue)
+    def useSinglePlatform = !platformValue.isEmpty() && 
!platformValue.contains(',')
+
+    // Multi-platform builds are not supported by dockerBuild, only by 
dockerPush
+    if (isMultiPlatform()) {
+      throw new GradleException("Multi-platform builds (SOLR_DOCKER_PLATFORM 
with multiple platforms) are not supported by dockerBuild.\n" +
+              "Please use 'dockerPush' instead, which will build and push the 
multi-platform image in a single step.\n" +
+              "Example: SOLR_DOCKER_PLATFORM=linux/arm64,linux/amd64 ./gradlew 
dockerPush")
+    }
+
+    if (useSinglePlatform) {
+      // Single-platform build with explicit platform using buildx
+      validateBuildxBuilder()
+
+      exec {
+        standardInput = solrTgzConfiguration.singleFile.newDataInputStream()
+        commandLine "docker", "buildx", "build",
+                "-f", "solr-${ -> project.version 
}${dockerImageDistSuffix}/docker/Dockerfile",
+                "--platform", platformValue,
+                "--build-arg", "BASE_IMAGE=${ -> 
inputs.properties.baseDockerImage}",
+                "--iidfile", imageIdFile,
+                "--load",
+                "-"
+      }
+    } else {
+      // Standard build for current architecture
+      exec {
+        standardInput = solrTgzConfiguration.singleFile.newDataInputStream()
+        commandLine "docker", "build",
+                "-f", "solr-${ -> project.version 
}${dockerImageDistSuffix}/docker/Dockerfile",
+                "--iidfile", imageIdFile,
+                "--build-arg", "BASE_IMAGE=${ -> 
inputs.properties.baseDockerImage}",
+                "-"
+      }
     }
   }
 
@@ -180,10 +249,13 @@ task dockerBuild() {
   doLast {
     def dockerImageId = file(imageIdFile).text
     project.logger.lifecycle("Solr Docker Image Created")
-    project.logger.lifecycle("\tID: \t${ -> dockerImageId }")
+    project.logger.lifecycle("\tID/Ref: \t${ -> dockerImageId }")
     project.logger.lifecycle("\tBase Image: \t${ -> baseDockerImage }")
     project.logger.lifecycle("\tSolr Version: \t${ -> project.version }")
     project.logger.lifecycle("\tSolr Distribution: \t${isImageSlim() ? "Slim" 
: "Full"}")
+    if (!dockerImagePlatforms.isEmpty()) {
+      project.logger.lifecycle("\tPlatforms: \t${ -> dockerImagePlatforms }")
+    }
   }
 
   outputs.files(imageIdFile)
@@ -239,6 +311,7 @@ task dockerPush(dependsOn: tasks.dockerTag) {
   // Ensure that the docker image is re-pushed if the image ID or tag changes
   inputs.properties([
           dockerImageName: dockerImageName,
+          dockerImagePlatforms: dockerImagePlatforms
   ])
   inputs.file(imageIdFile)
 
@@ -246,12 +319,55 @@ task dockerPush(dependsOn: tasks.dockerTag) {
   mustRunAfter tasks.testDocker
 
   doLast {
-    exec {
-      commandLine "docker", "push", dockerImageName
-    }
+    def platformValue = 
inputs.properties.dockerImagePlatforms.toString().trim()
+    validatePlatformFormat(platformValue)
+
+    if (isMultiPlatform()) {
+      // Multi-platform push requires buildx and rebuilding with --push
+
+      // Get builder info and validate it exists
+      def builderInfo = getBuildxBuilderInfo()
+
+      // Verify the builder supports the requested platforms (fail fast)
+      def requestedPlatforms = platformValue.split(",").collect { it.trim() }
+      def missingPlatforms = []
+      requestedPlatforms.each { platform ->
+        // Check for exact platform match - use delimiters instead of word 
boundaries
+        // Match platform at start/end or surrounded by commas/spaces
+        def platformPattern = "(?:^|[,\\s])" + Pattern.quote(platform) + 
"(?:[,\\s]|\$)"
+        if (!(builderInfo =~ platformPattern).find()) {
+          missingPlatforms.add(platform)
+        }
+      }
 
-    // Print information on the image after it has been created
-    project.logger.lifecycle("Solr Docker Image Pushed: \t$dockerImageName")
+      if (!missingPlatforms.isEmpty()) {
+        // Extract supported platforms from builder info using matcher
+        def supportedPlatforms = (builderInfo =~ 
/linux\/[\w-]+(?:\/[\w-]+)?/).collect().join(', ')
+        throw new GradleException("Current builder does not support all 
requested platforms: ${missingPlatforms}\n" +
+                "Supported platforms: ${supportedPlatforms}\n" +
+                "To add platform support, Install QEMU with proper binfmt 
support.")
+      }
+
+      def solrTgzConfiguration = getSolrTgzConfiguration()
+      exec {
+        standardInput = solrTgzConfiguration.singleFile.newDataInputStream()
+        commandLine "docker", "buildx", "build",
+                "-f", "solr-${ -> project.version 
}${dockerImageDistSuffix}/docker/Dockerfile",
+                "--platform", platformValue,
+                "--build-arg", "BASE_IMAGE=${ -> baseDockerImage}",
+                "--tag", dockerImageName,
+                "--push",
+                "-"
+      }
+      project.logger.lifecycle("Solr Docker Multi-Platform Image Pushed: 
\t$dockerImageName")
+      project.logger.lifecycle("\tPlatforms: \t$platformValue")
+    } else {
+      // Standard push (works for both standard builds and single-platform 
buildx builds)
+      exec {
+        commandLine "docker", "push", dockerImageName
+      }
+      project.logger.lifecycle("Solr Docker Image Pushed: \t$dockerImageName")
+    }
   }
 }
 

Reply via email to