Skip to content

Commit

Permalink
Add some helpers to simplify cross-version/cross-platform modules (#2406
Browse files Browse the repository at this point in the history
)

This PR attempts to minimize the boilerplate of defining cross-version
cross-platform Scala modules.

Currently, cross-version cross-platform modules involves a lot annoying
fiddling with overriding `millSourcePath` and `sources` and
`artifactName` and passing around `crossScalaVersion` and all that.
Simple `Cross`/`CrossScalaModule`s get some of the plumbing handled
behind the scenes, but anything more complicated and the plumbing spills
out again into plain view. And although `Cross`/`CrossScalaModule`s have
less boilerplate, it's still enough boilerplate you don't want to go
around defining them over and over

This PR tries to hide all of them behind helper traits
`CrossScalaModule.Wrapper` and `PlatformScalaModule`, so your build file
only has the logic you care about and none of the plumbing.

## Major changes

1. We replace the `artifactName` definition, so that instead of reading
from `millModuleSegments` directly, it reads from `artifactNameParts:
Seq[String]`. This makes it easier for us to splice the artifact name
parts to remove unwanted segments without needing to do fragile string
manipulation

2. Introduce `CrossScalaModule.Wrapper`. This can be used when the
`ScalaModule`s you are defining are not the direct children of the
`Cross` module, but are nested somewhere inside of it.
`CrossScalaModule.Wrapper` shades the `CrossScalaModule` trait, so that
any `CrossScalaModule` defined within its body would have the
`crossScalaVersion` automatically wired up and the `artifactNameParts`
properly adjusted to remove the cross segment in the middle of the
artifact name

3. Introduce `PlatformScalaModule`. This is analogous to
`CrossScalaModule`, in that it adjusts the
`millSourcePath`/`artifactNameParts` to drop the last segment, and
adjusts `sources` to include the `-{jvm,js,native}` suffix

## Tests

The changes are largely covered by existing tests, including the
`example/` tests which were adjusted to make use of the new traits

## Notes

1. We cannot make the platform-specific sub-modules into `Cross` modules
like we do with the version-specific sub-modules, since the
platform-specific submodules have pretty different types/targets defined
on them and `Cross` modules must all be of the same type


2. `6-cross-platform-publishing`, is a lot better than before, but there
is still a significant amount of boilerplate involved, especially the
lines around `object wrapper`. Not sure if there are other things we can
do to make it even easier to define these cross-version/platform modules
  • Loading branch information
lihaoyi committed Apr 8, 2023
1 parent d9cf409 commit 785e878
Show file tree
Hide file tree
Showing 19 changed files with 153 additions and 113 deletions.
17 changes: 17 additions & 0 deletions example/basic/5-nested-modules/baz/src/Baz.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package baz
import scalatags.Text.all._
import mainargs.{main, ParserForMethods, arg}

object Baz {
@main
def main(@arg(name = "bar-text") barText: String,
@arg(name = "qux-text") quxText: String,
@arg(name = "baz-text") bazText: String): Unit = {
foo.qux.Qux.main(barText, quxText)

val value = p(bazText)
println("Baz.value: " + value)
}

def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
}
29 changes: 15 additions & 14 deletions example/basic/5-nested-modules/build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ trait MyModule extends ScalaModule{
)
}

