A few questions about mill

Scala seems like a good language to me but SBT turned me away. mill seems far more reasonable but some things remain unclear to me about it. The mill documentation leaves a number of things unstated and even after giving myself a little exposure to Maven and Gradle to see what precedents might be there I’m still confused.

Let’s start with the very basic beginner example. Here is the build.sc:

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

object foo extends ScalaModule{
  def scalaVersion = "2.13.2"
}

OK, simple enough. And it follows that the file is in foo/src/. But how does it know to use Example.scala? For reference, here it is:

package foo

object Example{
  def main(args: Array[String]): Unit = {
    println("Hello World")
  }
}

Granted, it’s the only file in this directory in this example. But I extended it by adding another file, defining a trivial class in it, importing it into Example.scala, and making use of it in main. How does mill foo.run know how to use main in Example.scala? I assume it scans through every relevant Scala file until it finds one with a singleton class that implements main. Is that not so?

The next example comes from a book by the author of mill, Hands-On Scala Programming. This book isn’t open source and I understand that not everyone is going to have that, so I’ll reproduce what I think is relevant here and if anything further is needed, I’ll give that too. Here is the build.sc:

import mill._, scalalib._

object foo extends ScalaModule {
  def scalaVersion = "2.13.2"
  object test extends Tests {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.7.4")
    def testFrameworks = Seq("utest.runner.framework")
  }
}

This is also pretty straightforward but ivyDeps stands out to me. Apache Ivy, as I understand it, is a dependency management system devised for Apache Ant so what is the significance of ivyDeps here? Does it mean that dependencies are specified using the format of Ivy?

I think my last issue here is tests. There is one test file, foo/test/src/ExampleTests.scala. Here are the contents:

package foo
import utest._

object ExampleTests extends TestSuite {
  def tests = Tests {
    test("hello") {
      val result = Example.hello()
      assert(result == "Hello world")
      result
    }
  }
}

How does mill know to use this file? Is it because the singleton object has the same name as the one in foo/src with “Tests” appended? I’m not sure what the convention is here.

I think that’s all for now.

I think your assumptions are mostly correct.

In the case of tests it doesn’t have to match the name, it will run all tests it finds.

I don’t know exactly why dependencies are called ivyDeps but Ivy is certainly not Ant-specific. Sbt used Ivy until recently when the default was changed to coursier.

1 Like

What else do I have to know about Ivy? I saw one colon for Java dependencies and two for Scala. Can I use Java dependencies in Scala more or less directly?

I don’t know much about Scala or JVM development currently, but say I want to publish a library someday. Ignoring my lack of familiarity with Maven Central in general, in this case I would presumably use a backwards website format like everyone else, starting with “com” or “org” or whatever. The mill website shows that I can nest packages. I assume dependencies and pretty much everything else would be in the innermost package? It would appear only scalaVersion needs to be defined at each level.

I think this blog post by the author gives more insight into the workings of mill:
https://www.lihaoyi.com/post/BuildToolsasPureFunctionalPrograms.html

1 Like

This article seems like a big picture view for someone with a lot more experience with JVM development. Right now, I can’t appreciate the entire forest; I need to get acquainted with individual trees.

A package in a Scala program is unrelated to the name/coordinates of the library on maven central. You can choose to allign them but it’s not necessary and most Scala libraries don’t do it. In Java it’s more common for some reason.

But publishing libraries has a lot of complexity that’s mostly unrelated to the build tools themselves (they help cope with some of it but they don’t cause it). If you’re trying to understand the basic workings of a build tool, I wouldn’t go there yet.

1 Like

Hmm. Could you go into a little more detail about how packages in Scala can be disaligned with those on Maven Central? I don’t want to require any tedium from you personally but a simple example of an extant Scala library that does this would be adequate, I think.

These are great questions! I’m not so familiar with Mill but can try to answer your questions.

How does mill foo.run know how to use main in Example.scala ?

This functionality to automatically detect the main class is provided by Zinc, the incremental compiler that’s used by both Mill and sbt. I don’t know exactly how Zinc detects main classes, but one possible way to implement it is by walking through all the classfiles and testing if they declare a regular class with a static main method with the correct signature. This works regardless if the source code is written in Java or Scala.

Does it mean that dependencies are specified using the format of Ivy?

I think ivyDeps is an unfortunate choice of name in Mill/Ammonite since it supports both Ivy-style and Maven-style dependencies. In sbt, the equivalent setting is called libraryDependencies. Based on my limited understanding, the difference between Ivy-style and Maven-style dependencies is that Ivy-dependencies are more flexible/powerful since they can include custom attributes in the coordinates (such as sbt version). Maven-style dependencies are uniquely identified by the triple (organization, artifact name, version).

How does mill know to use this [ExampleTests.scala] file?

Similar to main method detection, this functionality is provided by Zinc. Testing libraries implement the following interface (https://github.com/sbt/test-interface) to declare what kind of “fingerprint” they support. For example, the utest test framework implementation supports the fingerprint "object that extends utest.TestSuite". Given a fingerprint, Zinc walks the compiled classfiles to detect what test suites to run. The same feature can be used to, for example, power tab-completions to run only a single test suite (I’m not sure if Mill supports tab completions in this way).

2 Likes

To be clear I’m talking about language level packages (e.g. your package foo) vs a “maven artifact” or jar-file with pom.xml. Mill modules are yet another thing. A maven artifact can correspond to a mill module. But I think it could also consist of multiple modules. And you probably don’t want to publish all submodules either because that would include your tests.

The Scala library com.lihaoyi::os-lib for example uses the language level package os. org.typelevel::cats-core simply uses cats.
The more Java friendly org.apache.spark::spark-core however uses org.apache.spark.

2 Likes

Excellent questions, and I think folks have addressed most of them. Just one additional nuance: the main() method is kind of magic, but that has nothing to do with Mill, or even Scala – that’s an ancient JVM convention, that if there exists one main() method, taking an Array of Strings, then that defines the primary entry point for the program. So for example, here is a typical Java Hello, World.

2 Likes

I’m entirely familiar with main() from having done C in middle and high school, plus what exposure to Java I do have. I just wasn’t clear on how it was being found.

Hi @readyready15728,

you can also have a look here: https://alvinalexander.com/scala/mill-build-tool/step-1-hello-world/

where Alvin Alexander goes over the first few steps of using mill for a project in a couple of pages, with more explanations to some of your questions.