Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,25 @@ 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

* IntelliJ doesn't run plugins during project build. Before importing,
`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.
58 changes: 42 additions & 16 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,30 @@ 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)
Comment thread
rbscgh marked this conversation as resolved.
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
)

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
)
Expand All @@ -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,
Expand All @@ -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"),
Comment on lines +73 to +87

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is probably unacceptable but hopefully will be fixed once 1.0 scalapb is released

Comment thread
rbscgh marked this conversation as resolved.
"org.json4s" %% "json4s-native" % "4.0.7",
"org.specs2" %% "specs2-core" % "4.20.9" % Test cross CrossVersion.for3Use2_13
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 22 additions & 10 deletions plugin/README.md
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
Expand Up @@ -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, _)) =>
Expand Down Expand Up @@ -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
}
}
)
Expand Down
2 changes: 2 additions & 0 deletions plugin/src/sbt-test/generator/e2e/build.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
enablePlugins(Twinagle)

scalaVersion := "2.13.17"

scalacOptions ++= Seq(
"-encoding",
"utf8",
Expand Down
1 change: 0 additions & 1 deletion plugin/src/sbt-test/generator/e2e/project/build.properties

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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 = ()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
}

Expand All @@ -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
}
}
}

Expand All @@ -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
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Loading