Skip to content

Cross-build to sbt 2 and Scala 3.#674

Merged
johanandren merged 32 commits intolightbend:mainfrom
anatoliykmetyuk:sbt2-port
Mar 2, 2026
Merged

Cross-build to sbt 2 and Scala 3.#674
johanandren merged 32 commits intolightbend:mainfrom
anatoliykmetyuk:sbt2-port

Conversation

@anatoliykmetyuk
Copy link
Copy Markdown
Contributor

@anatoliykmetyuk anatoliykmetyuk commented Feb 26, 2026

This PR cross-compiles the plugin to Scala 3 and sbt 2, making it possible to use from sbt 2 projects. It's a big PR, so here's a summary of all the changes by category to assist with reviewing. Virtually all the changes introduced by the PR are covered by one of the categories below.

CI/Testing

  1. JTidy's static mutable state caused flaky HTML encoding when tests ran in parallel; set Test / parallelExecution := false in core and tests to fix.
  2. CI runs verify instead of testgithubWorkflowBuild in build.sbt now invokes verify (Test/compile, Compile/doc, test, scripted, docs/paradox), so scripted tests run in CI. Also CI is running sbt with --batch flag to prevent interactive prompts on project loading failures which are not suitable for automated scripts.

Scala 3 Compatibility

  1. JavaConverters was removed in Scala 3; added version-specific compat/package.scala that maps to JavaConverters (2.12) or CollectionConverters (2.13/3) so core cross-compiles.
    1. In Scala 2: java.util.List.asScala (JavaConverters) returns scala.collection.mutable.Buffer, which extends scala.collection.Seq. In Scala 3: java.util.Collection.asScala (CollectionConverters) returns scala.collection.Iterable, which does not extend scala.collection.Seq. Therefore we standardize by adding .toSeq where APIs are expecting Seq[String].
  2. com.lightbend.paradox.markdown.Url had a custom copy(path, query, fragment) method that conflicted with the case class's synthetic copy in Scala 3; renamed to withComponents so core cross-compiles.
  3. MarkdownTestkit: disambiguated Path name collision between java.nio.file.Path and com.lightbend.paradox.markdown.Path. Scala 2 and 3 use different resolution strategies so a collision happens in Scala 3. The port added explicit java.nio.file.Path and ParadoxPath alias to avoid relying on context-dependent resolution.
  4. Scala 3 requires explicit types for implicit values. Test specs (e.g. LinkDirectiveSpec) that use implicit val context for the writer context now have an explicit type: Location[Page] => Writer.Context.

sbt 2 Compatibility

  1. Wrapped tasks that do not support sbt 2 caching out of the box in Def.uncached.
  2. Replaced sbt 1 in syntax (e.g. key in scope) with sbt 2 slash syntax (scope / key).
  3. Bumped sbt-web from 1.5.8 to 1.6.0-M2 to support sbt 2; the new API requires fileConverter.value for syncMappings and deduplicateMappings, due to changes in file types between sbt 1 and sbt 2.
  4. PluginCompat and sbt2-compat pattern is used for breaking API changes between sbt 1 and sbt 2.
  5. sbt 2 changed ScmInfo.browseUrl from java.net.URL to java.net.URI - standardize on java.net.URI.
  6. ParadoxPlugin.scala: inlined sourcesFor at its single call site in. Originally it was in the compat layer for sbt 0.13 support, but the plugin no longer cross-builds to sbt 0.13.
  7. On sbt 2, target.value inside inConfig(Compile) resolves to a versioned subdirectory (target/out/jvm/scala-3.8.1/<project>/) instead of target/ as on sbt 1. This makes the output path of the plugin inconsistent. Changed paths to preserve the sbt 1 behavior throughout ParadoxPlugin.scala.

