From e1c059851f7c4bbd7cd1a33058e74af02f900a1b Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 10 Oct 2025 09:16:20 +0200 Subject: [PATCH 1/5] Bump `scala-packager` to 0.2.1 --- .../src/main/scala/scala/cli/commands/package0/Package.scala | 3 ++- project/deps/package.mill.scala | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala index bc9884a02d..39454e2255 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala @@ -683,7 +683,8 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { repository = repository, tag = Some(tag), exec = exec, - dockerExecutable = None + dockerExecutable = None, + extraDirectories = Seq.empty ) val appPath = os.temp.dir(prefix = "scala-cli-docker") / "app" diff --git a/project/deps/package.mill.scala b/project/deps/package.mill.scala index 130c38669a..12ef205f33 100644 --- a/project/deps/package.mill.scala +++ b/project/deps/package.mill.scala @@ -134,7 +134,7 @@ object Deps { def maxScalaNativeForTypelevelToolkit = scalaNative04 def maxScalaNativeForScalaPy = scalaNative04 def maxScalaNativeForMillExport = scalaNative05 - def scalaPackager = "0.2.0" + def scalaPackager = "0.2.1" def signingCli = "0.2.11" def signingCliJvmVersion = Java.defaultJava def javaSemanticdb = "0.10.0" From e2534e6b3fc7995517b15d1ba322edb78a59f44e Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 10 Oct 2025 09:37:36 +0200 Subject: [PATCH 2/5] Add `--docker-extra-directories` command line option for the `package` sub-command --- .../src/main/scala/scala/cli/commands/package0/Package.scala | 2 +- .../scala/scala/cli/commands/package0/PackageOptions.scala | 3 ++- .../scala/scala/cli/commands/package0/PackagerOptions.scala | 5 +++++ .../scala/scala/build/options/packaging/DockerOptions.scala | 3 ++- website/docs/reference/cli-options.md | 4 ++++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala index 39454e2255..f859c5f581 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala @@ -684,7 +684,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { tag = Some(tag), exec = exec, dockerExecutable = None, - extraDirectories = Seq.empty + extraDirectories = packageOptions.dockerOptions.extraDirectories.map(_.toNIO) ) val appPath = os.temp.dir(prefix = "scala-cli-docker") / "app" diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala index de7a47d814..ee9f9c6236 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala @@ -219,7 +219,8 @@ final case class PackageOptions( imageRepository = packager.dockerImageRepository, imageTag = packager.dockerImageTag, cmd = packager.dockerCmd, - isDockerEnabled = Some(docker) + isDockerEnabled = Some(docker), + extraDirectories = packager.dockerExtraDirectories.map(os.Path(_, os.pwd)) ), nativeImageOptions = NativeImageOptions( graalvmJvmId = packager.graalvmJvmId.map(_.trim).filter(_.nonEmpty), diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/PackagerOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/PackagerOptions.scala index 82a92097b1..5ccf732b43 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/PackagerOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/PackagerOptions.scala @@ -138,6 +138,11 @@ final case class PackagerOptions( ) @Tag(tags.restricted) dockerCmd: Option[String] = None, + + @Group(HelpGroup.Docker.toString) + @HelpMessage("Extra directories to be added to the docker image") + @Tag(tags.restricted) + dockerExtraDirectories: List[String] = Nil, @Group(HelpGroup.NativeImage.toString) @HelpMessage(s"GraalVM Java major version to use to build GraalVM native images (${Constants.defaultGraalVMJavaVersion} by default)") diff --git a/modules/options/src/main/scala/scala/build/options/packaging/DockerOptions.scala b/modules/options/src/main/scala/scala/build/options/packaging/DockerOptions.scala index 71ae8d52f0..0346d8f9b1 100644 --- a/modules/options/src/main/scala/scala/build/options/packaging/DockerOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/packaging/DockerOptions.scala @@ -8,7 +8,8 @@ final case class DockerOptions( imageRepository: Option[String] = None, imageTag: Option[String] = None, cmd: Option[String] = None, - isDockerEnabled: Option[Boolean] = None + isDockerEnabled: Option[Boolean] = None, + extraDirectories: Seq[os.Path] = Nil ) object DockerOptions { diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index ccb9728014..7f108cf66b 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -972,6 +972,10 @@ The image tag; the default tag is `latest` Allows to override the executable used to run the application in docker, otherwise it defaults to sh for the JVM platform and node for the JS platform +### `--docker-extra-directories` + +Extra directories to be added to the docker image + ### `--graalvm-java-version` GraalVM Java major version to use to build GraalVM native images (17 by default) From 76f9e328b52388a9e7a428b9cc6807734e99a369 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 10 Oct 2025 09:54:11 +0200 Subject: [PATCH 3/5] Add `//> using packager.dockerExtraDirectories` directive --- .../errors/WrongDirectoryPathError.scala | 7 ++++ .../preprocessing/directives/Packaging.scala | 37 +++++++++++++++++-- website/docs/reference/directives.md | 7 ++++ 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 modules/directives/src/main/scala/scala/build/errors/WrongDirectoryPathError.scala diff --git a/modules/directives/src/main/scala/scala/build/errors/WrongDirectoryPathError.scala b/modules/directives/src/main/scala/scala/build/errors/WrongDirectoryPathError.scala new file mode 100644 index 0000000000..46530f6021 --- /dev/null +++ b/modules/directives/src/main/scala/scala/build/errors/WrongDirectoryPathError.scala @@ -0,0 +1,7 @@ +package scala.build.errors + +class WrongDirectoryPathError(cause: Throwable) extends BuildException( + message = s"""The directory path argument in the using directives at could not be found! + |${cause.getLocalizedMessage}""".stripMargin, + cause = cause + ) diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Packaging.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Packaging.scala index 3d7a2eab29..77d7f3775c 100644 --- a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Packaging.scala +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Packaging.scala @@ -10,11 +10,13 @@ import scala.build.errors.{ BuildException, CompositeBuildException, MalformedInputError, - ModuleFormatError + ModuleFormatError, + WrongDirectoryPathError } import scala.build.options.* import scala.build.options.packaging.{DockerOptions, NativeImageOptions} import scala.cli.commands.SpecificationLevel +import scala.util.Try @DirectiveGroupName("Packaging") @DirectivePrefix("packaging.") @@ -28,6 +30,10 @@ import scala.cli.commands.SpecificationLevel @DirectiveExamples("//> using packaging.dockerImageRepository scala-cli") @DirectiveExamples("//> using packaging.dockerCmd sh") @DirectiveExamples("//> using packaging.dockerCmd node") +@DirectiveExamples( + "//> using packaging.dockerExtraDirectories path/to/directory1 path/to/directory2" +) +@DirectiveExamples("//> using packaging.dockerExtraDirectory path/to/directory") @DirectiveUsage( """using packaging.packageType [package type] |using packaging.output [destination path] @@ -38,6 +44,7 @@ import scala.cli.commands.SpecificationLevel |using packaging.dockerImageRegistry [image registry] |using packaging.dockerImageRepository [image repository] |using packaging.dockerCmd [docker command] + |using packaging.dockerExtraDirectories [directories] |""".stripMargin, """`//> using packaging.packageType` _package-type_ | @@ -57,6 +64,9 @@ import scala.cli.commands.SpecificationLevel | |`//> using packaging.dockerCmd` _docker-command_ | + |`//> using packaging.dockerExtraDirectories` _directories_ + |`//> using packaging.dockerExtraDirectory` _directory_ + | |""".stripMargin ) @DirectiveDescription("Set parameters for packaging") @@ -70,7 +80,10 @@ final case class Packaging( dockerImageTag: Option[String] = None, dockerImageRegistry: Option[String] = None, dockerImageRepository: Option[String] = None, - dockerCmd: Option[String] = None + dockerCmd: Option[String] = None, + @DirectiveName("dockerExtraDirectory") + dockerExtraDirectories: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = + DirectiveValueParser.WithScopePath.empty(Nil) ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = either { val maybePackageTypeOpt = packageType @@ -110,6 +123,23 @@ final case class Packaging( .left.map(CompositeBuildException(_)) } + val cwd = dockerExtraDirectories.scopePath + val extraDirectories = value { + dockerExtraDirectories + .value + .map { posPathStr => + val eitherRootPathOrBuildException = + Directive.osRoot(cwd, posPathStr.positions.headOption) + eitherRootPathOrBuildException.flatMap { root => + Try(os.Path(posPathStr.value, root)) + .toEither + .left.map(new WrongDirectoryPathError(_)) + } + } + .sequence + .left.map(CompositeBuildException(_)) + } + BuildOptions( internal = InternalOptions( keepResolution = provided0.nonEmpty || packageTypeOpt.contains(PackageType.Spark) @@ -124,7 +154,8 @@ final case class Packaging( imageRegistry = dockerImageRegistry, imageRepository = dockerImageRepository, imageTag = dockerImageTag, - cmd = dockerCmd + cmd = dockerCmd, + extraDirectories = extraDirectories ), nativeImageOptions = NativeImageOptions( graalvmArgs = graalvmArgs diff --git a/website/docs/reference/directives.md b/website/docs/reference/directives.md index a52e7cef31..04a424d341 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -300,6 +300,9 @@ Set parameters for packaging `//> using packaging.dockerCmd` _docker-command_ +`//> using packaging.dockerExtraDirectories` _directories_ +`//> using packaging.dockerExtraDirectory` _directory_ + #### Examples @@ -323,6 +326,10 @@ Set parameters for packaging `//> using packaging.dockerCmd node` +`//> using packaging.dockerExtraDirectories path/to/directory1 path/to/directory2` + +`//> using packaging.dockerExtraDirectory path/to/directory` + ### Platform Set the default platform to Scala.js or Scala Native From 3139f7c5eba81d0aa2dd59ffb5b246078ecd5c9d Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 10 Oct 2025 11:14:57 +0200 Subject: [PATCH 4/5] Add a simple test for adding an extra directory to a docker image --- .../integration/PackageTestDefinitions.scala | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index b75064349c..be36e10ed4 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -1118,13 +1118,61 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio } } - if (Properties.isLinux) + def dockerWithExtraDirsTest(): Unit = { + val codePath = os.rel / "src" / "Hello.scala" + val extraFileName = "extraFile.txt" + val extraFileDir = os.rel / "extraDir" + val extraFilePath = extraFileDir / extraFileName + val imageName = "extradir" + val expectedOutput = "hello" + val inputs = TestInputs( + codePath -> + s"""//> using toolkit default + | + |object Smth extends App { + | val file = + | os.list(os.pwd) + | .filter(os.isFile) + | .filter(_.endsWith(os.rel / "$extraFileName")) + | .head + | val contents = os.read(file).trim() + | println(contents) + |} + |""".stripMargin, + extraFilePath -> expectedOutput + ) + inputs.fromRoot { root => + os.proc( + TestUtil.cli, + "--power", + "package", + codePath, + "--docker", + "--docker-image-repository", + imageName, + "--docker-extra-directories", + root / extraFileDir, + "-f" + ).call(cwd = root) + val output = os.proc("docker", "run", imageName).call(cwd = root).out.trim() + expect(output == expectedOutput) + } + } + + if (Properties.isLinux) { test("pass java options to docker") { TestUtil.retryOnCi() { javaOptionsDockerTest() } } + test("pass extra directory to docker") { + TestUtil.retryOnCi() { + dockerWithExtraDirsTest() + } + } + } + test("default values in help") { TestInputs.empty.fromRoot { root => val res = os.proc(TestUtil.cli, "--power", "package", extraOptions, "--help").call(cwd = root) From ea365d0e6ebdac874a3b05cb64edd1ee1bd56a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Tomala?= Date: Wed, 22 Oct 2025 09:04:44 +0200 Subject: [PATCH 5/5] fix 'pass extra directory to docker' test --- .../cli/integration/PackageTestDefinitions.scala | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index be36e10ed4..a6d089608a 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -1130,13 +1130,14 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio s"""//> using toolkit default | |object Smth extends App { - | val file = - | os.list(os.pwd) - | .filter(os.isFile) - | .filter(_.endsWith(os.rel / "$extraFileName")) - | .head - | val contents = os.read(file).trim() - | println(contents) + | val content = + | os.walk(os.pwd) + | .filter(os.isFile) + | .filter(_.endsWith(os.rel / "$extraFileName")) + | .headOption + | .map(file => os.read(file).trim()) + | .getOrElse("No matching files found") + | println(content) |} |""".stripMargin, extraFilePath -> expectedOutput