Skip to content

Commit d9459b9

Browse files
committed
prep: prepare smithy4play sbt2 migration
1 parent b637f4e commit d9459b9

18 files changed

Lines changed: 206 additions & 36 deletions

File tree

build.sbt

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,11 @@ lazy val smithy4playTest = project
136136
"-Dgatling.core.directory.binaries=target/gatling-binaries-smithy"
137137
),
138138
dependencyOverrides ++= Seq(
139-
// Play/Pekko + jackson-module-scala 2.14.x expects Jackson < 2.15
140-
"com.fasterxml.jackson.core" % "jackson-databind" % "2.14.3",
141-
"com.fasterxml.jackson.core" % "jackson-core" % "2.14.3",
142-
"com.fasterxml.jackson.core" % "jackson-annotations" % "2.14.3"
139+
// Pin Jackson to the version Play 3.1.0 / Pekko 1.5.0 ship (2.21.x). The prior 2.14.3
140+
// pin predates jackson-core's StreamReadConstraints (added in 2.15) that Pekko needs.
141+
"com.fasterxml.jackson.core" % "jackson-databind" % "2.21.2",
142+
"com.fasterxml.jackson.core" % "jackson-core" % "2.21.2",
143+
"com.fasterxml.jackson.core" % "jackson-annotations" % "2.21"
143144
),
144145
libraryDependencies ++= Seq(
145146
guice,
@@ -170,10 +171,11 @@ lazy val smithy4playMcp = project
170171
),
171172
Compile / smithy4sAllowedNamespaces := List("de.innfactory.smithy4play.mcp"),
172173
dependencyOverrides ++= Seq(
173-
// Play/Pekko + jackson-module-scala 2.14.x expects Jackson < 2.15
174-
"com.fasterxml.jackson.core" % "jackson-databind" % "2.14.3",
175-
"com.fasterxml.jackson.core" % "jackson-core" % "2.14.3",
176-
"com.fasterxml.jackson.core" % "jackson-annotations" % "2.14.3"
174+
// Pin Jackson to the version Play 3.1.0 / Pekko 1.5.0 ship (2.21.x). The prior 2.14.3
175+
// pin predates jackson-core's StreamReadConstraints (added in 2.15) that Pekko needs.
176+
"com.fasterxml.jackson.core" % "jackson-databind" % "2.21.2",
177+
"com.fasterxml.jackson.core" % "jackson-core" % "2.21.2",
178+
"com.fasterxml.jackson.core" % "jackson-annotations" % "2.21"
177179
),
178180
libraryDependencies ++= Seq(
179181
guice,
@@ -300,18 +302,27 @@ lazy val smithy4playGatling = project
300302
lazy val smithy4playSbtCodegen = project
301303
.in(file("smithy4play-sbt-codegen"))
302304
.settings(
303-
sbtPlugin := true,
304-
name := "smithy4play-sbt-codegen",
305-
organization := "de.innfactory",
306-
version := releaseVersion,
307-
scalaVersion := "2.12.21",
305+
sbtPlugin := true,
306+
name := "smithy4play-sbt-codegen",
307+
organization := "de.innfactory",
308+
version := releaseVersion,
309+
scalaVersion := "2.12.21",
310+
// Cross-build for both sbt 1.x (Scala 2.12) and sbt 2.x (Scala 3).
311+
// The sbt-2 axis pins Scala 3.8.4 to match sbt 2.0.0's own build (TASTy forward-compat).
312+
crossScalaVersions := Seq("2.12.21", "3.8.4"),
313+
pluginCrossBuild / sbtVersion := {
314+
scalaBinaryVersion.value match {
315+
case "2.12" => "1.12.12"
316+
case _ => "2.0.0"
317+
}
318+
},
308319
libraryDependencies ++= Seq(
309320
"io.github.classgraph" % "classgraph" % "4.8.179"
310321
),
311-
githubOwner := "innFactory",
312-
githubRepository := "smithy4play",
313-
githubTokenSource := TokenSource.GitConfig("github.token") || TokenSource.Environment("GITHUB_TOKEN"),
314-
credentials := Seq(
322+
githubOwner := "innFactory",
323+
githubRepository := "smithy4play",
324+
githubTokenSource := TokenSource.GitConfig("github.token") || TokenSource.Environment("GITHUB_TOKEN"),
325+
credentials := Seq(
315326
Credentials(
316327
"GitHub Package Registry",
317328
"maven.pkg.github.com",

docs/sbt2-migration.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# sbt 2 migration status
2+
3+
sbt 2.0.0 went GA on 2026-06-14. It is a Scala 3-based rewrite: build definitions and
4+
plugins compile against a Scala 3 API, every plugin needs a dedicated sbt 2 artifact, and
5+
all tasks are cached by default (results need a `sjsonnew.JsonFormat`, or must opt out via
6+
`Def.uncached`). Migrating the whole build is currently **blocked** on upstream releases,
7+
but the groundwork that can be done safely on sbt 1.x has been done.
8+
9+
> ⚠️ **Pre-release versions in use.** To get onto the sbt 2-capable lines, this branch now
10+
> depends on **Play 3.1.0-M9** (milestone — no GA yet), **scalatestplus-play 8.0.0-M2**
11+
> (milestone), and **Scala 3.8.4** (the "Next" line, *not* the 3.3 LTS). These are not
12+
> production-stable; the branch is a migration staging ground until those lines reach GA.
13+
14+
## Done (already on this branch)
15+
16+
The sbt-1.x-only prep below is fully backward compatible. The Play 3.1.0 upgrade after it
17+
pulls in the pre-release versions noted above. Everything is verified: all modules compile,
18+
`scalafmt*Check` is clean, all 50 tests pass, and the codegen plugin compiles on both the
19+
sbt 1.x and sbt 2.0.0 APIs.
20+
21+
- **Upgraded to the latest sbt 1.x**`project/build.properties` `1.9.8 → 1.12.12`. This
22+
is the recommended first migration step and unblocks plugins that now require sbt 1.12.9+.
23+
- **Bumped sbt-scalafmt** `2.5.2 → 2.6.1` (cross-published for sbt 1.x and 2.x; requires
24+
sbt ≥ 1.12.9). The scalafmt *engine* stays pinned at 3.7.15 in `.scalafmt.conf`, so
25+
formatting output is unchanged.
26+
- **Removed `com.lucidchart % sbt-cross`** — it was unused (no `crossBuild`/`CrossPlugin`
27+
references anywhere) and is abandoned (last release 2019) with no sbt 2 build. One blocker
28+
eliminated outright.
29+
- **Cross-built our own sbt plugin `smithy4play-sbt-codegen` for sbt 2.** It now compiles
30+
against both the sbt 1.x and sbt 2.0.0 APIs:
31+
- `crossScalaVersions := Seq("2.12.21", "3.8.4")` with a `pluginCrossBuild / sbtVersion`
32+
mapping (`2.12 → 1.12.12`, else `2.0.0`). Scala **3.8.4** matches sbt 2.0.0's own build
33+
(TASTy forward-compat requires our plugin's Scala ≥ sbt's).
34+
- A small hand-rolled `PluginCompat` shim in `src/main/scala-2` and `src/main/scala-3`
35+
bridges the two API differences we hit:
36+
1. `Attributed#data` on a classpath is `File` (sbt 1) vs `xsbti.HashedVirtualFileRef`
37+
(sbt 2) — `PluginCompat.toFiles(...)` resolves both to `Seq[File]` via `fileConverter`.
38+
2. `Def.uncached { ... }` opts the redefined `Compile / compile` task out of sbt 2's
39+
result cache (its `CompileAnalysis` return type has no `JsonFormat`). On sbt 1.x the
40+
shim provides `Def.uncached` as a no-op identity extension.
41+
- The meta-build compiles the plugin sources directly (`project/build.sbt`), so it also
42+
picks up the `scala-2` shim dir and depends on nothing new.
43+
44+
### Play 3.1.0 upgrade (pulls in pre-release versions — see warning above)
45+
46+
- **Play `3.0.11 → 3.1.0-M9`** (`project/Dependencies.scala` + `project/plugins.sbt`). This
47+
is the only sbt 2-capable Play line; GA is not out. Its sbt-plugin still publishes an
48+
sbt 1.x axis, so it works on sbt 1.12.12 today.
49+
- **Scala `3.3.8 → 3.8.4`** (`project/Dependencies.scala`). *Forced* by Play 3.1.0-M9, which
50+
is built with Scala 3.8.3 — the 3.3.8 compiler cannot read its newer TASTy (manifests as
51+
`AssertionError: ... has non-class parent` while reading `scala3-library` 3.8.3). 3.8.4 is
52+
the latest Next and matches sbt 2.0.0's own Scala version. **Leaves the 3.3 LTS line.**
53+
- **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.
54+
- **Jackson pin `2.14.3 → 2.21.2`** (`build.sbt`, both override blocks) — Play 3.1.0 / Pekko
55+
1.5.0 require `jackson-core`'s `StreamReadConstraints` (added in 2.15); the old 2.14.3 pin
56+
threw `ClassNotFoundException` at Guice injector creation. (`jackson-annotations` is `2.21`.)
57+
- **`javax.inject``jakarta.inject`** in 9 hand-written sources (core, mcp, test
58+
controllers/middlewares) — Play 3.1 completed the Jakarta EE namespace migration.
59+
60+
> We deliberately did **not** use the Scala Center `sbt2-compat` library even though it
61+
> provides exactly these helpers (`toFiles`, `Def.uncached`). When cross-building from an
62+
> sbt 1.x launcher, `addSbtPlugin` generates the wrong sbt-2 cross-coordinate
63+
> (`sbt2-compat_3_2.0`) while the artifact is published as `sbt2-compat_sbt2_3`, so it
64+
> fails to resolve. The hand-rolled shim is tiny, dependency-free, and avoids that.
65+
66+
## Blockers (cannot migrate until these ship)
67+
68+
| Dependency | Needs | Status (2026-06-16) |
69+
|---|---|---|
70+
| `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.** |
71+
| `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`. |
72+
| `com.codecommit %% sbt-github-packages` | sbt 2 build or replacement | Abandoned since 2021, no sbt 2. Used for publishing — see replacement note below. |
73+
74+
Ready / no longer blocking: `sbt-scalafmt` 2.6.1, `sbt-scoverage` 2.4.4, `sbt-jmh` 0.4.8
75+
(all publish an sbt 2 axis); `sbt-cross` removed; Play moved to the 3.1.0 line (sbt 2-capable,
76+
currently on milestone M9 — wait for 3.1.0 GA before considering this production-stable).
77+
78+
## Remaining steps for the actual switch (when blockers clear)
79+
80+
1. Bump `smithy4s` to the first 0.19.x that publishes an sbt 2 codegen plugin; regenerate
81+
and fix any generated-code/runtime changes (treat as its own migration — see the
82+
dependency check, this is a breaking minor).
83+
2. Move Play from milestone **3.1.0-M9 → 3.1.0 GA** once released (and scalatestplus-play
84+
8.0.0-M2 → GA); re-pin Jackson if the GA bumps it. Re-evaluate staying on Scala 3.8.x
85+
vs. a future LTS that supports the same TASTy.
86+
3. Resolve `gatling-sbt` — either wait for an sbt 2 build or move `smithy4play-gatling` to
87+
run Gatling another way.
88+
4. Replace `sbt-github-packages`. GitHub Packages is a plain Maven repo, so the plugin can
89+
be dropped in favour of native sbt config (no sbt 2 plugin needed):
90+
```scala
91+
publishTo := Some("GitHub" at "https://maven.pkg.github.com/innFactory/smithy4play")
92+
credentials += Credentials(
93+
"GitHub Package Registry", "maven.pkg.github.com", "innFactory", sys.env("GITHUB_TOKEN")
94+
)
95+
```
96+
(plus a matching resolver for consumers). Verify against the existing
97+
`publishSmithy4Play` / `publishLocalBundle` aliases in `build.sbt`.
98+
5. Set `project/build.properties` `sbt.version = 2.x`, convert build definitions per the
99+
[migration guide](https://www.scala-sbt.org/2.x/docs/en/changes/migrating-from-sbt-1.x.html),
100+
and re-run the whole build.
101+
6. **Publishing the codegen plugin for sbt 2:** the cross-build config is in place, but the
102+
sbt-2 artifact must be *published from an sbt 2.x launcher* so it gets the correct
103+
`_sbt2_3` coordinate. An sbt 1.x launcher would publish it as `_3_2.0`, which sbt 2
104+
consumers won't resolve. Wire this into the release once the project itself runs on sbt 2.
105+
106+
## References
107+
108+
- sbt 2.0.0 release / "Last mile towards sbt 2": <https://www.scala-lang.org/blog/2026/04/14/last-mile-towards-sbt2.html>
109+
- Migrating plugins with sbt2-compat: <https://www.scala-lang.org/blog/2026/03/02/sbt2-compat.html>
110+
- Migrating from sbt 1.x: <https://www.scala-sbt.org/2.x/docs/en/changes/migrating-from-sbt-1.x.html>
111+
- sbt 2.x plugin migration tracker: <https://github.com/sbt/sbt/wiki/sbt-2.x-plugin-migration>

project/Dependencies.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import sbt.*
44

55
object Dependencies {
66

7-
val playVersion = "3.0.11"
7+
val playVersion = "3.1.0-M9"
88
val typesafePlay = "org.playframework" %% "play" % playVersion
99

10-
val scalaVersion = "3.3.8"
10+
val scalaVersion = "3.8.4"
1111
val smithy4sVersion = "0.18.54"
1212
val smithyVersion = "1.71.0"
1313

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

3030
val scalatestPlus =
31-
"org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test
31+
"org.scalatestplus.play" %% "scalatestplus-play" % "8.0.0-M2" % Test
3232

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

project/build.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
sbt.version=1.9.8
1+
sbt.version=1.12.12
22
scala.version=3.3.8

project/build.sbt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
libraryDependencies += "io.github.classgraph" % "classgraph" % "4.8.179"
22

33
Compile / unmanagedSourceDirectories += baseDirectory.value.getParentFile / "smithy4play-sbt-codegen" / "src" / "main" / "scala"
4+
// The meta-build runs on Scala 2.12 (sbt 1.x), so it needs the sbt 1.x variant of the
5+
// PluginCompat shim. The matching scala-3 variant is used only when the plugin itself is
6+
// cross-compiled for sbt 2.x.
7+
Compile / unmanagedSourceDirectories += baseDirectory.value.getParentFile / "smithy4play-sbt-codegen" / "src" / "main" / "scala-2"

project/plugins.sbt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
addSbtPlugin("com.codecommit" %% "sbt-github-packages" % "0.5.3")
2-
addSbtPlugin("org.scalameta" %% "sbt-scalafmt" % "2.5.2")
3-
addSbtPlugin("org.playframework" %% "sbt-plugin" % "3.0.11")
2+
addSbtPlugin("org.scalameta" %% "sbt-scalafmt" % "2.6.1")
3+
addSbtPlugin("org.playframework" %% "sbt-plugin" % "3.1.0-M9")
44
addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "2.4.4")
55
addSbtPlugin("com.disneystreaming.smithy4s" %% "smithy4s-sbt-codegen" % "0.18.54")
6-
addSbtPlugin("com.lucidchart" % "sbt-cross" % "4.0")
76
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8")
87
addSbtPlugin("io.gatling" % "gatling-sbt" % "4.18.3")

smithy4play-mcp/src/main/scala/de/innfactory/smithy4play/mcp/AutoRouterWithMcp.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import play.api.libs.json.Json
1212
import play.api.mvc.*
1313
import play.api.routing.Router.Routes
1414

15-
import javax.inject.{ Inject, Singleton }
15+
import jakarta.inject.{ Inject, Singleton }
1616
import scala.concurrent.{ ExecutionContext, Future }
1717

1818
@Singleton

smithy4play-mcp/src/main/scala/de/innfactory/smithy4play/mcp/server/service/impl/McpToolRegistryServiceImpl.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import smithy4s.{ Document, Schema, Service }
1919

2020
import java.net.URLEncoder
2121
import java.nio.charset.StandardCharsets
22-
import javax.inject.Provider
22+
import jakarta.inject.Provider
2323
import scala.concurrent.{ ExecutionContext, Future }
2424

2525
@Singleton
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package de.innfactory.smithy4play.sbt
2+
3+
import sbt.*
4+
5+
/** sbt 1.x source-compatibility shim.
6+
*
7+
* On sbt 1.x a classpath's `Attributed#data` is already a `java.io.File`, so the `FileConverter` argument is accepted
8+
* (to keep the call site identical across sbt versions) but unused. The sbt 2.x counterpart lives in
9+
* `src/main/scala-3`.
10+
*/
11+
private[smithy4play] object PluginCompat {
12+
13+
def toFiles(cp: Seq[Attributed[File]])(@annotation.unused conv: xsbti.FileConverter): Seq[File] =
14+
cp.map(_.data)
15+
16+
/** sbt 1.x has no task cache, so `Def.uncached(...)` is a no-op identity. On sbt 2.x the native `Def.uncached`
17+
* (provided by the scala-3 variant's absence of this shim) opts the redefined task out of the result cache.
18+
* Importing `PluginCompat.*` brings this into scope.
19+
*/
20+
implicit class DefUncachedOps(private val d: sbt.Def.type) extends AnyVal {
21+
def uncached[A](a: A): A = a
22+
}
23+
24+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package de.innfactory.smithy4play.sbt
2+
3+
import sbt.*
4+
5+
/** sbt 2.x source-compatibility shim.
6+
*
7+
* On sbt 2.x a classpath's `Attributed#data` is an `xsbti.HashedVirtualFileRef` (part of the virtual-file migration),
8+
* so it must be resolved back to a real `java.io.File` through the build's `FileConverter`. The sbt 1.x counterpart
9+
* lives in `src/main/scala-2`.
10+
*/
11+
private[smithy4play] object PluginCompat {
12+
13+
def toFiles(cp: Seq[Attributed[xsbti.HashedVirtualFileRef]])(conv: xsbti.FileConverter): Seq[File] =
14+
cp.map(entry => conv.toPath(entry.data).toFile)
15+
16+
}

0 commit comments

Comments
 (0)