Scripted Tests

  1. sbt 2's config parser does not allow hyphens in config names; the paradox/docs-overlay scripted test was updated to use camelCase configs (docsFirstdocsSecond).
  2. paradox/snippet-noindent-writer and paradox/snippets expected HTML updated to match current source files. Scripted tests were not tested by the CI so the html files for those tests were broken by an scalafmt run a while ago.
  3. paradox/libraryDependencies test was filtered for sbt 2 as the material plugin is not yet ported to sbt 2. Exclusion logic in build.sbtverify-no-docker sbt command was changed from a simple alias to a command to enable filtering logic. scripted tests plugin checks for sbt version in build.properties to determine if it matches the sbt version the test is tested against - however this feature does not work in case of glob references, e.g. paradox/*. Logic in verify command works properly because no globs are used.

Two sbt 2 incompatibilities fixed:

- paradoxDirectives += CustomDirective fails on sbt 2 because the task
  result type (functions) has no JsonFormat, which sbt 2 requires for
  disk caching. Changed to := Def.uncached(Writer.defaultDirectives :+
  CustomDirective) to bypass the cache entirely.

- In sbt 2, Compile/target resolves to target/out/jvm/scala-<ver>/<name>
  instead of target/. The paradox output was landing in the wrong
  directory. Changed defineSiteMappings to anchor the output at
  baseDirectory.value / "target" so the path stays stable across sbt
  versions. Also wrapped SbtWeb.syncMappings in Def.uncached so sbt 2
  does not skip the file sync when restoring from task cache.
Fixes:
- docs-overlay: Rename configs to docsFirst/docsSecond, add docsOverlayParadox task
- generated-source: Replace in syntax with / in PageGenerator.scala
- libraryDependencies: Use builtinParadoxTheme("generic")
- snippet-noindent-writer, snippets: Regenerate expected HTML for Scala 3
- validation: Add sbt2-compat, use PluginCompat.toFileRefsMapping
- custom-directive: Use := Def.uncached(...) for paradoxDirectives; add sbt2-compat
- ParadoxPlugin: Use baseDirectory/target for output; wrap syncMappings in Def.uncached

Tradeoffs:
- docs-overlay: Hyphenated config invocation no longer tested; workaround via camelCase + wrapper task
- libraryDependencies: Material theme no longer exercised; only generic theme verified
- snippets: Expected HTML now Scala 3 format; Scala 2 brace-style output no longer covered
- validation: Test depends on sbt2-compat; no longer a minimal vanilla setup
- custom-directive: paradoxDirectives += no longer used; breaking change for users on sbt 2
@anatoliykmetyuk anatoliykmetyuk marked this pull request as ready for review February 26, 2026 09:08
Copy link
Copy Markdown
Contributor

@johanandren johanandren left a comment

Choose a reason for hiding this comment

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

I'm not deeploy familiar with all the required changes for sbt-2, but I think this looks good.

@anatoliykmetyuk
Copy link
Copy Markdown
Contributor Author

@johanandren thank you for taking a look! I've fixed the formatting with a scalafmt run, so the CI should pass now.

@eed3si9n
Copy link
Copy Markdown
Contributor

Thanks @anatoliykmetyuk! I can review at sbt 2.x specific parts, but at a glance, the lion's share of the changes seems to be to bring up old code base into Scala 3.x, which includes using 2.13 stdlib.

(paradoxTheme / WebKeys.deduplicators).value,
fileConverter.value
),
paradoxTheme / target := baseDirectory.value / "target" / "paradox" / "theme" / configTarget(configuration.value),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we use

target.value / "paradox" / "theme" / configTarget(configuration.value),

like it was before, so it's safe against cross building?

Copy link
Copy Markdown
Contributor Author

@anatoliykmetyuk anatoliykmetyuk Feb 27, 2026

Choose a reason for hiding this comment

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

The paths seem to differ between sbt 1 and 2 which causes inconsistent behavior. I've mentioned this one at the PR changes above:

On sbt 2, target.value inside inConfig(Compile) resolves to a versioned subdirectory (target/out/jvm/scala-3.8.1/<project>/) instead of target/ as on sbt 1. This makes the output path of the plugin inconsistent. Changed paths to preserve the sbt 1 behavior throughout ParadoxPlugin.scala.

The current setup should produce consistent paths during cross-build.

Copy link
Copy Markdown
Contributor

@eed3si9n eed3si9n left a comment

Choose a reason for hiding this comment

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

I have a minor question about target directory, but overall lgtm

@anatoliykmetyuk
Copy link
Copy Markdown
Contributor Author

anatoliykmetyuk commented Feb 27, 2026

The formatting failure seems to be due to the new using syntax in the Scala 3 specific sources that scalafmt recognized as invalid. I've set the dialect to scala3 with the latest commit which should solve it.

@johanandren johanandren merged commit cd57327 into lightbend:main Mar 2, 2026
6 checks passed
@anatoliykmetyuk anatoliykmetyuk deleted the sbt2-port branch March 2, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants