From d973d115932f5e17f6cf720dfe5892f622fc6bc4 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Tue, 30 Jun 2026 12:27:55 -0700 Subject: [PATCH 1/4] Migrate to sbt 2 Now that sbt-jcheckstyle 0.3.0 publishes an sbt2 cross-build, the sbt2 migration (superseding scala-steward PR #997) is unblocked. Bumps: - sbt: 1.12.13 -> 2.0.1 - sbt-jcheckstyle: 0.2.1 -> 0.3.0 (the actual blocker) - sbt-osgi: 0.10.0 -> 0.11.0-RC1 (only version with an sbt2 cross-build) - sbt-pgp, sbt-scalafmt, sbt-dynver already cross-built for sbt2 at their current versions build.sbt changes required by sbt 2's new task engine: - Wrap the jcheckStyle-dependsOn compile overrides in Def.uncached(...); CompileAnalysis has no JsonFormat to cache against - Add implicitConversions import and method-call dependsOn syntax to clear new Scala 3.8 strictness warnings sbt 2 itself requires JDK 17+ to run, while CI still needs to verify the library at runtime on JDK 8/11/17/21/24. Decouple the two via Test/fork + Test/javaHome (driven by a TEST_JAVA_HOME env var), and update CI.yml/release.yml/snapshot.yml to install a fixed modern JDK to launch sbt itself while forking tests onto the matrix JDK. Published bytecode is unaffected since it's already pinned to 1.8 via javacOptions. --- .github/workflows/CI.yml | 20 +++++++++++++++++++- .github/workflows/release.yml | 7 ++++--- .github/workflows/snapshot.yml | 4 +++- build.sbt | 12 +++++++++--- project/build.properties | 2 +- project/plugins.sbt | 4 ++-- 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 26c11bfb..9b10fa35 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -40,11 +40,16 @@ jobs: if: ${{ needs.changes.outputs.code == 'true' }} steps: - uses: actions/checkout@v6 + # sbt 2 requires JDK 17+ to run + - uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: '21' - name: jcheckstyle run: ./sbt jcheckStyle - name: scalafmtCheckAll run: ./sbt scalafmtCheckAll - + test: name: Test JDK${{ matrix.java }} runs-on: ubuntu-latest @@ -55,16 +60,29 @@ jobs: java: ['8', '11', '17', '21', '24'] steps: - uses: actions/checkout@v6 + # The JDK under test: msgpack-core forks its tests onto this JDK (via + # TEST_JAVA_HOME) so we still verify runtime behavior on each target JDK. - uses: actions/setup-java@v5 + id: target-jdk with: distribution: 'zulu' java-version: ${{ matrix.java }} + # sbt 2 itself requires JDK 17+ to run, so install a fixed, newer JDK to + # actually launch sbt. This becomes the default JDK on PATH/JAVA_HOME. + - uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: '21' - uses: actions/cache@v5 with: path: ~/.cache key: ${{ runner.os }}-jdk${{ matrix.java }}-${{ hashFiles('**/*.sbt') }} restore-keys: ${{ runner.os }}-jdk${{ matrix.java }}- - name: Test + env: + TEST_JAVA_HOME: ${{ steps.target-jdk.outputs.path }} run: ./sbt test - name: Universal Buffer Test + env: + TEST_JAVA_HOME: ${{ steps.target-jdk.outputs.path }} run: ./sbt test -J-Dmsgpack.universal-buffer=true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0cec5029..31b7d7ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,12 @@ jobs: fetch-depth: 10000 # Fetch all tags so that sbt-dynver can find the previous release version - run: git fetch --tags -f - # Install OpenJDK 8 + # sbt 2 requires JDK 17+ to run. Android compatibility (originally JDK8, + # see https://github.com/msgpack/msgpack-java/issues/516) is enforced + # independently via the -source/-target 1.8 javacOptions in build.sbt. - uses: actions/setup-java@v5 with: - # We need to use JDK8 for Android compatibility https://github.com/msgpack/msgpack-java/issues/516 - java-version: 8 + java-version: 21 distribution: adopt - name: Setup GPG env: diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index dc0fac1d..1164dc66 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -21,9 +21,11 @@ jobs: fetch-depth: 10000 # Fetch all tags so that sbt-dynver can find the previous release version - run: git fetch --tags + # sbt 2 requires JDK 17+ to run; published bytecode still targets 1.8 + # via the javacOptions in build.sbt. - uses: actions/setup-java@v5 with: - java-version: 11 + java-version: 21 distribution: adopt - name: Publish snapshots env: diff --git a/build.sbt b/build.sbt index 233abf0c..b43f6298 100644 --- a/build.sbt +++ b/build.sbt @@ -1,3 +1,5 @@ +import scala.language.implicitConversions + Global / onChangedBuildSource := ReloadOnSourceChanges // For performance testing, ensure each test run one-by-one @@ -69,6 +71,11 @@ val buildSettings = Seq[Setting[?]]( // JVM options for building scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked", "-feature"), Test / javaOptions ++= Seq("-ea"), + // sbt 2 itself requires JDK 17+ to run, but CI still needs to verify the library at + // runtime against older JDKs (e.g. 8). Fork tests so they can run on a JDK specified + // via TEST_JAVA_HOME, independent of the JDK running sbt. + Test / fork := true, + Test / javaHome := sys.env.get("TEST_JAVA_HOME").map(file), javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), Compile / compile / javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:unchecked", "-Xlint:deprecation"), @@ -92,8 +99,8 @@ val buildSettings = Seq[Setting[?]]( // Style check config: (sbt-jchekcstyle) jcheckStyleConfig := "facebook", // Run jcheckstyle both for main and test codes - Compile / compile := ((Compile / compile) dependsOn (Compile / jcheckStyle)).value, - Test / compile := ((Test / compile) dependsOn (Test / jcheckStyle)).value + Compile / compile := Def.uncached((Compile / compile).dependsOn(Compile / jcheckStyle).value), + Test / compile := Def.uncached((Test / compile).dependsOn(Test / jcheckStyle).value) ) val junitJupiter = "org.junit.jupiter" % "junit-jupiter" % "5.14.4" % "test" @@ -134,7 +141,6 @@ lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) "--add-opens=java.base/java.nio=ALL-UNNAMED", "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" ), - Test / fork := true, libraryDependencies ++= Seq( // msgpack-core should have no external dependencies diff --git a/project/build.properties b/project/build.properties index 2af813ac..4bf10378 100755 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1,2 @@ -sbt.version=1.12.13 +sbt.version=2.0.1 diff --git a/project/plugins.sbt b/project/plugins.sbt index 2d12cde5..e0c7486a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,8 +2,8 @@ addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") // TODO: Fixes jacoco error: // java.lang.NoClassDefFoundError: Could not initialize class org.jacoco.core.internal.flow.ClassProbesAdapter //addSbtPlugin("com.github.sbt" % "sbt-jacoco" % "3.3.0") -addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.2.1") -addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.10.0") +addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.3.0") +addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.11.0-RC1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.1") addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") From b4a383fd72ba209a31a12d3f3f1282c08a70291e Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Tue, 30 Jun 2026 12:41:55 -0700 Subject: [PATCH 2/4] Fork compilation onto the target JDK, not just test execution CI's JDK8 lane failed: javac resolves API calls against whichever JDK actually runs it, not the -source/-target level, so compiling on JDK21 (now required to launch sbt 2) bound to JDK9+-only covariant overloads like ByteBuffer.flip(): ByteBuffer that don't exist on a real JDK8 at runtime (NoSuchMethodError). This is exactly what the pre-migration setup avoided by compiling and running each CI lane on the same single JDK. Restore that by scoping the existing TEST_JAVA_HOME-driven javaHome setting to the whole build instead of just Test, so sbt forks an external javac from that JDK for compilation too. Considered --release 8 instead, but that also hides the sun.misc.Unsafe/sun.nio.ch.DirectBuffer internals this code intentionally relies on, requiring extra flags for a more fragile setup than just compiling with the real target JDK. Wire release.yml/snapshot.yml the same way so published bytecode is compiled with a real JDK8/JDK11 again, matching their original intent. --- .github/workflows/release.yml | 13 ++++++++++--- .github/workflows/snapshot.yml | 10 ++++++++-- build.sbt | 15 ++++++++++----- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31b7d7ec..84b7d9de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,9 +16,15 @@ jobs: fetch-depth: 10000 # Fetch all tags so that sbt-dynver can find the previous release version - run: git fetch --tags -f - # sbt 2 requires JDK 17+ to run. Android compatibility (originally JDK8, - # see https://github.com/msgpack/msgpack-java/issues/516) is enforced - # independently via the -source/-target 1.8 javacOptions in build.sbt. + # We need to compile with JDK8 for Android compatibility + # https://github.com/msgpack/msgpack-java/issues/516 + - uses: actions/setup-java@v5 + id: jdk8 + with: + java-version: 8 + distribution: adopt + # sbt 2 itself requires JDK 17+ to run; build.sbt forks the compiler (and tests) + # onto JDK8 via TEST_JAVA_HOME, so this becomes the default JDK on PATH/JAVA_HOME. - uses: actions/setup-java@v5 with: java-version: 21 @@ -30,6 +36,7 @@ jobs: - name: Build bundle env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + TEST_JAVA_HOME: ${{ steps.jdk8.outputs.path }} run: | ./sbt publishSigned - name: Release to Sonatype diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 1164dc66..02594583 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -21,8 +21,13 @@ jobs: fetch-depth: 10000 # Fetch all tags so that sbt-dynver can find the previous release version - run: git fetch --tags - # sbt 2 requires JDK 17+ to run; published bytecode still targets 1.8 - # via the javacOptions in build.sbt. + - uses: actions/setup-java@v5 + id: jdk11 + with: + java-version: 11 + distribution: adopt + # sbt 2 itself requires JDK 17+ to run; build.sbt forks the compiler onto JDK11 + # via TEST_JAVA_HOME, so this becomes the default JDK on PATH/JAVA_HOME. - uses: actions/setup-java@v5 with: java-version: 21 @@ -31,4 +36,5 @@ jobs: env: SONATYPE_USERNAME: '${{ secrets.SONATYPE_USERNAME }}' SONATYPE_PASSWORD: '${{ secrets.SONATYPE_PASSWORD }}' + TEST_JAVA_HOME: ${{ steps.jdk11.outputs.path }} run: ./sbt publish diff --git a/build.sbt b/build.sbt index b43f6298..304fbee7 100644 --- a/build.sbt +++ b/build.sbt @@ -71,11 +71,16 @@ val buildSettings = Seq[Setting[?]]( // JVM options for building scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked", "-feature"), Test / javaOptions ++= Seq("-ea"), - // sbt 2 itself requires JDK 17+ to run, but CI still needs to verify the library at - // runtime against older JDKs (e.g. 8). Fork tests so they can run on a JDK specified - // via TEST_JAVA_HOME, independent of the JDK running sbt. - Test / fork := true, - Test / javaHome := sys.env.get("TEST_JAVA_HOME").map(file), + // sbt 2 itself requires JDK 17+ to run, but each CI lane still needs to compile and + // test against its own target JDK (e.g. 8) to faithfully reproduce runtime behavior: + // javac resolves API calls against whichever JDK actually runs it (-source/-target + // only constrain language level and bytecode version, not API resolution), so e.g. + // compiling on JDK9+ can bind to covariant overloads like ByteBuffer.flip(): + // ByteBuffer that don't exist on a real JDK8 at runtime. When TEST_JAVA_HOME is set, + // fork both compilation and test execution onto that JDK; otherwise use the JDK + // running sbt, as before. + javaHome := sys.env.get("TEST_JAVA_HOME").map(file), + Test / fork := true, javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), Compile / compile / javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:unchecked", "-Xlint:deprecation"), From 226c8ff1c365a34179df66c3d7b8a1d867d6f233 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Tue, 30 Jun 2026 12:49:48 -0700 Subject: [PATCH 3/4] Pin scalac to -release 8 for JDK8-compatible test bytecode The previous fix forked javac onto the target JDK, but missed that the test suite is Scala, and the Scala compiler always runs in-process inside the sbt JVM (it doesn't fork the way javac does). With sbt 2 forcing that JVM to be JDK17+, test code calling e.g. ByteBuffer.flip() directly (as MessageUnpackerTest.scala does) was still resolving to JDK9's covariant ByteBuffer.flip():ByteBuffer override, which doesn't exist on real JDK8 -> NoSuchMethodError at runtime, regardless of the javac fork fix. scalac's -release flag avoids this the same way --release does for javac, without needing an ignore-symbol-file escape hatch, since the test sources never touch JDK-internal APIs the way the main sources' sun.misc.Unsafe usage does. Verified via javap that the compiled call site now resolves to Buffer.flip():Buffer instead of the covariant override. --- build.sbt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 304fbee7..87315d0f 100644 --- a/build.sbt +++ b/build.sbt @@ -69,7 +69,14 @@ val buildSettings = Seq[Setting[?]]( crossPaths := false, publishMavenStyle := true, // JVM options for building - scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked", "-feature"), + // -release 8 pins scalac's resolution of JDK API calls to the JDK8 surface (e.g. + // test code calling ByteBuffer.flip() resolves to the inherited Buffer.flip():Buffer + // rather than JDK9's covariant ByteBuffer.flip():ByteBuffer override, which doesn't + // exist on a real JDK8 at runtime -> NoSuchMethodError). Unlike javac's --release, + // this doesn't need an ignore-symbol-file escape hatch since test code never touches + // JDK-internal APIs the way the main sources' Unsafe usage does. + scalacOptions ++= + Seq("-encoding", "UTF-8", "-deprecation", "-unchecked", "-feature", "-release", "8"), Test / javaOptions ++= Seq("-ea"), // sbt 2 itself requires JDK 17+ to run, but each CI lane still needs to compile and // test against its own target JDK (e.g. 8) to faithfully reproduce runtime behavior: From 01b9d60df26f32245c86e1f682b6e40b74d52a28 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Tue, 30 Jun 2026 12:54:00 -0700 Subject: [PATCH 4/4] Fix MessageBufferInputTest flake: mkdirs() before createTempFile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit saveToTmpFile called File.createTempFile("testbuf", ".dat", new File("target")) before tmp.getParentFile.mkdirs() — createTempFile requires the parent directory to already exist, so under occasional filesystem timing in CI sandboxes this threw "No such file or directory" before the directory got created. Surfaced while validating the sbt 2 migration's CI matrix; unrelated to sbt 2 itself, but was intermittently failing the JDK8 test lane via fail-fast cancellation of the rest of the matrix. --- .../org/msgpack/core/buffer/MessageBufferInputTest.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala index 0465aa69..5d3c3d9f 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala @@ -58,8 +58,9 @@ class MessageBufferInputTest extends AirSpec: def toByteBuffer = ByteBuffer.wrap(b) def saveToTmpFile: File = - val tmp = File.createTempFile("testbuf", ".dat", new File("target")) - tmp.getParentFile.mkdirs() + val dir = new File("target") + dir.mkdirs() + val tmp = File.createTempFile("testbuf", ".dat", dir) tmp.deleteOnExit() withResource(new FileOutputStream(tmp)) { out => out.write(b)