diff --git a/README.md b/README.md index e1784b9..1be15c5 100644 --- a/README.md +++ b/README.md @@ -168,12 +168,22 @@ docker / imageNames := Seq( Use the key `docker / buildOptions` to set build options. -Example: +#### cross-platform +The `platforms` parameter enables the cross-platform build. +With valuing this parameter the docker image will build using `buildx` command and the host environment should already been set up for. + +⚠️ For using the cross builds you need QEMU binaries +```shell +docker run --privileged --rm tonistiigi/binfmt --install all +``` + +#### Example: ```scala docker / buildOptions := BuildOptions( cache = false, removeIntermediateContainers = BuildOptions.Remove.Always, pullBaseImage = BuildOptions.Pull.Always, + platforms = List("linux/arm64/v8"), additionalArguments = Seq("--add-host", "127.0.0.1:12345", "--compress") ) ``` diff --git a/project/build.properties b/project/build.properties index c8fcab5..8b9a0b0 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.2 +sbt.version=1.8.0 diff --git a/src/main/scala/sbtdocker/DockerBuild.scala b/src/main/scala/sbtdocker/DockerBuild.scala index 3f70c0e..c14087f 100644 --- a/src/main/scala/sbtdocker/DockerBuild.scala +++ b/src/main/scala/sbtdocker/DockerBuild.scala @@ -113,6 +113,8 @@ object DockerBuild { var lines = Seq.empty[String] def runBuild(buildKitSupport: Boolean): Int = { + val buildX = if (buildOptions.platforms.isEmpty) Nil else List("buildx") + val load = if (buildOptions.platforms.isEmpty) Nil else List("--load") val buildOptionFlags = generateBuildOptionFlags(buildOptions) val buildKitFlags = if (buildKitSupport) List("--progress=plain") else Nil val buildArgumentFlags = buildArguments.toList.flatMap { @@ -120,12 +122,14 @@ object DockerBuild { Seq(s"--build-arg", s"$key=$value") } val command: Seq[String] = dockerPath :: + buildX ::: "build" :: buildOptionFlags ::: buildKitFlags ::: buildArgumentFlags ::: + load ::: "--file" :: - dockerfilePath.name :: + dockerfileAbsolutePath.getPath :: dockerfileAbsolutePath.getParentFile.getPath :: Nil log.debug(s"Running command: '${command.mkString(" ")}'") @@ -187,18 +191,24 @@ object DockerBuild { } "--pull=" + value } + val platformsFlag: List[String] = buildOptions.platforms match { + case Seq() => Nil + case platforms => List(s"--platform=${platforms.mkString(",")}") + } - cacheFlag :: removeFlag :: pullFlag :: buildOptions.additionalArguments.toList + cacheFlag :: removeFlag :: pullFlag :: platformsFlag ::: buildOptions.additionalArguments.toList } private val SuccessfullyBuilt = "^Successfully built ([0-9a-f]+)$".r private val SuccessfullyBuiltBuildKit = ".* writing image sha256:([0-9a-f]+) .*\\bdone$".r + private val SuccessfullyBuiltBuildx = ".* exporting config sha256:([0-9a-f]+) .*\\bdone$".r private val SuccessfullyBuiltPodman = "^([0-9a-f]{64})$".r private[sbtdocker] def parseImageId(lines: Seq[String]): Option[ImageId] = { lines.collect { case SuccessfullyBuilt(id) => ImageId(id) case SuccessfullyBuiltBuildKit(id) => ImageId(id) + case SuccessfullyBuiltBuildx(id) => ImageId(id) case SuccessfullyBuiltPodman(id) => ImageId(id) }.lastOption } diff --git a/src/main/scala/sbtdocker/models.scala b/src/main/scala/sbtdocker/models.scala index d2d9940..8998b1e 100644 --- a/src/main/scala/sbtdocker/models.scala +++ b/src/main/scala/sbtdocker/models.scala @@ -31,12 +31,14 @@ object BuildOptions { * @param cache Use cache when building the image. * @param removeIntermediateContainers Remove intermediate containers after a build. * @param pullBaseImage Always attempts to pull a newer version of the base image. + * @param platforms Allows cross platform builds. Make sure that you already set docker's buildx up on the machine that uses this feature * @param additionalArguments Provide any other arguments to the `docker build` task, see reference at https://docs.docker.com/engine/reference/commandline/build/#options. For example `Seq("--add-host", "127.0.0.1:12345", "--compress")`. */ final case class BuildOptions( cache: Boolean = true, removeIntermediateContainers: BuildOptions.Remove.Option = BuildOptions.Remove.OnSuccess, pullBaseImage: BuildOptions.Pull.Option = BuildOptions.Pull.IfMissing, + platforms: Seq[String] = Seq.empty, additionalArguments: Seq[String] = Seq.empty ) diff --git a/src/test/scala/sbtdocker/DockerBuildSpec.scala b/src/test/scala/sbtdocker/DockerBuildSpec.scala index 72cb934..4e2f60b 100644 --- a/src/test/scala/sbtdocker/DockerBuildSpec.scala +++ b/src/test/scala/sbtdocker/DockerBuildSpec.scala @@ -98,6 +98,14 @@ class DockerBuildSpec extends AnyFreeSpec with Matchers { flags should contain("--pull=true") } + "Add platform argument for cross build" in { + val options = BuildOptions(platforms = List("linux/amd64", "linux/arm64")) + val flags = DockerBuild.generateBuildOptionFlags(options) + + flags should contain("--platform=linux/amd64,linux/arm64") + + } + "Custom arguments" in { val options = BuildOptions(additionalArguments = Seq("--add-host", "127.0.0.1:12345", "--compress")) val flags = DockerBuild.generateBuildOptionFlags(options) @@ -117,6 +125,18 @@ class DockerBuildSpec extends AnyFreeSpec with Matchers { DockerBuild.parseImageId(lines) shouldEqual Some(ImageId("353fcb84af6b")) } + "Docker buildx output" in { + val lines = Seq( + "#7 exporting layers 0.4s done", + "#7 exporting manifest sha256:427ff04564194e7d44a5790d5739465789861cb5ee6db60ccdb15388865dfd64 0.0s done", + "#7 exporting config sha256:d1d7dbb3987417e91239032f2a36a2ede76e62276849ebf362004c14d6fc82ac", + "#7 exporting config sha256:d1d7dbb3987417e91239032f2a36a2ede76e62276849ebf362004c14d6fc82ac 0.0s done", + "#7 sending tarball" + ) + DockerBuild.parseImageId(lines) shouldEqual Some(ImageId("d1d7dbb3987417e91239032f2a36a2ede76e62276849ebf362004c14d6fc82ac")) + + } + "Docker build output version 20.10.10" in { val lines = Seq( "#6 exporting to image",