Cross Builds

Mill handles cross-building of all sorts via the Cross[T] module.

Defining Cross Modules

You can use this as follows:

import mill._

object foo extends mill.Cross[FooModule]("2.10", "2.11", "2.12")
class FooModule(crossVersion: String) extends Module {
  def suffix = T { crossVersion }
  def bigSuffix = T { suffix().toUpperCase() }
}

This defines three copies of FooModule: "210", "211" and "212", each of which has their own suffix target. You can then run them via

mill show foo[2.10].suffix
mill show foo[2.10].bigSuffix
mill show foo[2.11].suffix
mill show foo[2.11].bigSuffix
mill show foo[2.12].suffix
mill show foo[2.12].bigSuffix

Please be aware that some shells like zsh interpret square brackets differently, so quoting or masking might be needed.

mill show foo\[2.10\].suffix
mill show 'foo[2.10].suffix'
mill show "foo[2.10].suffix"

The suffix targets will have the corresponding output paths for their metadata and files:

out/
├── foo/
│   ├── 2.10/
│   │   ├── bigSuffix.json
│   │   └── suffix.json
│   ├── 2.11/
│   │   ├── bigSuffix.json
│   │   └── suffix.json
│   └── 2.12/
│       ├── bigSuffix.json
│       └── suffix.json

If you want to have dedicated millSourcePaths, you can add the cross parameters to it:

import mill._

object foo extends mill.Cross[FooModule]("2.10", "2.11", "2.12")
class FooModule(crossVersion: String) extends Module {
  def millSourcePath = super.millSourcePath / crossVersion
}

Before Mill 0.11.0-M5, Cross modules which were not also CrossScalaModules, always added the cross parameters to the millSourcePath. This often led to setups like this:

def millSourcePath = super.millSourcePath / os.up

You can also have a cross-build with multiple inputs:

import mill._

val crossMatrix = for {
  crossVersion <- Seq("210", "211", "212")
  platform <- Seq("jvm", "js", "native")
  if !(platform == "native" && crossVersion != "212")
} yield (crossVersion, platform)

object foo extends mill.Cross[FooModule](crossMatrix:_*)
class FooModule(crossVersion: String, platform: String) extends Module {
  def suffix = T { crossVersion + "_" + platform }
}

Here, we define our cross-values programmatically using a for-loop that spits out tuples instead of individual values. Our FooModule template class then takes two parameters instead of one. This creates the following modules each with their own suffix target:

$ mill showNamed foo[_].suffix
[1/1] showNamed
[1/1] showNamed > [7/7] foo[212,native].suffix
{
  "foo[210,jvm].suffix": "210_jvm",
  "foo[210,js].suffix": "210_js",
  "foo[211,jvm].suffix": "211_jvm",
  "foo[211,js].suffix": "211_js",
  "foo[212,jvm].suffix": "212_jvm",
  "foo[212,js].suffix": "212_js",
  "foo[212,native].suffix": "212_native"
}

Using Cross Modules from Outside

You can refer to targets defined in cross-modules as follows:

import mill._

object foo extends mill.Cross[FooModule]("2.10", "2.11", "2.12")
class FooModule(crossVersion: String) extends Module {
  def suffix = T { crossVersion }
}

def bar = T { s"hello ${foo("2.10").suffix}" }

Here, foo("2.10") references the "2.10" instance of FooModule. You can refer to whatever versions of the cross-module you want, even using multiple versions of the cross-module in the same target:

import mill._

object foo extends mill.Cross[FooModule]("2.10", "2.11", "2.12")
class FooModule(crossVersion: String) extends Module {
  def suffix = T { crossVersion }
}

def bar = T { s"hello ${foo("2.10").suffix} world ${foo("2.12").suffix}" }

Using Cross Modules from other Cross Modules

Targets in cross-modules can depend on one another the same way than external targets:

import mill._

object foo extends mill.Cross[FooModule]("2.10", "2.11", "2.12")
class FooModule(crossVersion: String) extends Module {
  def suffix = T { crossVersion }
}

object bar extends mill.Cross[BarModule]("2.10", "2.11", "2.12")
class BarModule(crossVersion: String) extends Module {
  def bigSuffix = T { foo(crossVersion).suffix().toUpperCase() }
}

Here, you can run:

mill show foo[2.10].suffix
mill show foo[2.11].suffix
mill show foo[2.12].suffix
mill show bar[2.10].bigSuffix
mill show bar[2.11].bigSuffix
mill show bar[2.12].bigSuffix

or the more compact version:

$ mill showNamed foo[__].suffix
[1/1] showNamed
[1/1] showNamed > [3/3] foo[2.12].suffix
{
  "foo[2.10].suffix": "2.10",
  "foo[2.11].suffix": "2.11",
  "foo[2.12].suffix": "2.12"
}

Cross Resolvers

You can define an implicit mill.define.Cross.Resolver within your cross-modules, which would let you use a shorthand foo() syntax when referring to other cross-modules with an identical set of cross values:

import mill._

trait MyModule extends Module {
  def crossVersion: String
  implicit object resolver extends mill.define.Cross.Resolver[MyModule] {
    def resolve[V <: MyModule](c: Cross[V]): V = c.itemMap(List(crossVersion))
  }
}

object foo extends mill.Cross[FooModule]("2.10", "2.11", "2.12")
class FooModule(val crossVersion: String) extends MyModule {
  def suffix = T { crossVersion }
}

object bar extends mill.Cross[BarModule]("2.10", "2.11", "2.12")
class BarModule(val crossVersion: String) extends MyModule {
  def longSuffix = T { "_" + foo().suffix() }
}

While the example resolver simply looks up the target Cross value for the cross-module instance with the same crossVersion, you can make the resolver arbitrarily complex. E.g. the resolver for mill.scalalib.CrossSbtModule looks for a cross-module instance whose scalaVersion is binary compatible (e.g. 2.10.5 is compatible with 2.10.3) with the current cross-module.