Scala Packaging & Publishing

This page will discuss common topics around packaging and publishing your Scala projects for others to use

Building Executable Assemblies

Mill’s built in .assembly task makes it easy to generate an executable assembly jar from any JVM module. You can also customize the assembly jar as shown below:

build.mill (download, browse)
package build
import mill._, scalalib._
import mill.scalalib.Assembly._

object foo extends ScalaModule {
  def moduleDeps = Seq(bar)
  def scalaVersion = "2.13.8"
  def mvnDeps = Seq(mvn"com.lihaoyi::os-lib:0.10.7")
  def assemblyRules = Seq(
    // all application.conf files will be concatenated into single file
    Rule.Append("application.conf"),
    // all *.conf files will be concatenated into single file
    Rule.AppendPattern(".*\\.conf"),
    // all *.temp files will be excluded from a final jar
    Rule.ExcludePattern(".*\\.temp"),
    // the `shapeless` package will be relocated under the `shade` package
    Rule.Relocate("shapeless.**", "shade.shapless.@1")
  )

  // def manifest: T[JarManifest]
  // def prependShellScript: T[String]
}

object bar extends ScalaModule {
  def scalaVersion = "2.13.8"
}

The most common way of configuring an assembly is excluding some files from a final jar (like signature files, and manifest files from library jars), and merging duplicated files (for instance reference.conf files from library dependencies). This is done by overriding def assemblyRules as shown above

By default mill excludes all *.sf, *.dsa, *.rsa, and META-INF/MANIFEST.MF files from assembly, and concatenates all reference.conf files. You can also define your own merge/exclude rules.

> ./mill foo.assembly

> unzip -p ./out/foo/assembly.dest/out.jar application.conf || true
Bar Application Conf
Foo Application Conf

> java -jar ./out/foo/assembly.dest/out.jar
Loaded application.conf from resources:...
...Foo Application Conf
...Bar Application Conf

Note that when running the assembly directly via ./out.jar, you can configure JVM flags via the JAVA_OPTS environment variable, and select the JVM to use via JAVA_HOME.

> JAVA_OPTS=-Dtest.property=1337 ./out/foo/assembly.dest/out.jar
Loaded test.property: 1337

Building Native Image Binaries with Graal VM

build.mill (download, browse)
package build
import mill._, scalalib._
import mill.define.ModuleRef

object foo extends ScalaModule with NativeImageModule {
  def scalaVersion = "2.13.11"

  def nativeImageOptions = Seq("--no-fallback")

  def jvmWorker = ModuleRef(JvmWorkerGraalvm)
}

object JvmWorkerGraalvm extends JvmWorkerModule {
  def jvmId = "graalvm-community:17.0.7"
}

This example uses NativeImageModule to generate a native executable using Graal VM. We recommend you configure a specific JDK version via a custom JvmWorkerModule overriding def jvmId (shown above), as not every JVM can build Graal native images.

> ./mill show foo.nativeImage
GraalVM Native Image: Generating...native-executable...
Finished generating...native-executable...

> ./out/foo/nativeImage.dest/native-executable
Hello, World!

For another example building a slightly less trivial project into a Graal native image, see below:

build.mill (download, browse)
package build
import mill._, scalalib._
import mill.define.ModuleRef

object foo extends ScalaModule with NativeImageModule {
  def scalaVersion = "2.13.11"

  def nativeImageOptions = Seq("--no-fallback", "-Os")

  def mvnDeps = Seq(
    mvn"com.lihaoyi::scalatags:0.13.1",
    mvn"com.lihaoyi::mainargs:0.6.2"
  )

  def jvmWorker = ModuleRef(JvmWorkerGraalvm)
}

object JvmWorkerGraalvm extends JvmWorkerModule {
  def jvmId = "graalvm-community:23.0.1"
}

This example shows how to generate native images for projects using third-party libraries, in this case Scalatags and Mainargs. We also demonstrate setting using -Os to optimize for the smallest binary size which is available in the graalvm-community:23 JDK selected above

> ./mill show foo.nativeImage

> ./out/foo/nativeImage.dest/native-executable --text hello-world
<h1>hello-world</h1>

You can see the Graal documentation to see what flags are available:

