From b9824967392e4b674881e5dcec4c4fbb05e6cd9b Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Sun, 5 Aug 2018 23:23:12 +0900 Subject: Add support for Dotty projects (#397) * Abstract over the scala compiler organization * Support using a locally published compiler Publishing locally with sbt means publishing ivy-style, which uses a different naming convention than maven, we now handle both cases. * Add minimal support for Dotty projects * Rewrite scalalib.Dep, introduce scalalib.CrossVersion Instead of Dep being a trait with three cases (Java/Scala/Point), it is now a case class where the cross field is an instance of the CrossVersion trait which has three cases (Constant/Binary/Full). This is more versatile since it allows for non-empty constant suffixes which will be used to implement withDottyCompat in the next commit. It's also a cleaner separation of concerns. We also deduplicate various pieces of codes that computed the artifact name: this is now always handled in Dep and CrossVersion. * Add simple way to use Scala 2 deps in a Dotty project This is similar to the withDottyCompat method in the sbt-dotty plugin. * Turn off the Dotty test on Java >= 9 --- scalalib/src/mill/scalalib/Dep.scala | 148 ++++++++++++++-------- scalalib/src/mill/scalalib/Lib.scala | 71 ++++------- scalalib/src/mill/scalalib/ScalaModule.scala | 50 ++++++-- scalalib/src/mill/scalalib/publish/settings.scala | 52 +++----- 4 files changed, 182 insertions(+), 139 deletions(-) (limited to 'scalalib/src') diff --git a/scalalib/src/mill/scalalib/Dep.scala b/scalalib/src/mill/scalalib/Dep.scala index 9719bd2d..b419462c 100644 --- a/scalalib/src/mill/scalalib/Dep.scala +++ b/scalalib/src/mill/scalalib/Dep.scala @@ -1,32 +1,60 @@ package mill.scalalib import mill.util.JsonFormatters._ import upickle.default.{macroRW, ReadWriter => RW} -sealed trait Dep { - def configure(attributes: coursier.Attributes): Dep - def force: Boolean - def forceVersion(): Dep = this match { - case dep : Dep.Java => dep.copy(force = true) - case dep : Dep.Scala => dep.copy(force = true) - case dep : Dep.Point => dep.copy(force = true) - } - def exclude(exclusions: (String, String)*): Dep = this match { - case dep : Dep.Java => dep.copy(dep = dep.dep.copy(exclusions = dep.dep.exclusions ++ exclusions)) - case dep : Dep.Scala => dep.copy(dep = dep.dep.copy(exclusions = dep.dep.exclusions ++ exclusions)) - case dep : Dep.Point => dep.copy(dep = dep.dep.copy(exclusions = dep.dep.exclusions ++ exclusions)) + +import CrossVersion._ + +case class Dep(dep: coursier.Dependency, cross: CrossVersion, force: Boolean) { + import Dep.isDotty + + def artifactName(binaryVersion: String, fullVersion: String, platformSuffix: String) = { + val suffix = cross.suffixString(binaryVersion, fullVersion, platformSuffix) + dep.module.name + suffix } + def configure(attributes: coursier.Attributes): Dep = copy(dep = dep.copy(attributes = attributes)) + def forceVersion(): Dep = copy(force = true) + def exclude(exclusions: (String, String)*) = copy(dep = dep.copy(exclusions = dep.exclusions ++ exclusions)) def excludeOrg(organizations: String*): Dep = exclude(organizations.map(_ -> "*"): _*) def excludeName(names: String*): Dep = exclude(names.map("*" -> _): _*) - def withConfiguration(configuration: String): Dep = this match { - case dep : Dep.Java => dep.copy(dep = dep.dep.copy(configuration = configuration)) - case dep : Dep.Scala => dep.copy(dep = dep.dep.copy(configuration = configuration)) - case dep : Dep.Point => dep.copy(dep = dep.dep.copy(configuration = configuration)) - } + def toDependency(binaryVersion: String, fullVersion: String, platformSuffix: String) = + dep.copy(module = dep.module.copy(name = artifactName(binaryVersion, fullVersion, platformSuffix))) + def withConfiguration(configuration: String): Dep = copy(dep = dep.copy(configuration = configuration)) + + /** + * If scalaVersion is a Dotty version, replace the cross-version suffix + * by the Scala 2.x version that the Dotty version is retro-compatible with, + * otherwise do nothing. + * + * This setting is useful when your build contains dependencies that have only + * been published with Scala 2.x, if you have: + * {{{ + * def ivyDeps = Agg(ivy"a::b:c") + * }}} + * you can replace it by: + * {{{ + * def ivyDeps = Agg(ivy"a::b:c".withDottyCompat(scalaVersion())) + * }}} + * This will have no effect when compiling with Scala 2.x, but when compiling + * with Dotty this will change the cross-version to a Scala 2.x one. This + * works because Dotty is currently retro-compatible with Scala 2.x. + */ + def withDottyCompat(scalaVersion: String): Dep = + cross match { + case cross: Binary if isDotty(scalaVersion) => + copy(cross = Constant(value = "_2.12", platformed = cross.platformed)) + case _ => + this + } } -object Dep{ + +object Dep { val DefaultConfiguration = "default(compile)" - implicit def parse(signature: String) = { + def isDotty(scalaVersion: String) = + scalaVersion.startsWith("0.") + + implicit def parse(signature: String): Dep = { val parts = signature.split(';') val module = parts.head val attributes = parts.tail.foldLeft(coursier.Attributes()) { (as, s) => @@ -37,48 +65,60 @@ object Dep{ } } (module.split(':') match { - case Array(a, b, c) => Dep.Java(a, b, c, cross = false, force = false) - case Array(a, b, "", c) => Dep.Java(a, b, c, cross = true, force = false) - case Array(a, "", b, c) => Dep.Scala(a, b, c, cross = false, force = false) - case Array(a, "", b, "", c) => Dep.Scala(a, b, c, cross = true, force = false) - case Array(a, "", "", b, c) => Dep.Point(a, b, c, cross = false, force = false) - case Array(a, "", "", b, "", c) => Dep.Point(a, b, c, cross = true, force = false) + case Array(a, b, c) => Dep(a, b, c, cross = empty(platformed = false)) + case Array(a, b, "", c) => Dep(a, b, c, cross = empty(platformed = true)) + case Array(a, "", b, c) => Dep(a, b, c, cross = Binary(platformed = false)) + case Array(a, "", b, "", c) => Dep(a, b, c, cross = Binary(platformed = true)) + case Array(a, "", "", b, c) => Dep(a, b, c, cross = Full(platformed = false)) + case Array(a, "", "", b, "", c) => Dep(a, b, c, cross = Full(platformed = true)) case _ => throw new Exception(s"Unable to parse signature: [$signature]") }).configure(attributes = attributes) } - def apply(org: String, name: String, version: String, cross: Boolean): Dep = { - this(coursier.Dependency(coursier.Module(org, name), version, DefaultConfiguration), cross) - } - case class Java(dep: coursier.Dependency, cross: Boolean, force: Boolean) extends Dep { - def configure(attributes: coursier.Attributes): Dep = copy(dep = dep.copy(attributes = attributes)) + def apply(org: String, name: String, version: String, cross: CrossVersion, force: Boolean = false): Dep = { + apply(coursier.Dependency(coursier.Module(org, name), version, DefaultConfiguration), cross, force) } - object Java{ - implicit def rw: RW[Java] = macroRW - def apply(org: String, name: String, version: String, cross: Boolean, force: Boolean): Dep = { - Java(coursier.Dependency(coursier.Module(org, name), version, DefaultConfiguration), cross, force) + implicit def rw: RW[Dep] = macroRW +} + +sealed trait CrossVersion { + /** If true, the cross-version suffix should start with a platform suffix if it exists */ + def platformed: Boolean + + def isBinary: Boolean = + this.isInstanceOf[Binary] + def isConstant: Boolean = + this.isInstanceOf[Constant] + def isFull: Boolean = + this.isInstanceOf[Full] + + /** The string that should be appended to the module name to get the artifact name */ + def suffixString(binaryVersion: String, fullVersion: String, platformSuffix: String): String = { + val firstSuffix = if (platformed) platformSuffix else "" + this match { + case cross: Constant => + s"${firstSuffix}${cross.value}" + case cross: Binary => + s"${firstSuffix}_${binaryVersion}" + case cross: Full => + s"${firstSuffix}_${fullVersion}" } } - implicit def default(dep: coursier.Dependency): Dep = new Java(dep, false, false) - def apply(dep: coursier.Dependency, cross: Boolean) = Scala(dep, cross, false) - case class Scala(dep: coursier.Dependency, cross: Boolean, force: Boolean) extends Dep { - def configure(attributes: coursier.Attributes): Dep = copy(dep = dep.copy(attributes = attributes)) - } - object Scala{ - implicit def rw: RW[Scala] = macroRW - def apply(org: String, name: String, version: String, cross: Boolean, force: Boolean): Dep = { - Scala(coursier.Dependency(coursier.Module(org, name), version, DefaultConfiguration), cross, force) - } +} +object CrossVersion { + case class Constant(value: String, platformed: Boolean) extends CrossVersion + object Constant { + implicit def rw: RW[Constant] = macroRW } - case class Point(dep: coursier.Dependency, cross: Boolean, force: Boolean) extends Dep { - def configure(attributes: coursier.Attributes): Dep = copy(dep = dep.copy(attributes = attributes)) + case class Binary(platformed: Boolean) extends CrossVersion + object Binary { + implicit def rw: RW[Binary] = macroRW } - object Point{ - implicit def rw: RW[Point] = macroRW - def apply(org: String, name: String, version: String, cross: Boolean, force: Boolean): Dep = { - Point(coursier.Dependency(coursier.Module(org, name), version, DefaultConfiguration), cross, force) - } + case class Full(platformed: Boolean) extends CrossVersion + object Full { + implicit def rw: RW[Full] = macroRW } - implicit def rw = RW.merge[Dep]( - Java.rw, Scala.rw, Point.rw - ) + + def empty(platformed: Boolean) = Constant(value = "", platformed) + + implicit def rw: RW[CrossVersion] = RW.merge(Constant.rw, Binary.rw, Full.rw) } diff --git a/scalalib/src/mill/scalalib/Lib.scala b/scalalib/src/mill/scalalib/Lib.scala index b69409a6..e890caaf 100644 --- a/scalalib/src/mill/scalalib/Lib.scala +++ b/scalalib/src/mill/scalalib/Lib.scala @@ -10,6 +10,7 @@ import javax.tools.ToolProvider import ammonite.ops._ import ammonite.util.Util import coursier.{Cache, Dependency, Fetch, Repository, Resolution} +import Dep.isDotty import mill.Agg import mill.eval.{PathRef, Result} import mill.modules.Jvm @@ -56,58 +57,37 @@ object Lib{ private val ReleaseVersion = raw"""(\d+)\.(\d+)\.(\d+)""".r private val MinorSnapshotVersion = raw"""(\d+)\.(\d+)\.([1-9]\d*)-SNAPSHOT""".r + private val DottyVersion = raw"""0\.(\d+)\.(\d+).*""".r def scalaBinaryVersion(scalaVersion: String) = { scalaVersion match { case ReleaseVersion(major, minor, _) => s"$major.$minor" case MinorSnapshotVersion(major, minor, _) => s"$major.$minor" + case DottyVersion(minor, _) => s"0.$minor" case _ => scalaVersion } } - def grepJar(classPath: Agg[Path], s: String) = { + def grepJar(classPath: Agg[Path], name: String, version: String) = { + val mavenStylePath = s"$name-$version.jar" + val ivyStylePath = s"$version/$name.jar" + classPath - .find(_.toString.endsWith(s)) - .getOrElse(throw new Exception("Cannot find " + s)) - .toIO + .find(p => p.toString.endsWith(mavenStylePath) || p.toString.endsWith(ivyStylePath)) + .getOrElse(throw new Exception(s"Cannot find $mavenStylePath or $ivyStylePath")) } - def depToDependencyJava(dep: Dep, platformSuffix: String = ""): Dependency = { - dep match { - case Dep.Java(dep, cross, force) => - dep.copy( - module = dep.module.copy( - name = - dep.module.name + - (if (!cross) "" else platformSuffix) - ) - ) - } + assert(dep.cross.isConstant, s"Not a Java dependency: $dep") + depToDependency(dep, "", platformSuffix) } - def depToDependency(dep: Dep, scalaVersion: String, platformSuffix: String = ""): Dependency = - dep match { - case d: Dep.Java => depToDependencyJava(dep) - case Dep.Scala(dep, cross, force) => - dep.copy( - module = dep.module.copy( - name = - dep.module.name + - (if (!cross) "" else platformSuffix) + - "_" + scalaBinaryVersion(scalaVersion) - ) - ) - case Dep.Point(dep, cross, force) => - dep.copy( - module = dep.module.copy( - name = - dep.module.name + - (if (!cross) "" else platformSuffix) + - "_" + scalaVersion - ) - ) - } + def depToDependency(dep: Dep, scalaVersion: String, platformSuffix: String = ""): Dependency = + dep.toDependency( + binaryVersion = scalaBinaryVersion(scalaVersion), + fullVersion = scalaVersion, + platformSuffix = platformSuffix + ) def resolveDependenciesMetadata(repositories: Seq[Repository], depToDependency: Dep => coursier.Dependency, @@ -142,12 +122,17 @@ object Lib{ mapDependencies ) } - def scalaCompilerIvyDeps(scalaVersion: String) = Agg[Dep]( - ivy"org.scala-lang:scala-compiler:$scalaVersion".forceVersion(), - ivy"org.scala-lang:scala-reflect:$scalaVersion".forceVersion() - ) - def scalaRuntimeIvyDeps(scalaVersion: String) = Agg[Dep]( - ivy"org.scala-lang:scala-library:$scalaVersion".forceVersion() + def scalaCompilerIvyDeps(scalaOrganization: String, scalaVersion: String) = + if (isDotty(scalaVersion)) + Agg(ivy"$scalaOrganization::dotty-compiler:$scalaVersion".forceVersion()) + else + Agg( + ivy"$scalaOrganization:scala-compiler:$scalaVersion".forceVersion(), + ivy"$scalaOrganization:scala-reflect:$scalaVersion".forceVersion() + ) + + def scalaRuntimeIvyDeps(scalaOrganization: String, scalaVersion: String) = Agg[Dep]( + ivy"$scalaOrganization:scala-library:$scalaVersion".forceVersion() ) def listClassFiles(base: Path): Iterator[String] = { diff --git a/scalalib/src/mill/scalalib/ScalaModule.scala b/scalalib/src/mill/scalalib/ScalaModule.scala index 3cea7fab..1899ee14 100644 --- a/scalalib/src/mill/scalalib/ScalaModule.scala +++ b/scalalib/src/mill/scalalib/ScalaModule.scala @@ -8,6 +8,7 @@ import mill.define.TaskModule import mill.eval.{PathRef, Result} import mill.modules.Jvm import mill.modules.Jvm.{createJar, subprocess} +import Dep.isDotty import Lib._ import mill.util.Loose.Agg import mill.util.DummyInputStream @@ -17,6 +18,7 @@ import mill.util.DummyInputStream */ trait ScalaModule extends JavaModule { outer => trait Tests extends TestModule with ScalaModule{ + override def scalaOrganization = outer.scalaOrganization() def scalaVersion = outer.scalaVersion() override def repositories = outer.repositories override def scalacPluginIvyDeps = outer.scalacPluginIvyDeps @@ -25,12 +27,24 @@ trait ScalaModule extends JavaModule { outer => override def scalaWorker = outer.scalaWorker override def moduleDeps: Seq[JavaModule] = Seq(outer) } + + def scalaOrganization: T[String] = T { + if (isDotty(scalaVersion())) + "ch.epfl.lamp" + else + "org.scala-lang" + } + def scalaVersion: T[String] override def mapDependencies = T.task{ d: coursier.Dependency => - val artifacts = Set("scala-library", "scala-compiler", "scala-reflect") - if (d.module.organization != "org.scala-lang" || !artifacts(d.module.name)) d - else d.copy(version = scalaVersion()) + val artifacts = + if (isDotty(scalaVersion())) + Set("dotty-library", "dotty-compiler") + else + Set("scala-library", "scala-compiler", "scala-reflect") + if (!artifacts(d.module.name)) d + else d.copy(module = d.module.copy(organization = scalaOrganization()), version = scalaVersion()) } override def resolveCoursierDependency: Task[Dep => coursier.Dependency] = T.task{ @@ -77,25 +91,41 @@ trait ScalaModule extends JavaModule { outer => case _ => (scalaVersion(), Lib.scalaBinaryVersion(scalaVersion())) } + val (bridgeDep, bridgeName, bridgeVersion) = + if (isDotty(scalaVersion0)) { + val org = scalaOrganization() + val name = "dotty-sbt-bridge" + val version = scalaVersion() + (ivy"$org:$name:$version", name, version) + } else { + val org = "org.scala-sbt" + val name = "compiler-bridge" + val version = Versions.zinc + (ivy"$org::$name:$version", s"${name}_$scalaBinaryVersion0", version) + } + resolveDependencies( repositories, Lib.depToDependency(_, scalaVersion0, platformSuffix()), - Seq(ivy"org.scala-sbt::compiler-bridge:${Versions.zinc}"), + Seq(bridgeDep), sources = true - ).map(_.find(_.path.last == s"compiler-bridge_${scalaBinaryVersion0}-${Versions.zinc}-sources.jar").map(_.path).get) + ).map(deps => + grepJar(deps.map(_.path), bridgeName, s"$bridgeVersion-sources") + ) } def scalacPluginClasspath: T[Agg[PathRef]] = T { resolveDeps(scalacPluginIvyDeps)() } - def scalaLibraryIvyDeps = T{ scalaRuntimeIvyDeps(scalaVersion()) } + def scalaLibraryIvyDeps = T{ scalaRuntimeIvyDeps(scalaOrganization(), scalaVersion()) } /** * Classpath of the Scala Compiler & any compiler plugins */ def scalaCompilerClasspath: T[Agg[PathRef]] = T{ resolveDeps( - T.task{scalaCompilerIvyDeps(scalaVersion()) ++ scalaRuntimeIvyDeps(scalaVersion())} + T.task{scalaCompilerIvyDeps(scalaOrganization(), scalaVersion()) ++ + scalaRuntimeIvyDeps(scalaOrganization(), scalaVersion())} )() } override def compileClasspath = T{ @@ -155,7 +185,11 @@ trait ScalaModule extends JavaModule { outer => Result.Failure("repl needs to be run with the -i/--interactive flag") }else{ Jvm.interactiveSubprocess( - mainClass = "scala.tools.nsc.MainGenericRunner", + mainClass = + if (isDotty(scalaVersion())) + "dotty.tools.repl.Main" + else + "scala.tools.nsc.MainGenericRunner", classPath = runClasspath().map(_.path) ++ scalaCompilerClasspath().map(_.path), mainArgs = Seq("-usejavacp"), workingDir = pwd diff --git a/scalalib/src/mill/scalalib/publish/settings.scala b/scalalib/src/mill/scalalib/publish/settings.scala index 9a59f09d..bca81cf0 100644 --- a/scalalib/src/mill/scalalib/publish/settings.scala +++ b/scalalib/src/mill/scalalib/publish/settings.scala @@ -8,45 +8,29 @@ case class Artifact(group: String, id: String, version: String) { object Artifact { def fromDepJava(dep: Dep) = { - dep match { - case Dep.Java(dep, cross, force) => - Dependency( - Artifact(dep.module.organization, dep.module.name, dep.version), - Scope.Compile, - if (dep.configuration == "") None else Some(dep.configuration), - dep.exclusions.toList - ) - } + assert(dep.cross.isConstant, s"Not a Java dependency: $dep") + fromDep(dep, "", "", "") } + def fromDep(dep: Dep, scalaFull: String, scalaBin: String, platformSuffix: String): Dependency = { - dep match { - case d: Dep.Java => fromDepJava(d) - case Dep.Scala(dep, cross, force) => - Dependency( - Artifact( - dep.module.organization, - s"${dep.module.name}${platformSuffix}_${scalaBin}", - dep.version - ), - Scope.Compile, - if (dep.configuration == "") None else Some(dep.configuration), - dep.exclusions.toList - ) - case Dep.Point(dep, cross, force) => - Dependency( - Artifact( - dep.module.organization, - s"${dep.module.name}${platformSuffix}_${scalaFull}", - dep.version - ), - Scope.Compile, - if (dep.configuration == "") None else Some(dep.configuration), - dep.exclusions.toList - ) - } + val name = dep.artifactName( + binaryVersion = scalaBin, + fullVersion = scalaFull, + platformSuffix = platformSuffix + ) + Dependency( + Artifact( + dep.dep.module.organization, + name, + dep.dep.version + ), + Scope.Compile, + if (dep.dep.configuration == "") None else Some(dep.dep.configuration), + dep.dep.exclusions.toList + ) } } -- cgit v1.2.3