Skip to content
Open
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
45 changes: 28 additions & 17 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,11 @@ lazy val smithy4playTest = project
"-Dgatling.core.directory.binaries=target/gatling-binaries-smithy"
),
dependencyOverrides ++= Seq(
// Play/Pekko + jackson-module-scala 2.14.x expects Jackson < 2.15
"com.fasterxml.jackson.core" % "jackson-databind" % "2.14.3",
"com.fasterxml.jackson.core" % "jackson-core" % "2.14.3",
"com.fasterxml.jackson.core" % "jackson-annotations" % "2.14.3"
// Pin Jackson to the version Play 3.1.0 / Pekko 1.5.0 ship (2.21.x). The prior 2.14.3
// pin predates jackson-core's StreamReadConstraints (added in 2.15) that Pekko needs.
"com.fasterxml.jackson.core" % "jackson-databind" % "2.21.2",
"com.fasterxml.jackson.core" % "jackson-core" % "2.21.2",
"com.fasterxml.jackson.core" % "jackson-annotations" % "2.21"
),
libraryDependencies ++= Seq(
guice,
Expand Down Expand Up @@ -170,10 +171,11 @@ lazy val smithy4playMcp = project
),
Compile / smithy4sAllowedNamespaces := List("de.innfactory.smithy4play.mcp"),
dependencyOverrides ++= Seq(
// Play/Pekko + jackson-module-scala 2.14.x expects Jackson < 2.15
"com.fasterxml.jackson.core" % "jackson-databind" % "2.14.3",
"com.fasterxml.jackson.core" % "jackson-core" % "2.14.3",
"com.fasterxml.jackson.core" % "jackson-annotations" % "2.14.3"
// Pin Jackson to the version Play 3.1.0 / Pekko 1.5.0 ship (2.21.x). The prior 2.14.3
// pin predates jackson-core's StreamReadConstraints (added in 2.15) that Pekko needs.
"com.fasterxml.jackson.core" % "jackson-databind" % "2.21.2",
"com.fasterxml.jackson.core" % "jackson-core" % "2.21.2",
"com.fasterxml.jackson.core" % "jackson-annotations" % "2.21"
),
libraryDependencies ++= Seq(
guice,
Expand Down Expand Up @@ -300,18 +302,27 @@ lazy val smithy4playGatling = project
lazy val smithy4playSbtCodegen = project
.in(file("smithy4play-sbt-codegen"))
.settings(
sbtPlugin := true,
name := "smithy4play-sbt-codegen",
organization := "de.innfactory",
version := releaseVersion,
scalaVersion := "2.12.21",
sbtPlugin := true,
name := "smithy4play-sbt-codegen",
organization := "de.innfactory",
version := releaseVersion,
scalaVersion := "2.12.21",
// Cross-build for both sbt 1.x (Scala 2.12) and sbt 2.x (Scala 3).
// The sbt-2 axis pins Scala 3.8.4 to match sbt 2.0.0's own build (TASTy forward-compat).
crossScalaVersions := Seq("2.12.21", "3.8.4"),
pluginCrossBuild / sbtVersion := {
scalaBinaryVersion.value match {
case "2.12" => "1.12.12"
case _ => "2.0.0"
}
},
libraryDependencies ++= Seq(
"io.github.classgraph" % "classgraph" % "4.8.179"
),
githubOwner := "innFactory",
githubRepository := "smithy4play",
githubTokenSource := TokenSource.GitConfig("github.token") || TokenSource.Environment("GITHUB_TOKEN"),
credentials := Seq(
githubOwner := "innFactory",
githubRepository := "smithy4play",
githubTokenSource := TokenSource.GitConfig("github.token") || TokenSource.Environment("GITHUB_TOKEN"),
credentials := Seq(
Credentials(
"GitHub Package Registry",
"maven.pkg.github.com",
Expand Down
111 changes: 111 additions & 0 deletions docs/sbt2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# sbt 2 migration status

sbt 2.0.0 went GA on 2026-06-14. It is a Scala 3-based rewrite: build definitions and
plugins compile against a Scala 3 API, every plugin needs a dedicated sbt 2 artifact, and
all tasks are cached by default (results need a `sjsonnew.JsonFormat`, or must opt out via
`Def.uncached`). Migrating the whole build is currently **blocked** on upstream releases,
but the groundwork that can be done safely on sbt 1.x has been done.

> ⚠️ **Pre-release versions in use.** To get onto the sbt 2-capable lines, this branch now
> depends on **Play 3.1.0-M9** (milestone — no GA yet), **scalatestplus-play 8.0.0-M2**
> (milestone), and **Scala 3.8.4** (the "Next" line, *not* the 3.3 LTS). These are not
> production-stable; the branch is a migration staging ground until those lines reach GA.

## Done (already on this branch)

The sbt-1.x-only prep below is fully backward compatible. The Play 3.1.0 upgrade after it
pulls in the pre-release versions noted above. Everything is verified: all modules compile,
`scalafmt*Check` is clean, all 50 tests pass, and the codegen plugin compiles on both the
sbt 1.x and sbt 2.0.0 APIs.

- **Upgraded to the latest sbt 1.x** — `project/build.properties` `1.9.8 → 1.12.12`. This
is the recommended first migration step and unblocks plugins that now require sbt 1.12.9+.
- **Bumped sbt-scalafmt** `2.5.2 → 2.6.1` (cross-published for sbt 1.x and 2.x; requires
sbt ≥ 1.12.9). The scalafmt *engine* stays pinned at 3.7.15 in `.scalafmt.conf`, so
formatting output is unchanged.
- **Removed `com.lucidchart % sbt-cross`** — it was unused (no `crossBuild`/`CrossPlugin`
references anywhere) and is abandoned (last release 2019) with no sbt 2 build. One blocker
eliminated outright.
- **Cross-built our own sbt plugin `smithy4play-sbt-codegen` for sbt 2.** It now compiles
against both the sbt 1.x and sbt 2.0.0 APIs:
- `crossScalaVersions := Seq("2.12.21", "3.8.4")` with a `pluginCrossBuild / sbtVersion`
mapping (`2.12 → 1.12.12`, else `2.0.0`). Scala **3.8.4** matches sbt 2.0.0's own build
(TASTy forward-compat requires our plugin's Scala ≥ sbt's).
- A small hand-rolled `PluginCompat` shim in `src/main/scala-2` and `src/main/scala-3`
bridges the two API differences we hit:
1. `Attributed#data` on a classpath is `File` (sbt 1) vs `xsbti.HashedVirtualFileRef`
(sbt 2) — `PluginCompat.toFiles(...)` resolves both to `Seq[File]` via `fileConverter`.
2. `Def.uncached { ... }` opts the redefined `Compile / compile` task out of sbt 2's
result cache (its `CompileAnalysis` return type has no `JsonFormat`). On sbt 1.x the
shim provides `Def.uncached` as a no-op identity extension.
- The meta-build compiles the plugin sources directly (`project/build.sbt`), so it also
picks up the `scala-2` shim dir and depends on nothing new.

### Play 3.1.0 upgrade (pulls in pre-release versions — see warning above)

- **Play `3.0.11 → 3.1.0-M9`** (`project/Dependencies.scala` + `project/plugins.sbt`). This
is the only sbt 2-capable Play line; GA is not out. Its sbt-plugin still publishes an
sbt 1.x axis, so it works on sbt 1.12.12 today.
- **Scala `3.3.8 → 3.8.4`** (`project/Dependencies.scala`). *Forced* by Play 3.1.0-M9, which
is built with Scala 3.8.3 — the 3.3.8 compiler cannot read its newer TASTy (manifests as
`AssertionError: ... has non-class parent` while reading `scala3-library` 3.8.3). 3.8.4 is
the latest Next and matches sbt 2.0.0's own Scala version. **Leaves the 3.3 LTS line.**
- **scalatestplus-play `7.0.2 → 8.0.0-M2`** — 7.0.x targets Play 3.0.x; 8.0.0-Mx tracks 3.1.0.
- **Jackson pin `2.14.3 → 2.21.2`** (`build.sbt`, both override blocks) — Play 3.1.0 / Pekko
1.5.0 require `jackson-core`'s `StreamReadConstraints` (added in 2.15); the old 2.14.3 pin
threw `ClassNotFoundException` at Guice injector creation. (`jackson-annotations` is `2.21`.)
- **`javax.inject` → `jakarta.inject`** in 9 hand-written sources (core, mcp, test
controllers/middlewares) — Play 3.1 completed the Jakarta EE namespace migration.

> We deliberately did **not** use the Scala Center `sbt2-compat` library even though it
> provides exactly these helpers (`toFiles`, `Def.uncached`). When cross-building from an
> sbt 1.x launcher, `addSbtPlugin` generates the wrong sbt-2 cross-coordinate
> (`sbt2-compat_3_2.0`) while the artifact is published as `sbt2-compat_sbt2_3`, so it
> fails to resolve. The hand-rolled shim is tiny, dependency-free, and avoids that.

## Blockers (cannot migrate until these ship)

| Dependency | Needs | Status (2026-06-16) |
|---|---|---|
| `smithy4s-sbt-codegen` + `smithy4s-core` | sbt 2 codegen plugin | **No release.** sbt 2 support targets the 0.19.x line — PRs [disneystreaming/smithy4s#1974](https://github.com/disneystreaming/smithy4s/pull/1974) (series/0.19) and [#1973](https://github.com/disneystreaming/smithy4s/pull/1973) (backport to 0.18) opened 2026-06-15, unmerged. **Primary blocker.** |
| `io.gatling % gatling-sbt` | sbt 2 cross-build | No sbt 2 build started (still pinned to Scala 2.12 / sbt 1.x). Used by `smithy4play-gatling`. |
| `com.codecommit %% sbt-github-packages` | sbt 2 build or replacement | Abandoned since 2021, no sbt 2. Used for publishing — see replacement note below. |

Ready / no longer blocking: `sbt-scalafmt` 2.6.1, `sbt-scoverage` 2.4.4, `sbt-jmh` 0.4.8
(all publish an sbt 2 axis); `sbt-cross` removed; Play moved to the 3.1.0 line (sbt 2-capable,
currently on milestone M9 — wait for 3.1.0 GA before considering this production-stable).

## Remaining steps for the actual switch (when blockers clear)

1. Bump `smithy4s` to the first 0.19.x that publishes an sbt 2 codegen plugin; regenerate
and fix any generated-code/runtime changes (treat as its own migration — see the
dependency check, this is a breaking minor).
2. Move Play from milestone **3.1.0-M9 → 3.1.0 GA** once released (and scalatestplus-play
8.0.0-M2 → GA); re-pin Jackson if the GA bumps it. Re-evaluate staying on Scala 3.8.x
vs. a future LTS that supports the same TASTy.
3. Resolve `gatling-sbt` — either wait for an sbt 2 build or move `smithy4play-gatling` to
run Gatling another way.
4. Replace `sbt-github-packages`. GitHub Packages is a plain Maven repo, so the plugin can
be dropped in favour of native sbt config (no sbt 2 plugin needed):
```scala
publishTo := Some("GitHub" at "https://maven.pkg.github.com/innFactory/smithy4play")
credentials += Credentials(
"GitHub Package Registry", "maven.pkg.github.com", "innFactory", sys.env("GITHUB_TOKEN")
)
```
(plus a matching resolver for consumers). Verify against the existing
`publishSmithy4Play` / `publishLocalBundle` aliases in `build.sbt`.
5. Set `project/build.properties` `sbt.version = 2.x`, convert build definitions per the
[migration guide](https://www.scala-sbt.org/2.x/docs/en/changes/migrating-from-sbt-1.x.html),
and re-run the whole build.
6. **Publishing the codegen plugin for sbt 2:** the cross-build config is in place, but the
sbt-2 artifact must be *published from an sbt 2.x launcher* so it gets the correct
`_sbt2_3` coordinate. An sbt 1.x launcher would publish it as `_3_2.0`, which sbt 2
consumers won't resolve. Wire this into the release once the project itself runs on sbt 2.

## References

- sbt 2.0.0 release / "Last mile towards sbt 2": <https://www.scala-lang.org/blog/2026/04/14/last-mile-towards-sbt2.html>
- Migrating plugins with sbt2-compat: <https://www.scala-lang.org/blog/2026/03/02/sbt2-compat.html>
- Migrating from sbt 1.x: <https://www.scala-sbt.org/2.x/docs/en/changes/migrating-from-sbt-1.x.html>
- sbt 2.x plugin migration tracker: <https://github.com/sbt/sbt/wiki/sbt-2.x-plugin-migration>
6 changes: 3 additions & 3 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import sbt.*

object Dependencies {

val playVersion = "3.0.11"
val playVersion = "3.1.0-M9"
val typesafePlay = "org.playframework" %% "play" % playVersion

val scalaVersion = "3.3.8"
val scalaVersion = "3.8.4"
val smithy4sVersion = "0.18.54"
val smithyVersion = "1.71.0"

Expand All @@ -28,7 +28,7 @@ object Dependencies {
// "software.amazon.smithy" % "smithy-protocol-test-traits" % smithyVersion

val scalatestPlus =
"org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test
"org.scalatestplus.play" %% "scalatestplus-play" % "8.0.0-M2" % Test

val cats = "org.typelevel" %% "cats-core" % "2.13.0"

Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
sbt.version=1.9.8
sbt.version=1.12.12
scala.version=3.3.8
4 changes: 4 additions & 0 deletions project/build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
libraryDependencies += "io.github.classgraph" % "classgraph" % "4.8.179"

Compile / unmanagedSourceDirectories += baseDirectory.value.getParentFile / "smithy4play-sbt-codegen" / "src" / "main" / "scala"
// The meta-build runs on Scala 2.12 (sbt 1.x), so it needs the sbt 1.x variant of the
// PluginCompat shim. The matching scala-3 variant is used only when the plugin itself is
// cross-compiled for sbt 2.x.
Compile / unmanagedSourceDirectories += baseDirectory.value.getParentFile / "smithy4play-sbt-codegen" / "src" / "main" / "scala-2"
5 changes: 2 additions & 3 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
addSbtPlugin("com.codecommit" %% "sbt-github-packages" % "0.5.3")
addSbtPlugin("org.scalameta" %% "sbt-scalafmt" % "2.5.2")
addSbtPlugin("org.playframework" %% "sbt-plugin" % "3.0.11")
addSbtPlugin("org.scalameta" %% "sbt-scalafmt" % "2.6.1")
addSbtPlugin("org.playframework" %% "sbt-plugin" % "3.1.0-M9")
addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "2.4.4")
addSbtPlugin("com.disneystreaming.smithy4s" %% "smithy4s-sbt-codegen" % "0.18.54")
addSbtPlugin("com.lucidchart" % "sbt-cross" % "4.0")
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8")
addSbtPlugin("io.gatling" % "gatling-sbt" % "4.18.3")
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import play.api.libs.json.Json
import play.api.mvc.*
import play.api.routing.Router.Routes

import javax.inject.{ Inject, Singleton }
import jakarta.inject.{ Inject, Singleton }
import scala.concurrent.{ ExecutionContext, Future }

@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import smithy4s.{ Document, Schema, Service }

import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import javax.inject.Provider
import jakarta.inject.Provider
import scala.concurrent.{ ExecutionContext, Future }

@Singleton
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.innfactory.smithy4play.sbt

import sbt.*

/** sbt 1.x source-compatibility shim.
*
* On sbt 1.x a classpath's `Attributed#data` is already a `java.io.File`, so the `FileConverter` argument is accepted
* (to keep the call site identical across sbt versions) but unused. The sbt 2.x counterpart lives in
* `src/main/scala-3`.
*/
private[smithy4play] object PluginCompat {

def toFiles(cp: Seq[Attributed[File]])(@annotation.unused conv: xsbti.FileConverter): Seq[File] =
cp.map(_.data)

/** sbt 1.x has no task cache, so `Def.uncached(...)` is a no-op identity. On sbt 2.x the native `Def.uncached`
* (provided by the scala-3 variant's absence of this shim) opts the redefined task out of the result cache.
* Importing `PluginCompat.*` brings this into scope.
*/
implicit class DefUncachedOps(private val d: sbt.Def.type) extends AnyVal {
def uncached[A](a: A): A = a
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.innfactory.smithy4play.sbt

import sbt.*

/** sbt 2.x source-compatibility shim.
*
* On sbt 2.x a classpath's `Attributed#data` is an `xsbti.HashedVirtualFileRef` (part of the virtual-file migration),
* so it must be resolved back to a real `java.io.File` through the build's `FileConverter`. The sbt 1.x counterpart
* lives in `src/main/scala-2`.
*/
private[smithy4play] object PluginCompat {

def toFiles(cp: Seq[Attributed[xsbti.HashedVirtualFileRef]])(conv: xsbti.FileConverter): Seq[File] =
cp.map(entry => conv.toPath(entry.data).toFile)

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package de.innfactory.smithy4play.sbt

import sbt.*
import sbt.Keys.*
import de.innfactory.smithy4play.sbt.PluginCompat.*

import scala.io.Source
import scala.sys.process.*
Expand Down Expand Up @@ -137,15 +138,19 @@ object Smithy4PlayCodegenPlugin extends AutoPlugin {
smithy4playRegistryName := "Smithy4PlayGeneratedRegistry",
smithy4playRegistryOutputDir := (Compile / sourceManaged).value,

// After compilation, generate and compile the registry
Compile / compile := {
// After compilation, generate and compile the registry.
// `Def.uncached` opts this redefined task out of sbt 2.x's result cache (CompileAnalysis
// has no JsonFormat); on sbt 1.x it is a no-op identity provided by PluginCompat.
Compile / compile := Def.uncached {
val analysis = (Compile / compile).value
val log = streams.value.log
val classesDir = (Compile / classDirectory).value
val registryPackage = smithy4playRegistryPackage.value
val registryName = smithy4playRegistryName.value
val outputDir = smithy4playRegistryOutputDir.value
val fullClasspath = (Compile / dependencyClasspath).value.map(_.data)
// `Attributed#data` is `File` on sbt 1.x but `HashedVirtualFileRef` on sbt 2.x;
// PluginCompat.toFiles resolves both to `Seq[File]` via the fileConverter.
val fullClasspath = PluginCompat.toFiles((Compile / dependencyClasspath).value)(fileConverter.value)
val scalaInst = Keys.scalaInstance.value

generateAndCompileRegistry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import de.innfactory.smithy4play.routing.controller.ControllerRouter
import play.api.Application
import play.api.mvc.ControllerComponents

import javax.inject.{ Inject, Singleton }
import jakarta.inject.{ Inject, Singleton }
import scala.concurrent.ExecutionContext

@Singleton
Expand Down
2 changes: 1 addition & 1 deletion smithy4playTest/app/controller/McpTestController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import de.innfactory.smithy4play.ContextRoute
import de.innfactory.smithy4play.routing.Controller
import play.api.libs.ws.WSClient
import play.api.mvc.ControllerComponents
import javax.inject.{ Inject, Singleton }
import jakarta.inject.{ Inject, Singleton }
import scala.concurrent.{ ExecutionContext, Future }
import testDefinitions.test.*
import testDefinitions.test.McpControllerServiceGen.serviceInstance
Expand Down
2 changes: 1 addition & 1 deletion smithy4playTest/app/controller/TestController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import smithy4s.{ Blob, Service }
import testDefinitions.test._
import testDefinitions.test.TestControllerServiceGen.serviceInstance

import javax.inject.{ Inject, Singleton }
import jakarta.inject.{ Inject, Singleton }
import scala.concurrent.{ ExecutionContext, Future }

@Singleton
Expand Down
2 changes: 1 addition & 1 deletion smithy4playTest/app/controller/TestRouter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import de.innfactory.smithy4play.routing.middleware.Smithy4PlayMiddleware
import play.api.Application
import play.api.mvc.ControllerComponents

import javax.inject.{ Inject, Singleton }
import jakarta.inject.{ Inject, Singleton }
import scala.concurrent.ExecutionContext

@Singleton
Expand Down
2 changes: 1 addition & 1 deletion smithy4playTest/app/controller/XmlController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import de.innfactory.smithy4play.routing.Controller
import play.api.mvc.ControllerComponents
import testDefinitions.test.{ XmlControllerDef, XmlTestInputBody, XmlTestOutput, XmlTestWithInputAndOutputOutput }
import XmlControllerDef.serviceInstance
import javax.inject.{ Inject, Singleton }
import jakarta.inject.{ Inject, Singleton }
import scala.concurrent.{ ExecutionContext, Future }

@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import smithy.api.{ Auth, HttpBearerAuth }
import smithy4s.Blob
import smithy4s.http.HttpResponse

import javax.inject.{ Inject, Singleton }
import jakarta.inject.{ Inject, Singleton }
import scala.concurrent.{ ExecutionContext, Future }

@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import smithy.api.{ Auth, HttpBearerAuth }
import smithy4s.Blob
import smithy4s.http.HttpResponse
import play.api.mvc.Result
import javax.inject.{ Inject, Singleton }
import jakarta.inject.{ Inject, Singleton }
import scala.concurrent.{ ExecutionContext, Future }

@Singleton
Expand Down
Loading