object wrapper extends Module{
object foo extends MyModule {
object foo extends Module{
object bar extends MyModule

object qux extends MyModule {
def moduleDeps = Seq(bar)
}

object bar extends MyModule
}

object qux extends MyModule {
def moduleDeps = Seq(wrapper.bar, wrapper.foo)
object baz extends MyModule {
def moduleDeps = Seq(foo.bar, foo.qux)
}

// Modules can be nested arbitrarily deeply within each other. The outer module
Expand All @@ -31,16 +31,17 @@ object qux extends MyModule {
/* Example Usage
> ./mill resolve __.run
wrapper.foo.run
wrapper.bar.run
foo.bar.run
foo.qux.run
qux.run
> ./mill qux.run --foo-text hello --bar-text world --qux-text today
Foo.value: <h1>hello</h1>
Bar.value: <p>world</p>
Qux.value: <p>today</p>
> ./mill baz.run --bar-text hello --qux-text world --baz-text today
Bar.value: <h1>hello</h1>
Qux.value: <p>world</p>
Baz.value: <p>today</p>
> ./mill wrapper.foo.run --text hello
Foo.value: <h1>hello</h1>
> ./mill foo.qux.run --bar-text hello --qux-text world
Bar.value: <h1>hello</h1>
Qux.value: <p>world</p>
*/
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package bar
package foo.bar
import scalatags.Text.all._
import mainargs.{main, ParserForMethods}
object Bar {

object Bar {
@main
def main(text: String): Unit = {
val value = p(text)
val value = h1(text)
println("Bar.value: " + value)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package qux
package foo.qux
import scalatags.Text.all._
import mainargs.{main, ParserForMethods, arg}
object Qux {


object Qux {
@main
def main(@arg(name="foo-text") fooText: String,
@arg(name="bar-text") barText: String,
@arg(name="qux-text") quxText: String): Unit = {
foo.Foo.main(fooText)
bar.Bar.main(barText)
def main(@arg(name = "bar-text") barText: String,
@arg(name = "qux-text") quxText: String): Unit = {
foo.bar.Bar.main(barText)

val value = p(quxText)
println("Qux.value: " + value)
Expand Down
12 changes: 0 additions & 12 deletions example/basic/5-nested-modules/wrapper/foo/src/Foo.scala

This file was deleted.

3 changes: 1 addition & 2 deletions example/web/5-webapp-scalajs-shared/build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ object app extends RootModule with AppScalaModule{
}

object shared extends Module{
trait SharedModule extends AppScalaModule{
def millSourcePath = super.millSourcePath / os.up
trait SharedModule extends AppScalaModule with PlatformScalaModule {

def ivyDeps = Agg(
ivy"com.lihaoyi::scalatags::0.12.0",
Expand Down
106 changes: 47 additions & 59 deletions example/web/6-cross-platform-publishing/build.sc
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import mill._, scalalib._, scalajslib._, publish._

object wrapper extends Cross[WrapperModule]("2.13.10", "3.2.2")
class WrapperModule(val crossScalaVersion: String) extends Module {

trait MyModule extends CrossScalaModule with PublishModule {
def artifactName = millModuleSegments.parts.dropRight(1).last

def crossScalaVersion = WrapperModule.this.crossScalaVersion
def millSourcePath = super.millSourcePath / os.up
object foo extends Cross[FooModule]("2.13.10", "3.2.2")
class FooModule(val crossScalaVersion: String) extends CrossScalaModule.Base {
trait Shared extends CrossScalaModule with PlatformScalaModule with PublishModule {
def publishVersion = "0.0.1"

def pomSettings = PomSettings(
Expand All @@ -25,35 +20,28 @@ class WrapperModule(val crossScalaVersion: String) extends Module {
def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.7.11")
def testFramework = "utest.runner.Framework"
}

def sources = T.sources {
val platform = millModuleSegments.parts.last
super.sources().flatMap(source =>
Seq(
source,
PathRef(source.path / os.up / s"${source.path.last}-${platform}")
)
)
}
}
trait MyScalaJSModule extends MyModule with ScalaJSModule {

trait SharedJS extends Shared with ScalaJSModule {
def scalaJSVersion = "1.13.0"
}

object foo extends Module{
object jvm extends MyModule{
object bar extends Module {
object jvm extends Shared

object js extends SharedJS
}

object qux extends Module{
object jvm extends Shared{
def moduleDeps = Seq(bar.jvm)
def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.lihaoyi::upickle::3.0.0")
}
object js extends MyScalaJSModule {
object js extends SharedJS {
def moduleDeps = Seq(bar.js)
}
}

object bar extends Module{
object jvm extends MyModule
object js extends MyScalaJSModule
}
}

// This example demonstrates how to publish Scala modules which are both
Expand All @@ -62,43 +50,43 @@ class WrapperModule(val crossScalaVersion: String) extends Module {

/* Example Usage
> ./mill show wrapper[2.13.10].foo.jvm.sources # mac/linux
wrapper/foo/src
wrapper/foo/src-jvm
wrapper/foo/src-2.13.10
wrapper/foo/src-2.13.10-jvm
wrapper/foo/src-2.13
wrapper/foo/src-2.13-jvm
wrapper/foo/src-2
wrapper/foo/src-2-jvm
> ./mill show wrapper[3.2.2].bar.js.sources # mac/linux
wrapper/bar/src
wrapper/bar/src-js
wrapper/bar/src-3.2.2
wrapper/bar/src-3.2.2-js
wrapper/bar/src-3.2
wrapper/bar/src-3.2-js
wrapper/bar/src-3
wrapper/bar/src-3-js
> ./mill wrapper[2.13.10].foo.jvm.run
> ./mill show foo[2.13.10].bar.jvm.sources
foo/bar/src
foo/bar/src-jvm
foo/bar/src-2.13.10
foo/bar/src-2.13.10-jvm
foo/bar/src-2.13
foo/bar/src-2.13-jvm
foo/bar/src-2
foo/bar/src-2-jvm
> ./mill show foo[3.2.2].qux.js.sources
foo/qux/src
foo/qux/src-js
foo/qux/src-3.2.2
foo/qux/src-3.2.2-js
foo/qux/src-3.2
foo/qux/src-3.2-js
foo/qux/src-3
foo/qux/src-3-js
> ./mill foo[2.13.10].qux.jvm.run
Bar.value: <p>world Specific code for Scala 2.x</p>
Parsing JSON with ujson.read
Foo.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)
Qux.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)
> ./mill wrapper[3.2.2].foo.js.run
> ./mill foo[3.2.2].qux.js.run
Bar.value: <p>world Specific code for Scala 3.x</p>
Parsing JSON with js.JSON.parse
Foo.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)
Qux.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)
> ./mill __.publishLocal
Publishing Artifact(com.lihaoyi,bar_sjs1_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_sjs1_2.13,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo_2.13,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,bar_sjs1_3,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,bar_3,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo_sjs1_3,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo_3,0.0.1) to ivy repo
*/
Publishing Artifact(com.lihaoyi,foo-bar_sjs1_2.13,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo-bar_2.13,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo-qux_sjs1_2.13,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo-qux_2.13,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo-bar_sjs1_3,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo-bar_3,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo-qux_sjs1_3,0.0.1) to ivy repo
Publishing Artifact(com.lihaoyi,foo-qux_3,0.0.1) to ivy repo
*/
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package foo
package qux
import scala.scalajs.js
object FooPlatformSpecific {
object QuxPlatformSpecific {
def parseJsonGetKeys(s: String): Set[String] = {
println("Parsing JSON with js.JSON.parse")
js.JSON.parse(s).asInstanceOf[js.Dictionary[_]].keys.toSet
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package foo
object FooPlatformSpecific {
package qux
object QuxPlatformSpecific {
def parseJsonGetKeys(s: String): Set[String] = {
println("Parsing JSON with ujson.read")
ujson.read(s).obj.keys.toSet
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package foo
package qux
import scalatags.Text.all._
object Foo {
object Qux {
def main(args: Array[String]): Unit = {
println("Bar.value: " + bar.Bar.value)
val string = """{"i": "am", "cow": "hear", "me": "moo"}"""
println("Foo.main: " + FooPlatformSpecific.parseJsonGetKeys(string).map(p(_)))
println("Qux.main: " + QuxPlatformSpecific.parseJsonGetKeys(string).map(p(_)))
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package foo
package qux
import utest._
object FooTests extends TestSuite {
object QuxTests extends TestSuite {
def tests = Tests {
test("parseJsonGetKeys") {
val string = """{"i": "am", "cow": "hear", "me": "moo}"""
val keys = FooPlatformSpecific.parseJsonGetKeys(string)
val keys = QuxPlatformSpecific.parseJsonGetKeys(string)
assert(keys == Set("i", "cow", "me"))
keys
}
Expand Down
3 changes: 2 additions & 1 deletion scalalib/src/mill/scalalib/CrossModuleBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ trait CrossModuleBase extends ScalaModule {
protected def scalaVersionDirectoryNames: Seq[String] =
ZincWorkerUtil.matchingVersions(crossScalaVersion)

override def artifactName: T[String] = millModuleSegments.parts.init.mkString("-")
protected def wrapperSegments = millModuleSegments.parts
override def artifactNameParts = super.artifactNameParts().patch(wrapperSegments.size - 1, Nil, 1)
implicit def crossSbtModuleResolver: Resolver[CrossModuleBase] =
new Resolver[CrossModuleBase] {
def resolve[V <: CrossModuleBase](c: Cross[V]): V = {
Expand Down
24 changes: 21 additions & 3 deletions scalalib/src/mill/scalalib/CrossScalaModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import mill.api.PathRef
import mill.T

/**
* A [[ScalaModule]] with is suited to be used with [[mill.define.Cross]].
* It supports additional source directories with the scala version pattern as suffix (`src-{scalaversionprefix}`),
* e.g.
* A [[ScalaModule]] which is suited to be used with [[mill.define.Cross]].
* It supports additional source directories with the scala version pattern
* as suffix (`src-{scalaversionprefix}`), e.g.
*
* - src
* - src-2.11
* - src-2.12.3
Expand All @@ -25,3 +26,20 @@ trait CrossScalaModule extends ScalaModule with CrossModuleBase { outer =>
}
trait Tests extends CrossScalaModuleTests
}

object CrossScalaModule {

/**
* Used with a [[mill.define.Cross]] when you want [[CrossScalaModule]]'s
* nested within it
*/
trait Base extends mill.Module {
def crossScalaVersion: String
private def wrapperSegments0 = millModuleSegments.parts
trait CrossScalaModule extends mill.scalalib.CrossScalaModule {
override def wrapperSegments = wrapperSegments0
def crossScalaVersion = Base.this.crossScalaVersion

}
}
}
4 changes: 3 additions & 1 deletion scalalib/src/mill/scalalib/JavaModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,9 @@ trait JavaModule
* For example, by default a scala module foo.baz might be published as foo-baz_2.12 and a java module would be foo-baz.
* Setting this to baz would result in a scala artifact baz_2.12 or a java artifact baz.
*/
def artifactName: T[String] = millModuleSegments.parts.mkString("-")
def artifactName: T[String] = artifactNameParts().mkString("-")

def artifactNameParts: T[Seq[String]] = millModuleSegments.parts

/**
* The exact id of the artifact to be published. You probably don't want to override this.
Expand Down
29 changes: 29 additions & 0 deletions scalalib/src/mill/scalalib/PlatformScalaModule.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package mill.scalalib
import mill._

/**
* A [[ScalaModule]] intended for defining `.jvm`/`.js`/`.native` submodules
* It supports additional source directories per platform, e.g. `src-jvm/` or
* `src-js/` and can be used inside a [[CrossScalaModule.Base]], to get one
* source folder per platform per version e.g. `src-2.12-jvm/`.
*
* Adjusts the [[millSourcePath]] and [[artifactNameParts]] to ignore the last
* path segment, which is assumed to be the name of the platform the module is
* built against and not something that should affect the filesystem path or
* artifact name
*/
trait PlatformScalaModule extends ScalaModule {
override def millSourcePath = super.millSourcePath / os.up

override def sources = T.sources {
val platform = millModuleSegments.parts.last
super.sources().flatMap(source =>
Seq(
source,
PathRef(source.path / os.up / s"${source.path.last}-${platform}")
)
)
}

override def artifactNameParts = super.artifactNameParts().dropRight(1)
}

0 comments on commit 785e878

Please sign in to comment.