diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 641564a..bbf7f49 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,14 +18,14 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Install JDK 1.8 + - name: Install JDK 17 uses: actions/setup-java@v4 with: - java-version: 8 + java-version: 17 distribution: temurin - uses: sc-actions-forks/setup-sbt@96cf3f09dc501acdad7807fffe97dba9fa0709be #v1.1.5 - name: Run tests - run: sbt +test scripted + run: sbt +test +publishLocal +plugin/scripted - name: Lint run: sbt scalafmtCheckAll - name: Release diff --git a/README.md b/README.md index 106b94b..ebdcc54 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,18 @@ Thanks for your interest in Twinagle, we're welcome your contributions! For larger changes, please open an issue to discuss them before spending lots of time implementing things. For small changes, hack away and submit a pull request. -Please ensure that `sbt scalafmtCheckAll +test scripted` passes when submitting code changes. +Please ensure that `sbt scalafmtCheckAll +test +publishLocal +plugin/scripted` passes when submitting code changes. + +# sbt 1.x and sbt 2.x + +The `twinagle-scalapb-plugin` is cross-built for **both sbt 1.x and sbt 2.x**. +The build itself runs on an sbt 1.x launcher and cross-publishes both plugin +artifacts via `crossScalaVersions = [2.12, 3]` + `pluginCrossBuild / sbtVersion` +(Scala 2.12 → sbt 1.x, Scala 3 → sbt 2.0.0). This follows the pattern used by +sbt-protoc / sbt-assembly; see the +[sbt 2 plugin cross-building guide](https://www.scala-sbt.org/2.x/docs/en/changes/migrating-from-sbt-1.x.html#cross-building-sbt-plugins). + +**Building requires JDK 17** (sbt 2.0.0 will not run on JDK 8/11). # Notes @@ -24,4 +35,6 @@ Please ensure that `sbt scalafmtCheckAll +test scripted` passes when submitting `sbt compile` may be necessary. * In order to run the full test suite (i.e. the unit tests & the end-to-end tests - for code-generation) use `sbt +test scripted` + for code-generation) use `sbt +test +publishLocal +plugin/scripted`. The + `+plugin/scripted` runs the end-to-end test against **both** sbt 1.x and sbt 2.x; + bare `scripted` only exercises the sbt 1.x axis. diff --git a/build.sbt b/build.sbt index faaf502..24ca9ba 100644 --- a/build.sbt +++ b/build.sbt @@ -2,24 +2,22 @@ import sbt.CrossVersion lazy val scala212 = "2.12.20" lazy val scala213 = "2.13.17" -lazy val scala3LTS = "3.3.7" +lazy val scala3LTS = "3.3.7" // runtime stays on LTS — it is the end-user library +lazy val scala3 = "3.8.4" // codegen + plugin Scala-3 axis (matches sbt 2.0.0 DSL) +lazy val sbt2 = "2.0.0" +lazy val sbt1 = "1.9.9" // pluginCrossBuild sbt version for the 2.12 axis (oldest sbt 1.x we support) lazy val commonSettings = List( scalaVersion := scala212, scalacOptions ++= { + val common = Seq("-encoding", "utf8", "-deprecation", "-unchecked") CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, _)) => scalacOptions ++= Seq("-Xlint") - case _ => () + // Scala 3 renamed -Xfatal-warnings to -Werror (the old alias is itself deprecated). + case Some((2, _)) => common ++ Seq("-Xlint", "-Xfatal-warnings") + case _ => common ++ Seq("-Werror") } - Seq( - "-encoding", - "utf8", - "-deprecation", - "-unchecked", - "-Xfatal-warnings" - ) }, - Compile / console / scalacOptions --= Seq("-deprecation", "-Xfatal-warnings", "-Xlint"), + Compile / console / scalacOptions --= Seq("-deprecation", "-Xfatal-warnings", "-Werror", "-Xlint"), scalafmtOnCompile := true ) @@ -27,7 +25,7 @@ lazy val codegen = (project in file("codegen")) .settings( commonSettings, name := "twinagle-codegen", - crossScalaVersions := Seq(scala212, scala213, scala3LTS), + crossScalaVersions := Seq(scala212, scala213, scala3), libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % scalapb.compiler.Version.scalapbVersion, publishLocal := publishLocal.dependsOn(runtime / publishLocal).value ) @@ -37,8 +35,23 @@ lazy val plugin = (project in file("plugin")) .dependsOn(codegen) .settings( commonSettings, - name := "twinagle-scalapb-plugin", - addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.7"), + name := "twinagle-scalapb-plugin", + crossScalaVersions := Seq(scala212, scala3), + scalaVersion := scala212, + pluginCrossBuild / sbtVersion := { + scalaBinaryVersion.value match { + case "2.12" => sbt1 + case _ => sbt2 + } + }, + scriptedSbt := { + scalaBinaryVersion.value match { + case "2.12" => sbtVersion.value + case _ => sbt2 + } + }, + addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.1.0-RC1"), + addSbtPlugin("com.github.sbt" % "sbt2-compat" % "0.1.0"), buildInfoKeys := Seq[BuildInfoKey](version, scalaBinaryVersion), buildInfoPackage := "com.soundcloud.twinagle.plugin", buildInfoUsePackageAsPath := true, @@ -57,8 +70,21 @@ lazy val runtime = (project in file("runtime")).settings( libraryDependencies ++= { Seq( "com.twitter" %% "finagle-http" % "24.2.0" cross CrossVersion.for3Use2_13, - "com.thesamet.scalapb" %% "scalapb-runtime" % "0.11.20", - "com.thesamet.scalapb" %% "scalapb-json4s" % "0.12.1", + "com.thesamet.scalapb" %% "scalapb-runtime" % "1.0.0-alpha.5", + // scalapb-json4s has only published 1.0.0-alpha.1, which pulls scalapb-runtime alpha.1. + // Exclude its transitive scalapb-runtime so only our alpha.5 above remains: sbt's + // early-semver check otherwise flags the alpha.1 -> alpha.5 eviction as a hard conflict, + // and a per-dependency exclude (unlike dependencyOverrides) serializes into the published + // POM/ivy so downstream consumers inherit the coherent single version. + // + // The Scala-binary suffixes (_2.12/_2.13/_3) are spelled out explicitly rather than via + // CrossVersion.binary: exclusions match on the literal published artifactId, and a + // CrossVersion.binary rule serializes to the suffix-less "scalapb-runtime", which does + // NOT match "scalapb-runtime_2.13" in a downstream consumer's POM resolution. + ("com.thesamet.scalapb" %% "scalapb-json4s" % "1.0.0-alpha.1") + .exclude("com.thesamet.scalapb", "scalapb-runtime_2.12") + .exclude("com.thesamet.scalapb", "scalapb-runtime_2.13") + .exclude("com.thesamet.scalapb", "scalapb-runtime_3"), "org.json4s" %% "json4s-native" % "4.0.7", "org.specs2" %% "specs2-core" % "4.20.9" % Test cross CrossVersion.for3Use2_13 ) diff --git a/codegen/src/main/scala/com/soundcloud/twinagle/codegen/TwinagleServicePrinter.scala b/codegen/src/main/scala/com/soundcloud/twinagle/codegen/TwinagleServicePrinter.scala index b46d7dc..ef5d557 100644 --- a/codegen/src/main/scala/com/soundcloud/twinagle/codegen/TwinagleServicePrinter.scala +++ b/codegen/src/main/scala/com/soundcloud/twinagle/codegen/TwinagleServicePrinter.scala @@ -9,24 +9,24 @@ final class TwinagleServicePrinter( ) { import implicits._ - private[this] val twitterUtil = "_root_.com.twitter.util" - private[this] val finagle = "_root_.com.twitter.finagle" - private[this] val finagleHttp = s"$finagle.http" - private[this] val twinagle = "_root_.com.soundcloud.twinagle" - - private[this] val Future = s"$twitterUtil.Future" - private[this] val Service = s"$finagle.Service" - private[this] val Filter = s"$finagle.Filter" - private[this] val Request = s"$finagleHttp.Request" - private[this] val Response = s"$finagleHttp.Response" - - private[this] val EndpointMetadata = s"$twinagle.EndpointMetadata" - private[this] val MessageFilter = s"$twinagle.MessageFilter" - private[this] val ClientEndpointBuilder = s"$twinagle.ClientEndpointBuilder" - private[this] val ServerBuilder = s"$twinagle.ServerBuilder" - private[this] val ProtoService = s"$twinagle.ProtoService" - private[this] val AsProtoService = s"$twinagle.AsProtoService" - private[this] val ProtoRpcBuilder = s"$twinagle.ProtoRpcBuilder" + private val twitterUtil = "_root_.com.twitter.util" + private val finagle = "_root_.com.twitter.finagle" + private val finagleHttp = s"$finagle.http" + private val twinagle = "_root_.com.soundcloud.twinagle" + + private val Future = s"$twitterUtil.Future" + private val Service = s"$finagle.Service" + private val Filter = s"$finagle.Filter" + private val Request = s"$finagleHttp.Request" + private val Response = s"$finagleHttp.Response" + + private val EndpointMetadata = s"$twinagle.EndpointMetadata" + private val MessageFilter = s"$twinagle.MessageFilter" + private val ClientEndpointBuilder = s"$twinagle.ClientEndpointBuilder" + private val ServerBuilder = s"$twinagle.ServerBuilder" + private val ProtoService = s"$twinagle.ProtoService" + private val AsProtoService = s"$twinagle.AsProtoService" + private val ProtoRpcBuilder = s"$twinagle.ProtoRpcBuilder" def generateServiceObject(m: ServiceDescriptor): String = { val serviceName = getServiceName(m) diff --git a/docs/index.md b/docs/index.md index bc8a280..fe22cbc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,6 +26,8 @@ over http. ## Project setup To get started with twinagle, you'll need to add a plugin dependency to your project and enable the plugin for your build. +The plugin is published for both **sbt 1.x and sbt 2.x** — sbt resolves the matching +artifact automatically, so the same line works in either case. Add the folowing line to `project/plugins.sbt`: ```scala diff --git a/plugin/README.md b/plugin/README.md index cb6fb77..ef84ece 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -1,22 +1,34 @@ # Twinagle ScalaPB Plugin -To test the generator, within SBT: +This plugin is cross-built for **sbt 1.x and sbt 2.x**. The end-to-end test under +`plugin/src/sbt-test/generator/e2e` runs against both: on the Scala 2.12 axis it +launches sbt 1.x, and on the Scala 3 axis it launches sbt 2.0.0. + +## Running the generator tests + +To test the generator across both sbt axes, within sbt (requires JDK 17): ``` -> scripted +> +publishLocal +> +plugin/scripted ``` -This would publish your generator locally and run it over a test project -located under `codegen/src/sbt-test/generator/e2e`. +`+publishLocal` cross-publishes `twinagle-runtime`, `twinagle-codegen`, and both +plugin artifacts (`_2.12_1.0` and `_sbt2_3`) to your local ivy repo. `+plugin/scripted` +then publishes nothing further and runs the test project once per sbt axis. + +> Note: a bare `scripted` only runs the sbt 1.x axis. Use `+plugin/scripted` to +> cover sbt 2.x as well. + +## Iterating on the test project without republishing -Assuming that the plugin If you would like to test the test project without republishing the plugin -each time (assuming it does not change), then publish it locally using -`publishLocal`, then in `codegen/src/sbt-test` run SBT and pass it the version number, -for example: +If the plugin itself hasn't changed, publish it once and run the test project +directly, passing the version number: ``` -cd codegen/src/sbt-test/generator/e2e` -sbt -Dplugin.version=0.1.1-SNAPSHOT +sbt +publishLocal # note the version it prints, e.g. 1.4.6-SNAPSHOT +cd plugin/src/sbt-test/generator/e2e +sbt -Dplugin.version=1.4.6-SNAPSHOT > test ``` diff --git a/plugin/src/main/scala/com/soundcloud/twinagle/plugin/Twinagle.scala b/plugin/src/main/scala/com/soundcloud/twinagle/plugin/Twinagle.scala index 265a56a..791d2c1 100644 --- a/plugin/src/main/scala/com/soundcloud/twinagle/plugin/Twinagle.scala +++ b/plugin/src/main/scala/com/soundcloud/twinagle/plugin/Twinagle.scala @@ -13,7 +13,10 @@ object Twinagle extends AutoPlugin { override def trigger: PluginTrigger = NoTrigger - override def projectSettings: Seq[Def.Setting[_]] = List( + // Return type is inherited from AutoPlugin.projectSettings. We avoid writing it + // explicitly because sbt 1 (Scala 2.12) types it as Seq[Setting[_]] while sbt 2 + // (Scala 3) uses Seq[Setting[?]], and `_` is a fatal deprecation under -Werror on 3.x. + override def projectSettings = List( scalapbCodeGeneratorOptions := { CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => @@ -42,9 +45,14 @@ object Twinagle extends AutoPlugin { "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf" ), excludeDependencies ++= { + // ExclusionRule directly (rather than `org % name`): the implicit + // OrganizationArtifactName -> InclExclRule conversion sbt 1 relied on here is + // gone in sbt 2 / Scala 3. The _2.13 suffix is the literal artifact name finagle + // drags in via for3Use2_13, not a cross-versioned suffix. CrossVersion.partialVersion(scalaVersion.value) match { - case Some((3, _)) => Seq("org.scala-lang.modules" % "scala-collection-compat_2.13") - case _ => Seq.empty + case Some((3, _)) => + Seq(ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13")) + case _ => Seq.empty } } ) diff --git a/plugin/src/sbt-test/generator/e2e/build.sbt b/plugin/src/sbt-test/generator/e2e/build.sbt index 76c075c..ea4495e 100644 --- a/plugin/src/sbt-test/generator/e2e/build.sbt +++ b/plugin/src/sbt-test/generator/e2e/build.sbt @@ -1,5 +1,7 @@ enablePlugins(Twinagle) +scalaVersion := "2.13.17" + scalacOptions ++= Seq( "-encoding", "utf8", diff --git a/plugin/src/sbt-test/generator/e2e/project/build.properties b/plugin/src/sbt-test/generator/e2e/project/build.properties deleted file mode 100644 index 72413de..0000000 --- a/plugin/src/sbt-test/generator/e2e/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.8.3 diff --git a/plugin/src/sbt-test/generator/e2e/src/test/scala/proto/test/OptionalFieldsSpec.scala b/plugin/src/sbt-test/generator/e2e/src/test/scala/proto/test/OptionalFieldsSpec.scala index 83c5c44..fccf613 100644 --- a/plugin/src/sbt-test/generator/e2e/src/test/scala/proto/test/OptionalFieldsSpec.scala +++ b/plugin/src/sbt-test/generator/e2e/src/test/scala/proto/test/OptionalFieldsSpec.scala @@ -15,10 +15,8 @@ class OptionalFieldsSpec extends Specification { Future.value(req) } - @annotation.nowarn private def iTakeAnOptionInt[T](a: T)(implicit ev: T =:= Option[Int]): Unit = () - @annotation.nowarn private def iTakeAnOptionString[T](a: T)(implicit ev: T =:= Option[String]): Unit = () } diff --git a/plugin/src/sbt-test/generator/e2e/src/test/scala/twitch/twirp/example/haberdasher/HaberdasherSpec.scala b/plugin/src/sbt-test/generator/e2e/src/test/scala/twitch/twirp/example/haberdasher/HaberdasherSpec.scala index b745e67..d607a56 100644 --- a/plugin/src/sbt-test/generator/e2e/src/test/scala/twitch/twirp/example/haberdasher/HaberdasherSpec.scala +++ b/plugin/src/sbt-test/generator/e2e/src/test/scala/twitch/twirp/example/haberdasher/HaberdasherSpec.scala @@ -2,7 +2,7 @@ package twitch.twirp.example.haberdasher import com.soundcloud.twinagle.{ErrorCode, ServerBuilder, TwinagleException} import com.twitter.finagle.{Service, http} -import com.twitter.util.{Await, Future, Throw} +import com.twitter.util.{Await, Future} import org.specs2.mutable.Specification class HaberdasherSpec extends Specification { @@ -36,9 +36,9 @@ class HaberdasherSpec extends Specification { } "produces TwinagleExceptions for error responses" in { - val Throw(ex: TwinagleException) = Await.result(client.makeHat(Size(-1)).liftToTry) - - ex.code ==== ErrorCode.InvalidArgument + Await.result(client.makeHat(Size(-1))) must throwA[TwinagleException].like { case ex: TwinagleException => + ex.code ==== ErrorCode.InvalidArgument + } } } @@ -59,9 +59,9 @@ class HaberdasherSpec extends Specification { } "produces TwinagleExceptions for error responses" in { - val Throw(ex: TwinagleException) = Await.result(client.makeHat(Size(-1)).liftToTry) - - ex.code ==== ErrorCode.InvalidArgument + Await.result(client.makeHat(Size(-1))) must throwA[TwinagleException].like { case ex: TwinagleException => + ex.code ==== ErrorCode.InvalidArgument + } } } @@ -78,9 +78,9 @@ class HaberdasherSpec extends Specification { } "produces TwinagleExceptions for error responses" in { - val Throw(ex: TwinagleException) = Await.result(client.makeHat(Size(-1)).liftToTry) - - ex.code ==== ErrorCode.InvalidArgument + Await.result(client.makeHat(Size(-1))) must throwA[TwinagleException].like { case ex: TwinagleException => + ex.code ==== ErrorCode.InvalidArgument + } } } } diff --git a/project/plugins.sbt b/project/plugins.sbt index 88b4bdc..221be3b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -8,7 +8,7 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") libraryDependencies ++= Seq( "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value, - "com.thesamet.scalapb" %% "compilerplugin" % "0.11.20" + "com.thesamet.scalapb" %% "compilerplugin" % "1.0.0-alpha.5" ) // only necessary so we can generate protos for tests -addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.7") +addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.1.0-RC1")