Building Scala with Mill

Mill supports three main ways of defining Scala projects:

  • Config-Based Modules: simple projects that require basic setting of dependencies and versions

  • Single-File Scripts: small programs with both code and build configuration contained within a single file

  • Programmatic Modules: builds which require custom logic, e.g. code generation or resource processing

This page walks through a series of Mill builds of increasing complexity to show you the key features and usage of the Mill build tool. The other pages of this section of the docs go into more depth into individual Mill features: testing, publishing, and so on. Every example can be downloaded to try it yourself.

The API reference for Mill’s Scala toolchain can be found at mill.scalalib

Config-Based Modules

build.mill.yaml (download, browse)
extends: [mill.scalalib.ScalaModule]
scalaVersion: 3.7.2
mvnDeps:
- "com.lihaoyi::scalatags:0.13.1"
- "com.lihaoyi::mainargs:0.7.7"
test/package.mill.yaml (download, browse)
extends: [build.ScalaTests, mill.javalib.TestModule.Utest]
mvnDeps:
- "com.lihaoyi::utest:0.9.1"

This is a basic Mill build for a single ScalaModule, with two third-party dependencies and a test suite using the JUnit framework. This uses Mill’s config-based .mill.yaml files for defining the modules:

  • build.mill.yaml defines the configuration for the root module

  • test/package.mill.yaml defines the submodule test/ containing the test suites.

This example project uses mvnDeps to include two third-party dependencies - MainArgs for CLI argument parsing, Scalatags for HTML generation - and uses them to wrap a given input string in HTML templates with proper escaping. The test/ submodule contains its own mvnDeps for dependencies that are only used in the test suite.

Basic Folder Layout

The source code for this module lives in the src/ folder. Output for this module (compiled files, resolved dependency lists, …​) lives in out/. A typical filesystem layout is shown below:

build.mill.yaml
src/
    Foo.scala
resources/
    ...
test/
    package.mill.yaml
    src/
        FooTests.scala
out/
    compile.json
    compile.dest/
    ...
    test/
        compile.json
        compile.dest/
        ...
The default Mill source folder layout src/ differs from that of sbt's src/main/scala. If you wish to use the sbt source folder layout, e.g. for migrating an existing codebase, you should use sbt-Compatible Modules

Typical Usage

Typical usage of this example project from the command line is shown below:

> ./mill resolve _ # List what tasks are available to run
assembly
...
compile
...
run
...
> ./mill inspect compile # Show documentation and inputs of a task
compile(ScalaModule.scala:...)
    Compiles the current module to generate compiled classfiles/bytecode.
Inputs:
    upstreamCompileOutput
    allSourceFiles
    compileClasspath
> ./mill compile # compile sources into classfiles
...
compiling 1 Scala source to...
> ./mill test
...
+ foo.FooTests...simple ...  "<h1>hello</h1>"
+ foo.FooTests...escaping ...  "<h1>&lt;hello&gt;</h1>"
> ./mill run --text hello
<h1>hello</h1>
> ./mill assembly # bundle classfiles and libraries into a jar for deployment

> ./mill show assembly # show the output of the assembly task
".../out/assembly.dest/out.jar"

> java -jar ./out/assembly.dest/out.jar --text hello
<h1>hello</h1>

> ./out/assembly.dest/out.jar --text hello # mac/linux
<h1>hello</h1>

> # Note that on windows you need to rename `out.jar` to `out.bat` to run it without `java -jar`
> cp ./out/assembly.dest/out.jar out.bat # windows

> ./out.bat --text hello # windows
<h1>hello</h1>

The output of every Mill task is stored in the out/ folder under a name corresponding to the task that created it. e.g. The assembly task puts its metadata output in out/assembly.json, and its output files in out/assembly.dest. You can also use show to print out the metadata output for a particular task, or inspect to print metadata about the task itself (docs, source location, etc.)

Additional Mill tasks you would likely need include:

> ./mill resolve __    # recursively list all tasks and modules that are available

> ./mill runBackground # run the main method in the background

> ./mill clean <task>  # delete the cached output of a task, terminate any runBackground

> ./mill launcher      # prepares a out/launcher.dest/run you can run later

> ./mill jar           # bundle the classfiles into a jar suitable for publishing

> ./mill -w compile    # watch input files and re-compile whenever a file changes

You can start a Scala REPL attached to your root module or the test submodule via

> ./mill -i console    # start a Scala console within your project
> ./mill -i test.console

> ./mill -i repl       # start an Ammonite Scala REPL within your project

