diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..3f2ba725b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +dist: trusty +group: stable + +language: scala +jdk: oraclejdk8 + +script: sbt scripted + +# Undo _JAVA_OPTIONS environment variable +before_script: + - unset _JAVA_OPTIONS + +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt + +before_cache: + - find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete + - find $HOME/.sbt -name "*.lock" -delete diff --git a/README.markdown b/README.markdown new file mode 100644 index 000000000..ca91dc2f3 --- /dev/null +++ b/README.markdown @@ -0,0 +1,46 @@ +sbt-projectmatrix +================= + +cross building using subprojects. + +This is an experimental plugin that implements better cross building. + +setup +----- + +**Requirements**: Requires sbt 1.2.0-M1 or above. + +In `project/plugins.sbt`: + +```scala +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.1.0") +``` + +usage +----- + +To use `projectMatrix`: + +```scala +lazy val core = (projectMatrix in file("core")) + .scalaVersions("2.12.6", "2.11.12") + .settings( + name := "core" + ) + .jvmPlatform() + +lazy val app = (projectMatrix in file("app")) + .dependsOn(core) + .scalaVersions("2.12.6") + .settings( + name := "app" + ) + .jvmPlatform() +``` + +This sets up basic project matrices one supporting both 2.11 and 2.12, and the other supporting only 2.12. + +license +------- + +MIT License diff --git a/build.sbt b/build.sbt new file mode 100644 index 000000000..5252f3387 --- /dev/null +++ b/build.sbt @@ -0,0 +1,19 @@ +ThisBuild / organization := "com.eed3si9n" +ThisBuild / version := "0.1.0-SNAPSHOT" +ThisBuild / description := "sbt plugin to define project matrix for cross building" +ThisBuild / licenses := Seq("MIT License" -> url("https://github.com/sbt/sbt-projectmatrix/blob/master/LICENSE")) + +lazy val root = (project in file(".")) + .enablePlugins(SbtPlugin) + .settings( + sbtPlugin := true, + name := "sbt-projectmatrix", + scalacOptions := Seq("-deprecation", "-unchecked"), + publishMavenStyle := false, + bintrayOrganization in bintray := None, + bintrayRepository := "sbt-plugins", + scriptedLaunchOpts := { scriptedLaunchOpts.value ++ + Seq("-Xmx1024M", "-Dplugin.version=" + version.value) + }, + scriptedBufferLog := false, + ) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 000000000..cec4f6083 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.0-M1 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 000000000..e5ae08691 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") diff --git a/src/main/scala/sbt/ProjectMatrix.scala b/src/main/scala/sbt/ProjectMatrix.scala new file mode 100644 index 000000000..7b696b3d4 --- /dev/null +++ b/src/main/scala/sbt/ProjectMatrix.scala @@ -0,0 +1,289 @@ +package sbt + +import java.util.Locale +import scala.collection.immutable.ListMap +import Keys._ +import sbt.librarymanagement.CrossVersion.partialVersion + +/** + * A project matrix is an implementation of a composite project + * that represents cross building across some axis (such as platform) + * and Scala version. + * + * {{{ + * lazy val core = (projectMatrix in file("core")) + * .scalaVersions("2.12.6", "2.11.12") + * .settings( + * name := "core" + * ) + * .jvmPlatform() + * }}} + */ +sealed trait ProjectMatrix extends CompositeProject { + def scalaVersions(sv: String*): ProjectMatrix + + def id: String + + /** The base directory for the project matrix.*/ + def base: sbt.File + + def withId(id: String): ProjectMatrix + + /** Sets the base directory for this project matrix.*/ + def in(dir: sbt.File): ProjectMatrix + + /** Adds new configurations directly to this project. To override an existing configuration, use `overrideConfigs`. */ + def configs(cs: Configuration*): ProjectMatrix + + /** Adds classpath dependencies on internal or external projects. */ + def dependsOn(deps: MatrixClasspathDep[ProjectMatrixReference]*): ProjectMatrix + + /** + * Adds projects to be aggregated. When a user requests a task to run on this project from the command line, + * the task will also be run in aggregated projects. + */ + def aggregate(refs: ProjectMatrixReference*): ProjectMatrix + + /** Appends settings to the current settings sequence for this project. */ + def settings(ss: Def.SettingsDefinition*): ProjectMatrix + + /** + * Sets the [[AutoPlugin]]s of this project. + * A [[AutoPlugin]] is a common label that is used by plugins to determine what settings, if any, to enable on a project. + */ + def enablePlugins(ns: Plugins*): ProjectMatrix + + /** Disable the given plugins on this project. */ + def disablePlugins(ps: AutoPlugin*): ProjectMatrix + + def custom( + idSuffix: String, + directorySuffix: String, + scalaVersions: Seq[String], + process: Project => Project + ): ProjectMatrix + + def jvmPlatform(settings: Setting[_]*): ProjectMatrix + + def jvm: ProjectFinder + + def projectRefs: Seq[ProjectReference] +} + +/** Represents a reference to a project matrix with an optional configuration string. + */ +sealed trait MatrixClasspathDep[MR <: ProjectMatrixReference] { + def matrix: MR; def configuration: Option[String] +} + +trait ProjectFinder { + def apply(scalaVersion: String): Project + def get: Seq[Project] +} + +object ProjectMatrix { + import sbt.io.syntax._ + + val jvmIdSuffix: String = "JVM" + val jvmDirectorySuffix: String = "-jvm" + + /** A row in the project matrix, typically representing a platform. + */ + final class ProjectRow( + val idSuffix: String, + val directorySuffix: String, + val scalaVersions: Seq[String], + val process: Project => Project + ) {} + + final case class MatrixClasspathDependency( + matrix: ProjectMatrixReference, + configuration: Option[String] + ) extends MatrixClasspathDep[ProjectMatrixReference] + + private final class ProjectMatrixDef( + val id: String, + val base: sbt.File, + val scalaVersions: Seq[String], + val rows: Seq[ProjectRow], + val aggregate: Seq[ProjectMatrixReference], + val dependencies: Seq[MatrixClasspathDep[ProjectMatrixReference]], + val settings: Seq[Def.Setting[_]], + val configurations: Seq[Configuration], + val plugins: Plugins + ) extends ProjectMatrix { self => + lazy val projectMatrix: ListMap[(ProjectRow, String), Project] = { + ListMap((for { + r <- rows + svs = if (r.scalaVersions.nonEmpty) r.scalaVersions + else if (scalaVersions.nonEmpty) scalaVersions + else sys.error(s"project matrix $id must specify scalaVersions.") + sv <- svs + } yield { + val idSuffix = r.idSuffix + scalaVersionIdSuffix(sv) + val svDirSuffix = r.directorySuffix + "-" + scalaVersionDirSuffix(sv) + val childId = self.id + idSuffix + val deps = dependencies map { + case MatrixClasspathDependency(matrix: LocalProjectMatrix, configuration) => + ClasspathDependency(LocalProject(matrix.id + idSuffix), configuration) + } + val aggs = aggregate map { + case ref: LocalProjectMatrix => LocalProject(ref.id + idSuffix) + } + val p = Project(childId, new sbt.File(childId).getAbsoluteFile) + .dependsOn(deps: _*) + .aggregate(aggs: _*) + .setPlugins(plugins) + .configs(configurations: _*) + .settings( + Keys.scalaVersion := sv, + target := base.getAbsoluteFile / "target" / svDirSuffix.dropWhile(_ == '-'), + crossTarget := Keys.target.value, + sourceDirectory := base.getAbsoluteFile / "src", + inConfig(Compile)(makeSources(r.directorySuffix, svDirSuffix)), + inConfig(Test)(makeSources(r.directorySuffix, svDirSuffix)) + ) + .settings(self.settings) + + (r, sv) -> r.process(p) + }): _*) + } + + override lazy val componentProjects: Seq[Project] = projectMatrix.values.toList + + private def makeSources(dirSuffix: String, svDirSuffix: String): Setting[_] = { + unmanagedSourceDirectories ++= Seq( + scalaSource.value.getParentFile / s"scala${dirSuffix}", + scalaSource.value.getParentFile / s"scala$svDirSuffix" + ) + } + + private def scalaVersionIdSuffix(sv: String): String = { + scalaVersionDirSuffix(sv).toLowerCase(Locale.ENGLISH).replaceAll("""\W+""", "_") + } + + private def scalaVersionDirSuffix(sv: String): String = + partialVersion(sv) match { + case Some((m, n)) => s"$m.$n" + case _ => sv + } + + override def withId(id: String): ProjectMatrix = copy(id = id) + + override def in(dir: sbt.File): ProjectMatrix = copy(base = dir) + + override def configs(cs: Configuration*): ProjectMatrix = + copy(configurations = configurations ++ cs) + + override def scalaVersions(sv: String*): ProjectMatrix = + copy(scalaVersions = sv) + + override def aggregate(refs: ProjectMatrixReference*): ProjectMatrix = + copy(aggregate = (aggregate: Seq[ProjectMatrixReference]) ++ refs) + + override def dependsOn(deps: MatrixClasspathDep[ProjectMatrixReference]*): ProjectMatrix = + copy(dependencies = dependencies ++ deps) + + /** Appends settings to the current settings sequence for this project. */ + override def settings(ss: Def.SettingsDefinition*): ProjectMatrix = + copy(settings = (settings: Seq[Def.Setting[_]]) ++ Def.settings(ss: _*)) + + override def enablePlugins(ns: Plugins*): ProjectMatrix = + setPlugins(ns.foldLeft(plugins)(Plugins.and)) + + override def disablePlugins(ps: AutoPlugin*): ProjectMatrix = + setPlugins(Plugins.and(plugins, Plugins.And(ps.map(p => Plugins.Exclude(p)).toList))) + + def setPlugins(ns: Plugins): ProjectMatrix = copy(plugins = ns) + + override def jvmPlatform(settings: Setting[_]*): ProjectMatrix = + custom(jvmIdSuffix, jvmDirectorySuffix, Nil, { _.settings(settings) }) + + override def jvm: ProjectFinder = new ProjectFinder { + def get: Seq[Project] = projectMatrix.toSeq collect { + case ((r, sv), v) if r.idSuffix == jvmIdSuffix => v + } + def apply(sv: String): Project = + (projectMatrix.toSeq collect { + case ((r, `sv`), v) if r.idSuffix == jvmIdSuffix => v + }).headOption.getOrElse(sys.error(s"$sv was not found")) + } + + override def projectRefs: Seq[ProjectReference] = + componentProjects map { case p => (p: ProjectReference) } + + override def custom( + idSuffix: String, + directorySuffix: String, + scalaVersions: Seq[String], + process: Project => Project + ): ProjectMatrix = + copy(rows = rows :+ new ProjectRow(idSuffix, directorySuffix, scalaVersions, process)) + + def copy( + id: String = id, + base: sbt.File = base, + scalaVersions: Seq[String] = scalaVersions, + rows: Seq[ProjectRow] = rows, + aggregate: Seq[ProjectMatrixReference] = aggregate, + dependencies: Seq[MatrixClasspathDep[ProjectMatrixReference]] = dependencies, + settings: Seq[Setting[_]] = settings, + configurations: Seq[Configuration] = configurations, + plugins: Plugins = plugins + ): ProjectMatrix = + unresolved( + id, + base, + scalaVersions, + rows, + aggregate, + dependencies, + settings, + configurations, + plugins + ) + } + + def apply(id: String, base: sbt.File): ProjectMatrix = { + unresolved(id, base, Nil, Nil, Nil, Nil, Nil, Nil, Plugins.Empty) + } + + private[sbt] def unresolved( + id: String, + base: sbt.File, + scalaVersions: Seq[String], + rows: Seq[ProjectRow], + aggregate: Seq[ProjectMatrixReference], + dependencies: Seq[MatrixClasspathDep[ProjectMatrixReference]], + settings: Seq[Def.Setting[_]], + configurations: Seq[Configuration], + plugins: Plugins + ): ProjectMatrix = + new ProjectMatrixDef( + id, + base, + scalaVersions, + rows, + aggregate, + dependencies, + settings, + configurations, + plugins + ) + + implicit def projectMatrixToLocalProjectMatrix(m: ProjectMatrix): LocalProjectMatrix = + LocalProjectMatrix(m.id) + + import scala.reflect.macros._ + + def projectMatrixMacroImpl(c: blackbox.Context): c.Expr[ProjectMatrix] = { + import c.universe._ + val enclosingValName = std.KeyMacro.definingValName( + c, + methodName => + s"""$methodName must be directly assigned to a val, such as `val x = $methodName`. Alternatively, you can use `sbt.ProjectMatrix.apply`""" + ) + val name = c.Expr[String](Literal(Constant(enclosingValName))) + reify { ProjectMatrix(name.splice, new sbt.File(name.splice)) } + } +} diff --git a/src/main/scala/sbt/ProjectMatrixReference.scala b/src/main/scala/sbt/ProjectMatrixReference.scala new file mode 100644 index 000000000..2f7d4b8c2 --- /dev/null +++ b/src/main/scala/sbt/ProjectMatrixReference.scala @@ -0,0 +1,7 @@ +package sbt + +/** Identifies a project matrix. */ +sealed trait ProjectMatrixReference + +/** Identifies a project in the current build context. */ +final case class LocalProjectMatrix(id: String) extends ProjectMatrixReference diff --git a/src/main/scala/sbtprojectmatrix/ProjectMatrixPlugin.scala b/src/main/scala/sbtprojectmatrix/ProjectMatrixPlugin.scala new file mode 100644 index 000000000..0fceae49d --- /dev/null +++ b/src/main/scala/sbtprojectmatrix/ProjectMatrixPlugin.scala @@ -0,0 +1,18 @@ +package sbtprojectmatrix + +import sbt._ +import java.util.concurrent.atomic.AtomicBoolean +import scala.language.experimental.macros + +object ProjectMatrixPlugin extends AutoPlugin { + override val requires = sbt.plugins.CorePlugin + override val trigger = allRequirements + object autoImport { + def projectMatrix: ProjectMatrix = macro ProjectMatrix.projectMatrixMacroImpl + + implicit def matrixClasspathDependency[T]( + m: T + )(implicit ev: T => ProjectMatrixReference): ProjectMatrix.MatrixClasspathDependency = + ProjectMatrix.MatrixClasspathDependency(m, None) + } +} diff --git a/src/sbt-test/projectMatrix/custom/build.sbt b/src/sbt-test/projectMatrix/custom/build.sbt new file mode 100644 index 000000000..ac0310c4a --- /dev/null +++ b/src/sbt-test/projectMatrix/custom/build.sbt @@ -0,0 +1,34 @@ +ThisBuild / organization := "com.example" +ThisBuild / version := "0.1.0-SNAPSHOT" +ThisBuild / publishMavenStyle := true + +ThisBuild / ivyPaths := { + val base = (ThisBuild / baseDirectory).value + IvyPaths(base, Some(base / "ivy-cache")) +} +publish / skip := true + +lazy val core = (projectMatrix in file("core")) + .scalaVersions("2.12.6", "2.11.12") + .settings( + name := "core", + ivyPaths := (ThisBuild / ivyPaths).value + ) + .custom( + idSuffix = "config1_2_", + directorySuffix = "-config1.2", + scalaVersions = Nil, + _.settings( + moduleName := name.value + "_config1.2", + libraryDependencies += "com.typesafe" % "config" % "1.2.1" + ) + ) + .custom( + idSuffix = "config1_3_", + directorySuffix = "-config1.3", + scalaVersions = Nil, + _.settings( + moduleName := name.value + "_config1.3", + libraryDependencies += "com.typesafe" % "config" % "1.3.3" + ) + ) diff --git a/src/sbt-test/projectMatrix/custom/project/plugins.sbt b/src/sbt-test/projectMatrix/custom/project/plugins.sbt new file mode 100644 index 000000000..4e80bbafc --- /dev/null +++ b/src/sbt-test/projectMatrix/custom/project/plugins.sbt @@ -0,0 +1,5 @@ +sys.props.get("plugin.version") match { + case Some(x) => addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % x) + case _ => sys.error("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} diff --git a/src/sbt-test/projectMatrix/custom/test b/src/sbt-test/projectMatrix/custom/test new file mode 100644 index 000000000..e1b9b9ed6 --- /dev/null +++ b/src/sbt-test/projectMatrix/custom/test @@ -0,0 +1,6 @@ +> publishLocal + +$ exists ivy-cache/local/com.example/core_config1.2_2.11/0.1.0-SNAPSHOT/poms/core_config1.2_2.11.pom +$ exists ivy-cache/local/com.example/core_config1.2_2.12/0.1.0-SNAPSHOT/poms/core_config1.2_2.12.pom +$ exists ivy-cache/local/com.example/core_config1.3_2.11/0.1.0-SNAPSHOT/poms/core_config1.3_2.11.pom +$ exists ivy-cache/local/com.example/core_config1.3_2.12/0.1.0-SNAPSHOT/poms/core_config1.3_2.12.pom diff --git a/src/sbt-test/projectMatrix/jvm/build.sbt b/src/sbt-test/projectMatrix/jvm/build.sbt new file mode 100644 index 000000000..19a0b6165 --- /dev/null +++ b/src/sbt-test/projectMatrix/jvm/build.sbt @@ -0,0 +1,19 @@ +// lazy val root = (project in file(".")) +// .aggregate(core.projectRefs ++ app.projectRefs: _*) +// .settings( +// ) + +lazy val core = (projectMatrix in file("core")) + .scalaVersions("2.12.6", "2.11.12") + .settings( + name := "core" + ) + .jvmPlatform() + +lazy val app = (projectMatrix in file("app")) + .dependsOn(core) + .scalaVersions("2.12.6") + .settings( + name := "app" + ) + .jvmPlatform() diff --git a/src/sbt-test/projectMatrix/jvm/core/src/main/scala/Core.scala b/src/sbt-test/projectMatrix/jvm/core/src/main/scala/Core.scala new file mode 100644 index 000000000..274e01225 --- /dev/null +++ b/src/sbt-test/projectMatrix/jvm/core/src/main/scala/Core.scala @@ -0,0 +1,6 @@ +package a + +class Core { +} + +object Core extends Core diff --git a/src/sbt-test/projectMatrix/jvm/project/plugins.sbt b/src/sbt-test/projectMatrix/jvm/project/plugins.sbt new file mode 100644 index 000000000..4e80bbafc --- /dev/null +++ b/src/sbt-test/projectMatrix/jvm/project/plugins.sbt @@ -0,0 +1,5 @@ +sys.props.get("plugin.version") match { + case Some(x) => addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % x) + case _ => sys.error("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} diff --git a/src/sbt-test/projectMatrix/jvm/test b/src/sbt-test/projectMatrix/jvm/test new file mode 100644 index 000000000..9873bfe86 --- /dev/null +++ b/src/sbt-test/projectMatrix/jvm/test @@ -0,0 +1,4 @@ +> compile + +$ exists core/target/jvm-2.12/classes/a/Core.class +$ exists core/target/jvm-2.11/classes/a/Core.class