Or access the native-image compiler directly via show foo.nativeImageTool if you want to experiment it or view its --help text to see what you need to pass to nativeImageOptions:

> ./mill show foo.nativeImageTool # mac/linux
".../bin/native-image"

> ./mill show foo.nativeImageTool # windows
".../bin/native-image.cmd"

For more details on using Graal, check the this blog post:

Publishing Locally

build.mill (download, browse)
package build
import mill._, scalalib._, publish._

object foo extends ScalaModule with PublishModule {
  def scalaVersion = "2.13.8"
  def publishVersion = "0.0.1"

  def pomSettings = PomSettings(
    description = "Hello",
    organization = "com.lihaoyi",
    url = "https://github.com/lihaoyi/example",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github("lihaoyi", "example"),
    developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
  )
}

This is an example ScalaModule with added publishing capabilities via PublishModule. This requires that you define an additional publishVersion and pomSettings with the relevant metadata, and provides the .publishLocal and publishSigned tasks for publishing locally to the machine or to the central maven repository

> mill foo.publishLocal # publish specific modules

> mill __.publishLocal # publish every eligible module
Publishing Artifact(com.lihaoyi,foo_2.13,0.0.1) to ivy repo...

publishLocal publishes the artifacts to the ~/.ivy2/local folder on your machine, allowing them to be resolved by other projects and build tools. This is useful as a lightweight way of testing out the published artifacts, without the setup overhead and long latencies of publishing artifacts globally accessible to anyone in the world.

publishLocal accepts options like --doc=false and --sources=false, to disable publishing javadoc JARs and source JARs, which are generated and published by default. This can be helpful if you’re not interested in javadoc JARs, and javadoc generation fails or takes too much time. When using Scala 2, disabling javadoc generation can bring large speedups, given it entails compiling your code a second time.

publishLocal also accepts --transitive=true, to also publish locally the transitive dependencies of the module being published. This ensures the module can be resolved from the local repository, with no missing dependencies.

Publlishing to Sonatype Maven Central

build.mill (download, browse)
package build
import mill._, scalalib._, publish._

object foo extends ScalaModule with SonatypeCentralPublishModule {
  def scalaVersion = "2.13.8"
  def publishVersion = "0.0.1"

  def pomSettings = PomSettings(
    description = "Hello",
    organization = "com.lihaoyi",
    url = "https://github.com/lihaoyi/example",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github("lihaoyi", "example"),
    developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
  )
}

This example configured for publishing to Sonatype Maven Central via Central Portal. Extends SonatypeCentralPublishModule which provides simplified publishing tasks without requiring Nexus repository manager.

Instructions for Publishing to Maven Central via Central Portal

Once you’ve mixed in PublishModule, apart from publishing locally, you can also publish your project’s modules to maven central

GPG

If you’ve never created a keypair before that can be used to sign your artifacts you’ll need to do this. Sonatype’s GPG Documentation has the instructions on how to do this

Publishing Secrets

Mill uses the following environment variables as a way to pass the necessary secrets for publishing:

# Sonatype Central Portal needs your public key to be uploaded so it can use for verification of artifacts from their end.
#
# Send your public key to ubuntu server so Sonatype Maven Central can use for verification of the artifacts
gpg --keyserver  keyserver.ubuntu.com --send-keys $LONG_ID
#
# Check the server for information about the public key. information will be displayed if found
gpg --keyserver  keyserver.ubuntu.com --recv-keys $LONG_ID
#
# The LHS and RHS of the User Token, accessible through the sonatype
# website `Profile` / `User Token` / `Access User Token`
export MILL_SONATYPE_USERNAME=...
export MILL_SONATYPE_PASSWORD=...

# The base-64 encoded PGP key, which can be encoded in the following way
# for each OS:
#
# MacOS or FreeBSD
# gpg --export-secret-key -a $LONG_ID | base64
#
# Ubuntu (assuming GNU base64)
# gpg --export-secret-key -a $LONG_ID | base64 -w0
#
# Arch
# gpg --export-secret-key -a $LONG_ID | base64 | sed -z 's;\n;;g'
#
# Windows
# gpg --export-secret-key -a %LONG_ID% | openssl base64
export MILL_PGP_SECRET_BASE64=...

