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
extends: [mill.scalalib.ScalaModule]
scalaVersion: 3.7.2
mvnDeps:
- "com.lihaoyi::scalatags:0.13.1"
- "com.lihaoyi::mainargs:0.7.7"
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.yamldefines the configuration for the root module -
test/package.mill.yamldefines the submoduletest/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><hello></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.
extends: [mill.scalalib.ScalaModule]
moduleDeps: [bar]
scalaVersion: 3.7.1
mvnDeps:
- com.lihaoyi::mainargs:0.7.7
extends: [mill.scalalib.ScalaModule]
scalaVersion: 3.7.1
mvnDeps:
- com.lihaoyi::scalatags:0.13.1
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.compileand 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.compileif 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:
Wildcard |
Function |
|
matches a single segment of the task path |
|
matches arbitrary segments of the task path |
|
is equal to specifying two tasks |
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:
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
./millwithout 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:
//| 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
mvnDepsneeded 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:
//| 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><hello></h1>")
result
}
}
}
> ./mill FooTests.scala
+ FooTests.simple ... "<h1>hello</h1>"
+ FooTests.escaping ... "<h1><hello></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><hello></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
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><hello></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
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 resourcesreplaces the previousresourcefolder provided byScalaModule(labelledresources.superbelow), including both the previous resource foldersuper.resources()together with theTask.destfolder of the new task which contains alint-count.txtfile we write.
-
os.read.linesandos.writecome 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
overridekeyword 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
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:
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.
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
CrossScalaModulescompiled against two Scala versions, that depend on each other as well as on aJavaModule -
With unit testing and publishing set up
-
With version-specific sources
-
With generated sources to include the
publishVersionas 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