diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2018-03-27 20:31:00 -0700 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2018-03-27 20:31:00 -0700 |
commit | 0a1ba59f4956cbeb899753df3e2430752c308d1e (patch) | |
tree | 6d103d58ec34b3496f1a4dcfbc76f767a835e8d0 | |
parent | 25b321ea9431590d5852366585f1691717651fea (diff) | |
parent | 277d44208fae50efc20d902c0cd6d127516cfadb (diff) | |
download | mill-0a1ba59f4956cbeb899753df3e2430752c308d1e.tar.gz mill-0a1ba59f4956cbeb899753df3e2430752c308d1e.tar.bz2 mill-0a1ba59f4956cbeb899753df3e2430752c308d1e.zip |
Merge branch 'master' of github.com:lihaoyi/mill
24 files changed, 917 insertions, 21 deletions
diff --git a/.appveyor.yml b/.appveyor.yml index e353b6a6..693bd85e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -36,6 +36,7 @@ build_script: C:\%MSYS2_DIR%\usr\bin\bash -lc "curl -Lo /usr/local/bin/mill %MILL_URL%" && C:\%MSYS2_DIR%\usr\bin\bash -lc 'chmod +x /usr/local/bin/mill' && C:\%MSYS2_DIR%\usr\bin\bash -lc "cd /c/mill && mill -i all __.publishLocal release" && + rd /s /q %USERPROFILE%\.mill && C:\%MSYS2_DIR%\usr\bin\bash -lc "cd /c/mill && out/release/dest/mill -i all main.test scalajslib.test") - if [%COMPILER%]==[cygwin] ( SET "PATH=%JAVA_HOME%\bin;C:\%CYGWIN_DIR%\bin;C:\%CYGWIN_DIR%\usr\bin;%PATH%" && @@ -189,7 +189,9 @@ def testRepos = T{ "MILL_AMMONITE_REPO" -> shared.downloadTestRepo("lihaoyi/ammonite", "96ea548d5e3b72ab6ad4d9765e205bf6cc1c82ac", T.ctx().dest/"ammonite"), "MILL_UPICKLE_REPO" -> - shared.downloadTestRepo("lihaoyi/upickle", "7f33085c890db7550a226c349832eabc3cd18769", T.ctx().dest/"upickle") + shared.downloadTestRepo("lihaoyi/upickle", "7f33085c890db7550a226c349832eabc3cd18769", T.ctx().dest/"upickle"), + "MILL_PLAY_JSON_REPO" -> + shared.downloadTestRepo("playframework/play-json", "0a5ba16a03f3b343ac335117eb314e7713366fd4", T.ctx().dest/"play-json") ) } diff --git a/ci/test-mill-dev.sh b/ci/test-mill-dev.sh index 11d64154..ae8556fb 100755 --- a/ci/test-mill-dev.sh +++ b/ci/test-mill-dev.sh @@ -5,9 +5,11 @@ set -eux # Starting from scratch... git clean -xdf -# Build Mill using SBT +# Build Mill mill -i dev.assembly +rm -fR ~/.mill + # Second build & run tests using Mill out/dev/assembly/dest/mill -i all {clientserver,main,scalalib,scalajslib}.test diff --git a/ci/test-mill-release.sh b/ci/test-mill-release.sh index 8c0aca2f..19173827 100755 --- a/ci/test-mill-release.sh +++ b/ci/test-mill-release.sh @@ -9,6 +9,8 @@ ci/publish-local.sh git clean -xdf +rm -fR ~/.mill + # Second build & run tests using Mill -~/mill-release -i integration.test "mill.integration.forked.{AcyclicTests,UpickleTests}" +~/mill-release -i integration.test "mill.integration.forked.{AcyclicTests,UpickleTests,PlayJsonTests}" diff --git a/integration/test/resources/play-json/.travis.yml b/integration/test/resources/play-json/.travis.yml new file mode 100644 index 00000000..e045541b --- /dev/null +++ b/integration/test/resources/play-json/.travis.yml @@ -0,0 +1,29 @@ +language: scala +sudo: required +dist: trusty + +scala: +- 2.13.0-M3 +- 2.12.4 +- 2.11.12 +- 2.10.7 + +jdk: +- oraclejdk8 +- oraclejdk9 + +matrix: + exclude: + - scala: 2.10.7 + jdk: oraclejdk9 + - scala: 2.11.12 + jdk: oraclejdk9 + +script: + - curl -L -o ~/bin/mill https://github.com/lihaoyi/mill/releases/download/0.1.7/0.1.7-8-b913c6 && chmod +x ~/bin/mill + - export PATH=~/bin/mill:$PATH + - mill all validateCode reportBinaryIssues __[$TRAVIS_SCALA_VERSION].test + +cache: + directories: + - $HOME/.coursier diff --git a/integration/test/resources/play-json/README.md b/integration/test/resources/play-json/README.md new file mode 100644 index 00000000..d71acae9 --- /dev/null +++ b/integration/test/resources/play-json/README.md @@ -0,0 +1,30 @@ +## Play JSON build + +This is a port of sbt build for [Play JSON](https://github.com/playframework/play-json) library. It has roughly the same features as sbt version, for instance it cross-builds Play JSON under 4 major scala versions both js and JVM. It cam compile sources, run tests, publish artifacts to Sonatype, run benchmarks, reformat code, add license headers, use mima to diagnose binary compatibility issues. + +The main build file is [build.sc](/build.sc), other `.sc` files in root directory provide additional "plugin"-like features. + +### Example commands + +Compile play json for JVM on scala 2.12.4: +```bash +mill playJsonJvm[2.12.4].compile +``` + +Run test on all modules: +```bash +mill __.test +``` + +Run benchmarks on scala 2.12.4: +```bash +mill benchmarks[2.12.4].runJmh +``` + +### CI + +Example CI configuration is in [.travis.yml](/.travis.yml) . It does the same thing as [original one](https://github.com/playframework/play-json/blob/master/.travis.yml) . You can check status of Play JSON built with this `.travis.yml` file [here](travis-ci.org/rockjam/play-json) + +### Release + +To make release run `./release.sh`. Don't forget to export `SONATYPE_CREDENTIALS` and `GPG_PASSPHRASE` env variables. diff --git a/integration/test/resources/play-json/build.sc b/integration/test/resources/play-json/build.sc new file mode 100644 index 00000000..e5222ae1 --- /dev/null +++ b/integration/test/resources/play-json/build.sc @@ -0,0 +1,371 @@ +import mill._, mill.scalalib._, mill.scalalib.publish._, mill.scalajslib._ +import $file.version +import $file.reformat +import reformat.Scalariform +import $file.mima +import mima.MiMa +import $file.headers +import $file.jmh +import jmh.Jmh +import headers.Headers +import com.typesafe.tools.mima.core._ + +import ammonite.ops._ +import mill.define.Task + +val ScalaVersions = Seq("2.10.7", "2.11.12", "2.12.4", "2.13.0-M3") + +trait BaseModule extends CrossSbtModule with Scalariform with Headers + +trait PlayJsonModule extends BaseModule with PublishModule with MiMa { + + def pomSettings = PomSettings( + description = artifactName(), + organization = "com.typesafe.play", + url = "https://github.com/playframework/play-json", + licenses = Seq(License.`Apache-2.0`), + versionControl = VersionControl.github("playframework", "play-json"), + developers = Seq( + Developer( + id = "playframework", + name = "Play Framework Team", + url = "https://github.com/playframework" + ) + ) + ) + + trait Tests extends super.Tests with Scalariform with Headers { + val specs2Core = T { + val v = Lib.scalaBinaryVersion(scalaVersion()) match { + case "2.10" => "3.9.1" + case _ => "4.0.2" + } + ivy"org.specs2::specs2-core:$v" + } + } + + def scalacOptions = Seq("-deprecation", "-feature", "-unchecked", "-encoding", "utf8") + def javacOptions = Seq("-encoding", "UTF-8", "-Xlint:-options") + + def publishVersion = version.current +} + +abstract class PlayJson(val platformSegment: String) extends PlayJsonModule { + def crossScalaVersion: String + def millSourcePath = pwd / "play-json" + def artifactName = "play-json" + + def sources = T.sources( + millSourcePath / platformSegment / "src" / "main", + millSourcePath / "shared" / "src" / "main" + ) + + def ivyDeps = Agg( + ivy"org.scala-lang:scala-reflect:${scalaVersion()}", + ivy"org.typelevel::macro-compat::1.1.1" + ) + + private val macroParadise = ivy"org.scalamacros:::paradise:2.1.0" + + def compileIvyDeps = Agg( + macroParadise, + ivy"org.scala-lang:scala-compiler:${scalaVersion()}" + ) + + def scalacPluginIvyDeps = Agg(macroParadise) + + def mimaBinaryIssueFilters = Seq( + // AbstractFunction1 is in scala.runtime and isn't meant to be used by end users + ProblemFilters.exclude[MissingTypesProblem]("play.api.libs.json.JsArray$"), + ProblemFilters.exclude[MissingTypesProblem]("play.api.libs.json.JsObject$"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.DefaultWrites.BigIntWrites"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.DefaultWrites.BigIntegerWrites"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.DefaultReads.BigIntReads"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.DefaultReads.BigIntegerReads"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.DefaultWrites.BigIntWrites"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.DefaultWrites.BigIntegerWrites"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.DefaultReads.BigIntReads"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.DefaultReads.BigIntegerReads"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.json.JsonConfiguration.optionHandlers") + ) + + def generatedSources = T { + import ammonite.ops._ + + val dir = T.ctx().dest + mkdir(dir / "play-json") + val file = dir / "play-json" / "Generated.scala" + + val (writes, reads) = (1 to 22).map { i => + def commaSeparated(s: Int => String) = (1 to i).map(s).mkString(", ") + + def newlineSeparated(s: Int => String) = (1 to i).map(s).mkString("\n") + + val writerTypes = commaSeparated(j => s"T$j: Writes") + val readerTypes = commaSeparated(j => s"T$j: Reads") + val typeTuple = commaSeparated(j => s"T$j") + val written = commaSeparated(j => s"implicitly[Writes[T$j]].writes(x._$j)") + val readValues = commaSeparated(j => s"t$j") + val readGenerators = newlineSeparated(j => s"t$j <- implicitly[Reads[T$j]].reads(arr(${j - 1}))") + ( + s""" + implicit def Tuple${i}W[$writerTypes]: Writes[Tuple${i}[$typeTuple]] = Writes[Tuple${i}[$typeTuple]]( + x => JsArray(Array($written)) + ) + """, + s""" + implicit def Tuple${i}R[$readerTypes]: Reads[Tuple${i}[$typeTuple]] = Reads[Tuple${i}[$typeTuple]]{ + case JsArray(arr) if arr.size == $i => + for{ + $readGenerators + } yield Tuple$i($readValues) + + case _ => + JsError(Seq(JsPath() -> Seq(JsonValidationError("Expected array of $i elements")))) + } + """) + }.unzip + + write(file, s""" + package play.api.libs.json + + trait GeneratedReads { + ${reads.mkString("\n")} + } + + trait GeneratedWrites{ + ${writes.mkString("\n")} + } + """) + + Seq(PathRef(dir)) + } +} + +object playJsonJvm extends Cross[PlayJsonJvm](ScalaVersions:_*) +class PlayJsonJvm(val crossScalaVersion: String) extends PlayJson("jvm") { + def moduleDeps = Seq(playFunctionalJvm(crossScalaVersion)) + + val jacksonVersion = "2.9.3" + + def ivyDeps = super.ivyDeps() ++ Agg( + ivy"joda-time:joda-time:2.9.9", + ivy"com.fasterxml.jackson.core:jackson-core:$jacksonVersion", + ivy"com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion", + ivy"com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", + ivy"com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion", + ivy"com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" + ) + + object test extends Tests { + def ivyDeps = + Agg( + ivy"org.scalatest::scalatest:3.0.5-M1", + ivy"org.scalacheck::scalacheck:1.13.5", + ivy"com.chuusai::shapeless:2.3.3", + ivy"ch.qos.logback:logback-classic:1.2.3", + specs2Core() + ) + + def sources = { + val docSpecs = ls.rec(millSourcePath / up / "docs" / "manual" / "working" / "scalaGuide").filter(_.isDir).filter(_.last=="code").map(PathRef(_)) + T.sources( + docSpecs ++ + Seq( + PathRef(millSourcePath / platformSegment / "src" / "test"), + PathRef(millSourcePath / "shared" / "src" / "test") + ) + ) + } + + def testFrameworks = Seq( + "org.scalatest.tools.Framework", + "org.specs2.runner.Specs2Framework" + ) + } + +} + +object playJsonJs extends Cross[PlayJsonJs](ScalaVersions:_*) +class PlayJsonJs(val crossScalaVersion: String) extends PlayJson("js") with ScalaJSModule { + def moduleDeps = Seq(playFunctionalJs(crossScalaVersion)) + + def scalaJSVersion = "0.6.22" + + // TODO: remove super[PlayJson].Tests with super[ScalaJSModule].Tests hack + object test extends super[PlayJson].Tests with super[ScalaJSModule].Tests with Scalariform with Headers { + def ivyDeps = + Agg( + ivy"org.scalatest::scalatest::3.0.5-M1", + ivy"org.scalacheck::scalacheck::1.13.5", + ivy"com.chuusai::shapeless::2.3.3" + ) + + def sources = T.sources( + millSourcePath / platformSegment / "src" / "test", + millSourcePath / "shared" / "src" / "test" + ) + + def testFrameworks = Seq( + "org.scalatest.tools.Framework" + ) + } +} + +trait PlayFunctional extends PlayJsonModule { + def millSourcePath = pwd / "play-functional" + def artifactName = "play-functional" +} + +object playFunctionalJvm extends Cross[PlayFunctionalJvm](ScalaVersions:_*) +class PlayFunctionalJvm(val crossScalaVersion: String) extends PlayFunctional + +object playFunctionalJs extends Cross[PlayFunctionalJs](ScalaVersions:_*) +class PlayFunctionalJs(val crossScalaVersion: String) extends PlayFunctional with ScalaJSModule { + def scalaJSVersion = "0.6.22" +} + +object playJoda extends Cross[PlayJoda](ScalaVersions:_*) +class PlayJoda(val crossScalaVersion: String) extends PlayJsonModule { + def moduleDeps = Seq(playJsonJvm(crossScalaVersion)) + + def millSourcePath = pwd / "play-json-joda" + def artifactName = "play-json-joda" + + def ivyDeps = Agg( + ivy"joda-time:joda-time:2.9.9" + ) + + object test extends Tests { + def ivyDeps = Agg(specs2Core()) + + def testFrameworks = Seq( + "org.specs2.runner.Specs2Framework" + ) + } + +} + +object benchmarks extends Cross[Benchmarks](ScalaVersions:_*) +class Benchmarks(val crossScalaVersion: String) extends BaseModule with Jmh { + def moduleDeps = Seq(playJsonJvm(crossScalaVersion)) + + def millSourcePath = pwd / "benchmarks" +} + +// TODO: we should have a way to "take all modules in this build" +val testModules = Seq( + playJsonJvm("2.12.4").test, + playJsonJs("2.12.4").test, + playJoda("2.12.4").test +) + +val sourceModules = Seq( + playJsonJvm("2.12.4"), + playJsonJs("2.12.4"), + playJoda("2.12.4"), +) + +val allModules = testModules ++ sourceModules + +def reportBinaryIssues() = T.command { + Task.traverse(sourceModules) { module => + module.mimaReportBinaryIssues.map(issues => module.millModuleSegments.render -> issues) + }.map { issues => + val issuesByModules = issues.flatMap { case (moduleName, issues) => + val messageForModule = issues.foldLeft(Seq.empty[String]) { case (acc, (artifact, problems)) => + val elem = if(problems.nonEmpty) { + Some( + s"""Compared to artifact: ${artifact} + |found ${problems.size} binary incompatibilities: + |${problems.mkString("\n")} + """.stripMargin + ) + } else { + None + } + acc ++ elem + } + if(messageForModule.nonEmpty) { + Some( + s""" + |For module: ${moduleName}: + |${messageForModule.mkString("\n")} + """.stripMargin + ) + } else { + None + } + } + + if(issuesByModules.nonEmpty) { + sys.error(issuesByModules.mkString("\n")) + } + } +} + +def validateCode() = T.command { + Task.traverse(allModules)(_.checkCodeFormat()).zip(Task.traverse(allModules)(_.headerCheck())) +} + +/** + * Release steps are: + * 1) clean + * 2) run tests + * 3) set release version + * 4) commit release version + * 5) make release tag + * 6) publish all modules to sonatype + * 7) set next version + * 8) commit next version + * 9) push everything to git + */ +object release extends Module { + + implicit val wd = pwd + + val versionFile = wd / "version.sc" + + private val ReleaseVersion = raw"""(\d+)\.(\d+)\.(\d+)""".r + private val MinorSnapshotVersion = raw"""(\d+)\.(\d+)\.(\d+)-SNAPSHOT""".r + + private val releaseVersion = version.current match { + case MinorSnapshotVersion(major, minor, patch) => + s"${major}.${minor}.${patch.toInt}" + case ReleaseVersion(major, minor, patch) => + s"${major}.${minor}.${patch.toInt}" + } + + private val nextVersion = version.current match { + case v@MinorSnapshotVersion(major, minor, patch) => v + case ReleaseVersion(major, minor, patch) => + s"${major}.${minor}.${patch.toInt + 1}-SNAPSHOT" + } + + def setReleaseVersion = T { + T.ctx.log.info(s"Setting release version to ${releaseVersion}") + + write.over( + versionFile, + s"""def current = "${releaseVersion}" + | + """.stripMargin + ) + + %%("git", "commit", "-am", s"Setting release version to ${releaseVersion}") + %%("git", "tag", s"$releaseVersion") + } + + def setNextVersion = T { + T.ctx.log.info(s"Setting next version to ${nextVersion}") + + write.over( + versionFile, + s"""def current = "${nextVersion}"""" + ) + + %%("git", "commit", "-am", s"Setting next version to ${nextVersion}") + %%("git", "push", "origin", "master", "--tags") + } + +} diff --git a/integration/test/resources/play-json/headers.sc b/integration/test/resources/play-json/headers.sc new file mode 100644 index 00000000..a66eaedd --- /dev/null +++ b/integration/test/resources/play-json/headers.sc @@ -0,0 +1,63 @@ +import $ivy.`com.github.rockjam::license-headers:0.0.1` + +import ammonite.ops._ +import mill._, scalalib._ +import mill.eval.PathRef +import de.heikoseeberger.sbtheader.{CommentStyle, FileType, HeaderCreator, License} + +trait Headers extends ScalaModule { + + val headerMappings = Map( + FileType.scala.extension -> (FileType.scala, CommentStyle.cStyleBlockComment), + FileType.java.extension -> (FileType.java, CommentStyle.cStyleBlockComment) + ) + + val license = { + val currentYear = java.time.Year.now(java.time.Clock.systemUTC).getValue + License.Custom( + s"Copyright (C) 2009-$currentYear Lightbend Inc. <https://www.lightbend.com>" + ) + } + + def headerCreate() = T.command { + val withoutHeaders = filesWithoutHeaders(sources() ++ resources()) + + val updatedFiles = withoutHeaders.map { + case (file, updated) => + write.over(file, updated) + file + } + + if (updatedFiles.nonEmpty) { + T.ctx.log.info( + s"Headers created for ${updatedFiles.size} files:\n${updatedFiles.mkString("\n")}") + } + updatedFiles + } + + def headerCheck() = T.command { + val withoutHeaders = filesWithoutHeaders(sources() ++ resources()).map(_._1) + + if (withoutHeaders.nonEmpty) { + sys.error( + s"There are files without headers!\n${withoutHeaders.mkString("\n")}" + ) + } + } + + private def filesWithoutHeaders(input: Seq[PathRef]) = { + for { + ref <- input if exists(ref.path) + file <- ls.rec(ref.path) if file.isFile + (fileType, commentStyle) <- headerMappings.get(file.ext) + updatedContent <- HeaderCreator( + fileType, + commentStyle, + license, + log = _ => (), + read.getInputStream(file) + ).createText + } yield file -> updatedContent + } + +} diff --git a/integration/test/resources/play-json/jmh.sc b/integration/test/resources/play-json/jmh.sc new file mode 100644 index 00000000..76ed43fc --- /dev/null +++ b/integration/test/resources/play-json/jmh.sc @@ -0,0 +1,61 @@ +import ammonite.ops._ +import mill._, scalalib._, modules._ + +trait Jmh extends ScalaModule { + + def ivyDeps = super.ivyDeps() ++ Agg(ivy"org.openjdk.jmh:jmh-core:1.19") + + def runJmh(args: String*) = T.command { + val (_, resources) = generateBenchmarkSources() + Jvm.interactiveSubprocess( + "org.openjdk.jmh.Main", + classPath = (runClasspath() ++ generatorDeps()).map(_.path) ++ + Seq(compileGeneratedSources().path, resources), + mainArgs = args, + workingDir = T.ctx.dest + ) + } + + def compileGeneratedSources = T { + val dest = T.ctx.dest + val (sourcesDir, _) = generateBenchmarkSources() + val sources = ls.rec(sourcesDir).filter(_.isFile) + %%("javac", + sources.map(_.toString), + "-cp", + (runClasspath() ++ generatorDeps()).map(_.path.toString).mkString(":"), + "-d", + dest)(wd = dest) + PathRef(dest) + } + + // returns sources and resources directories + def generateBenchmarkSources = T { + val dest = T.ctx().dest + + val sourcesDir = dest / 'jmh_sources + val resourcesDir = dest / 'jmh_resources + + rm(sourcesDir) + mkdir(sourcesDir) + rm(resourcesDir) + mkdir(resourcesDir) + + Jvm.subprocess( + "org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator", + (runClasspath() ++ generatorDeps()).map(_.path), + mainArgs = Array( + compile().classes.path, + sourcesDir, + resourcesDir, + "default" + ).map(_.toString) + ) + + (sourcesDir, resourcesDir) + } + + def generatorDeps = resolveDeps( + T { Agg(ivy"org.openjdk.jmh:jmh-generator-bytecode:1.19") } + ) +} diff --git a/integration/test/resources/play-json/mima.sc b/integration/test/resources/play-json/mima.sc new file mode 100644 index 00000000..ebab2c72 --- /dev/null +++ b/integration/test/resources/play-json/mima.sc @@ -0,0 +1,58 @@ +import ammonite.ops._ +import $ivy.`com.typesafe::mima-reporter:0.1.18` +import com.typesafe.tools.mima.lib.MiMaLib +import com.typesafe.tools.mima.core._ +import mill._, scalalib._ + +trait MiMa extends ScalaModule with PublishModule { + def previousVersions = T { + scalaVersion().split('.')(1) match { + case "10" => Seq("2.6.0") + case "11" => Seq("2.6.0") + case "12" => Seq("2.6.0") + case _ => Nil + } + } + + def mimaBinaryIssueFilters: Seq[ProblemFilter] = Seq.empty + + def previousDeps = T { + Agg.from(previousVersions().map { version => + Dep.Java( + pomSettings().organization, + artifactId(), + version, + cross = false + ) + }) + } + + def previousArtifacts = T { + resolveDeps(previousDeps)().filter(_.path.segments.contains(artifactId())) + } + + def mimaReportBinaryIssues: T[List[(String, List[String])]] = T { + val currentClassfiles = compile().classes.path + val classpath = runClasspath() + + val lib = { + com.typesafe.tools.mima.core.Config.setup("sbt-mima-plugin", Array.empty) + val cpstring = classpath + .map(_.path) + .filter(exists) + .mkString(System.getProperty("path.separator")) + new MiMaLib( + com.typesafe.tools.mima.core.reporterClassPath(cpstring) + ) + } + + previousArtifacts().toList.map { path => + val problems = + lib.collectProblems(path.path.toString, currentClassfiles.toString) + path.path.toString -> problems.filter { problem => + mimaBinaryIssueFilters.forall(_.apply(problem)) + }.map(_.description("current")) + } + } + +} diff --git a/integration/test/resources/play-json/reformat.sc b/integration/test/resources/play-json/reformat.sc new file mode 100644 index 00000000..c44b2f97 --- /dev/null +++ b/integration/test/resources/play-json/reformat.sc @@ -0,0 +1,71 @@ +import $ivy.`org.scalariform::scalariform:0.2.5` + +import mill._, scalalib._ +import ammonite.ops._ +import scalariform.formatter._ +import scalariform.formatter.preferences._ +import scalariform.parser.ScalaParserException + +trait Scalariform extends ScalaModule { + val playJsonPreferences = FormattingPreferences() + .setPreference(SpacesAroundMultiImports, true) + .setPreference(SpaceInsideParentheses, false) + .setPreference(DanglingCloseParenthesis, Preserve) + .setPreference(PreserveSpaceBeforeArguments, true) + .setPreference(DoubleIndentConstructorArguments, false) + + def compile = T { + reformat() + super.compile() + } + + def reformat = T { + val files = filesToFormat(sources()) + T.ctx.log.info(s"Formatting ${files.size} Scala sources") + files.map { path => + try { + val formatted = ScalaFormatter.format( + read(path), + playJsonPreferences, + scalaVersion = scalaVersion() + ) + write.over(path, formatted) + } catch { + case ex: ScalaParserException => + T.ctx.log.error(s"Failed to format file: ${path}. Error: ${ex.getMessage}") + } + path + } + } + + def checkCodeFormat() = T.command { + filesToFormat(sources()).foreach { path => + try { + val input = read(path) + val formatted = ScalaFormatter.format( + input, + playJsonPreferences, + scalaVersion = scalaVersion() + ) + if (input != formatted) sys.error( + s""" + |ERROR: Scalariform check failed at file: ${path} + |To fix, format your sources using `mill __.reformat` before submitting a pull request. + |Additionally, please squash your commits (eg, use git commit --amend) if you're going to update this pull request. + """.stripMargin + ) + } catch { + case ex: ScalaParserException => + T.ctx.log.error(s"Failed to format file: ${path}. Error: ${ex.getMessage}") + } + } + } + + private def filesToFormat(sources: Seq[PathRef]) = { + for { + pathRef <- sources if exists(pathRef.path) + file <- ls.rec(pathRef.path) if file.isFile && file.ext == "scala" + } yield file + } + +} diff --git a/integration/test/resources/play-json/release.sh b/integration/test/resources/play-json/release.sh new file mode 100755 index 00000000..009300a0 --- /dev/null +++ b/integration/test/resources/play-json/release.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -eux + +rm -rf out +mill __.test +mill release.setReleaseVersion +mill mill.scalalib.PublishModule/publishAll \ + "$SONATYPE_CREDENTIALS" \ + "$GPG_PASSPHRASE" \ + __.publishArtifacts \ + --release \ + true +mill release.setNextVersion diff --git a/integration/test/resources/play-json/version.sc b/integration/test/resources/play-json/version.sc new file mode 100644 index 00000000..e9dbb12f --- /dev/null +++ b/integration/test/resources/play-json/version.sc @@ -0,0 +1 @@ +def current = "2.7.0-SNAPSHOT" diff --git a/integration/test/src/mill/integration/IntegrationTestSuite.scala b/integration/test/src/mill/integration/IntegrationTestSuite.scala index 756b724a..d24261f3 100644 --- a/integration/test/src/mill/integration/IntegrationTestSuite.scala +++ b/integration/test/src/mill/integration/IntegrationTestSuite.scala @@ -17,9 +17,15 @@ abstract class IntegrationTestSuite(repoKey: String, val workspaceSlug: String, wrapper } + def buildFiles: Seq[Path] = { + Seq(buildFilePath / "build.sc") + } + override def initWorkspace() = { super.initWorkspace() - cp.over(buildFilePath / "build.sc", workspacePath / "build.sc") + buildFiles.foreach { file => + cp.over(file, workspacePath / file.name) + } assert(!ls.rec(workspacePath).exists(_.ext == "class")) } } diff --git a/integration/test/src/mill/integration/PlayJsonTests.scala b/integration/test/src/mill/integration/PlayJsonTests.scala new file mode 100644 index 00000000..c78a256e --- /dev/null +++ b/integration/test/src/mill/integration/PlayJsonTests.scala @@ -0,0 +1,57 @@ +package mill.integration + +import ammonite.ops._ +import utest._ + +class PlayJsonTests(fork: Boolean) extends IntegrationTestSuite("MILL_PLAY_JSON_REPO", "play-json", fork) { + + override def buildFiles: Seq[Path] = { + ls(buildFilePath).filter(_.ext == "sc") + } + + val tests = Tests{ + initWorkspace() + + 'jvm - { + assert(eval("playJsonJvm[2.12.4].test")) + val jvmMeta = meta("playJsonJvm[2.12.4].test.test") + + assert( + jvmMeta.contains("play.api.libs.json.JsonSharedSpec"), + jvmMeta.contains("JSON should support basic array operations") + ) + + assert( + jvmMeta.contains("play.api.libs.json.JsonValidSpec"), + jvmMeta.contains("JSON reads should::validate Dates") + ) + } + 'js - { + assert(eval("playJsonJs[2.12.4].test")) + val jsMeta = meta("playJsonJs[2.12.4].test.test") + + assert( + jsMeta.contains("play.api.libs.json.JsonSharedSpec"), + jsMeta.contains("JSON should support basic array operations") + ) + + assert( + jsMeta.contains("play.api.libs.json.JsonSpec"), + jsMeta.contains("Complete JSON should create full object when lose precision when parsing BigDecimals") + ) + } + 'playJoda - { + assert(eval("playJoda[2.12.4].test")) + val metaFile = meta("playJoda[2.12.4].test.test") + + assert( + metaFile.contains("play.api.libs.json.JsonJodaValidSpec"), + metaFile.contains("JSON reads should::validate Dates") + ) + } + + 'benchmarks - { +// "benchmarks[2.12.4].runJmh" -i 1 -wi 1 -f1 -t1 + } + } +} diff --git a/integration/test/src/mill/integration/forked/Tests.scala b/integration/test/src/mill/integration/forked/Tests.scala index 6933bbbe..4a2e9f53 100644 --- a/integration/test/src/mill/integration/forked/Tests.scala +++ b/integration/test/src/mill/integration/forked/Tests.scala @@ -4,4 +4,5 @@ object AcyclicTests extends mill.integration.AcyclicTests(fork = true) object AmmoniteTests extends mill.integration.AmmoniteTests(fork = true) object BetterFilesTests extends mill.integration.BetterFilesTests(fork = true) object JawnTests extends mill.integration.JawnTests(fork = true) -object UpickleTests extends mill.integration.UpickleTests(fork = true)
\ No newline at end of file +object UpickleTests extends mill.integration.UpickleTests(fork = true) +object PlayJsonTests extends mill.integration.PlayJsonTests(fork = true) diff --git a/integration/test/src/mill/integration/local/Tests.scala b/integration/test/src/mill/integration/local/Tests.scala index 21b9581c..b4f78ee4 100644 --- a/integration/test/src/mill/integration/local/Tests.scala +++ b/integration/test/src/mill/integration/local/Tests.scala @@ -4,4 +4,5 @@ object AcyclicTests extends mill.integration.AcyclicTests(fork = false) object AmmoniteTests extends mill.integration.AmmoniteTests(fork = false) object BetterFilesTests extends mill.integration.BetterFilesTests(fork = false) object JawnTests extends mill.integration.JawnTests(fork = false) -object UpickleTests extends mill.integration.UpickleTests(fork = false)
\ No newline at end of file +object UpickleTests extends mill.integration.UpickleTests(fork = false) +object PlayJsonTests extends mill.integration.PlayJsonTests(fork = false) diff --git a/scalalib/src/mill/scalalib/Dep.scala b/scalalib/src/mill/scalalib/Dep.scala index 78f454f2..f20480b7 100644 --- a/scalalib/src/mill/scalalib/Dep.scala +++ b/scalalib/src/mill/scalalib/Dep.scala @@ -3,6 +3,20 @@ import mill.util.JsonFormatters._ import upickle.default.{macroRW, ReadWriter => RW} sealed trait Dep { def configure(attributes: coursier.Attributes): Dep + 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)) + } + 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)) + } } object Dep{ diff --git a/scalalib/src/mill/scalalib/publish/Ivy.scala b/scalalib/src/mill/scalalib/publish/Ivy.scala index 644e2c53..3b271fa8 100644 --- a/scalalib/src/mill/scalalib/publish/Ivy.scala +++ b/scalalib/src/mill/scalalib/publish/Ivy.scala @@ -41,8 +41,12 @@ object Ivy { } private def renderDependency(dep: Dependency) = { - val scope = scopeToConf(dep.scope) - <dependency org={dep.artifact.group} name={dep.artifact.id} rev={dep.artifact.version} conf={s"$scope->default(compile)"}></dependency> + if (dep.exclusions.isEmpty) + <dependency org={dep.artifact.group} name={dep.artifact.id} rev={dep.artifact.version} conf={s"${dep.scope}->default(${dep.configuration.getOrElse("compile")})"} /> + else + <dependency org={dep.artifact.group} name={dep.artifact.id} rev={dep.artifact.version} conf={s"${dep.scope}->default(${dep.configuration.getOrElse("compile")})"}> + {dep.exclusions.map(ex => <exclude org={ex._1} name={ex._2} matcher="exact"/>).toSeq} + </dependency> } private def scopeToConf(s: Scope): String = s match { diff --git a/scalalib/src/mill/scalalib/publish/Pom.scala b/scalalib/src/mill/scalalib/publish/Pom.scala index 1a86e7de..3c8ba4dc 100644 --- a/scalalib/src/mill/scalalib/publish/Pom.scala +++ b/scalalib/src/mill/scalalib/publish/Pom.scala @@ -90,12 +90,28 @@ object Pom { case Scope.Test => <scope>test</scope> case Scope.Runtime => <scope>runtime</scope> } - <dependency> - <groupId>{d.artifact.group}</groupId> - <artifactId>{d.artifact.id}</artifactId> - <version>{d.artifact.version}</version> - {scope} - </dependency> + if (d.exclusions.isEmpty) + <dependency> + <groupId>{d.artifact.group}</groupId> + <artifactId>{d.artifact.id}</artifactId> + <version>{d.artifact.version}</version> + {scope} + </dependency> + else + <dependency> + <groupId>{d.artifact.group}</groupId> + <artifactId>{d.artifact.id}</artifactId> + <version>{d.artifact.version}</version> + <exclusions> + {d.exclusions.map(ex => + <exclude> + <groupId>{ex._1}</groupId> + <artifactId>{ex._2}</artifactId> + </exclude> + )}.toSeq + </exclusions> + {scope} + </dependency> } } diff --git a/scalalib/src/mill/scalalib/publish/settings.scala b/scalalib/src/mill/scalalib/publish/settings.scala index 1076fb41..34f7e7ad 100644 --- a/scalalib/src/mill/scalalib/publish/settings.scala +++ b/scalalib/src/mill/scalalib/publish/settings.scala @@ -15,7 +15,9 @@ object Artifact { case Dep.Java(dep, cross) => Dependency( Artifact(dep.module.organization, dep.module.name, dep.version), - Scope.Compile + Scope.Compile, + if (dep.configuration == "" ) None else Some(dep.configuration), + dep.exclusions.toList ) case Dep.Scala(dep, cross) => Dependency( @@ -24,7 +26,9 @@ object Artifact { s"${dep.module.name}_${scalaBin}", dep.version ), - Scope.Compile + Scope.Compile, + if (dep.configuration == "") None else Some(dep.configuration), + dep.exclusions.toList ) case Dep.Point(dep, cross) => Dependency( @@ -33,7 +37,9 @@ object Artifact { s"${dep.module.name}_${scalaFull}", dep.version ), - Scope.Compile + Scope.Compile, + if (dep.configuration == "") None else Some(dep.configuration), + dep.exclusions.toList ) } } @@ -49,7 +55,9 @@ object Scope { case class Dependency( artifact: Artifact, - scope: Scope + scope: Scope, + configuration: Option[String] = None, + exclusions: Seq[(String, String)] = Nil ) case class Developer( diff --git a/scalalib/test/src/mill/scalalib/ResolveDepsTests.scala b/scalalib/test/src/mill/scalalib/ResolveDepsTests.scala index e9734557..b1fcec3e 100644 --- a/scalalib/test/src/mill/scalalib/ResolveDepsTests.scala +++ b/scalalib/test/src/mill/scalalib/ResolveDepsTests.scala @@ -26,6 +26,24 @@ object ResolveDepsTests extends TestSuite { assert(paths.items.next.path.toString.contains("natives-macos")) } + 'excludeTransitiveDeps - { + val deps = Agg(ivy"com.lihaoyi::pprint:0.5.3".exclude("com.lihaoyi" -> "fansi_2.12")) + val Success(paths) = evalDeps(deps) + assert(!paths.exists(_.path.toString.contains("fansi_2.12"))) + } + + 'excludeTransitiveDepsByOrg - { + val deps = Agg(ivy"com.lihaoyi::pprint:0.5.3".excludeOrg("com.lihaoyi")) + val Success(paths) = evalDeps(deps) + assert(!paths.exists(path => path.path.toString.contains("com/lihaoyi") && !path.path.toString.contains("pprint_2.12"))) + } + + 'excludeTransitiveDepsByName - { + val deps = Agg(ivy"com.lihaoyi::pprint:0.5.3".excludeName("fansi_2.12")) + val Success(paths) = evalDeps(deps) + assert(!paths.exists(_.path.toString.contains("fansi_2.12"))) + } + 'errOnInvalidOrgDeps - { val deps = Agg(ivy"xxx.yyy.invalid::pprint:0.5.3") val Failure(errMsg, _) = evalDeps(deps) diff --git a/scalalib/test/src/mill/scalalib/publish/IvyTests.scala b/scalalib/test/src/mill/scalalib/publish/IvyTests.scala new file mode 100644 index 00000000..0f275dd9 --- /dev/null +++ b/scalalib/test/src/mill/scalalib/publish/IvyTests.scala @@ -0,0 +1,60 @@ +package mill.scalalib.publish + +import utest._ +import mill._ + +import scala.xml.{Node, NodeSeq, XML} + +object IvyTests extends TestSuite { + + def tests: Tests = Tests { + val artifactId = "mill-scalalib_2.12" + val artifact = + Artifact("com.lihaoyi", "mill-scalalib_2.12", "0.0.1") + val deps = Agg( + Dependency(Artifact("com.lihaoyi", "mill-main_2.12", "0.1.4"), + Scope.Compile), + Dependency(Artifact("org.scala-sbt", "test-interface", "1.0"), + Scope.Compile), + Dependency(Artifact("com.lihaoyi", "pprint_2.12", "0.5.3"), + Scope.Compile, exclusions = List("com.lihaoyi" -> "fansi_2.12", "*" -> "sourcecode_2.12")) + ) + + 'fullIvy - { + val fullIvy = XML.loadString(Ivy(artifact, deps)) + + 'topLevel - { + val info = singleNode(fullIvy \ "info") + assert( + singleAttr(info, "organisation") == artifact.group + , singleAttr(info, "module") == artifact.id + , singleAttr(info, "revision") == artifact.version + ) + } + + 'dependencies - { + val dependencies = fullIvy \ "dependencies" \ "dependency" + val ivyDeps = deps.indexed + + assert(dependencies.size == ivyDeps.size) + + dependencies.zipWithIndex.foreach { case (dep, index) => + assert( + singleAttr(dep, "org") == ivyDeps(index).artifact.group + , singleAttr(dep, "name") == ivyDeps(index).artifact.id + , singleAttr(dep, "rev") == ivyDeps(index).artifact.version + , (dep \ "exclude").zipWithIndex forall { case (exclude, j) => + singleAttr(exclude, "org") == ivyDeps(index).exclusions(j)._1 && + singleAttr(exclude, "name") == ivyDeps(index).exclusions(j)._2 + } + ) + } + } + } + } + + def singleNode(seq: NodeSeq): Node = + seq.headOption.getOrElse(throw new RuntimeException("empty seq")) + def singleAttr(node: Node, attr: String): String = + node.attribute(attr).flatMap(_.headOption.map(_.text)).getOrElse(throw new RuntimeException(s"empty attr $attr")) +} diff --git a/scalalib/test/src/mill/scalalib/publish/PomTests.scala b/scalalib/test/src/mill/scalalib/publish/PomTests.scala index 31e87446..c15ff172 100644 --- a/scalalib/test/src/mill/scalalib/publish/PomTests.scala +++ b/scalalib/test/src/mill/scalalib/publish/PomTests.scala @@ -15,7 +15,9 @@ object PomTests extends TestSuite { Dependency(Artifact("com.lihaoyi", "mill-main_2.12", "0.1.4"), Scope.Compile), Dependency(Artifact("org.scala-sbt", "test-interface", "1.0"), - Scope.Compile) + Scope.Compile), + Dependency(Artifact("com.lihaoyi", "pprint_2.12", "0.5.3"), + Scope.Compile, exclusions = List("com.lihaoyi" -> "fansi_2.12", "*" -> "sourcecode_2.12")) ) val settings = PomSettings( description = "mill-scalalib", @@ -104,7 +106,7 @@ object PomTests extends TestSuite { 'dependencies - { val dependencies = fullPom \ "dependencies" \ "dependency" - assert(dependencies.size == 2) + assert(dependencies.size == 3) val pomDeps = deps.indexed @@ -114,7 +116,11 @@ object PomTests extends TestSuite { singleText(dep \ "groupId") == pomDeps(index).artifact.group, singleText(dep \ "artifactId") == pomDeps(index).artifact.id, singleText(dep \ "version") == pomDeps(index).artifact.version, - optText(dep \ "scope").isEmpty + optText(dep \ "scope").isEmpty, + (dep \ "exclusions").zipWithIndex.forall { case (node, j) => + singleText(node \ "exclude" \ "groupId") == pomDeps(index).exclusions(j)._1 && + singleText(node \ "exclude" \ "artifactId") == pomDeps(index).exclusions(j)._2 + } ) } } |