# The passphrase associated with your PGP key
export MILL_PGP_PASSPHRASE=...

Publishing

You can publish all eligible modules in your Mill project using mill.scalalib.SonatypeCentralPublishModule/:

> mill mill.scalalib.SonatypeCentralPublishModule/

You can also specify individual modules you want to publish in two ways:

> mill foo.publishSonatypeCentral
> mill mill.scalalib.SonatypeCentralPublishModule/ --publishArtifacts foo.publishArtifacts

Publishing Using Github Actions

To publish on Github Actions, you can use something like this:

# .github/workflows/publish-artifacts.yml
name: Publish Artifacts
on:
  push:
    tags:
      - '**'
  workflow_dispatch:
jobs:
  publish-artifacts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
      - run: ./mill mill.scalalib.SonatypeCentralPublishModule/
        env:
          MILL_PGP_PASSPHRASE: ${{ secrets.MILL_PGP_PASSPHRASE }}
          MILL_PGP_SECRET_BASE64: ${{ secrets.MILL_PGP_SECRET_BASE64 }}
          MILL_SONATYPE_PASSWORD: ${{ secrets.MILL_SONATYPE_PASSWORD }}
          MILL_SONATYPE_USERNAME: ${{ secrets.MILL_SONATYPE_USERNAME }}

Where MILL_PGP_PASSPHRASE, MILL_PGP_SECRET_BASE64, MILL_SONATYPE_PASSWORD, and MILL_SONATYPE_USERNAME configured for the repository’s or organization’s Github Actions workflows. See Using Secrets in Github Actions for more details.

Instructions for Publishing to Maven Central via Legacy OSSHR (Deprecated)

Publishing via the legacy OSSRH (OSS Repository Hosting) is deprecated and will reach end-of-life on June 30, 2025, due to the retirement of Sonatype’s Nexus Repository Manager v2. Sonatype now recommends using the Central Portal for all new publishing. Migration is strongly encouraged to avoid disruptions. For full details, see the OSSRH Sunset Announcement.

Just like publishing via the Central Portal requires a GPG key and publish secrets, publishing via the legacy OSSHR(OSS Repository Hosting) also requires them.

Publishing

You can publish all eligible modules in your Mill project using the default task of the External Module mill.scalalib.PublishModule:

> mill mill.scalalib.PublishModule/

You can also specify individual modules you want to publish via a selector:

> mill mill.scalalib.PublishModule/ --publishArtifacts foo.publishArtifacts

The default URL for publishing to sonatype’s Maven Central is oss.sonatype.org. Newer projects registered on sonatype may need to publish using s01.oss.sonatype.org. In that case, you can pass in a --sonatypeUri:

> mill mill.scalalib.PublishModule/ \
        --sonatypeUri https://s01.oss.sonatype.org/service/local

This also allows you to publish to your own internal corporate sonatype deployment, by passing in --sonatypeUri example.company.com instead.

Since Feb. 2021 any new Sonatype accounts have been created on s01.oss.sonatype.org, so you’ll want to ensure you set the relevant URIs to match.

The symptom of using the "wrong" URL for publishing is typically a 403 error code, in response to the publish request.

Typically

Publishing Using Github Actions

To publish on Github Actions, you can use something like this:

# .github/workflows/publish-artifacts.yml
name: Publish Artifacts
on:
  push:
    tags:
      - '**'
  workflow_dispatch:
jobs:
  publish-artifacts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
      - run: ./mill mill.scalalib.PublishModule/
        env:
          MILL_PGP_PASSPHRASE: ${{ secrets.MILL_PGP_PASSPHRASE }}
          MILL_PGP_SECRET_BASE64: ${{ secrets.MILL_PGP_SECRET_BASE64 }}
          MILL_SONATYPE_PASSWORD: ${{ secrets.MILL_SONATYPE_PASSWORD }}
          MILL_SONATYPE_USERNAME: ${{ secrets.MILL_SONATYPE_USERNAME }}

Where MILL_PGP_PASSPHRASE, MILL_PGP_SECRET_BASE64, MILL_SONATYPE_PASSWORD, and MILL_SONATYPE_USERNAME configured for the repository’s or organization’s Github Actions workflows. See Using Secrets in Github Actions for more details.

Non-Staging Releases (classic Maven uploads)

