Multi-File Builds

Mill allows you to break up your build.mill file into smaller files by defining the build-related logic for any particular subfolder as a package.mill file in that subfolder. This can be very useful to keep large Mill builds maintainable, as each folder’s build logic gets co-located with the files that need to be built, and speeds up compilation of the build logic since each build.mill or package.mill file can be compiled independently when it is modified without re-compiling all the others.

This means that instead of one large build.mill file at the root of your repo:

build.mill

You have a smaller build.mill with the config for each sub-folder broken out into that folder’s respective package.mill

foo/
    src/...
    package.mill
bar/
    package.mill
    qux/
        mymodule/src/...
        package.mill
build.mill

This is useful in larger projects, as it co-locates the build definition for each module in the same sub-folder as the relevant code. This can make things easier to find compared to having every module’s build configuration in a single large build.mill that may grow to thousands of lines long.

Example Project

build.mill (download, browse)
package build

import mill.*, scalalib.*

trait MyModule extends ScalaModule {
  def scalaVersion = "3.7.4"
}
foo/package.mill (download, browse)
package build.foo
import mill.*, scalalib.*

object `package` extends build.MyModule {
  def moduleDeps = Seq(build.bar.qux.mymodule)
  def mvnDeps = Seq(mvn"com.lihaoyi::mainargs:0.7.8")
}
bar/package.mill (download, browse)
package build.bar
bar/qux/package.mill (download, browse)
package build.bar.qux
import mill.*, scalalib.*

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

In this example, the root build.mill only contains the trait MyModule, but it is foo/package.mill and bar/qux/package.mill that define modules using it. The modules defined in foo/package.mill and bar/qux/package.mill are automatically nested within foo and bar.qux respectively, and can be referenced from the command line as below:

> ./mill resolve __
bar
...
bar.qux.mymodule
...
bar.qux.mymodule.compile
...
foo
...
foo.compile

> ./mill bar.qux.mymodule.compile

> ./mill foo.compile

> ./mill foo.run --foo-text hello --bar-qux-text world
Foo.value: hello
BarQux.value: <p>world</p>

Note that in this example, foo/package.mill defines object package extends mill.Module, and so the name .package does not need to be provided at the command line. In contrast, bar/qux/package.mill defines object mymodule which is not named package, and so we need to explicitly reference it with a .mymodule suffix.

package.mill files are only discovered in direct subfolders of the root build.mill or subfolders of another folder containing a package.mill; Hence in this example, we need an bar/package.mill to be present for bar/qux/package.mill to be discovered, even though bar/package.mill is empty.

Helper Files

Apart from having package files in subfolders to define modules, Mill also allows you to have helper code in any *.mill file in the same folder as your build.mill or a package.mill.

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

object `package` extends MyModule {
  def forkEnv = Map(
    "MY_SCALA_VERSION" -> build.scalaVersion(),
    "MY_PROJECT_VERSION" -> foo.myProjectVersion
  )
}
util.mill (download, browse)
package build
import mill.*, scalalib.*

def myScalaVersion = "2.13.16"

trait MyModule extends ScalaModule {
  def scalaVersion = myScalaVersion
}
foo/package.mill (download, browse)
package build.foo
import mill.*, scalalib.*
object `package` extends build.MyModule {
  def forkEnv = Map(
    "MY_SCALA_VERSION" -> build.myScalaVersion,
    "MY_PROJECT_VERSION" -> myProjectVersion
  )
}
foo/versions.mill (download, browse)
package build.foo

def myProjectVersion = "0.0.1"

Different helper scripts and build.mill/package files can all refer to each other using the build object, which marks the root object of your build. In this example:

  • build.mill, util.mill and other adjacent files can be referred to as simple build

  • foo/package, foo/versions.mill, and other adjacent files can be referred to as simple build.foo

Helper files are very handy for you to put custom logic which you use in your build.mill or package.mill module definitions. This can help keep your build.mill or package.mill files concise and understandable.

> ./mill run
Main Env build.util.myScalaVersion: 2.13.16
Main Env build.foo.versions.myProjectVersion: 0.0.1

> ./mill foo.run
Foo Env build.util.myScalaVersion: 2.13.16
Foo Env build.foo.versions.myProjectVersion: 0.0.1

Nested Build Files

Mill supports nested build.mill files in subdirectories. Unlike package.mill files which must use package declarations matching their directory path (e.g., package build.deps.foo), nested build.mill files can use a simple package build declaration regardless of where they are located (when marked with //| mill-allow-nested-build-mill: true).

This makes it easy to:

  • Keep module definitions self-contained in subdirectories that can be built independently

  • Refactor or move modules around without updating package declarations

  • Extract reusable traits and objects into separate helper files

Limitation

When using nested build.mill files, you can only import traits or objects defined in helper files (like versions.mill or module.mill), not from the build.mill itself.

Example Project

The FooModule trait is defined in the nested deps/foo/module.mill helper file, and we can reference it from the root build using the hierarchical path build.deps.foo.FooModule. Additional helpers like versions.mill provide shared configuration via imports like build.deps.foo.myScalaVersion.

build.mill (download, browse)
//| mill-allow-nested-build-mill: true

package build

import mill.*, scalalib.*

/** Reference FooModule from the nested helper file `deps/foo/module.mill` */
trait BarModule extends build.deps.foo.FooModule
deps/package.mill (download, browse)
// deps/package.mill
package build.deps
deps/foo/build.mill (download, browse)
// deps/foo/build.mill
// This nested build.mill file uses `package build` instead of the
// directory-based `package build.deps.foo`
package build

import mill.*, scalalib.*

object `package` extends FooModule
deps/foo/module.mill (download, browse)
// deps/foo/module.mill
// This helper file demonstrates importing definitions from sibling helper files
// using `import build.something`. With nested `build.mill` support, `build` here
// refers to the nested build package (deps/foo), not the root build.
package build

import mill.*, scalalib.*
import build.myScalaVersion // Import from versions.mill via the build alias

trait FooModule extends ScalaModule { // trait definition in Helper Files
  def scalaVersion = myScalaVersion
}
deps/foo/versions.mill (download, browse)
// deps/foo/versions.mill
package build

def myScalaVersion = "2.13.16"

In this example:

  • The root build.mill uses package build and defines a BarModule trait

  • deps/package.mill uses package build.deps to create the intermediate namespace

  • deps/foo/build.mill also uses package build (with mill-allow-nested-build-mill header)

  • deps/foo/module.mill is a helper file that contains the FooModule trait definition

  • deps/foo/versions.mill is a helper file that contains shared version configuration

Helper files in nested directories can be referenced from parent build files: - Traits and objects defined in versions.mill can be imported like build.deps.foo.versions.myScalaVersion - Traits and objects defined in module.mill can be imported like build.deps.foo.FooModule

Within a nested build, helper files can import from each other using import build.something. The build alias refers to the enclosing nested build package, so import build.myScalaVersion in module.mill correctly imports from versions.mill in the same nested build directory.

The nested deps/foo/build.mill simply composes these helper files and references the root build as needed. Note the use of build to reference the generated build package object from parent scopes.

> ./mill show deps.foo.scalaVersion
"2.13.16"

Each nested build.mill with mill-allow-nested-build-mill can use the simple package build declaration while still being accessible through its full hierarchical path from parent build files. Shared logic should be extracted into helper files.