You can also start a standalone Scala REPL with the Bundled Libraries included via ./mill --repl. This is useful if you just need a Scala REPL to experiment with at the command line without being attached to a particular project or module.

> ./mill --repl

The most common tasks that Mill can run are cached tasks, such as compile, and un-cached commands such as run. Cached tasks do not re-evaluate unless one of their inputs changes, whereas commands re-run every time. See the documentation for Tasks for details on the different task types.

This example uses Mill’s config-based YAML syntax, which is a good fit for simple builds where you are just setting some configuration keys. This is discussed more in the documentation for:

Multi-Module Projects

This example contains a simple Mill build with two modules, foo and bar, defined by their respective package.mill.yaml config files, and on which you can run tasks on such as foo.run or bar.run. There is also a bar.test module defined by bar/test/package.mill.yaml.

foo/package.mill.yaml (download, browse)
extends: [mill.scalalib.ScalaModule]
moduleDeps: [bar]
scalaVersion: 3.7.1
mvnDeps:
- com.lihaoyi::mainargs:0.7.7
bar/package.mill.yaml (download, browse)
extends: [mill.scalalib.ScalaModule]
scalaVersion: 3.7.1
mvnDeps:
- com.lihaoyi::scalatags:0.13.1
bar/test/package.mill.yaml (download, browse)
extends: [build.bar.ScalaTests, mill.javalib.TestModule.Utest]
mvnDeps:
- com.lihaoyi::utest:0.9.1

Inter-module dependencies are defined by the moduleDeps key, and modules can also be nested within each other, as bar.test is nested within bar.

Multi-Module Folder Layout

This multi-module example build expects the following folder layout:

build.mill
foo/
    src/
        Foo.scala
bar/
    src/
        Bar.scala
    test/
        src/
            BarTests.scala
out/
    foo/
        compile.json
        compile.dest/
        ...
    bar/
        compile.json
        compile.dest/
        ...
        test/
            compile.json
            compile.dest/
            ...

Typically, source, output files, and task names in Mill follow the module hierarchy, so e.g. input to the foo module lives in foo/src/, can be compiled via foo.compile and, and its compiled output files live in out/foo/compile.dest.

You can use mill resolve to list out what tasks you can run, e.g. mill resolve __.run below which lists out all the run tasks:

> ./mill resolve __.run
foo.run
bar.run

> ./mill foo.run --text "hello-world"
<h1>hello-world</h1>

> ./mill bar.test
...
...bar.BarTests...simple...
...bar.BarTests...escaping...

In general, Mill will ensure that the specified tasks and their upstream dependencies are executed in the right order, and re-executed as necessary when source code in each module changes. For example if you run foo.compile:

  • Mill will automatically run bar.compile and any other upstream tasks first if they have not been executed yet, or are out of date

  • Mill will automatically re-use the cached output for bar.compile if it was executed earlier and its upstream source files have not changed

Task Query Syntax

You can use wildcards and brace-expansion to select multiple tasks at once or to shorten the path to deeply nested tasks, as we saw with __.run above. Some more examples of this are shown below:

Table 1. Wildcards and brace-expansion

Wildcard

Function

_

matches a single segment of the task path

__

matches arbitrary segments of the task path

{a,b}

is equal to specifying two tasks a and b

You can use the + symbol to add another task with optional arguments. If you need to feed a + as argument to your task, you can mask it by preceding it with a backslash (\).

> ./mill bar._.compile            # Runs `compile` for all direct sub-modules of `foo`

> ./mill bar.__.test              # Runs `test` for all transitive sub-modules of `foo`

> ./mill {foo,bar}.compile        # Runs `compile` for `foo` and `bar`

> ./mill __.compile + bar.__.test # Runs all `compile` tasks and all tests under `foo`.

If you provide optional task arguments and your wildcard or brace-expansion is resolved to multiple tasks, the arguments will be applied to each of the tasks. For more details on the query syntax, see the page Task Query Syntax

SBT Compatible Modules

Mill’s default folder layout of src/ and test/src differs from the src/main/scala/ and src/test/scala/ common in Maven, Gradle, or SBT. If you are adopting Mill in an existing codebase, you can use Mill’s SbtModule and SbtTests as shown below to preserve filesystem compatibility with an existing build:

build.mill.yaml (download, browse)
extends: [mill.scalalib.SbtModule]
scalaVersion: 3.7.2
mvnDeps:
- "com.lihaoyi::scalatags:0.13.1"
- "com.lihaoyi::mainargs:0.7.7"

object test:
  extends: [SbtTests, mill.javalib.TestModule.Utest]
  mvnDeps:
  - "com.lihaoyi::utest:0.9.1"

object integration:
  extends: [SbtTests, mill.javalib.TestModule.Utest]
  mvnDeps:
  - "com.lihaoyi::utest:0.9.1"

Note the object test: and object integration: keys in the above file: these allow you to define submodules in the same build.mill.yaml file as the parent module without needing a separate package.mill.yaml in a subfolder. This is necessary in scenarios like this one where we want test and integration submodules but the filesystem layout means we do not have test/ or integration/ folders to put a package.mill.yaml in.

SbtModule/CrossSbtModule are variants of ScalaModule/CrossScalaModule that use the more verbose folder layout of SBT, Maven, and other tools:

  • src/main/scala/

  • src/test/scala/

  • src/integration/scala/

Rather than Mill’s

  • src/

  • test/src/

  • integration/src/

One use case of these compatibility modules is migrations: while migrating to Mill, a project may be built using your previous build tool and Mill at the same time. Using SbtModules means that during migration, you can leave all your source files in place while setting up your Mill build, and do not need to invasively move them around to match the Mill default module layout.

Although the source layout of these compatibility modules is different from the default ScalaModule, the command-line usage is the same:

> ./mill compile
compiling 1 Scala source...

> ./mill test.compile
compiling 1 Scala source...

> ./mill test.testForked
+ foo.FooTests...hello ...

> ./mill test
+ foo.FooTests.hello ...

> ./mill integration
+ foo.FooIntegrationTests.hello ...

For more details on migrating from other build tools, see Migrating to Mill

Single-File Scripts

Mill allows you to run single-file Scala programs easily from the command-line, even those that contain third-party dependencies, specific JVM versions, or other such build configuration. Unlike Scala programs built with other tools, Mill scripts run via a ./mill bootstrap script run reproducibly without needing any prior installation or setup. This can be useful for several purposes:

  • A replacement for Bash scripts: Mill instead lets you write small Scala scripts or programs in Scala with full access to third-party libraries that run in a reproducible way across diverse dev/test/prod environments

  • Self-contained examples or issue reproductions, as you can include both the code and dependencies necessary in a self-contained file that can be run using ./mill without manual setup or installation

  • Small projects or experiments, where setting up and maintaining a separate build file can be a hassle, and putting both code and config in a single file makes things easier to manage

For example the Scala program below can be run directly using Mill, which will automatically download and cache the specified third-party dependencies as necessary:

Foo.scala (download, browse)
//| jvmId: 11.0.28
//| mvnDeps:
//| - "com.lihaoyi::scalatags:0.13.1"
import scalatags.Text.all.*

def generateHtml(text: String) = {
  h1(text).toString
}

@main
def main(text: String) = {
  println("Jvm Version: " + System.getProperty("java.version"))
  println(generateHtml(text))
}

In this example we use the @main method from MainArgs, one of the Bundled Libraries, but you can define an explicit def main(args: Array[String]): Unit as well.

Apart from the mvnDeps config that allows you to use third-party libraries in your script, the jvmId config lets you specify exactly what JVM version you wish to use in this script. Like other Mill modules, Mill scripts will default to a Mill’s own default JVM version of zulu:21 if a jvmId is not provided. If you want to use the environmentally installed java command available on your path, you must explicitly set jvmId: system.

This script can be run as shown below:

> ./mill Foo.scala --text hello
compiling 1 Scala source to...
<h1>hello</h1>
Jvm Version: 11.0.28

Mill will automatically download and cache the necessary artifacts on your behalf, allowing you run these scripts via ./mill and have them behave the same regardless of what environment it is running in without needing any setup.

The ./mill Foo.scala syntax is shorthand for ./mill Foo.scala:run. You can also call other tasks on your script modules, such as Foo.scala:assembly below:

> ./mill Foo.scala:run --text hello
<h1>hello</h1>
> ./mill show Foo.scala:assembly # show the output of the assembly task
".../out/Foo.scala/assembly.dest/out.jar"

> java -jar ./out/Foo.scala/assembly.dest/out.jar --text hello
<h1>hello</h1>

> ./out/Foo.scala/assembly.dest/out.jar --text hello # mac/linux
<h1>hello</h1>

Scala scripts support the same configuration keys as Config-Based Modules as part of their //| header comment, and support most the tasks via the :run :assembly etc. command-line syntax shown above. You can list these tasks via ./mill resolve as shown below:

> ./mill resolve Foo.scala:_
run
runMain
compile
assembly
...

Testing Scripts

Script files can have test suites, usually written in a separate test script. The test script configuration specifies

  • What test framework it uses via extends

  • What script it tests via moduleDeps

  • Any mvnDeps needed by the code, in addition to those of the upstream script.

The test script can then run tests that exercise the upstream script as shown below:

FooTests.scala (download, browse)
//| extends: [mill.script.ScalaModule.Utest]
//| moduleDeps: [./Foo.scala]
//| mvnDeps:
//| - "com.lihaoyi::utest:0.9.1"

import utest.*

object FooTests extends TestSuite {
  def tests = Tests {
    test("simple") {
      val result = generateHtml("hello")
      assert(result == "<h1>hello</h1>")
      result
    }
    test("escaping") {
      val result = generateHtml("<hello>")
      assert(result == "<h1>&lt;hello&gt;</h1>")
      result
    }
  }
}
> ./mill FooTests.scala
+ FooTests.simple ...  "<h1>hello</h1>"
+ FooTests.escaping ...  "<h1>&lt;hello&gt;</h1>"

Again, you can pass the name of the task explicitly via :, e.g. :testForked below, or any other task that is available on a test module:

> ./mill FooTests.scala:testForked # specifying the test task explicitly
+ FooTests.simple ...  "<h1>hello</h1>"
+ FooTests.escaping ...  "<h1>&lt;hello&gt;</h1>"

The testing framework used in a script is defined by the class specified in the extends clause. The different testing frameworks supported in Testing Scala Projects can be used for your scripts: Junit4, Junit5, TestNg, Munit, ScalaTest, Specs2, Utest, Weaver, ZioTest. If you need something not on this list, you can define a Custom Script Module Class.

For scripts that grow larger than a single file, you should convert them to Config-Based Modules.

Programmatic Modules

build.mill (download, browse)
package build
import mill.*, scalalib.*

object foo extends ScalaModule {
  def scalaVersion = "3.7.1"
  def mvnDeps = Seq(
    mvn"com.lihaoyi::scalatags:0.13.1",
    mvn"com.lihaoyi::mainargs:0.7.7"
  )

  object test extends ScalaTests {
    def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.8.9")
    def testFramework = "utest.runner.Framework"
  }
}

This is an example of Mill’s programmatic configuration syntax. This is slightly more verbose than the YAML syntax shown above, but in exchange allows more flexibility in how tasks are defined and their values are computed. Keys such as mvnDeps: in the YAML syntax correspond directly to the def mvnDeps methods in the programmatic syntax.

build.mill
foo/
    src/
        Foo.scala
    resources/
        ...
    test/
        src/
            FooTests.scala
out/foo/
    compile.json
    compile.dest/
    ...
    test/
        compile.json
        compile.dest/
        ...

This example places the ScalaModule in the foo/ subfolder, but you can also use define a Root Module to place it at the root of your repository similar to the build.mill.yaml examples discussed in Config-Based Modules

Usage of programmatic Mill builds is similar to usage of simple config-based Mill builds:

> ./mill resolve foo._ # List what tasks are available to run
foo.assembly
...
foo.compile
...
foo.run
...
> ./mill foo.run --text hello
<h1>hello</h1>
> ./mill foo.test
...
+ foo.FooTests...simple ...  <h1>hello</h1>
+ foo.FooTests...escaping ...  <h1>&lt;hello&gt;</h1>
> ./mill foo.assembly # bundle classfiles and libraries into a jar for deployment

> ./mill show foo.assembly # show the output of the assembly task
".../out/foo/assembly.dest/out.jar"

> java -jar ./out/foo/assembly.dest/out.jar --text hello
<h1>hello</h1>

Programmatic Mill build files are written in Scala, but you do not need to have prior experience in Scala to read or write them. Like Gradle Groovy or Maven XML, anyone can learn enough Scala for Mill without needing to become an expert in the language. For simpler builds that do not need the flexibility that programmatic builds provide, YAML-based Config-Based Module definitions are a great alternative.

Custom Build Logic

Programmatic Mill builds make it very easy to customize your build graph, overriding portions of it with custom logic. In this example, we override the JVM resources of our ScalaModule - normally the resources/ folder - to also contain a single generated text file containing the line count of all the source files in that module

build.mill (download, browse)
package build
import mill.*, scalalib.*

object foo extends ScalaModule {
  def scalaVersion = "3.7.1"

  /** Total number of lines in module source files */
  def lineCount = Task {
    allSourceFiles().map(f => os.read.lines(f.path).size).sum
  }

  /** Generate resources using lineCount of sources */
  override def resources = Task {
    os.write(Task.dest / "line-count.txt", "" + lineCount())
    super.resources() ++ Seq(PathRef(Task.dest))
  }
}
  • override def resources replaces the previous resource folder provided by ScalaModule (labelled resources.super below), including both the previous resource folder super.resources() together with the Task.dest folder of the new task which contains a lint-count.txt file we write.

G allSourceFiles allSourceFiles lineCount lineCount allSourceFiles->lineCount resources resources lineCount->resources run run resources->run resources.super resources.super resources.super->resources
  • os.read.lines and os.write come from the OS-Lib library, which is one of Mill’s Bundled Libraries. You can also import any JVM library you want from Maven Central using //| mvnDeps, so you are not limited to what is bundled with Mill.

  • The override keyword is optional in Mill. It is shown above for clarity, but can be elided for conciseness.

In general, this use of method defs, method calls, override, and super should be very familiar to anyone with prior experience working in Scala. These are the same concepts that have underpinned object-oriented programming for decades, and work exactly the same here as they work anywhere else.

This generated line-count.txt file can then be loaded and used at runtime, as see in the output of mill run below.

> ./mill foo.run
...
Line Count: 17

> ./mill show foo.lineCount
17

> ./mill inspect foo.lineCount
foo.lineCount(build.mill:...)
    Total number of lines in module source files
Inputs:
    foo.allSourceFiles

If you’re not familiar with what tasks you can override or how they are related, you can explore the existing tasks via autocomplete in your IDE, or use the mill visualize.

Custom user-defined tasks in Mill such as def lineCount above benefit from all the same things that built-in tasks do: automatic caching (in the out/ folder), parallelism (configurable via -j/--jobs flag), inspectability (via show and inspect), and so on. While these things may not matter for such a simple example that runs quickly, they ensure that custom build logic remains performant and maintainable even as the complexity of your project grows.

Programmatic Multi-Module Project

build.mill (download, browse)
package build
import mill.*, scalalib.*

trait MyModule extends ScalaModule {
  def scalaVersion = "3.7.1"
  object test extends ScalaTests {
    def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.8.9")
    def testFramework = "utest.runner.Framework"
  }
}

object foo extends MyModule {
  def moduleDeps = Seq(bar)
  def mvnDeps = Seq(mvn"com.lihaoyi::mainargs:0.7.7")
}

object bar extends MyModule {
  def mvnDeps = Seq(mvn"com.lihaoyi::scalatags:0.13.1")
}

This is similar to the Multi-Module Projects example above, but using Mill’s programmatic configuration syntax. You can define multiple modules the same way you define a single module, using def moduleDeps to define the relationship between them. Modules can also be nested within each other, as foo.test and bar.test are nested within foo and bar respectively

Note that we split out the test submodule configuration common to both modules into a separate trait MyModule. This Trait Module works like a class in Java, and lets us avoid the need to copy-paste common settings, while still letting us define any per-module configuration such as mvnDeps specific to a particular module. This is a common pattern within Mill builds.

> ./mill bar.test
+ bar.BarTests.simple...
+ bar.BarTests.escaping...
> ./mill foo.run --foo-text hello --bar-text world
Foo.value: hello
Bar.value: <h1>world</h1>

You can also put the configuration for each submodule in it’s respective folder’s package.mill file, as described in Multi-File Builds. This can be helpful in larger projects to avoid having your build.mill grow large, and aid in discoverability by keeping the build configuration for each module close to the code it is configuring

Programmatic Compatibility Modules

This example is similar to the sbt-Compatible Modules above, but using Mill’s programmatic build.mill files. These are more flexible than the config-driven build.mill.yaml files, and additionally allows use of Cross modules such as CrossSbtModule:

build.mill (download, browse)
package build
import mill.*, scalalib.*

object foo extends SbtModule {
  def scalaVersion = "3.7.1"
  object test extends SbtTests {
    def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.9.1")
    def testFramework = "utest.runner.Framework"
  }
  object integration extends SbtTests {
    def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.9.1")
    def testFramework = "utest.runner.Framework"
  }
}

object bar extends Cross[BarModule]("2.12.20", "2.13.16")
trait BarModule extends CrossSbtModule {
  object test extends CrossSbtTests {
    def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.9.1")
    def testFramework = "utest.runner.Framework"
  }
}

SbtModule/CrossSbtModule are variants of ScalaModule/CrossScalaModule that use the more verbose folder layout of sbt, Maven, and other tools:

  • foo/src/main/scala

  • foo/src/main/scala-2.12

  • foo/src/main/scala-2.13

  • foo/src/test/scala

  • foo/src/integration/scala

Rather than Mill’s

  • foo/src

  • foo/src-2.12

  • foo/src-2.13

  • foo/test/src

> ./mill foo.compile
compiling 1 Scala source...

> ./mill foo.test.compile
compiling 1 Scala source...

> ./mill foo.test.testForked
+ foo.FooTests...hello ...

> ./mill foo.test
+ foo.FooTests.hello ...

> ./mill foo.integration
+ foo.FooIntegrationTests.hello ...

> ./mill bar[2.13.16].run
Bar.value: Hello World Scala library version 2.13.16...

> ./mill bar[2.12.20].run
Bar.value: Hello World Scala library version 2.12.20...

Realistic Java Example Project

Below, we should a realistic example of a build for a Scala project. This example touches on library dependencies, testing, publishing, code generation, and other topics covered in more detail in the Scala section of the Mill docs, and you can browse each respective page if you want to learn more.

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

trait MyModule extends PublishModule {
  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"))
  )
}

trait MyScalaModule extends MyModule, CrossScalaModule {
  def mvnDeps = Seq(mvn"com.lihaoyi::scalatags:0.13.1")
  object test extends ScalaTests {
    def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.9.1")
    def testFramework = "utest.runner.Framework"
  }
}

val scalaVersions = Seq("2.13.16", "3.3.6")

object foo extends Cross[FooModule](scalaVersions)
trait FooModule extends MyScalaModule {
  def moduleDeps = Seq(bar(), qux)

  def generatedSources = Task {
    os.write(
      Task.dest / "Version.scala",
      s"""
         |package foo
         |object Version{
         |  def value = "${publishVersion()}"
         |}
      """.stripMargin
    )
    Seq(PathRef(Task.dest))
  }
}

object bar extends Cross[BarModule](scalaVersions)
trait BarModule extends MyScalaModule {
  def moduleDeps = Seq(qux)
}

object qux extends JavaModule, MyModule

A semi-realistic build setup, combining all the individual Mill concepts:

  • Two CrossScalaModules compiled against two Scala versions, that depend on each other as well as on a JavaModule

  • With unit testing and publishing set up

  • With version-specific sources

  • With generated sources to include the publishVersion as a string in the code, so it can be printed at runtime

Note that for multi-module builds like this, using using queries to run tasks on multiple modules at once can be very convenient:

__.test
__.publishLocal

Also note that ScalaModules can depend on JavaModules, and when multiple inter-dependent modules are published they automatically will include the inter-module dependencies in the publish metadata.

Also note how you can use traits to bundle together common combinations of modules: MyScalaModule not only defines a ScalaModule with some common configuration, but it also defines a object test module within it with its own configuration. This is a very useful technique for managing the often repetitive module structure in a typical project

> ./mill resolve __.run
bar[2.13.16].run
bar[2.13.16].test.run
bar[3.3.6].run
bar[3.3.6].test.run
foo[2.13.16].run
foo[2.13.16].test.run
foo[3.3.6].run
foo[3.3.6].test.run
qux.run

> ./mill foo[2.13.16].run
foo version 0.0.1
Foo.value: <h1>hello</h1>
Bar.value: <p>world Specific code for Scala 2.x</p>
Qux.value: 31337

> ./mill bar[3.3.6].test
+ bar.BarTests.test ... "<p>world Specific code for Scala 3.x</p>"

> ./mill qux.run
Qux.value: 31337

> ./mill __.compile

> ./mill __.test
+ bar.BarTests.test ... "<p>world Specific code for Scala 2.x</p>"
+ bar.BarTests.test ... "<p>world Specific code for Scala 3.x</p>"
+ foo.FooTests.test ... "<h1>hello</h1>"
+ foo.FooTests.test ... "<h1>hello</h1>"

> ./mill __.publishLocal
Publishing Artifact(com.lihaoyi,foo_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux,0.0.1) to ivy repo...

> ./mill show foo[2.13.16].assembly # mac/linux
".../out/foo/2.13.16/assembly.dest/out.jar"

> ./out/foo/2.13.16/assembly.dest/out.jar # mac/linux
foo version 0.0.1
Foo.value: <h1>hello</h1>
Bar.value: <p>world Specific code for Scala 2.x</p>
Qux.value: 31337