If the site does not support staging releases as oss.sonatype.org and s01.oss.sonatype.org do (for example, a self-hosted OSS nexus site), you can pass in the --stagingRelease false option to simply upload release artifacts to corresponding maven path under sonatypeUri instead of staging path.

> mill mill.scalalib.PublishModule/ \
        --publishArtifacts foo.publishArtifacts \
        --sonatypeCreds lihaoyi:$SONATYPE_PASSWORD \
        --sonatypeUri http://example.company.com/release \
        --stagingRelease false

SonatypeCentralPublishModule Configurations

This module provides settings and a CLI interface for publishing artifacts to Sonatype Maven Central. You can configure it through your build.mill file or by passing command-line options to it.

Module-Level Settings

You can override default publishing settings in your build.mill like this:

object mymodule extends SonatypeCentralPublishModule {
  override def sonatypeCentralGpgArgs: T[String] = "--batch, --yes, -a, -b"

  override def sonatypeCentralConnectTimeout: T[Int] = 5000

  override def sonatypeCentralReadTimeout: T[Int] = 60000

  override def sonatypeCentralAwaitTimeout: T[Int] = 120 * 1000

  override def sonatypeCentralShouldRelease: T[Boolean] = true
  ...
}

Argument Reference

publishAll

The publishAll task can be called from the CLI. If a required value is not provided via the CLI option, it will fall back to an environment variable (if available) or raise an error if missing.

The ./mill mill.scalalib.SonatypeCentralPublishModule/publishAll takes the following options:

username: The username for calling the Sonatype Central publishing api. Defaults to the SONATYPE_USERNAME environment variable if unset. If neither the parameter nor the environment variable are set, an error will be thrown.

password: The password for calling the Sonatype Central publishing api. Defaults to the SONATYPE_PASSWORD environment variable if unset. If neither the parameter nor the environment variable are set, an error will be thrown.

gpgArgs: Arguments to pass to the gpg package for signing artifacts. Uses the MILL_PGP_PASSPHRASE environment variable if set. Default: [--passphrase=$MILL_PGP_PASSPHRASE], --no-tty, --pinentry-mode, loopback, --batch, --yes, -a, -b.

publishArtifacts: The command for generating all publishable artifacts (ex. __.publishArtifacts). Required.

readTimeout: The timeout for receiving a response from Sonatype Central after the initial connection has occurred. Default: 60000.

awaitTimeout: The overall timeout for all retries (including exponential backoff) of the bundle upload. Default: 120 * 1000.

connectTimeout: The timeout for the initial connection to Sonatype Central if there is no response. Default: 5000.

shouldRelease: Whether the bundle should be automatically released when uploaded to Sonatype Central. If false, the bundle will still be uploaded, but users will need to manually log in to Sonatype Central and publish the bundle from the portal. Default: true

bundleName: If set, all packages will be uploaded in a single bundle with the given name. If unset, packages will be uploaded separately. Recommended bundle name syntax: groupName-artifactId-versionNumber. As an example, if publishing the com.lihaoyi requests package, without the bundle name, four different bundles will be uploaded, one for each scala version supported. With a bundle name of com.lihaoyi-requests-<new_version>, a single bundle will be uploaded that contains all packages across scala versions. It is recommended to set the bundle name, so that packages can be verified and deployed together. Default: No bundle name is set and packages will be uploaded separately

Example command

$ mill -i \
mill.scalalib.SonatypeCentralPublishModule/publishAll \
--username myusername \
--password mypassword \
--gpgArgs --passphrase=$MILL_PGP_PASSPHRASE,--no-tty,--pinentry-mode,loopback,--batch,--yes,-a,-b \
--publishArtifacts __.publishArtifacts \
--readTimeout  36000 \
--awaitTimeout 36000 \
--connectTimeout 36000 \
--shouldRelease false \
--bundleName com.lihaoyi-requests:1.0.0

publishSonatypeCentral

The __.publishSonatypeCentral command takes the username and password arguments, documented above.

Publishing to other repositories

While Sonatype Maven Central is the default publish repository for JVM ecosystem projects, there are also others that you can use. Mill supports these largely through contrib plugins:

Mill has builtin support for the JLink and JPackage command line tools. For more details, see: