Common Project Layouts

Earlier, we have shown how to work with the Mill default Scala module layout. Here we will explore some other common project layouts that you may want in your Scala build:

Java Project with Test Suite

build.sc
object core extends JavaModule {
  object test extends Tests with TestModule.Junit4
}
object app extends JavaModule {
  def moduleDeps = Seq(core)
  object test extends Tests with TestModule.Junit4
}

This build is a two-module Java project with junit test suites. It expects the following filesystem layout:

build.sc
app/
    src/hello/
        Main.java
    test/src/hello/
            MyAppTests.java
core/
    src/hello/
        Core.java
    test/src/hello/
            MyCoreTests.java

You can then run the junit tests using mill app.test or mill core.test, and configure which exact tests you want to run using the flags defined on the JUnit Test Interface.

For a more more complex, real-world example of a Java build, check out our example build for the popular Caffeine project:

Cross Scala-Version Modules

build.sc
import mill._
import mill.scalalib._
object foo extends Cross[FooModule]("2.10.6", "2.11.11", "2.12.4")
class FooModule(val crossScalaVersion: String) extends CrossScalaModule {
   ...
   object test extends Tests {
     ...
   }
}

Mill provides a CrossScalaModule template, which can be used with Cross to cross-build Scala modules across different versions of Scala. The default configuration for CrossScalaModule expects a filesystem layout as follows:

build.sc
foo/
    src/
    src-2.10/
    src-2.11/
    src-2.12/
    test/
        src/
        src-2.10/
        src-2.11/
        src-2.12/

Code common to all Scala versions lives in src, while code specific to one version lives in src-x.y.

Scala.js Modules

build.sc
import mill._
import mill.scalalib._
import mill.scalajslib._
import mill.scalajslib.api._

object foo extends ScalaJSModule {
  def scalaVersion = "2.13.8"
  def scalaJSVersion = "1.10.1"
  def ivyDeps = Agg(ivy"org.scala-js::scalajs-dom::2.2.0")
  def moduleKind = T { ModuleKind.CommonJSModule }
  object test extends Tests with TestModule.Utest {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.8.0")
  }
}

ScalaJSModule is a variant of ScalaModule that builds your code using Scala.js. In addition to the standard foo.compile and foo.run commands (the latter of which runs your code on Node.js, which must be pre-installed) ScalaJSModule also exposes the foo.fastLinkJS and foo.fullLinkJS tasks for generating the optimized Javascript file.

ScalaJSModule requires declaring it’s dependencies as third party libraries (don’t forget to use double-colons :: between the library name and the version for Scala.js dependencies).

If you are following the Scala.js Tutorial, in the section where you test the code, it’s mentioned that you need to make the DOM available to Node.js. Install the jsdom node library as instructed then add def jsEnvConfig = T(JsEnvConfig.JsDom()) to your build.sc object. In Scala 3, the "scalajs-env-jsdom-nodejs" mentioned in the tutorial is no longer required.

Scala Native Modules

build.sc
import mill._, scalalib._, scalanativelib._, mill.scalanativelib.api._

object hello extends ScalaNativeModule {
  def scalaVersion = "2.11.12"
  def scalaNativeVersion = "0.3.8"
  def logLevel = NativeLogLevel.Info // optional
  def releaseMode = ReleaseMode.Debug // optional
}
.
├── build.sc
└── hello
    ├── src
    │   └── hello
    │       └── Hello.scala
hello/src/hello/Hello.scala
package hello
import scalatags.Text.all._
object Hello{
  def main(args: Array[String]): Unit = {
    println("Hello! " + args.toList)
    println(div("one"))
  }
}

The normal commands mill hello.compile, mill hello.run, all work. If you want to build a standalone executable, you can use mill show hello.nativeLink to create it.

ScalaNativeModule builds scala sources to executable binaries using Scala Native. You will need to have the relevant parts of the LLVM toolchain installed on your system. Optimized binaries can be built by setting releaseMode (see above) and more verbose logging can be enabled using logLevel. Currently two test frameworks are supported utest and scalatest. Support for scalacheck should be possible when the relevant artifacts have been published for scala native.

Here’s a slightly larger example, demonstrating how to use third party dependencies (note the two sets of double-colons :: necessary) and a test suite:

build.sc
import mill._, scalalib._, scalanativelib._

object hello extends ScalaNativeModule {
  def scalaNativeVersion = "0.3.8"
  def scalaVersion = "2.11.12"
  def ivyDeps = Agg(ivy"com.lihaoyi::scalatags::0.6.7")
  object test extends ScalaNativeTests with TestModule.Utest {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.6.3")
  }
}
.
├── build.sc
└── hello
    ├── src
    │   └── hello
    │       └── Hello.scala
    └── test
        └── src
            └── HelloTests.scala
hello/test/src/HelloTests.scala
package hello
import utest._
import scalatags.Text.all._
object HelloTests extends TestSuite{
  val tests = Tests{
    'pass - {
      assert(div("1").toString == "<div>1</div>")
    }
    'fail - {
      assert(123 == 1243)
    }
  }
}

The same mill hello.compile or mill hello.run still work, as does `mill hello.test to run the test suite defined here.

SBT-Compatible Modules

build.sc
import mill._
import mill.scalalib._

object foo extends SbtModule {
  def scalaVersion = "2.12.4"
}

These are basically the same as normal ScalaModules, but configured to follow the SBT project layout:

build.sc
foo/
    src/
        main/
            scala/
        test/
            scala/

Useful if you want to migrate an existing project built with SBT without having to re-organize all your files

SBT-Compatible Cross Scala-Version Modules

build.sc
import mill._
import mill.scalalib._
object foo extends Cross[FooModule]("2.10.6", "2.11.11", "2.12.4")
class FooModule(val crossScalaVersion: String) extends CrossSbtModule {
   ...
   object test extends Tests {
     ...
   }
}

A CrossSbtModule is a version of CrossScalaModule configured with the SBT project layout:

build.sc
foo/
    src/
        main/
            scala/
            scala-2.10/
            scala-2.11/
            scala-2.12/
        test/
            scala/
            scala-2.10/
            scala-2.11/
            scala-2.12/

Publishing

build.sc
import mill._
import mill.scalalib._
import mill.scalalib.publish._
object foo extends ScalaModule with PublishModule {
  def scalaVersion = "2.12.4"
  def publishVersion = "0.0.1"
  def pomSettings = PomSettings(
    description = "My first library",
    organization = "com.lihaoyi",
    url = "https://github.com/com-lihaoyi/mill",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github("lihaoyi", "mill"),
    developers = Seq(
      Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi")
    )
  )
}

You can make a module publishable by extending PublishModule.

PublishModule then needs you to define a publishVersion and pomSettings. The artifactName defaults to the name of your module (in this case foo) but can be overridden. The organization is defined in pomSettings.

You may also check and update the values of sonatypeUri and sonatypeSnapshotUri, which may not be correct if you have a newer Sonatype account (when created after Feb. 2021).

Staging Releases

Once you’ve mixed in PublishModule, you can publish your libraries to maven central via:

mill mill.scalalib.PublishModule/publishAll \
        foo.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b

This uploads them to oss.sonatype.org where you can log-in and stage/release them manually. You can also pass in the --release true flag to perform the staging/release automatically:

mill mill.scalalib.PublishModule/publishAll \
        foo.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b \
        --release true

If you want to publish/release multiple modules, you can use the or _ wildcard syntax:

mill mill.scalalib.PublishModule/publishAll \
        __.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b \
        --release true

To publish to repository other than oss.sonaytype.org such as internal hosted nexus at example.company.com, you can pass in the --sonatypeUri and --sonatypeSnapshotUri parameters to uploads to different site:

mill mill.scalalib.PublishModule/publishAll \
        foo.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --sonatypeUri http://example.company.com/release \
        --sonatypeSnaphostUri http://example.company.com/snapshot

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.

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/publishAll \
        foo.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --sonatypeUri http://example.company.com/release \
        --stagingRelease false

Example Builds

Mill comes bundled with example builds for existing open-source projects, as integration tests and examples:

Acyclic

A small single-module cross-build, with few sources, minimal dependencies, and wired up for publishing to Maven Central.

Jawn

A collection of relatively small modules, all cross-built across the same few versions of Scala.

Upickle

A single cross-platform Scala.js/Scala-JVM module cross-built against multiple versions of Scala, including the setup necessary for publishing to Maven Central.

Ammonite

A relatively complex build with numerous submodules, some cross-built across Scala major versions while others are cross-built against Scala minor versions.

Also demonstrates how to pass one module’s compiled artifacts to the run/test commands of another, via their forkEnv.