From 90aa8d79d12dc004a59c382ffc2738e216ebe82f Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Mon, 9 Mar 2026 14:33:37 +0100 Subject: [PATCH 01/20] Bump Mill to 1.1.3 (was 1.1.2) (#4169) --- .mill-version | 2 +- mill.bat | 2 +- millw | 2 +- website/docs/reference/cli-options.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.mill-version b/.mill-version index 45a1b3f445..781dcb07cd 100644 --- a/.mill-version +++ b/.mill-version @@ -1 +1 @@ -1.1.2 +1.1.3 diff --git a/mill.bat b/mill.bat index ef5140a0aa..8a7f54e414 100755 --- a/mill.bat +++ b/mill.bat @@ -2,7 +2,7 @@ setlocal enabledelayedexpansion -if [!DEFAULT_MILL_VERSION!]==[] ( set "DEFAULT_MILL_VERSION=1.1.2" ) +if [!DEFAULT_MILL_VERSION!]==[] ( set "DEFAULT_MILL_VERSION=1.1.3" ) if [!MILL_GITHUB_RELEASE_CDN!]==[] ( set "MILL_GITHUB_RELEASE_CDN=" ) diff --git a/millw b/millw index bc04bdcd12..77380ebba9 100755 --- a/millw +++ b/millw @@ -2,7 +2,7 @@ set -e -if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="1.1.2"; fi +if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="1.1.3"; fi if [ -z "${GITHUB_RELEASE_CDN}" ] ; then GITHUB_RELEASE_CDN=""; fi diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 056df024c6..f945537b38 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -388,7 +388,7 @@ Version of SBT to be used for the export (1.12.4 by default) ### `--mill-version` -Version of Mill to be used for the export (1.1.2 by default) +Version of Mill to be used for the export (1.1.3 by default) ### `--mvn-version` From e4acce5ee12dbbc39f0c8b46fb0af2af0f8b5bd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:38:01 +0100 Subject: [PATCH 02/20] Bump @algolia/client-search in /website in the npm-dependencies group (#4173) Bumps the npm-dependencies group in /website with 1 update: [@algolia/client-search](https://github.com/algolia/algoliasearch-client-javascript). Updates `@algolia/client-search` from 5.49.1 to 5.49.2 - [Release notes](https://github.com/algolia/algoliasearch-client-javascript/releases) - [Changelog](https://github.com/algolia/algoliasearch-client-javascript/blob/main/CHANGELOG.md) - [Commits](https://github.com/algolia/algoliasearch-client-javascript/compare/5.49.1...5.49.2) --- updated-dependencies: - dependency-name: "@algolia/client-search" dependency-version: 5.49.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- website/package.json | 2 +- website/yarn.lock | 54 ++++++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/website/package.json b/website/package.json index 54557eaba2..a4f98cc740 100644 --- a/website/package.json +++ b/website/package.json @@ -14,7 +14,7 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@algolia/client-search": "^5.49.1", + "@algolia/client-search": "^5.49.2", "@docusaurus/core": "^3.9.2", "@docusaurus/plugin-content-docs": "^3.9.2", "@docusaurus/preset-classic": "^3.9.2", diff --git a/website/yarn.lock b/website/yarn.lock index a2bf5b1c23..52f36738dd 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -92,10 +92,10 @@ resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.46.0.tgz#004ad40adbdc6da7e23e4ef4d7a0ff48422af012" integrity sha512-0emZTaYOeI9WzJi0TcNd2k3SxiN6DZfdWc2x2gHt855Jl9jPUOzfVTL6gTvCCrOlT4McvpDGg5nGO+9doEjjig== -"@algolia/client-common@5.49.1": - version "5.49.1" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.49.1.tgz#2b52313a9027bba5c57abd76d652fd4b16f56a32" - integrity sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg== +"@algolia/client-common@5.49.2": + version "5.49.2" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.49.2.tgz#cb93f1ea9a60f7ffec65474e075afb52900f2434" + integrity sha512-bn0biLequn3epobCfjUqCxlIlurLr4RHu7RaE4trgN+RDcUq6HCVC3/yqq1hwbNYpVtulnTOJzcaxYlSr1fnuw== "@algolia/client-insights@5.46.0": version "5.46.0" @@ -127,15 +127,15 @@ "@algolia/requester-fetch" "5.46.0" "@algolia/requester-node-http" "5.46.0" -"@algolia/client-search@5.46.0", "@algolia/client-search@^5.48.1", "@algolia/client-search@^5.49.1": - version "5.49.1" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.49.1.tgz#c16518fb5003b4a35b74bdee7a168d4d7a09877b" - integrity sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA== +"@algolia/client-search@5.46.0", "@algolia/client-search@^5.48.1", "@algolia/client-search@^5.49.2": + version "5.49.2" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.49.2.tgz#3c823ffaf333ce70fedfb3d45361661c8b227806" + integrity sha512-y1IOpG6OSmTpGg/CT0YBb/EAhR2nsC18QWp9Jy8HO9iGySpcwaTvs5kHa17daP3BMTwWyaX9/1tDTDQshZzXdg== dependencies: - "@algolia/client-common" "5.49.1" - "@algolia/requester-browser-xhr" "5.49.1" - "@algolia/requester-fetch" "5.49.1" - "@algolia/requester-node-http" "5.49.1" + "@algolia/client-common" "5.49.2" + "@algolia/requester-browser-xhr" "5.49.2" + "@algolia/requester-fetch" "5.49.2" + "@algolia/requester-node-http" "5.49.2" "@algolia/events@^4.0.1": version "4.0.1" @@ -179,12 +179,12 @@ dependencies: "@algolia/client-common" "5.46.0" -"@algolia/requester-browser-xhr@5.49.1": - version "5.49.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.49.1.tgz#0096c02e6d60fc79a71f80b51315e9a8fe7da5dc" - integrity sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA== +"@algolia/requester-browser-xhr@5.49.2": + version "5.49.2" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.49.2.tgz#4493ed41dc84948693b34174ccc08ac5dfabd3dd" + integrity sha512-3UhYCcWX6fbtN8ABcxZlhaQEwXFh3CsFtARyyadQShHMPe3mJV9Wel4FpJTa+seugRkbezFz0tt6aPTZSYTBuA== dependencies: - "@algolia/client-common" "5.49.1" + "@algolia/client-common" "5.49.2" "@algolia/requester-fetch@5.46.0": version "5.46.0" @@ -193,12 +193,12 @@ dependencies: "@algolia/client-common" "5.46.0" -"@algolia/requester-fetch@5.49.1": - version "5.49.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.49.1.tgz#bc41f0d03d0bc3c8decdc65ec08d29e992b16f1c" - integrity sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw== +"@algolia/requester-fetch@5.49.2": + version "5.49.2" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.49.2.tgz#fd64e1ec726ffb63dce22112354119125c67a27e" + integrity sha512-G94VKSGbsr+WjsDDOBe5QDQ82QYgxvpxRGJfCHZBnYKYsy/jv9qGIDb93biza+LJWizQBUtDj7bZzp3QZyzhPQ== dependencies: - "@algolia/client-common" "5.49.1" + "@algolia/client-common" "5.49.2" "@algolia/requester-node-http@5.46.0": version "5.46.0" @@ -207,12 +207,12 @@ dependencies: "@algolia/client-common" "5.46.0" -"@algolia/requester-node-http@5.49.1": - version "5.49.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.49.1.tgz#34846a9a2d7fee6667a4767588b8e77b2c6d9e12" - integrity sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA== +"@algolia/requester-node-http@5.49.2": + version "5.49.2" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.49.2.tgz#ac71c6502b8ba8760d22afd3f38620934a28a68f" + integrity sha512-UuihBGHafG/ENsrcTGAn5rsOffrCIRuHMOsD85fZGLEY92ate+BMTUqxz60dv5zerh8ZumN4bRm8eW2z9L11jA== dependencies: - "@algolia/client-common" "5.49.1" + "@algolia/client-common" "5.49.2" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1": version "7.27.1" From 9c2adc061b19323772d6b413b509b953aafe56f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:15:33 +0100 Subject: [PATCH 03/20] Bump the github-actions group with 4 updates (#4172) Bumps the github-actions group with 4 updates: [docker/login-action](https://github.com/docker/login-action), [docker/metadata-action](https://github.com/docker/metadata-action), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/login-action` from 3 to 4 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v4) Updates `docker/metadata-action` from 5 to 6 - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/v5...v6) Updates `docker/setup-buildx-action` from 3 to 4 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) Updates `docker/build-push-action` from 6 to 7 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/metadata-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish-docker.yml | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc653938c1..94ae018a4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1065,7 +1065,7 @@ jobs: path: test-report.xml - name: Login to GitHub Container Registry if: startsWith(github.ref, 'refs/tags/v') - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -1178,7 +1178,7 @@ jobs: path: test-report.xml - name: Login to GitHub Container Registry if: startsWith(github.ref, 'refs/tags/v') - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 71da926e89..973cdfa933 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -35,7 +35,7 @@ jobs: uses: actions/checkout@v6 - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ env.REGISTRY_LOGIN }} @@ -44,19 +44,19 @@ jobs: # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ${{ env.DOCKERFILE }} @@ -106,10 +106,10 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ env.REGISTRY_LOGIN }} @@ -117,7 +117,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} From 4763be0b0adccfaeb0cf919640c3c75a19cc8c21 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Thu, 12 Mar 2026 12:42:29 +0100 Subject: [PATCH 04/20] Update Scala 3 Next RC to 3.8.3-RC2 (#4175) --- project/deps/package.mill | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/deps/package.mill b/project/deps/package.mill index f12ef82f94..e8f98b0216 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -21,8 +21,8 @@ object Scala { def scala3NextPrefix = "3.8" def scala3Next = s"$scala3NextPrefix.2" // the newest/next version of Scala def scala3NextAnnounced = scala3Next // the newest/next version of Scala that's been announced - def scala3NextRc = "3.8.3-RC1" // the latest RC version of Scala Next - def scala3NextRcAnnounced = "3.8.2-RC3" // the latest announced RC version of Scala Next + def scala3NextRc = "3.8.3-RC2" // the latest RC version of Scala Next + def scala3NextRcAnnounced = "3.8.3-RC1" // the latest announced RC version of Scala Next // The Scala version used to build the CLI itself. def defaultInternal = sys.props.get("scala.version.internal").getOrElse(scala3Lts) From 1631f8c7ea5c7ea0e4b0deccb213b484ccae548d Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Thu, 12 Mar 2026 13:33:31 +0100 Subject: [PATCH 05/20] Refer the Scala 3 compiler policy on usage of LLM-based tools in contributions --- CONTRIBUTING.md | 3 +++ LLM_POLICY.md | 7 +++++++ 2 files changed, 10 insertions(+) create mode 100644 LLM_POLICY.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08fa9c9af0..1b30ec4cc6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,9 @@ A subsequent PR from `stable` back to `main` is created automatically. Whenever reasonable, we try to follow the following set of rules when merging code to the repository. Following those will save you from getting a load of comments and speed up the code review. +- If you are using LLM-based tools to assist you in your contribution, state that clearly in the PR description + and refer to our [LLM usage policy](LLM_POLICY.md) for rules and guidelines regarding usage of LLM-based tools + in contributions. - If the PR is meant to be merged as a single commit (`squash & merge`), please make sure that you modify only one thing. - This means such a PR shouldn't include code clean-up, a secondary feature or bug fix, just the single thing diff --git a/LLM_POLICY.md b/LLM_POLICY.md new file mode 100644 index 0000000000..f666ad5968 --- /dev/null +++ b/LLM_POLICY.md @@ -0,0 +1,7 @@ +# Policy regarding LLM-generated code in contributions to Scala CLI + +Scala CLI accepts contributions containing code produced with AI assistance. This means that using LLM-based +tooling aiding software development (like Cursor, Claude Code, Copilot or whatever else) is allowed. + +All such contributions are regulated by the policy defined in the Scala 3 compiler repository, which can be found at: +https://github.com/scala/scala3/blob/main/LLM_POLICY.md \ No newline at end of file From 03ec06a3651761070d497e5cafcd312ca0241958 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Thu, 12 Mar 2026 13:33:50 +0100 Subject: [PATCH 06/20] Add a PR template --- .github/pull_request_template.md | 38 ++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..e006e2b969 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,38 @@ + + + + + + +## Checklist +- [ ] tested the solution locally and it works +- [ ] ran the code formatter (`scala-cli fmt .`) +- [ ] ran `scalafix` (`./mill -i __.fix`) +- [ ] ran reference docs auto-generation (`./mill -i 'generate-reference-doc[]'.run`) + +## How much have your relied on LLM-based tools in this contribution? + + + + + +## How was the solution tested? + + + +## Additional notes + + + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b30ec4cc6..9f297b2938 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,7 @@ will save you from getting a load of comments and speed up the code review. Other notes: -- give a short explanation on what the PR is meant to achieve in the description, unless covered by the PR title; +- fill the pull request template; - make sure to add tests wherever possible; - favor unit tests over integration tests where applicable; - try to add scaladocs for key classes and functions; From 48fa471e316ac3a727396b5c781f100289e28219 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Thu, 12 Mar 2026 13:37:55 +0100 Subject: [PATCH 07/20] Support `--cross` with the `package` sub-command (#4171) --- .../scala/cli/commands/package0/Package.scala | 85 +++++++++++++++++-- .../integration/PackageTestDefinitions.scala | 34 ++++---- .../cli/integration/PackageTestsDefault.scala | 81 +++++++++++++++++- 3 files changed, 172 insertions(+), 28 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala index 6fecd3c98d..a5828a837d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala @@ -101,16 +101,16 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => - res.orReport(logger).map(_.builds).foreach { + res.orReport(logger).map(_.all).foreach { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } successfulBuilds.foreach(_.copyOutput(options.shared)) - val mtimeDestPath = doPackage( + val mtimeDestPath = doPackageCrossBuilds( logger = logger, outputOpt = options.output.filter(_.nonEmpty), force = options.force, forcedPackageTypeOpt = options.forcedPackageTypeOpt, - builds = successfulBuilds, + allBuilds = successfulBuilds, extraArgs = args.unparsed, expectedModifyEpochSecondOpt = expectedModifyEpochSecondOpt, allowTerminate = !options.watch.watchMode, @@ -141,16 +141,16 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics ) .orExit(logger) - .builds match { + .all match { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } successfulBuilds.foreach(_.copyOutput(options.shared)) - val res0 = doPackage( + val res0 = doPackageCrossBuilds( logger = logger, outputOpt = options.output.filter(_.nonEmpty), force = options.force, forcedPackageTypeOpt = options.forcedPackageTypeOpt, - builds = successfulBuilds, + allBuilds = successfulBuilds, extraArgs = args.unparsed, expectedModifyEpochSecondOpt = None, allowTerminate = !options.watch.watchMode, @@ -183,6 +183,69 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { buildOptions } + private def insertSuffixBeforeExtension(name: String, suffix: String): String = + if suffix.isEmpty then name + else { + val dotIdx = name.lastIndexOf('.') + if dotIdx > 0 then name.substring(0, dotIdx) + suffix + name.substring(dotIdx) + else name + suffix + } + + private def doPackageCrossBuilds( + logger: Logger, + outputOpt: Option[String], + force: Boolean, + forcedPackageTypeOpt: Option[PackageType], + allBuilds: Seq[Build.Successful], + extraArgs: Seq[String], + expectedModifyEpochSecondOpt: Option[Long], + allowTerminate: Boolean, + mainClassOptions: MainClassOptions, + withTestScope: Boolean + ): Either[BuildException, Option[Long]] = either { + val crossBuildGroups = allBuilds.groupedByCrossParams.toSeq + val multipleCrossGroups = crossBuildGroups.size > 1 + + if multipleCrossGroups then + logger.message(s"Packaging ${crossBuildGroups.size} cross builds...") + + val platforms = crossBuildGroups.map(_._1.platform).distinct + val needsPlatformInSuffix = platforms.size > 1 + + val results = value { + crossBuildGroups.map { (crossParams, builds) => + val crossSuffix = + if multipleCrossGroups then { + val versionPart = s"_${crossParams.scalaVersion}" + if needsPlatformInSuffix then s"${versionPart}_${crossParams.platform}" + else versionPart + } + else "" + + if multipleCrossGroups then + logger.message(s"Packaging for ${crossParams.asString}...") + + doPackage( + logger = logger, + outputOpt = outputOpt, + force = force, + forcedPackageTypeOpt = forcedPackageTypeOpt, + builds = builds, + extraArgs = extraArgs, + expectedModifyEpochSecondOpt = expectedModifyEpochSecondOpt, + allowTerminate = allowTerminate, + mainClassOptions = mainClassOptions, + withTestScope = withTestScope, + crossSuffix = crossSuffix + ) + } + .sequence + .left.map(CompositeBuildException(_)) + } + + results.lastOption.flatten + } + private def doPackage( logger: Logger, outputOpt: Option[String], @@ -193,7 +256,8 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { expectedModifyEpochSecondOpt: Option[Long], allowTerminate: Boolean, mainClassOptions: MainClassOptions, - withTestScope: Boolean + withTestScope: Boolean, + crossSuffix: String ): Either[BuildException, Option[Long]] = either { if mainClassOptions.mainClassLs.contains(true) then value { @@ -285,7 +349,12 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { } .orElse(builds.flatMap(_.sources.paths).collectFirst(_._1.baseName + extension)) .getOrElse(defaultName) - val destPath = os.Path(dest, Os.pwd) + val destPath = { + val base = os.Path(dest, Os.pwd) + if crossSuffix.nonEmpty then + base / os.up / insertSuffixBeforeExtension(base.last, crossSuffix) + else base + } val printableDest = CommandUtils.printablePath(destPath) def alreadyExistsCheck(): Either[BuildException, Unit] = diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index ed2d70c6a0..3be1068114 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -1498,15 +1498,17 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio } } - if (actualScalaVersion == Constants.scala3Next) - test(s"package ($packageDescription, --cross)") { + if (actualScalaVersion == Constants.scala3Next) { + val crossScalaVersions = + Seq(actualScalaVersion, Constants.scala213, Constants.scala212) + val numberOfBuilds = crossScalaVersions.size + test(s"package ($packageDescription, --cross) produces $numberOfBuilds artifacts") { TestUtil.retryOnCi() { val crossDirective = - s"//> using scala $actualScalaVersion ${Constants.scala213} ${Constants.scala212}" - val mainClass = "TestScopeMain" - val mainFile = s"$mainClass.scala" - val message = "Hello" - val outputFile = mainClass + extension + s"//> using scala ${crossScalaVersions.mkString(" ")}" + val mainClass = "TestScopeMain" + val mainFile = s"$mainClass.scala" + val message = "Hello" TestInputs( os.rel / "Messages.scala" -> s"""$crossDirective @@ -1524,21 +1526,15 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio packageOpts ) .call(cwd = root) - val outputFilePath = root / outputFile - expect(os.isFile(outputFilePath)) - val output = - if (packageDescription == libraryArg) - os.proc(TestUtil.cli, "run", outputFilePath).call(cwd = root).out.trim() - else if (packageDescription == jsArg) - os.proc(node, outputFilePath).call(cwd = root).out.trim() - else { - expect(Files.isExecutable(outputFilePath.toNIO)) - TestUtil.maybeUseBash(outputFilePath)(cwd = root).out.trim() - } - expect(output == message) + + crossScalaVersions.foreach { version => + val outputFilePath = root / s"${mainClass}_$version$extension" + expect(os.isFile(outputFilePath)) + } } } } + } } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestsDefault.scala index 2932204d86..7f14733e08 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestsDefault.scala @@ -2,6 +2,10 @@ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect +import java.nio.file.Files + +import scala.util.Properties + class PackageTestsDefault extends PackageTestDefinitions with TestDefault { test("reuse run native binary") { TestUtil.retryOnCi() { @@ -25,10 +29,85 @@ class PackageTestsDefault extends PackageTestDefinitions with TestDefault { val packageOutput = packageRes.out.trim() val topPackageOutput = packageOutput.linesIterator.takeWhile(!_.startsWith("Wrote ")).toVector - // no compilation or Scala Native pipeline output, as this should just re-use what the run command wrote expect(topPackageOutput.forall(!_.startsWith("[info] "))) } } } + for { + (packageOpts, extension) <- Seq( + Nil -> (if (Properties.isWin) ".bat" else ""), + Seq("--library") -> ".jar" + ) ++ + (if (!TestUtil.isNativeCli || !Properties.isWin) Seq( + Seq("--assembly") -> ".jar" + ) + else Nil) + packageDescription = packageOpts.headOption.getOrElse("bootstrap") + crossScalaVersions = Seq(actualScalaVersion, Constants.scala213, Constants.scala212) + numberOfBuilds = crossScalaVersions.size + } { + test(s"package --cross ($packageDescription) produces $numberOfBuilds artifacts") { + TestUtil.retryOnCi() { + val mainClass = "Main" + val message = "Hello" + TestInputs( + os.rel / "project.scala" -> s"//> using scala ${crossScalaVersions.mkString(" ")}", + os.rel / s"$mainClass.scala" -> + s"""object $mainClass extends App { println("$message") }""" + ).fromRoot { root => + os.proc( + TestUtil.cli, + "--power", + "package", + "--cross", + extraOptions, + ".", + packageOpts + ).call(cwd = root) + + crossScalaVersions.foreach { version => + val expectedFile = root / s"${mainClass}_$version$extension" + expect(os.isFile(expectedFile)) + } + + if packageDescription == "bootstrap" then + crossScalaVersions.foreach { version => + val outputFile = root / s"${mainClass}_$version$extension" + expect(Files.isExecutable(outputFile.toNIO)) + val output = TestUtil.maybeUseBash(outputFile)(cwd = root).out.trim() + expect(output == message) + } + } + } + } + + test(s"package without --cross ($packageDescription) produces single artifact") { + TestUtil.retryOnCi() { + val mainClass = "Main" + val message = "Hello" + TestInputs( + os.rel / "project.scala" -> s"//> using scala ${crossScalaVersions.mkString(" ")}", + os.rel / s"$mainClass.scala" -> + s"""object $mainClass extends App { println("$message") }""" + ).fromRoot { root => + val r = os.proc( + TestUtil.cli, + "--power", + "package", + extraOptions, + ".", + packageOpts + ).call(cwd = root, stderr = os.Pipe) + + val expectedFile = root / s"$mainClass$extension" + expect(os.isFile(expectedFile)) + + expect(r.err.trim().contains(s"ignoring ${numberOfBuilds - 1} builds")) + expect(r.err.trim().contains(s"Defaulting to Scala $actualScalaVersion")) + } + } + } + } + } From 6c20fe0124a57362832a7d4964ff84a89376b486 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Thu, 12 Mar 2026 14:55:23 +0100 Subject: [PATCH 08/20] Allow to `--watch` extra paths with `--watching` (#4174) --- .../src/main/scala/scala/build/Build.scala | 13 ++ .../DirectivesPreprocessingUtils.scala | 1 + .../scala/cli/commands/compile/Compile.scala | 3 + .../cli/commands/compile/CompileOptions.scala | 2 +- .../commands/package0/PackageOptions.scala | 4 +- .../scala/cli/commands/publish/Publish.scala | 3 + .../cli/commands/publish/PublishLocal.scala | 3 + .../publish/PublishLocalOptions.scala | 2 +- .../cli/commands/publish/PublishOptions.scala | 2 +- .../scala/scala/cli/commands/repl/Repl.scala | 2 +- .../scala/scala/cli/commands/run/Run.scala | 2 +- .../scala/cli/commands/run/RunOptions.scala | 10 +- .../shared/HasSharedWatchOptions.scala | 11 ++ .../cli/commands/shared/SharedOptions.scala | 7 +- .../commands/shared/SharedWatchOptions.scala | 15 +- .../scala/scala/cli/commands/test/Test.scala | 2 +- .../preprocessing/directives/Watching.scala | 48 +++++++ .../integration/CompileTestDefinitions.scala | 41 ++++++ .../integration/PackageTestDefinitions.scala | 42 ++++++ .../RunWithWatchTestDefinitions.scala | 129 ++++++++++++++++++ .../cli/integration/TestTestDefinitions.scala | 49 ++++++- .../scala/cli/integration/TestUtil.scala | 30 ++++ .../scala/build/options/BuildOptions.scala | 1 + .../scala/build/options/WatchOptions.scala | 10 ++ website/docs/commands/compile.md | 16 +++ website/docs/commands/package.md | 22 +++ website/docs/commands/repl.md | 20 +++ website/docs/commands/run.md | 16 +++ website/docs/commands/test.md | 22 +++ website/docs/reference/cli-options.md | 6 + website/docs/reference/directives.md | 13 ++ 31 files changed, 532 insertions(+), 15 deletions(-) create mode 100644 modules/cli/src/main/scala/scala/cli/commands/shared/HasSharedWatchOptions.scala create mode 100644 modules/directives/src/main/scala/scala/build/preprocessing/directives/Watching.scala create mode 100644 modules/options/src/main/scala/scala/build/options/WatchOptions.scala diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index c631c88ea0..89377a0daa 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -17,6 +17,7 @@ import scala.build.errors.* import scala.build.input.* import scala.build.internal.resource.ResourceMapper import scala.build.internal.{Constants, MainClass, Name, Util} +import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.options.* import scala.build.options.validation.ValidationException import scala.build.postprocessing.* @@ -791,6 +792,7 @@ object Build { def doWatch(): Unit = either { val (crossSources: CrossSources, inputs0: Inputs) = value(allInputs(inputs, options, logger)) + val mergedOptions = crossSources.sharedOptions(options) val elements: Seq[Element] = if res == null then inputs0.elements else @@ -851,6 +853,17 @@ object Build { watcher0.register(artifact.toNIO, depth) watcher0.addObserver(onChangeBufferedObserver(_ => watcher.schedule())) } + + val extraWatchPaths = mergedOptions.watchOptions.extraWatchPaths.distinct + for (extraPath <- extraWatchPaths) + if os.exists(extraPath) then { + val depth = if os.isFile(extraPath) then -1 else Int.MaxValue + val watcher0 = watcher.newWatcher() + watcher0.register(extraPath.toNIO, depth) + watcher0.addObserver(onChangeBufferedObserver(_ => watcher.schedule())) + } + else + logger.message(s"$warnPrefix provided watched path doesn't exist: $extraPath") } try doWatch() diff --git a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala index 96c9edc30a..5b439b07fc 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala @@ -31,6 +31,7 @@ object DirectivesPreprocessingUtils { directives.ScalaNative.handler, directives.ScalaVersion.handler, directives.Sources.handler, + directives.Watching.handler, directives.Tests.handler ).map(_.mapE(_.buildOptions)) diff --git a/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala b/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala index bb0203273f..7a5e36dfc9 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala @@ -23,6 +23,9 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers { override def sharedOptions(options: CompileOptions): Option[SharedOptions] = Some(options.shared) + override def buildOptions(options: CompileOptions): Some[scala.build.options.BuildOptions] = + Some(options.buildOptions().orExit(options.shared.logger)) + override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.MUST val primaryHelpGroups: Seq[HelpGroup] = Seq( diff --git a/modules/cli/src/main/scala/scala/cli/commands/compile/CompileOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/compile/CompileOptions.scala index fafbb6d8c9..cdd8326ee9 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/compile/CompileOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/compile/CompileOptions.scala @@ -23,7 +23,7 @@ final case class CompileOptions( @Tag(tags.should) @Tag(tags.inShortHelp) printClassPath: Boolean = false -) extends HasSharedOptions +) extends HasSharedOptions with HasSharedWatchOptions // format: on object CompileOptions { diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala index d9d3a350f9..6a7c7e5f41 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala @@ -141,7 +141,7 @@ final case class PackageOptions( @Tag(tags.restricted) @Tag(tags.inShortHelp) nativeImage: Boolean = false -) extends HasSharedOptions { +) extends HasSharedOptions with HasSharedWatchOptions { // format: on def packageTypeOpt: Option[PackageType] = @@ -177,7 +177,7 @@ final case class PackageOptions( .left.map(CompositeBuildException(_)) def baseBuildOptions(logger: Logger): Either[BuildException, BuildOptions] = either { - val baseOptions = value(shared.buildOptions()) + val baseOptions = value(buildOptions()) baseOptions.copy( mainClass = mainClass.mainClass.filter(_.nonEmpty), notForBloopOptions = baseOptions.notForBloopOptions.copy( diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index c832c9daf7..efe1f1e7ab 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -84,6 +84,9 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { override def sharedOptions(options: PublishOptions): Option[SharedOptions] = Some(options.shared) + override def buildOptions(options: PublishOptions): Some[BuildOptions] = + Some(options.buildOptions().orExit(options.shared.logger)) + def mkBuildOptions( baseOptions: BuildOptions, sharedVersionOptions: SharedVersionOptions, diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala index 8355c2df8c..d81158c7ff 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala @@ -20,6 +20,9 @@ object PublishLocal extends ScalaCommand[PublishLocalOptions] { override def sharedOptions(options: PublishLocalOptions): Option[SharedOptions] = Some(options.shared) + override def buildOptions(options: PublishLocalOptions): Some[scala.build.options.BuildOptions] = + Some(options.buildOptions().orExit(options.shared.logger)) + override def names: List[List[String]] = List( List("publish", "local") ) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala index f131f33a73..e41bca8793 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala @@ -22,7 +22,7 @@ final case class PublishLocalOptions( sharedPublish: SharedPublishOptions = SharedPublishOptions(), @Recurse scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions(), -) extends HasSharedOptions +) extends HasSharedOptions with HasSharedWatchOptions // format: on object PublishLocalOptions { diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishOptions.scala index ac9a88c8d6..5a10a2049d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishOptions.scala @@ -38,7 +38,7 @@ final case class PublishOptions( @Tag(tags.restricted) @Hidden parallelUpload: Option[Boolean] = None -) extends HasSharedOptions +) extends HasSharedOptions with HasSharedWatchOptions // format: on object PublishOptions { diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index 209ecf5797..d0dff113e9 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -62,7 +62,7 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { val logger = ops.shared.logger val ammoniteVersionOpt = ammoniteVersion.map(_.trim).filter(_.nonEmpty) - val baseOptions = shared.buildOptions().orExit(logger) + val baseOptions = shared.buildOptions(watchOptions = watch).orExit(logger) val maybeDowngradedScalaVersion = { val isDefaultAmmonite = ammonite.contains(true) && ammoniteVersionOpt.isEmpty diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index b453f326b0..78b53b5bfb 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -71,7 +71,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { import options.* import options.sharedRun.* val logger = options.shared.logger - val baseOptions = shared.buildOptions().orExit(logger) + val baseOptions = options.buildOptions().orExit(logger) baseOptions.copy( mainClass = mainClass.mainClass, javaOptions = baseOptions.javaOptions.copy( diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/RunOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/run/RunOptions.scala index 3007b74cba..194e122f5d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/RunOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/RunOptions.scala @@ -4,7 +4,9 @@ import caseapp.* import caseapp.core.help.Help import scala.cli.ScalaCli -import scala.cli.commands.shared.{HasSharedOptions, HelpMessages, SharedOptions} +import scala.cli.commands.shared.{ + HasSharedOptions, HasSharedWatchOptions, HelpMessages, SharedOptions, SharedWatchOptions +} @HelpMessage(RunOptions.helpMessage, "", RunOptions.detailedHelpMessage) // format: off @@ -13,8 +15,10 @@ final case class RunOptions( shared: SharedOptions = SharedOptions(), @Recurse sharedRun: SharedRunOptions = SharedRunOptions() -) extends HasSharedOptions -// format: on +) extends HasSharedOptions with HasSharedWatchOptions { + // format: on + override def watch: SharedWatchOptions = sharedRun.watch +} object RunOptions { implicit lazy val parser: Parser[RunOptions] = Parser.derive diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/HasSharedWatchOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/HasSharedWatchOptions.scala new file mode 100644 index 0000000000..eef9534d53 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/HasSharedWatchOptions.scala @@ -0,0 +1,11 @@ +package scala.cli.commands.shared + +import scala.build.errors.BuildException + +trait HasSharedWatchOptions { this: HasSharedOptions => + def watch: SharedWatchOptions + + def buildOptions(ignoreErrors: Boolean = + false): Either[BuildException, scala.build.options.BuildOptions] = + shared.buildOptions(ignoreErrors = ignoreErrors, watchOptions = watch) +} diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index fe4b8903d9..6be30799ea 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -290,7 +290,10 @@ final case class SharedOptions( def scalacOptions: List[String] = scalac.scalacOption ++ scalacOptionsFromFiles - def buildOptions(ignoreErrors: Boolean = false) + def buildOptions( + ignoreErrors: Boolean = false, + watchOptions: SharedWatchOptions = SharedWatchOptions() + ) : Either[BuildException, scala.build.options.BuildOptions] = either { val releaseOpt = scalacOptions.getScalacOption("-release") @@ -441,7 +444,7 @@ final case class SharedOptions( scalaPyVersion = sharedPython.scalaPyVersion ), useBuildServer = compilationServer.server - ) + ).orElse(watchOptions.buildOptions()) } private def resolvedDependencies( diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala index df4e28ec7e..2fd9a45cdf 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala @@ -2,6 +2,7 @@ package scala.cli.commands.shared import caseapp.* +import scala.build.options.{BuildOptions, WatchOptions} import scala.cli.commands.tags // format: off @@ -18,10 +19,22 @@ final case class SharedWatchOptions( @Tag(tags.should) @Tag(tags.inShortHelp) @Name("revolver") - restart: Boolean = false + restart: Boolean = false, + @Group(HelpGroup.Watch.toString) + @HelpMessage("Watch additional paths for changes (used together with --watch or --restart)") + @Tag(tags.experimental) + @Name("watchingPath") + watching: List[String] = Nil ) { // format: on lazy val watchMode: Boolean = watch || restart + + def buildOptions(cwd: os.Path = os.pwd): BuildOptions = + BuildOptions( + watchOptions = WatchOptions( + extraWatchPaths = watching.map(os.Path(_, cwd)) + ) + ) } object SharedWatchOptions { diff --git a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala index c1d528a530..a236a5a93c 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala @@ -37,7 +37,7 @@ object Test extends ScalaCommand[TestOptions] { override def buildOptions(opts: TestOptions): Option[BuildOptions] = Some { import opts.* - val baseOptions = shared.buildOptions().orExit(opts.shared.logger) + val baseOptions = shared.buildOptions(watchOptions = watch).orExit(opts.shared.logger) baseOptions.copy( javaOptions = baseOptions.javaOptions.copy( javaOpts = diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Watching.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Watching.scala new file mode 100644 index 0000000000..dbfeb6603c --- /dev/null +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Watching.scala @@ -0,0 +1,48 @@ +package scala.build.preprocessing.directives + +import scala.build.Positioned +import scala.build.directives.* +import scala.build.errors.BuildException +import scala.build.options.{BuildOptions, WatchOptions} +import scala.cli.commands.SpecificationLevel + +@DirectiveGroupName("Watch additional inputs") +@DirectiveExamples("//> using watching ./data") +@DirectiveUsage( + """//> using watching _path_ + | + |//> using watching _path1_ _path2_ …""".stripMargin, + """`//> using watching` _path_ + | + |`//> using watching` _path1_ _path2_ … + | + |""".stripMargin +) +@DirectiveDescription("Watch additional files or directories when using watch mode") +@DirectiveLevel(SpecificationLevel.EXPERIMENTAL) +final case class Watching( + watching: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = + DirectiveValueParser.WithScopePath.empty(Nil) +) extends HasBuildOptions { + def buildOptions: Either[BuildException, BuildOptions] = + Watching.buildOptions(watching) +} + +object Watching { + val handler: DirectiveHandler[Watching] = DirectiveHandler.derive + + def buildOptions( + watching: DirectiveValueParser.WithScopePath[List[Positioned[String]]] + ): Either[BuildException, BuildOptions] = Right { + val paths = watching.value.map(_.value) + val (_, rootOpt) = Directive.osRootResource(watching.scopePath) + val resolvedPaths = rootOpt.toList.flatMap { root => + paths.map(os.Path(_, root)) + } + BuildOptions( + watchOptions = WatchOptions( + extraWatchPaths = resolvedPaths + ) + ) + } +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala index 050c22e15e..2c7f777471 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala @@ -4,7 +4,9 @@ import com.eed3si9n.expecty.Expecty.expect import java.io.File +import scala.cli.integration.TestUtil.ProcOps import scala.cli.integration.util.BloopUtil +import scala.concurrent.duration.DurationInt import scala.util.Properties abstract class CompileTestDefinitions @@ -907,4 +909,43 @@ abstract class CompileTestDefinitions ) } } + + if (!Properties.isMac || !TestUtil.isCI) + test("--watching with --watch re-compiles on external file change") { + val sourceFile = os.rel / "Main.scala" + val externalFile = os.rel / "data" / "input.txt" + TestInputs( + sourceFile -> + """object Main { + | def value = 1 + |} + |""".stripMargin, + externalFile -> "Hello" + ).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "--power", + "compile", + ".", + "--watch", + "--watching", + "data", + extraOptions + ) + .spawn(cwd = root, stderr = os.Pipe), + timeout = 120.seconds + ) { (proc, timeout, ec) => + implicit val ec0 = ec + val initialOutput = proc.readStderrUntilWatchingMessage(timeout) + expect(initialOutput.exists(_.contains("Compiled"))) + + Thread.sleep(2000L) + os.write.over(root / externalFile, "World") + + val rerunOutput = proc.readStderrUntilWatchingMessage(timeout) + expect(rerunOutput.nonEmpty) + } + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index 3be1068114..784a94c356 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -9,6 +9,7 @@ import java.util import java.util.zip.ZipFile import scala.cli.integration.TestUtil.* +import scala.concurrent.duration.DurationInt import scala.jdk.CollectionConverters.* import scala.util.{Properties, Using} @@ -1559,4 +1560,45 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio expect(res.out.trim().contains(s"$moduleName.js")) } } + + if (!Properties.isMac || !TestUtil.isCI) + test("--watching with --watch re-packages on external file change") { + val sourceFile = os.rel / "Main.scala" + val externalFile = os.rel / "data" / "input.txt" + TestInputs( + sourceFile -> + """object Main extends App { + | println("Hello") + |} + |""".stripMargin, + externalFile -> "Hello" + ).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "--power", + "package", + ".", + "--watch", + "--watching", + "data", + "-o", + "app", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 120.seconds + ) { (proc, timeout, ec) => + implicit val ec0 = ec + val initialOutput = proc.readOutputUntilWatchingMessage(timeout) + expect(initialOutput.exists(_.contains("Wrote"))) + + Thread.sleep(2000L) + os.write.over(root / externalFile, "World") + + val rerunOutput = proc.readOutputUntilWatchingMessage(timeout) + expect(rerunOutput.nonEmpty) + } + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala index afdeb5a67a..2a125f96c0 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala @@ -77,6 +77,135 @@ trait RunWithWatchTestDefinitions { this: RunTestDefinitions => } } + if (!Properties.isMac || !TestUtil.isCI) { + test("--watching with CLI option triggers re-run on external file change") { + val sourceFile = os.rel / "app.scala" + val externalFile = os.rel / "data" / "input.txt" + val code = + """object App { + | def main(args: Array[String]): Unit = { + | val content = scala.io.Source.fromFile("data/input.txt").mkString.trim + | println(content) + | } + |} + |""".stripMargin + + TestInputs( + sourceFile -> code, + externalFile -> "Hello" + ).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "--power", + "run", + ".", + "--watch", + "--watching", + "data", + extraOptions + ) + .spawn(cwd = root, stderr = os.Pipe), + timeout = 120.seconds + ) { (proc, timeout, ec) => + val output1 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output1 == "Hello") + proc.printStderrUntilRerun(timeout)(ec) + Thread.sleep(2000L) + os.write.over(root / externalFile, "World") + val output2 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output2 == "World") + } + } + } + + test("//> using watching directive triggers re-run on external file change") { + val sourceFile = os.rel / "app.scala" + val externalFile = os.rel / "data" / "input.txt" + val code = + """//> using watching ./data + |object App { + | def main(args: Array[String]): Unit = { + | val content = scala.io.Source.fromFile("data/input.txt").mkString.trim + | println(content) + | } + |} + |""".stripMargin + + TestInputs( + sourceFile -> code, + externalFile -> "Hello" + ).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc(TestUtil.cli, "--power", "run", ".", "--watch", extraOptions) + .spawn(cwd = root, stderr = os.Pipe), + timeout = 120.seconds + ) { (proc, timeout, ec) => + val output1 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output1 == "Hello") + proc.printStderrUntilRerun(timeout)(ec) + Thread.sleep(2000L) + os.write.over(root / externalFile, "World") + val output2 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output2 == "World") + } + } + } + + test("--watching CLI + //> using watching directive union") { + val sourceFile = os.rel / "app.scala" + val directiveWatchFile = os.rel / "data1" / "input1.txt" + val cliWatchFile = os.rel / "data2" / "input2.txt" + val code = + """//> using watching ./data1 + |object App { + | def main(args: Array[String]): Unit = { + | val fromDirective = scala.io.Source.fromFile("data1/input1.txt").mkString.trim + | val fromCli = scala.io.Source.fromFile("data2/input2.txt").mkString.trim + | println(s"$fromDirective|$fromCli") + | } + |} + |""".stripMargin + + TestInputs( + sourceFile -> code, + directiveWatchFile -> "Hello", + cliWatchFile -> "World" + ).fromRoot { root => + TestUtil.withProcessWatching( + proc = + os.proc( + TestUtil.cli, + "--power", + "run", + ".", + "--watch", + "--watching", + "data2", + extraOptions + ) + .spawn(cwd = root, stderr = os.Pipe), + timeout = 120.seconds + ) { (proc, timeout, ec) => + val output1 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output1 == "Hello|World") + + proc.printStderrUntilRerun(timeout)(ec) + Thread.sleep(2000L) + os.write.over(root / directiveWatchFile, "Bonjour") + val output2 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output2 == "Bonjour|World") + + proc.printStderrUntilRerun(timeout)(ec) + Thread.sleep(2000L) + os.write.over(root / cliWatchFile, "Universe") + val output3 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output3 == "Bonjour|Universe") + } + } + } + } + for { (platformDescription, platformOpts) <- Seq( "JVM" -> Nil, diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala index 2e3bdd6f55..363fd3ed1a 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala @@ -4,7 +4,9 @@ import com.eed3si9n.expecty.Expecty.expect import scala.annotation.tailrec import scala.cli.integration.Constants.munitVersion -import scala.cli.integration.TestUtil.StringOps +import scala.cli.integration.TestUtil.{ProcOps, StringOps} +import scala.concurrent.duration.DurationInt +import scala.util.Properties abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs { this: TestScalaVersion => @@ -232,6 +234,51 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr } } + if (!Properties.isMac || !TestUtil.isCI) + test("--watching with --watch re-runs tests on external file change") { + val sourceFile = os.rel / "MyTests.test.scala" + val externalFile = os.rel / "data" / "input.txt" + TestInputs( + sourceFile -> + s"""//> using dep org.scalameta::munit::$munitVersion + | + |class MyTests extends munit.FunSuite { + | test("watched input") { + | val content = scala.io.Source.fromFile("data/input.txt").mkString.trim + | println(content) + | assert(content.nonEmpty) + | } + |} + |""".stripMargin, + externalFile -> "Hello" + ).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "--power", + "test", + ".", + "--watch", + "--watching", + "data", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 120.seconds + ) { (proc, timeout, ec) => + implicit val ec0 = ec + val initialOutput = proc.readOutputUntilWatchingMessage(timeout) + expect(initialOutput.exists(_.contains("Hello"))) + + Thread.sleep(2000L) + os.write.over(root / externalFile, "World") + + val rerunOutput = proc.readOutputUntilWatchingMessage(timeout) + expect(rerunOutput.exists(_.contains("World"))) + } + } + } + if (actualScalaVersion.startsWith("2")) test("successful test JVM 8") { successfulTestInputs().fromRoot { root => diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala b/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala index d8bcc49a03..3153a629dc 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala @@ -408,6 +408,26 @@ object TestUtil { while (!revertTriggered()) Thread.sleep(100L) } + def readLinesUntil( + stream: os.SubProcess.OutputStream, + ec: ExecutionContext, + timeout: Duration + )(condition: String => Boolean): Seq[String] = { + val lines = scala.collection.mutable.ListBuffer.empty[String] + var done = false + while (!done) { + val line = TestUtil.readLine(stream, ec, timeout) + if (line == null) done = true + else { + lines += line + done = condition(line) + } + } + lines.toSeq + } + + private val watchingSourcesCondition: String => Boolean = _.contains("Watching sources") + implicit class ProcOps(proc: os.SubProcess) { def printStderrUntilJlineRevertsToDumbTerminal(proc: os.SubProcess)( f: String => Unit @@ -416,6 +436,16 @@ object TestUtil { def printStderrUntilRerun(timeout: Duration)(implicit ec: ExecutionContext): Unit = TestUtil.printStderrUntilCondition(proc, timeout)(_.contains("re-run"))() + + def readStderrUntilWatchingMessage(timeout: Duration)(implicit + ec: ExecutionContext + ): Seq[String] = + TestUtil.readLinesUntil(proc.stderr, ec, timeout)(watchingSourcesCondition) + + def readOutputUntilWatchingMessage(timeout: Duration)(implicit + ec: ExecutionContext + ): Seq[String] = + TestUtil.readLinesUntil(proc.stdout, ec, timeout)(watchingSourcesCondition) } // based on the implementation from bloop-rifle: diff --git a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala index 2375f28840..49b9a4cf5c 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -46,6 +46,7 @@ final case class BuildOptions( mainClass: Option[String] = None, testOptions: TestOptions = TestOptions(), notForBloopOptions: PostBuildOptions = PostBuildOptions(), + watchOptions: WatchOptions = WatchOptions(), sourceGeneratorOptions: SourceGeneratorOptions = SourceGeneratorOptions(), useBuildServer: Option[Boolean] = None ) { diff --git a/modules/options/src/main/scala/scala/build/options/WatchOptions.scala b/modules/options/src/main/scala/scala/build/options/WatchOptions.scala new file mode 100644 index 0000000000..cf01e3f75b --- /dev/null +++ b/modules/options/src/main/scala/scala/build/options/WatchOptions.scala @@ -0,0 +1,10 @@ +package scala.build.options + +final case class WatchOptions( + extraWatchPaths: Seq[os.Path] = Nil +) + +object WatchOptions { + implicit val hasHashData: HasHashData[WatchOptions] = HasHashData.nop + implicit val monoid: ConfigMonoid[WatchOptions] = ConfigMonoid.derive +} diff --git a/website/docs/commands/compile.md b/website/docs/commands/compile.md index 503d71d7fb..c480725366 100644 --- a/website/docs/commands/compile.md +++ b/website/docs/commands/compile.md @@ -64,6 +64,22 @@ Watching sources, press Ctrl+C to exit. +### Watching additional paths + +Use `--watching` to re-trigger compilation when files outside your Scala sources change: + +```bash ignore +scala-cli compile --watch --watching ./data Hello.scala +``` + +You can also configure this from sources with: + +```scala +//> using watching ./data +``` + +If you use both, Scala CLI watches every path from both the command line and the directive. + ## Scala version Scala CLI uses the latest stable version of Scala which was tested in Scala CLI (see our list diff --git a/website/docs/commands/package.md b/website/docs/commands/package.md index 0320371109..906cf480a8 100644 --- a/website/docs/commands/package.md +++ b/website/docs/commands/package.md @@ -56,6 +56,28 @@ Hello Hello --> +## Watch mode + +Use `--watch` to rebuild the package whenever sources change: + +```bash ignore +scala-cli --power package --watch Hello.scala -o hello +``` + +You can watch additional inputs too: + +```bash ignore +scala-cli --power package --watch --watching ./data Hello.scala -o hello +``` + +This also works from sources: + +```scala +//> using watching ./data +``` + +When both are present, Scala CLI watches all of the configured paths. + ## Library JARs *Library JARs* are suitable if you plan to put the resulting JAR in a class path, rather than running it as is. diff --git a/website/docs/commands/repl.md b/website/docs/commands/repl.md index 98df8b4675..650cd3ef37 100644 --- a/website/docs/commands/repl.md +++ b/website/docs/commands/repl.md @@ -80,6 +80,26 @@ scala> :quit +## Watch mode + +Use `--watch` to recompile your inputs and restart the REPL session when sources change: + +```bash ignore +scala-cli repl --watch Main.scala +``` + +`--watching` lets you include additional files or directories: + +```bash ignore +scala-cli repl --watch --watching ./data Main.scala +``` + +You can also configure extra watched paths in sources: + +```scala +//> using watching ./data +``` + ## Passing REPL options It is also possible to manually pass REPL-specific options. It can be done in a couple ways: diff --git a/website/docs/commands/run.md b/website/docs/commands/run.md index d90e8fdb27..2c8a2c72fa 100644 --- a/website/docs/commands/run.md +++ b/website/docs/commands/run.md @@ -227,6 +227,22 @@ Watching sources while your program is running. +### Watching additional paths + +`--watching` lets you specify additional files or directories to watch while using `--watch` or `--restart`: + +```bash ignore +scala-cli run --watch --watching ./data --watching ./templates Hello.scala +``` + +You can also declare extra watched paths from your sources: + +```scala +//> using watching ./data +``` + +When both `--watching` and `//> using watching` are used, Scala CLI watches all of the specified paths. + ## Scala.js Scala.js applications can also be compiled and run with the `--js` option. diff --git a/website/docs/commands/test.md b/website/docs/commands/test.md index d5e1287429..17f50622a5 100644 --- a/website/docs/commands/test.md +++ b/website/docs/commands/test.md @@ -171,6 +171,28 @@ tests.only.BarTests: + bar --> +## Watch mode + +Use `--watch` to re-run your tests whenever sources change: + +```bash ignore +scala-cli test --watch MyTests.test.scala +``` + +`--watching` can extend that to files or directories outside your Scala sources: + +```bash ignore +scala-cli test --watch --watching ./data MyTests.test.scala +``` + +You can declare the same extra watched paths from sources: + +```scala +//> using watching ./data +``` + +If both are used, Scala CLI watches all of the configured paths. + ## Filter test case ### Munit diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index f945537b38..b104a3cc79 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -1957,6 +1957,12 @@ Aliases: `--revolver` Run the application in the background, automatically kill the process and restart if sources have been changed +### `--watching` + +Aliases: `--watching-path` + +Watch additional paths for changes (used together with --watch or --restart) + ## Internal options ### Add path options diff --git a/website/docs/reference/directives.md b/website/docs/reference/directives.md index 9a9f0456bb..8bd2d05a20 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -695,6 +695,19 @@ Use a toolkit as dependency (not supported in Scala 2.12), 'default' version for `//> using test.toolkit default` +### Watch additional inputs + +Watch additional files or directories when using watch mode + +`//> using watching` _path_ + +`//> using watching` _path1_ _path2_ … + + + +#### Examples +`//> using watching ./data` + ## target directives From f0fc0c3b72df99a044e6af3cc0f10add0c5bb696 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 12:05:36 +0100 Subject: [PATCH 09/20] Bump undici from 7.18.2 to 7.24.1 in /website (#4182) Bumps [undici](https://github.com/nodejs/undici) from 7.18.2 to 7.24.1. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.18.2...v7.24.1) --- updated-dependencies: - dependency-name: undici dependency-version: 7.24.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 52f36738dd..055193af3c 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -9273,9 +9273,9 @@ undici-types@~7.16.0: integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== undici@^7.12.0: - version "7.18.2" - resolved "https://registry.yarnpkg.com/undici/-/undici-7.18.2.tgz#6cf724ef799a67d94fd55adf66b1e184176efcdf" - integrity sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw== + version "7.24.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-7.24.1.tgz#3fd0fe40e67388860810ad3275f9a23b322de650" + integrity sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" From bdaa4f42199836fd6935f8b6fd2d046adf9ed3da Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Sun, 15 Mar 2026 12:55:10 +0100 Subject: [PATCH 10/20] Use targeted Java/Scala mappings with the `doc` sub-command (#4180) --- build.mill | 1 + .../scala/scala/cli/commands/doc/Doc.scala | 31 ++++++---- .../scala/cli/commands/tests/DocTests.scala | 40 +++++++++++++ .../cli/integration/DocTestDefinitions.scala | 57 +++++++++++++++++++ 4 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 modules/cli/src/test/scala/cli/commands/tests/DocTests.scala diff --git a/build.mill b/build.mill index f98bd81673..509c58b757 100644 --- a/build.mill +++ b/build.mill @@ -528,6 +528,7 @@ trait Core extends ScalaCliCrossSbtModule | def minimumBloopJavaVersion = ${Java.minimumBloopJava} | def minimumInternalJavaVersion = ${Java.minimumInternalJava} | def defaultJavaVersion = ${Java.defaultJava} + | def mainJavaVersions = Seq(${Java.mainJavaVersions.sorted.mkString(", ")}) | | def defaultScalaVersion = "${Scala.defaultUser}" | def defaultScala212Version = "${Scala.scala212}" diff --git a/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala b/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala index 521c53c72a..58bd0852e1 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala @@ -125,16 +125,26 @@ object Doc extends ScalaCommand[DocOptions] { logger.message(s"Wrote Scaladoc to $printableOutput") } + private def javadocBaseUrl(javaVersion: Int): String = + if javaVersion >= 11 then + s"https://docs.oracle.com/en/java/javase/$javaVersion/docs/api/java.base/" + else + s"https://docs.oracle.com/javase/$javaVersion/docs/api/" + + private def scaladocBaseUrl(scalaVersion: String): String = + s"https://scala-lang.org/api/$scalaVersion/" + // from https://github.com/VirtusLab/scala-cli/pull/103/files#diff-1039b442cbd23f605a61fdb9c3620b600aa4af6cab757932a719c54235d8e402R60 - private def defaultScaladocArgs = Seq( - "-snippet-compiler:compile", - "-Ygenerate-inkuire", - "-external-mappings:" + - ".*/scala/.*::scaladoc3::https://scala-lang.org/api/3.x/," + - ".*/java/.*::javadoc::https://docs.oracle.com/javase/8/docs/api/", - "-author", - "-groups" - ) + private[commands] def defaultScaladocArgs(scalaVersion: String, javaVersion: Int): Seq[String] = + Seq( + "-snippet-compiler:compile", + "-Ygenerate-inkuire", + "-external-mappings:" + + s".*/scala/.*::scaladoc3::${scaladocBaseUrl(scalaVersion)}," + + s".*/java/.*::javadoc::${javadocBaseUrl(javaVersion)}", + "-author", + "-groups" + ) def generateScaladocDirPath( builds: Seq[Build.Successful], @@ -171,10 +181,11 @@ object Doc extends ScalaCommand[DocOptions] { "-d", destDir.toString ) + val javaVersion = builds.head.options.javaHome().value.version val defaultArgs = if builds.head.options.notForBloopOptions.packageOptions.useDefaultScaladocOptions .getOrElse(true) - then defaultScaladocArgs + then defaultScaladocArgs(scalaParams.scalaVersion, javaVersion) else Nil val args = baseArgs ++ builds.head.project.scalaCompiler.map(_.scalacOptions).getOrElse(Nil) ++ diff --git a/modules/cli/src/test/scala/cli/commands/tests/DocTests.scala b/modules/cli/src/test/scala/cli/commands/tests/DocTests.scala new file mode 100644 index 0000000000..3835125d3f --- /dev/null +++ b/modules/cli/src/test/scala/cli/commands/tests/DocTests.scala @@ -0,0 +1,40 @@ +package scala.cli.commands.tests + +import com.eed3si9n.expecty.Expecty.assert as expect + +import scala.build.internal.Constants +import scala.cli.commands.doc.Doc + +class DocTests extends munit.FunSuite { + + for (javaVersion <- Constants.mainJavaVersions) + test(s"correct external mappings for JVM $javaVersion") { + val args = Doc.defaultScaladocArgs(Constants.defaultScalaVersion, javaVersion) + val mappingsArg = args.find(_.startsWith("-external-mappings:")).get + if javaVersion >= 11 then + expect(mappingsArg.contains(s"javase/$javaVersion/docs/api/java.base/")) + else + expect(mappingsArg.contains(s"javase/$javaVersion/docs/api/")) + expect(!mappingsArg.contains("java.base/")) + expect(mappingsArg.contains(s"scala-lang.org/api/${Constants.defaultScalaVersion}/")) + } + + test(s"correct external mappings for Scala 3 LTS (${Constants.scala3Lts})") { + val args = Doc.defaultScaladocArgs(Constants.scala3Lts, Constants.defaultJavaVersion) + val mappingsArg = args.find(_.startsWith("-external-mappings:")).get + expect(mappingsArg.contains(s"scala-lang.org/api/${Constants.scala3Lts}/")) + expect( + mappingsArg.contains(s"javase/${Constants.defaultJavaVersion}/docs/api/java.base/") + ) + } + + test(s"correct external mappings for default Scala (${Constants.defaultScalaVersion})") { + val args = + Doc.defaultScaladocArgs(Constants.defaultScalaVersion, Constants.defaultJavaVersion) + val mappingsArg = args.find(_.startsWith("-external-mappings:")).get + expect(mappingsArg.contains(s"scala-lang.org/api/${Constants.defaultScalaVersion}/")) + expect( + mappingsArg.contains(s"javase/${Constants.defaultJavaVersion}/docs/api/java.base/") + ) + } +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala index f82f40bdc9..54e8ae6a5a 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala @@ -97,4 +97,61 @@ abstract class DocTestDefinitions extends ScalaCliSuite with TestScalaVersionArg |""".stripMargin ).fromRoot(root => os.proc(TestUtil.cli, "doc", ".", extraOptions).call(cwd = root)) } + + if actualScalaVersion.startsWith("3") then + for { + javaVersion <- + if isScala38OrNewer then + Constants.allJavaVersions.filter(_ >= Constants.scala38MinJavaVersion) + else Constants.allJavaVersions + } + test(s"doc generates correct external mapping URLs for JVM $javaVersion") { + TestUtil.retryOnCi() { + val dest = os.rel / "doc-out" + val inputs = TestInputs( + os.rel / "Lib.scala" -> + """package mylib + | + |/** A wrapper around [[java.util.HashMap]] and [[scala.Option]]. */ + |class Lib: + | /** Returns a [[java.util.HashMap]]. */ + | def getMap: java.util.HashMap[String, String] = new java.util.HashMap() + | /** Returns a [[scala.Option]]. */ + | def getOpt: Option[String] = Some("hello") + |""".stripMargin + ) + inputs.fromRoot { root => + os.proc( + TestUtil.cli, + "doc", + extraOptions, + ".", + "-o", + dest, + "--jvm", + javaVersion.toString + ).call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) + + val docDir = root / dest + expect(os.isDir(docDir)) + + val htmlContent = os.walk(docDir) + .filter(_.last.endsWith(".html")) + .map(os.read(_)) + .mkString + + val expectedJavadocFragment = + if javaVersion >= 11 then + s"docs.oracle.com/en/java/javase/$javaVersion/docs/api/java.base/" + else + s"docs.oracle.com/javase/$javaVersion/docs/api/" + expect(htmlContent.contains(expectedJavadocFragment)) + + if javaVersion < 11 then + expect(!htmlContent.contains("java.base/")) + + expect(htmlContent.contains(s"scala-lang.org/api/$actualScalaVersion/")) + } + } + } } From bd1b92ea399edea89d1c189c64fd98a20d03b9dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:49:58 +0100 Subject: [PATCH 11/20] Bump webfactory/ssh-agent in the github-actions group (#4187) Bumps the github-actions group with 1 update: [webfactory/ssh-agent](https://github.com/webfactory/ssh-agent). Updates `webfactory/ssh-agent` from 0.9.1 to 0.10.0 - [Release notes](https://github.com/webfactory/ssh-agent/releases) - [Changelog](https://github.com/webfactory/ssh-agent/blob/master/CHANGELOG.md) - [Commits](https://github.com/webfactory/ssh-agent/compare/a6f90b1f127823b31d4d4a8d96047790581349bd...e83874834305fe9a4a2997156cb26c5de65a8555) --- updated-dependencies: - dependency-name: webfactory/ssh-agent dependency-version: 0.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94ae018a4a..54100be807 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1456,7 +1456,7 @@ jobs: MILL_PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} MILL_SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} MILL_SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd + - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 with: ssh-private-key: | ${{ secrets.SSH_PRIVATE_KEY_SCALA_CLI }} @@ -1624,7 +1624,7 @@ jobs: - name: Display structure of downloaded files run: ls -R working-directory: artifacts/ - - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd + - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 with: ssh-private-key: | ${{ secrets.SCALA_CLI_PACKAGES_KEY }} From c1d45d7a98d90c9efbb083f27f69f60207576076 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 17 Mar 2026 09:50:20 +0100 Subject: [PATCH 12/20] Correct Native test bridge error message and parse META-INF service file by lines to improve framework discovery (#4185) --- .../scala/scala/build/internal/Runner.scala | 2 +- .../build/tests/FrameworkDiscoveryTests.scala | 49 +++++++++++++++++++ .../NoFrameworkFoundByNativeBridgeError.scala | 4 ++ .../build/testrunner/AsmTestRunner.scala | 12 ++++- 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 modules/build/src/test/scala/scala/build/tests/FrameworkDiscoveryTests.scala create mode 100644 modules/core/src/main/scala/scala/build/errors/NoFrameworkFoundByNativeBridgeError.scala diff --git a/modules/build/src/main/scala/scala/build/internal/Runner.scala b/modules/build/src/main/scala/scala/build/internal/Runner.scala index 2309922eb2..c02df7ee8b 100644 --- a/modules/build/src/main/scala/scala/build/internal/Runner.scala +++ b/modules/build/src/main/scala/scala/build/internal/Runner.scala @@ -539,7 +539,7 @@ object Runner { |""".stripMargin ) - if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError) + if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByNativeBridgeError) else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector) } finally if adapter != null then adapter.close() diff --git a/modules/build/src/test/scala/scala/build/tests/FrameworkDiscoveryTests.scala b/modules/build/src/test/scala/scala/build/tests/FrameworkDiscoveryTests.scala new file mode 100644 index 0000000000..862ed6a203 --- /dev/null +++ b/modules/build/src/test/scala/scala/build/tests/FrameworkDiscoveryTests.scala @@ -0,0 +1,49 @@ +package scala.build.tests + +import java.nio.file.Files + +import scala.build.errors.NoFrameworkFoundByNativeBridgeError +import scala.build.testrunner.AsmTestRunner + +class FrameworkDiscoveryTests extends TestUtil.ScalaCliBuildSuite { + + test( + "findFrameworkServices parses Java ServiceLoader format (trim, skip comments and empty lines)" + ) { + val dir = Files.createTempDirectory("scala-cli-framework-services-") + try { + val servicesDir = dir.resolve("META-INF").resolve("services") + Files.createDirectories(servicesDir) + val serviceFile = servicesDir.resolve("sbt.testing.Framework") + // Content with newlines, comments, and surrounding whitespace + val content = + """munit.Framework + |# comment line + | + | munit.native.Framework + | + |""".stripMargin + Files.writeString(serviceFile, content) + + val found = AsmTestRunner.findFrameworkServices(Seq(dir)) + assertEquals( + found.sorted, + Seq("munit.Framework", "munit.native.Framework"), + clue = "Service file lines should be trimmed; comments and empty lines skipped" + ) + } + finally { + def deleteRecursively(p: java.nio.file.Path): Unit = { + if Files.isDirectory(p) then Files.list(p).forEach(deleteRecursively) + Files.deleteIfExists(p) + } + deleteRecursively(dir) + } + } + + test("NoFrameworkFoundByNativeBridgeError has Native-specific message (not Scala.js)") { + val err = new NoFrameworkFoundByNativeBridgeError + assert(err.getMessage.contains("Scala Native"), clue = "Message should mention Scala Native") + assert(!err.getMessage.contains("Scala.js"), clue = "Message should not mention Scala.js") + } +} diff --git a/modules/core/src/main/scala/scala/build/errors/NoFrameworkFoundByNativeBridgeError.scala b/modules/core/src/main/scala/scala/build/errors/NoFrameworkFoundByNativeBridgeError.scala new file mode 100644 index 0000000000..343a5ff3ae --- /dev/null +++ b/modules/core/src/main/scala/scala/build/errors/NoFrameworkFoundByNativeBridgeError.scala @@ -0,0 +1,4 @@ +package scala.build.errors + +final class NoFrameworkFoundByNativeBridgeError + extends TestError("No framework found by Scala Native test bridge") diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/AsmTestRunner.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/AsmTestRunner.scala index bfffee9d1a..5cd3f75e09 100644 --- a/modules/test-runner/src/main/scala/scala/build/testrunner/AsmTestRunner.scala +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/AsmTestRunner.scala @@ -195,9 +195,19 @@ object AsmTestRunner { .iterator .flatMap(findInClassPath(_, name).iterator) + /** Parse Java ServiceLoader format: one class name per line; # comments and empty lines ignored. + */ + private def parseServiceFileContent(content: String): Seq[String] = + content + .split("[\r\n]+") + .iterator + .map(_.trim) + .filter(line => line.nonEmpty && !line.startsWith("#")) + .toSeq + def findFrameworkServices(classPath: Seq[Path]): Seq[String] = findInClassPath(classPath, "META-INF/services/sbt.testing.Framework") - .map(b => new String(b, StandardCharsets.UTF_8)) + .flatMap(b => parseServiceFileContent(new String(b, StandardCharsets.UTF_8))) .toSeq def findFrameworks( From ab8f4b9d41772c0338a4112e7cc6abf4fd1da944 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 17 Mar 2026 11:50:49 +0100 Subject: [PATCH 13/20] Bump `coursier` to 2.1.25-M24 (#4184) --- .github/scripts/get-latest-cs.sh | 2 +- build.mill | 2 +- mill | 2 +- project/deps/package.mill | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/scripts/get-latest-cs.sh b/.github/scripts/get-latest-cs.sh index 2072b6d0c4..7f54dbde9c 100644 --- a/.github/scripts/get-latest-cs.sh +++ b/.github/scripts/get-latest-cs.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -CS_VERSION="2.1.25-M23" +CS_VERSION="2.1.25-M24" DIR="$(cs get --archive "https://github.com/coursier/coursier/releases/download/v$CS_VERSION/cs-x86_64-pc-win32.zip")" diff --git a/build.mill b/build.mill index 509c58b757..83484c3460 100644 --- a/build.mill +++ b/build.mill @@ -4,7 +4,7 @@ //| - io.github.alexarchambault.mill::mill-native-image-upload:0.2.4 //| - com.goyeau::mill-scalafix::0.6.0 //| - com.lumidion::sonatype-central-client-requests:0.6.0 -//| - io.get-coursier:coursier-launcher_2.13:2.1.25-M23 +//| - io.get-coursier:coursier-launcher_2.13:2.1.25-M24 //| - org.eclipse.jgit:org.eclipse.jgit:7.5.0.202512021534-r package build diff --git a/mill b/mill index 90eb89e197..601a73c0c7 100755 --- a/mill +++ b/mill @@ -2,7 +2,7 @@ # Adapted from -coursier_version="2.1.25-M23" +coursier_version="2.1.25-M24" COMMAND=$@ # necessary for Windows various shell environments diff --git a/project/deps/package.mill b/project/deps/package.mill index e8f98b0216..3d21cca463 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -124,7 +124,7 @@ object Deps { def ammoniteForScala3Lts = ammonite def argonautShapeless = "1.3.1" // jni-utils version may need to be sync-ed when bumping the coursier version - def coursierDefault = "2.1.25-M23" + def coursierDefault = "2.1.25-M24" def coursier = coursierDefault def coursierCli = coursierDefault def coursierPublish = "0.4.4" From 4c529d6b7748a67b3e3cc13ee73b4b15b3355932 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 17 Mar 2026 12:03:16 +0100 Subject: [PATCH 14/20] Add support for `--cross` in the `doc` sub-command (#4183) --- .../scala/scala/cli/commands/doc/Doc.scala | 102 +++++++++++++++--- .../scala/cli/commands/doc/DocOptions.scala | 6 +- .../scala/cli/commands/tests/DocTests.scala | 41 +++++++ .../cli/integration/DocTestDefinitions.scala | 35 ++++++ website/docs/commands/doc.md | 51 ++++++++- website/docs/reference/cli-options.md | 2 +- website/docs/reference/commands.md | 2 +- .../reference/scala-command/cli-options.md | 2 +- .../docs/reference/scala-command/commands.md | 2 +- 9 files changed, 217 insertions(+), 26 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala b/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala index 58bd0852e1..ff6c36d046 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala @@ -9,13 +9,15 @@ import java.io.File import scala.build.* import scala.build.EitherCps.{either, value} +import scala.build.Ops.* import scala.build.compiler.{ScalaCompilerMaker, SimpleScalaCompilerMaker} -import scala.build.errors.BuildException +import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.interactive.InteractiveFileOps import scala.build.internal.Runner import scala.build.options.{BuildOptions, Scope} import scala.cli.CurrentParams import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions} +import scala.cli.commands.util.BuildCommandHelpers import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel} import scala.cli.config.Keys import scala.cli.errors.ScaladocGenerationFailedError @@ -23,7 +25,7 @@ import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils import scala.util.Properties -object Doc extends ScalaCommand[DocOptions] { +object Doc extends ScalaCommand[DocOptions] with BuildCommandHelpers { override def group: String = HelpCommandGroup.Main.toString override def sharedOptions(options: DocOptions): Option[SharedOptions] = Some(options.shared) @@ -52,39 +54,104 @@ object Doc extends ScalaCommand[DocOptions] { configDb.get(Keys.actions).getOrElse(None) ) + val cross = options.compileCross.cross.getOrElse(false) val withTestScope = options.shared.scope.test.getOrElse(false) - Build.build( + val buildResult = Build.build( inputs, initialBuildOptions, compilerMaker, docCompilerMakerOpt, logger, - crossBuilds = false, + crossBuilds = cross, buildTests = withTestScope, partial = None, actionableDiagnostics = actionableDiagnostics ) - .orExit(logger).docBuilds match { + val docBuilds = buildResult.orExit(logger).allDoc + docBuilds match { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } - val res0 = doDoc( - logger, - options.output.filter(_.nonEmpty), - options.force, - successfulBuilds, - args.unparsed, - withTestScope - ) - res0.orExit(logger) + if cross && successfulBuilds.nonEmpty then + doDocCrossBuilds( + logger = logger, + outputOpt = options.output.filter(_.nonEmpty), + force = options.force, + allBuilds = successfulBuilds, + extraArgs = args.unparsed, + withTestScope = withTestScope + ).orExit(logger) + else + doDoc( + logger, + options.output.filter(_.nonEmpty), + options.force, + successfulBuilds, + args.unparsed, + withTestScope + ).orExit(logger) case b if b.exists(bb => !bb.success && !bb.cancelled) => - System.err.println("Compilation failed") + logger.error("Compilation failed") sys.exit(1) case _ => - System.err.println("Build cancelled") + logger.error("Build cancelled") sys.exit(1) } } + /** Determines the output subdirectory name for one cross build when using `--cross`. Used so that + * each Scala version (and optionally platform) gets a distinct directory. + */ + def crossDocSubdirName( + crossParams: CrossBuildParams, + multipleCrossGroups: Boolean, + needsPlatformInSuffix: Boolean + ): String = + if !multipleCrossGroups then "" + else if needsPlatformInSuffix then s"${crossParams.scalaVersion}_${crossParams.platform}" + else crossParams.scalaVersion + + private def doDocCrossBuilds( + logger: Logger, + outputOpt: Option[String], + force: Boolean, + allBuilds: Seq[Build.Successful], + extraArgs: Seq[String], + withTestScope: Boolean + ): Either[BuildException, Unit] = either { + val crossBuildGroups = allBuilds.groupedByCrossParams.toSeq + val multipleCrossGroups = crossBuildGroups.size > 1 + if multipleCrossGroups then + logger.message(s"Generating documentation for ${crossBuildGroups.size} cross builds...") + val defaultName = "scala-doc" + val baseOutputPath = outputOpt.map(p => os.Path(p, Os.pwd)).getOrElse(os.pwd / defaultName) + val platforms = crossBuildGroups.map(_._1.platform).distinct + val needsPlatformInSuffix = platforms.size > 1 + value { + crossBuildGroups + .map { (crossParams, builds) => + if multipleCrossGroups then + logger.message(s"Generating documentation for ${crossParams.asString}...") + val crossSubDir = + Doc.crossDocSubdirName(crossParams, multipleCrossGroups, needsPlatformInSuffix) + val groupOutputOpt = + if crossSubDir.nonEmpty then Some((baseOutputPath / crossSubDir).toString) + else outputOpt.filter(_.nonEmpty).orElse(Some(defaultName)) + doDoc( + logger = logger, + outputOpt = groupOutputOpt, + force = force, + builds = builds, + extraArgs = extraArgs, + withTestScope = withTestScope + ) + } + .sequence + .left + .map(CompositeBuildException(_)) + .map(_ => ()) + } + } + private def doDoc( logger: Logger, outputOpt: Option[String], @@ -106,7 +173,7 @@ object Doc extends ScalaCommand[DocOptions] { builds.head.options.interactive.map { interactive => InteractiveFileOps.erasingPath(interactive, printableDest, destPath) { () => val msg = s"$printableDest already exists" - System.err.println(s"Error: $msg. Pass -f or --force to force erasing it.") + logger.error(s"$msg. Pass -f or --force to force erasing it.") sys.exit(1) } } @@ -118,6 +185,7 @@ object Doc extends ScalaCommand[DocOptions] { val docJarPath = value(generateScaladocDirPath(builds, logger, extraArgs, withTestScope)) value(alreadyExistsCheck()) + os.makeDir.all(destPath / os.up) if force then os.copy.over(docJarPath, destPath) else os.copy(docJarPath, destPath) val printableOutput = CommandUtils.printablePath(destPath) diff --git a/modules/cli/src/main/scala/scala/cli/commands/doc/DocOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/doc/DocOptions.scala index ac16c335e0..1cc5005750 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/doc/DocOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/doc/DocOptions.scala @@ -4,7 +4,9 @@ import caseapp.* import caseapp.core.help.Help import scala.cli.ScalaCli.fullRunnerName -import scala.cli.commands.shared.{HasSharedOptions, HelpGroup, HelpMessages, SharedOptions} +import scala.cli.commands.shared.{ + CrossOptions, HasSharedOptions, HelpGroup, HelpMessages, SharedOptions +} import scala.cli.commands.tags // format: off @@ -12,6 +14,8 @@ import scala.cli.commands.tags final case class DocOptions( @Recurse shared: SharedOptions = SharedOptions(), + @Recurse + compileCross: CrossOptions = CrossOptions(), @Group(HelpGroup.Doc.toString) @Tag(tags.must) @HelpMessage("Set the destination path") diff --git a/modules/cli/src/test/scala/cli/commands/tests/DocTests.scala b/modules/cli/src/test/scala/cli/commands/tests/DocTests.scala index 3835125d3f..62b8fffb15 100644 --- a/modules/cli/src/test/scala/cli/commands/tests/DocTests.scala +++ b/modules/cli/src/test/scala/cli/commands/tests/DocTests.scala @@ -2,11 +2,52 @@ package scala.cli.commands.tests import com.eed3si9n.expecty.Expecty.assert as expect +import scala.build.CrossBuildParams import scala.build.internal.Constants import scala.cli.commands.doc.Doc class DocTests extends munit.FunSuite { + test("crossDocSubdirName: single cross group yields empty subdir") { + val params = CrossBuildParams(Constants.defaultScala213Version, "jvm") + expect(Doc.crossDocSubdirName( + params, + multipleCrossGroups = false, + needsPlatformInSuffix = false + ) == "") + expect(Doc.crossDocSubdirName( + params, + multipleCrossGroups = false, + needsPlatformInSuffix = true + ) == "") + } + + test("crossDocSubdirName: multiple groups, single platform uses only Scala version") { + val params = CrossBuildParams(Constants.scala3Lts, "jvm") + expect( + Doc.crossDocSubdirName(params, multipleCrossGroups = true, needsPlatformInSuffix = false) == + Constants.scala3Lts + ) + } + + test("crossDocSubdirName: multiple groups and platforms include platform in suffix") { + val paramsJvm = CrossBuildParams(Constants.defaultScala213Version, "jvm") + val paramsJs = CrossBuildParams(Constants.defaultScala213Version, "js") + val paramsNat = CrossBuildParams(Constants.scala3Lts, "native") + expect( + Doc.crossDocSubdirName(paramsJvm, multipleCrossGroups = true, needsPlatformInSuffix = true) == + s"${Constants.defaultScala213Version}_jvm" + ) + expect( + Doc.crossDocSubdirName(paramsJs, multipleCrossGroups = true, needsPlatformInSuffix = true) == + s"${Constants.defaultScala213Version}_js" + ) + expect( + Doc.crossDocSubdirName(paramsNat, multipleCrossGroups = true, needsPlatformInSuffix = true) == + s"${Constants.scala3Lts}_native" + ) + } + for (javaVersion <- Constants.mainJavaVersions) test(s"correct external mappings for JVM $javaVersion") { val args = Doc.defaultScaladocArgs(Constants.defaultScalaVersion, javaVersion) diff --git a/modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala index 54e8ae6a5a..2cfdb38fd4 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/DocTestDefinitions.scala @@ -154,4 +154,39 @@ abstract class DocTestDefinitions extends ScalaCliSuite with TestScalaVersionArg } } } + + test(s"doc --cross with multiple Scala versions produces doc output per cross") { + val crossScalaVersions = Seq(actualScalaVersion, Constants.scala213, Constants.scala212) + val dest = os.rel / "doc-cross" + TestInputs( + os.rel / "project.scala" -> s"//> using scala ${crossScalaVersions.mkString(" ")}", + os.rel / "Lib.scala" -> + """package mylib + | + |/** A sample class. */ + |class Lib { + | def value: Int = 42 + |} + |""".stripMargin + ).fromRoot { root => + os.proc( + TestUtil.cli, + "doc", + "--cross", + "--power", + extraOptions, + ".", + "-o", + dest + ).call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) + + val baseDocPath = root / dest + expect(os.isDir(baseDocPath)) + crossScalaVersions.foreach { version => + val subDir = baseDocPath / version + expect(os.isDir(subDir)) + expect(os.list(subDir).exists(_.last.endsWith(".html"))) + } + } + } } diff --git a/website/docs/commands/doc.md b/website/docs/commands/doc.md index c05b50a53e..f630ade249 100644 --- a/website/docs/commands/doc.md +++ b/website/docs/commands/doc.md @@ -3,18 +3,20 @@ title: Doc sidebar_position: 18 --- -Scala CLI can generate the API documentation of your Scala 2, Scala 3, and Java projects. It provides features similar to `javadoc`. +Scala CLI can generate the API documentation of your Scala 2, Scala 3, and Java projects. It provides features similar +to `javadoc`. The API documentation is generated in a directory whose files make up a static website: ```scala title=Hello.scala package hello + /** Hello object for running main method */ object Hello { /** - * Main method - * @param args The command line arguments. - **/ + * Main method + * @param args The command line arguments. + * */ def main(args: Array[String]): Unit = println("Hello") } @@ -31,6 +33,47 @@ Wrote Scaladoc to ./scala-doc The output directory `scala-doc` contains the static site files with your documentation. +## Cross-building documentation ⚡️ + +:::caution +The `--cross` option is experimental and requires setting the `--power` option to be used. +You can pass it explicitly or set it globally by running: + + scala-cli config power true + +::: + +Use `--cross` (with `--power`) to build and generate Scaladoc for **every** Scala version and platform combination +configured for your project—the same behavior as `run` and `package` with `--cross`. This is useful when you have +multiple Scala versions or platforms and want documentation for each. + +Example: a library that supports both Scala 2.13 and 3.3 LTS: + +```scala title=Example.scala +//> using scala 2.13 3.3.7 +package lib + +/** Example class for cross-built documentation. */ +class Example { + /** Returns a greeting. */ + def greet: String = "Hello" +} +``` + +When `--cross` produces multiple cross builds, the output directory is split into one subdirectory per combination: by +default a subdirectory per Scala version (e.g. `doc-out/2.13.18`, `doc-out/3.3.7`), and when targeting multiple +platforms, each subdirectory name includes the platform (e.g. `doc-out/3.3.7_jvm`). This avoids overwriting docs from +different builds. + +```bash +scala-cli --power doc --cross . -o doc-out +# Wrote Scaladoc to doc-out/2.13.18 +# Wrote Scaladoc to doc-out/3.3.7 +``` + +Without `--cross`, only a single build (the default Scala version and platform) is documented and written to the given +output path. + After opening the generated static documentation (you have to open `scala-doc/index.html` in your browser), you will see the generated scaladoc documentation. The following screen shows the definition of the `main` method: diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index b104a3cc79..5a764875ef 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -246,7 +246,7 @@ Disable using the network to download artifacts, use the local cache only Available in commands: -[`compile`](./commands.md#compile), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`compile`](./commands.md#compile), [`doc`](./commands.md#doc), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index 8b5b788835..3b375f4054 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -95,7 +95,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/doc -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [doc](./cli-options.md#doc-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [doc](./cli-options.md#doc-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) ## export diff --git a/website/docs/reference/scala-command/cli-options.md b/website/docs/reference/scala-command/cli-options.md index 5c7e54b124..2c2f7e14e4 100644 --- a/website/docs/reference/scala-command/cli-options.md +++ b/website/docs/reference/scala-command/cli-options.md @@ -200,7 +200,7 @@ Force overwriting values for key Available in commands: -[`compile`](./commands.md#compile), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`compile`](./commands.md#compile), [`doc`](./commands.md#doc), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) diff --git a/website/docs/reference/scala-command/commands.md b/website/docs/reference/scala-command/commands.md index 9d190bca31..0d8583fa93 100644 --- a/website/docs/reference/scala-command/commands.md +++ b/website/docs/reference/scala-command/commands.md @@ -88,7 +88,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/doc -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [doc](./cli-options.md#doc-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [doc](./cli-options.md#doc-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) ### repl From bc4f0b06826ac77e6d7eef2bcd01fe4fffccada4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:39:57 +0100 Subject: [PATCH 15/20] Bump sass in /website in the npm-dependencies group (#4188) Bumps the npm-dependencies group in /website with 1 update: [sass](https://github.com/sass/dart-sass). Updates `sass` from 1.97.3 to 1.98.0 - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.97.3...1.98.0) --- updated-dependencies: - dependency-name: sass dependency-version: 1.98.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- website/package.json | 2 +- website/yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/website/package.json b/website/package.json index a4f98cc740..f47853b8d4 100644 --- a/website/package.json +++ b/website/package.json @@ -28,7 +28,7 @@ "react-dom": "^19.2.4", "react-loadable": "^5.5.0", "react-player": "^3.4.0", - "sass": "^1.97.3", + "sass": "^1.98.0", "search-insights": "^2.17.3", "@svta/cml-cta": "1.0.5", "@svta/cml-structured-field-values": "1.1.2", diff --git a/website/yarn.lock b/website/yarn.lock index 055193af3c..8b7e1fbe4e 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -5631,7 +5631,7 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -immutable@^5.0.2: +immutable@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165" integrity sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A== @@ -8550,13 +8550,13 @@ sass-loader@^16.0.2: dependencies: neo-async "^2.6.2" -sass@^1.97.3: - version "1.97.3" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.97.3.tgz#9cb59339514fa7e2aec592b9700953ac6e331ab2" - integrity sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg== +sass@^1.98.0: + version "1.98.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.98.0.tgz#924ce85a3745ccaccd976262fdc1bc0c13aa8e57" + integrity sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A== dependencies: chokidar "^4.0.0" - immutable "^5.0.2" + immutable "^5.1.5" source-map-js ">=0.6.2 <2.0.0" optionalDependencies: "@parcel/watcher" "^2.4.1" From 7df6c78a4b27663b12ea0938823c355c4b9ff6c3 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 17 Mar 2026 19:16:27 +0100 Subject: [PATCH 16/20] Warn when `.java` & `.scala` sources are used in a mixed compilation with disabled build server (#4181) --- .../src/main/scala/scala/build/Build.scala | 18 +++++ .../scala/build/tests/BuildTestsScalac.scala | 31 ++++++++- .../scala/scala/build/tests/TestInputs.scala | 14 ++-- .../scala/scala/build/tests/TestLogger.scala | 35 ++++++++++ .../integration/CompileTestDefinitions.scala | 66 +++++++++++++------ 5 files changed, 137 insertions(+), 27 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index 89377a0daa..04816c8299 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -1199,6 +1199,24 @@ object Build { ) } + if sources.hasJava && sources.hasScala && options.useBuildServer.contains(false) then { + val javaPaths = sources.paths + .filter(_._1.last.endsWith(".java")) + .map(_._1.toString) ++ + sources.inMemory + .filter(_.generatedRelPath.last.endsWith(".java")) + .map(_.originalPath.fold(identity, _._2.toString)) + val javaPathsList = + javaPaths.map(p => s" $p").mkString(System.lineSeparator()) + logger.message( + s"""$warnPrefix With ${Console.BOLD}--server=false${Console.RESET}, .java files are not compiled to .class files. + |scalac parses .java sources for type information (cross-compilation), but without the build server (Bloop/Zinc) nothing compiles them to bytecode. + |Affected .java files: + |$javaPathsList + |Remove --server=false or compile Java files separately to avoid runtime NoClassDefFoundError.""".stripMargin + ) + } + buildClient.clear() buildClient.setGeneratedSources(scope, generatedSources) diff --git a/modules/build/src/test/scala/scala/build/tests/BuildTestsScalac.scala b/modules/build/src/test/scala/scala/build/tests/BuildTestsScalac.scala index a8052a6303..7d32c6f614 100644 --- a/modules/build/src/test/scala/scala/build/tests/BuildTestsScalac.scala +++ b/modules/build/src/test/scala/scala/build/tests/BuildTestsScalac.scala @@ -1,3 +1,32 @@ package scala.build.tests -class BuildTestsScalac extends BuildTests(server = false) +class BuildTestsScalac extends BuildTests(server = false) { + + test("warn about Java files in mixed compilation with --server=false") { + val recordingLogger = new RecordingLogger() + val inputs = TestInputs( + os.rel / "Side.java" -> + """public class Side { + | public static String message = "Hello"; + |} + |""".stripMargin, + os.rel / "Main.scala" -> + """@main def main() = println(Side.message) + |""".stripMargin + ) + val options = defaultScala3Options.copy(useBuildServer = Some(false)) + inputs.withBuild(options, buildThreads, bloopConfigOpt, logger = Some(recordingLogger)) { + (_, _, maybeBuild) => + assert(maybeBuild.isRight) + val hasWarning = recordingLogger.messages.exists { msg => + msg.contains(".java files are not compiled to .class files") && + msg.contains("--server=false") && + msg.contains("Affected .java files") + } + assert( + hasWarning, + s"Expected warning about Java files with --server=false in: ${recordingLogger.messages.mkString("\n")}" + ) + } + } +} diff --git a/modules/build/src/test/scala/scala/build/tests/TestInputs.scala b/modules/build/src/test/scala/scala/build/tests/TestInputs.scala index d6b34a6df2..45afb7f5fb 100644 --- a/modules/build/src/test/scala/scala/build/tests/TestInputs.scala +++ b/modules/build/src/test/scala/scala/build/tests/TestInputs.scala @@ -8,7 +8,7 @@ import scala.build.compiler.{BloopCompilerMaker, SimpleScalaCompilerMaker} import scala.build.errors.BuildException import scala.build.input.{Inputs, ScalaCliInvokeData} import scala.build.options.{BuildOptions, Scope} -import scala.build.{Build, BuildThreads, Builds} +import scala.build.{Build, BuildThreads, Builds, Logger} import scala.util.Try import scala.util.control.NonFatal @@ -94,7 +94,8 @@ final case class TestInputs( fromDirectory: Boolean = false, buildTests: Boolean = true, actionableDiagnostics: Boolean = false, - skipCreatingSources: Boolean = false + skipCreatingSources: Boolean = false, + logger: Option[Logger] = None )(f: (os.Path, Inputs, Either[BuildException, Builds]) => T): T = withCustomInputs(fromDirectory, None, skipCreatingSources) { (root, inputs) => val compilerMaker = bloopConfigOpt match { @@ -108,13 +109,14 @@ final case class TestInputs( case None => SimpleScalaCompilerMaker("java", Nil) } + val log = logger.getOrElse(TestLogger()) val builds = Build.build( inputs, options, compilerMaker, None, - TestLogger(), + log, crossBuilds = false, buildTests = buildTests, partial = None, @@ -131,7 +133,8 @@ final case class TestInputs( buildTests: Boolean = true, actionableDiagnostics: Boolean = false, scope: Scope = Scope.Main, - skipCreatingSources: Boolean = false + skipCreatingSources: Boolean = false, + logger: Option[Logger] = None )(f: (os.Path, Inputs, Either[BuildException, Build]) => T): T = withBuilds( options, @@ -140,7 +143,8 @@ final case class TestInputs( fromDirectory, buildTests = buildTests, actionableDiagnostics = actionableDiagnostics, - skipCreatingSources = skipCreatingSources + skipCreatingSources = skipCreatingSources, + logger = logger ) { (p, i, builds) => f( diff --git a/modules/build/src/test/scala/scala/build/tests/TestLogger.scala b/modules/build/src/test/scala/scala/build/tests/TestLogger.scala index 35de6eed7e..bd6841d75d 100644 --- a/modules/build/src/test/scala/scala/build/tests/TestLogger.scala +++ b/modules/build/src/test/scala/scala/build/tests/TestLogger.scala @@ -10,8 +10,43 @@ import java.io.PrintStream import scala.build.Logger import scala.build.errors.{BuildException, Diagnostic} import scala.build.internals.FeatureType +import scala.collection.mutable.ListBuffer import scala.scalanative.build as sn +/** Logger that records all message() and log() calls for test assertions. */ +final class RecordingLogger(delegate: Logger = TestLogger()) extends Logger { + val messages: ListBuffer[String] = ListBuffer.empty + + override def error(message: String): Unit = delegate.error(message) + override def message(message: => String): Unit = { + val msg = message + messages += msg + delegate.message(msg) + } + override def log(s: => String): Unit = { + val msg = s + messages += msg + delegate.log(msg) + } + override def log(s: => String, debug: => String): Unit = delegate.log(s, debug) + override def debug(s: => String): Unit = delegate.debug(s) + override def log(diagnostics: Seq[Diagnostic]): Unit = delegate.log(diagnostics) + override def log(ex: BuildException): Unit = delegate.log(ex) + override def debug(ex: BuildException): Unit = delegate.debug(ex) + override def exit(ex: BuildException): Nothing = delegate.exit(ex) + override def coursierLogger(message: String): CacheLogger = delegate.coursierLogger(message) + override def bloopRifleLogger: BloopRifleLogger = delegate.bloopRifleLogger + override def scalaJsLogger: ScalaJsLogger = delegate.scalaJsLogger + override def scalaNativeTestLogger: sn.Logger = delegate.scalaNativeTestLogger + override def scalaNativeCliInternalLoggerOptions: List[String] = + delegate.scalaNativeCliInternalLoggerOptions + override def compilerOutputStream: PrintStream = delegate.compilerOutputStream + override def verbosity: Int = delegate.verbosity + override def experimentalWarning(featureName: String, featureType: FeatureType): Unit = + delegate.experimentalWarning(featureName, featureType) + override def flushExperimentalWarnings: Unit = delegate.flushExperimentalWarnings +} + case class TestLogger(info: Boolean = true, debug: Boolean = false) extends Logger { override def log(diagnostics: Seq[Diagnostic]): Unit = { diagnostics.foreach { d => diff --git a/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala index 2c7f777471..61efd2a41f 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala @@ -62,9 +62,7 @@ abstract class CompileTestDefinitions |""".stripMargin ) - test( - "java files with no using directives should not produce warnings about using directives in multiple files" - ) { + { val inputs = TestInputs( os.rel / "Bar.java" -> """public class Bar {} @@ -73,12 +71,23 @@ abstract class CompileTestDefinitions """public class Foo {} |""".stripMargin ) - - inputs.fromRoot { root => - val warningMessage = "Using directives detected in multiple files" - val output = os.proc(TestUtil.cli, "compile", extraOptions, ".") - .call(cwd = root, stderr = os.Pipe).err.trim() - expect(!output.contains(warningMessage)) + test( + "java files with no using directives should not produce warnings about using directives in multiple files" + ) { + inputs.fromRoot { root => + val warningMessage = "Using directives detected in multiple files" + val output = os.proc(TestUtil.cli, "compile", extraOptions, ".") + .call(cwd = root, stderr = os.Pipe).err.trim() + expect(!output.contains(warningMessage)) + } + } + test("Pure Java with --server=false: no warning about .java files not being compiled") { + inputs.fromRoot { root => + val warningMessage = ".java files are not compiled to .class files" + val output = os.proc(TestUtil.cli, "compile", "--server=false", extraOptions, ".") + .call(cwd = root, stderr = os.Pipe).err.text() + expect(!output.contains(warningMessage)) + } } } @@ -140,7 +149,7 @@ abstract class CompileTestDefinitions } test( - "having target + using directives in files should not produce warnings about using directives in multiple files" + "having target + using directives in files: no using-directives or .java-not-compiled warnings" ) { val inputs = TestInputs( os.rel / "Bar.java" -> @@ -160,14 +169,14 @@ abstract class CompileTestDefinitions val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".") .call(cwd = root).err.trim() expect(!output.contains(warningMessage)) + expect(!output.contains(".java files are not compiled to .class files")) } } - test( - "warn about directives in multiple files" - ) { - val inputs = TestInputs( - os.rel / "Bar.java" -> + { + val javaSourceFile = "Bar.java" + val inputs = TestInputs( + os.rel / javaSourceFile -> """//> using jvm 17 |public class Bar {} |""".stripMargin, @@ -176,12 +185,24 @@ abstract class CompileTestDefinitions |class Foo {} |""".stripMargin ) + test("warn about directives in multiple files") { + inputs.fromRoot { root => + val warningMessage = "Using directives detected in multiple files" + val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".") + .call(cwd = root, stderr = os.Pipe).err.trim() + expect(output.contains(warningMessage)) + } + } - inputs.fromRoot { root => - val warningMessage = "Using directives detected in multiple files" - val output = os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".") - .call(cwd = root, stderr = os.Pipe).err.trim() - expect(output.contains(warningMessage)) + test("mixed .java/.scala: with --server=false warn about .java not compiled") { + inputs.fromRoot { root => + val warningMessage = ".java files are not compiled to .class files" + val output = + os.proc(TestUtil.cli, "--power", "compile", extraOptions, ".", "--server=false") + .call(cwd = root, stderr = os.Pipe).err.trim() + expect(output.contains(warningMessage)) + expect(output.contains(javaSourceFile)) + } } } @@ -699,7 +720,9 @@ abstract class CompileTestDefinitions } } - test("pass java options to scalac when server=false") { + test( + "pass java options to scalac when server=false (Scala-only, no .java-not-compiled warning)" + ) { val inputs = TestInputs( os.rel / "Main.scala" -> """object Main extends App { @@ -721,6 +744,7 @@ abstract class CompileTestDefinitions val out = res.out.text() expect(out.contains("Error occurred during initialization of VM")) expect(out.contains("Too small maximum heap")) + expect(!out.contains(".java files are not compiled to .class files")) } } From 2352850e9169b843592de66eb0bba5f92b15dfea Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 18 Mar 2026 08:40:14 +0100 Subject: [PATCH 17/20] Add AGENTS.md & `agentskills` for directives & integration tests (#4178) --- AGENTS.md | 165 +++++++++++++++++++++++++ agentskills/README.md | 5 + agentskills/adding-directives/SKILL.md | 21 ++++ agentskills/integration-tests/SKILL.md | 19 +++ 4 files changed, 210 insertions(+) create mode 100644 AGENTS.md create mode 100644 agentskills/README.md create mode 100644 agentskills/adding-directives/SKILL.md create mode 100644 agentskills/integration-tests/SKILL.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..84d41dd48d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,165 @@ +# AGENTS.md — Guidance for AI agents contributing to Scala CLI + +Short reference for AI agents. For task-specific guidance (directives, integration tests), load skills from * +*[agentskills/](agentskills/)** when relevant. + +> **LLM Policy**: All AI-assisted contributions must comply with the +> [LLM usage policy](https://github.com/scala/scala3/blob/HEAD/LLM_POLICY.md). The contributor (human) is responsible +> for every line. State LLM usage in the PR description. See [LLM_POLICY.md](LLM_POLICY.md). + +## Human-facing docs + +- **[DEV.md](DEV.md)** — Setup, run from source, tests, launchers, GraalVM. +- **[CONTRIBUTING.md](CONTRIBUTING.md)** — PR workflow, formatting, reference doc generation. +- **[INTERNALS.md](INTERNALS.md)** — Modules, `Inputs → Sources → Build`, preprocessing. + +## Build system + +The project uses [Mill](https://mill-build.org/). Mill launchers ship with the repo (`./mill`). JVM 17 required. +Cross-compilation: default `Scala.defaultInternal`; `[]` = default version, `[_]` = all. + +### Key build files + +| File | Purpose | +|---------------------------------|------------------------------------------------------------------------------------------| +| `build.mill` | Root build definition: all module declarations, CI helper tasks, integration test wiring | +| `project/deps/package.mill` | Dependency versions and definitions (`Deps`, `Scala`, `Java` objects) | +| `project/settings/package.mill` | Shared traits, utils (`HasTests`, `CliLaunchers`, `FormatNativeImageConf`, etc.) | +| `project/publish/package.mill` | Publishing settings | +| `project/website/package.mill` | Website-related build tasks | + +### Essential commands + +```bash +./mill -i clean # Clean Mill context +./mill -i scala …args… # Run Scala CLI from source +./mill -i __.compile # Compile everything +./mill -i unitTests # All unit tests +./mill -i 'build-module[].test' # Unit tests for a specific module +./mill -i 'build-module[].test' 'scala.build.tests.BuildTestsScalac.*' # Filter by suite +./mill -i 'build-module[].test' 'scala.build.tests.BuildTests.simple' # Single test by name +./mill -i integration.test.jvm # Integration tests (JVM launcher) +./mill -i integration.test.jvm 'scala.cli.integration.RunTestsDefault.*' # Integration: filter by suite +./mill -i 'generate-reference-doc[]'.run # Regenerate reference docs +./mill -i __.fix # Fix import ordering (scalafix) +scala-cli fmt . # Format all code (scalafmt) +``` + +## Project modules + +Modules live under `modules/`. The dependency graph flows roughly as: + +``` +specification-level → config → core → options → directives → build-module → cli +``` + +### Module overview + +The list below may not be exhaustive — check `modules/` and `build.mill` for the current set. + +| Module | Purpose | +|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| `specification-level` | Defines `SpecificationLevel` (MUST / SHOULD / IMPLEMENTATION / RESTRICTED / EXPERIMENTAL) for SIP-46 compliance. | +| `config` | Scala CLI configuration keys and persistence. | +| `build-macros` | Compile-time macros (e.g. `EitherCps`). | +| `core` | Core types: `Inputs`, `Sources`, build constants, Bloop integration, JVM/JS/Native tooling. | +| `options` | `BuildOptions`, `SharedOptions`, and all option types. | +| `directives` | Using directive handlers — the bridge between `//> using` directives and `BuildOptions`. | +| `build-module` (aliased from `build` in mill) | The main build pipeline: preprocessing, compilation, post-processing. Most business logic lives here. | +| `cli` | Command definitions, argument parsing (CaseApp), the `ScalaCli` entry point. Packaged as the native image. | +| `runner` | Lightweight app that runs a main class and pretty-prints exceptions. Fetched at runtime. | +| `test-runner` | Discovers and runs test frameworks/suites. Fetched at runtime. | +| `tasty-lib` | Edits file names in `.tasty` files for source mapping. | +| `scala-cli-bsp` | BSP protocol types. | +| `integration` | Integration tests (see dedicated section below). | +| `docs-tests` | Tests that validate documentation (`Sclicheck`). | +| `generate-reference-doc` | Generates reference documentation from CLI option/directive metadata. | + +## Specification levels + +Every command, CLI option, and using directive has a `SpecificationLevel`. This is central to how features are exposed. + +| Level | In the Scala Runner spec? | Available without `--power`? | Stability | +|------------------|---------------------------|------------------------------|---------------------------------| +| `MUST` | Yes | Yes | Stable | +| `SHOULD` | Yes | Yes | Stable | +| `IMPLEMENTATION` | No | Yes | Stable | +| `RESTRICTED` | No | No (requires `--power`) | Stable | +| `EXPERIMENTAL` | No | No (requires `--power`) | Unstable — may change/disappear | + +**New features contributed by agents should generally be marked `EXPERIMENTAL`** unless the maintainers explicitly +request otherwise. This applies to new sub-commands, options, and directives alike. + +The specification level is set via: + +- **Directives**: `@DirectiveLevel(SpecificationLevel.EXPERIMENTAL)` annotation on the directive case class. +- **CLI options**: `@Tag(tags.experimental)` annotation on option fields. +- **Commands**: Override `scalaSpecificationLevel` in the command class. + +## Using directives + +Using directives are in-source configuration comments: + +```scala +//> using scala 3 +//> using dep com.lihaoyi::os-lib:0.11.4 +//> using test.dep org.scalameta::munit::1.1.1 +``` + +Directives are parsed by `using_directives`, then `ExtractedDirectives` → `DirectivesPreprocessor` → `BuildOptions`/ +`BuildRequirements`. **CLI options override directive values.** To add a new directive, +see [agentskills/adding-directives/](agentskills/adding-directives/SKILL.md). + +## Testing + +> **Every contribution that changes logic must include automated tests.** A PR without tests for +> new or changed behavior will not be accepted. If testing is truly infeasible, explain why in the +> PR description — but this should be exceptional. + +> **Unit tests are always preferred over integration tests.** Unit tests are faster, more reliable, +> easier to debug, and cheaper to run on CI. Only add integration tests when the behavior cannot be +> adequately verified at the unit level (e.g. end-to-end CLI invocation, launcher-specific behavior, +> cross-process interactions). + +> **Always re-run and verify tests locally before submitting.** After any logic change, run the +> relevant test suites on your machine and confirm they pass. Do not rely on CI to catch failures — +> CI resources are shared, and broken PRs waste maintainer time. + +**Unit tests**: munit, in each module’s `test` submodule. Run commands above; add tests in `modules/build/.../tests/` or +`modules/cli/src/test/scala/`. Prefer unit over integration. + +**Integration tests**: `modules/integration/`; they run the CLI as a subprocess. +See [agentskills/integration-tests/](agentskills/integration-tests/SKILL.md) for structure and how to add tests. + +## Pre-PR checklist + +1. Code compiles: `./mill -i __.compile` +2. Tests added and passing locally (unit tests first, integration if needed) +3. Code formatted: `scala-cli fmt .` +4. Imports ordered: `./mill -i __.fix` +5. Reference docs regenerated (if options/directives changed): `./mill -i 'generate-reference-doc[]'.run` +6. PR template filled, LLM usage stated + +## Code style + +Code style is enforced. + +**Scala 3**: Prefer `if … then … else`, `for … do`/`yield`, `enum`, `extension`, `given`/`using`, braceless blocks, +top-level defs. Use union/intersection types when they simplify signatures. Always favor Scala 3 idiomatic syntax. + +**Functional**: Prefer `val`, immutable collections, `case class`.copy(). Prefer expressions over statements; prefer +`map`/`flatMap`/`fold`/`for`-comprehensions over loops. Use `@tailrec` for tail recursion. Avoid `null`; use `Option`/ +`Either`/`EitherCps` (build-macros). Keep functions small; extract helpers. + +**No duplication**: Extract repeated logic into shared traits or utils (`*Options` traits, companion helpers, +`CommandHelpers`, `TestUtil`). Check for existing abstractions before copying. + +**Logging**: Use the project `Logger` only — never `System.err` or `System.out`. Logger respects verbosity (`-v`, `-q`). +Use `logger.message(msg)` (default), `logger.log(msg)` (verbose), `logger.debug(msg)` (debug), `logger.error(msg)` ( +always). In commands: `options.shared.logging.logger`; in build code it is passed in; in tests use `TestLogger`. + +**Mutability**: OK in hot paths or when a Java API requires it; keep scope minimal. + +## Further reference + +[DEV.md](DEV.md), [CONTRIBUTING.md](CONTRIBUTING.md), [INTERNALS.md](INTERNALS.md). diff --git a/agentskills/README.md b/agentskills/README.md new file mode 100644 index 0000000000..e79a6b9e40 --- /dev/null +++ b/agentskills/README.md @@ -0,0 +1,5 @@ +# Agent skills (Scala CLI) + +This directory holds **agent skills** — task-specific guidance loaded on demand by AI coding agents. The layout is tool-agnostic; Cursor, Claude Code, Codex, and other tools that support a standard skill directory can use this (e.g. by configuring or symlinking to `.agents/skills/` if required). + +Each subdirectory contains a `SKILL.md` with frontmatter and instructions. See [agentskills/agentskills](https://github.com/agentskills/agentskills) for the open standard. diff --git a/agentskills/adding-directives/SKILL.md b/agentskills/adding-directives/SKILL.md new file mode 100644 index 0000000000..54776d1134 --- /dev/null +++ b/agentskills/adding-directives/SKILL.md @@ -0,0 +1,21 @@ +--- +name: scala-cli-adding-directives +description: Add or change using directives in Scala CLI. Use when adding a new //> using directive, registering a directive handler, or editing directive preprocessing. +--- + +# Adding a new directive (Scala CLI) + +1. **Create a case class** in `modules/directives/src/main/scala/scala/build/preprocessing/directives/` extending one of: + - `HasBuildOptions` — produces `BuildOptions` directly + - `HasBuildOptionsWithRequirements` — produces `BuildOptions` with scoped requirements (e.g. `test.dep`) + - `HasBuildRequirements` — produces `BuildRequirements` (for `//> require`) + +2. **Annotate**: `@DirectiveLevel(SpecificationLevel.EXPERIMENTAL)`, `@DirectiveDescription("…")`, `@DirectiveUsage("…")`, `@DirectiveExamples("…")`, `@DirectiveName("key")` on fields. + +3. **Companion**: `val handler: DirectiveHandler[YourDirective] = DirectiveHandler.derive` + +4. **Register** in `modules/build/.../DirectivesPreprocessingUtils.scala` in the right list: `usingDirectiveHandlers`, `usingDirectiveWithReqsHandlers`, or `requireDirectiveHandlers`. + +5. **Regenerate reference docs**: `./mill -i 'generate-reference-doc[]'.run` + +CLI options always override directive values when both set the same thing. diff --git a/agentskills/integration-tests/SKILL.md b/agentskills/integration-tests/SKILL.md new file mode 100644 index 0000000000..ec77e99465 --- /dev/null +++ b/agentskills/integration-tests/SKILL.md @@ -0,0 +1,19 @@ +--- +name: scala-cli-integration-tests +description: Add or run Scala CLI integration tests. Use when adding integration tests, debugging RunTests/CompileTests/etc., or working in modules/integration. +--- + +# Integration tests (Scala CLI) + +**Location**: `modules/integration/`. Tests invoke the CLI as an external process. + +**Run**: `./mill -i integration.test.jvm` (all). Filter: `./mill -i integration.test.jvm 'scala.cli.integration.RunTestsDefault.*'` or by test name. Native: `./mill -i integration.test.native`. + +**Structure**: `*TestDefinitions.scala` (abstract, holds test logic) → `*TestsDefault`, `*Tests213`, etc. (concrete, Scala version trait). Traits: `TestDefault`, `Test212`, `Test213`, `Test3Lts`, `Test3NextRc`. + +**Adding a test**: +1. Open the right `*TestDefinitions` (e.g. `RunTestDefinitions` for `run`). +2. Add `test("description") { … }` using `TestInputs(os.rel / "Main.scala" -> "…").fromRoot { root => … }` and `os.proc(TestUtil.cli, "run", …).call(cwd = root)`. +3. Assert on stdout/stderr. + +**Helpers**: `TestInputs(...).fromRoot`, `TestUtil.cli`. Test groups (CI): `SCALA_CLI_IT_GROUP=1..5`; see `modules/integration/` for group mapping. From 5ed4f795adcd4e5a55797b9102bd633ee83dccb1 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 18 Mar 2026 08:49:02 +0100 Subject: [PATCH 18/20] Add support for local `.m2` in `publish local` (#4179) --- .../scala/cli/commands/publish/Publish.scala | 17 ++- .../cli/commands/publish/PublishLocal.scala | 11 ++ .../publish/PublishLocalOptions.scala | 16 ++- .../cli/commands/publish/RepoParams.scala | 17 +++ .../PublishLocalTestDefinitions.scala | 110 ++++++++++++++++++ website/docs/reference/cli-options.md | 18 +++ website/docs/reference/commands.md | 4 +- 7 files changed, 189 insertions(+), 4 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index efe1f1e7ab..2f253e011d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -256,6 +256,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { workingDir, ivy2HomeOpt, publishLocal = false, + m2Local = false, + m2HomeOpt = None, forceSigningExternally = options.signingCli.forceSigningExternally.getOrElse(false), parallelUpload = options.parallelUpload, options.watch.watch, @@ -279,6 +281,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { workingDir: => os.Path, ivy2HomeOpt: Option[os.Path], publishLocal: Boolean, + m2Local: Boolean = false, + m2HomeOpt: Option[os.Path] = None, forceSigningExternally: Boolean, parallelUpload: Option[Boolean], watch: Boolean, @@ -309,6 +313,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { workingDir = workingDir, ivy2HomeOpt = ivy2HomeOpt, publishLocal = publishLocal, + m2Local = m2Local, + m2HomeOpt = m2HomeOpt, logger = logger, allowExit = false, forceSigningExternally = forceSigningExternally, @@ -342,6 +348,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { workingDir = workingDir, ivy2HomeOpt = ivy2HomeOpt, publishLocal = publishLocal, + m2Local = m2Local, + m2HomeOpt = m2HomeOpt, logger = logger, allowExit = true, forceSigningExternally = forceSigningExternally, @@ -363,6 +371,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { workingDir: os.Path, ivy2HomeOpt: Option[os.Path], publishLocal: Boolean, + m2Local: Boolean, + m2HomeOpt: Option[os.Path], logger: Logger, allowExit: Boolean, forceSigningExternally: Boolean, @@ -419,6 +429,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { workingDir = workingDir, ivy2HomeOpt = ivy2HomeOpt, publishLocal = publishLocal, + m2Local = m2Local, + m2HomeOpt = m2HomeOpt, logger = logger, forceSigningExternally = forceSigningExternally, parallelUpload = parallelUpload, @@ -687,6 +699,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { workingDir: os.Path, ivy2HomeOpt: Option[os.Path], publishLocal: Boolean, + m2Local: Boolean, + m2HomeOpt: Option[os.Path], logger: Logger, forceSigningExternally: Boolean, parallelUpload: Option[Boolean], @@ -741,7 +755,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { lazy val es = Executors.newSingleThreadScheduledExecutor(Util.daemonThreadFactory("publish-retry")) - if publishLocal then RepoParams.ivy2Local(ivy2HomeOpt) + if publishLocal && m2Local then RepoParams.m2Local(m2HomeOpt) + else if publishLocal then RepoParams.ivy2Local(ivy2HomeOpt) else value { publishOptions.contextual(isCi).repository match { diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala index d81158c7ff..c91ac6c241 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala @@ -35,6 +35,11 @@ object PublishLocal extends ScalaCommand[PublishLocalOptions] { Publish.maybePrintLicensesAndExit(options.publishParams) Publish.maybePrintChecksumsAndExit(options.sharedPublish) + if options.m2 && options.sharedPublish.ivy2Home.exists(_.trim.nonEmpty) then { + logger.error("--m2 and --ivy2-home are mutually exclusive.") + sys.exit(1) + } + val baseOptions = buildOptionsOrExit(options) val inputs = options.shared.inputs(args.all).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) @@ -71,6 +76,10 @@ object PublishLocal extends ScalaCommand[PublishLocalOptions] { .filter(_.trim.nonEmpty) .map(os.Path(_, os.pwd)) + val m2HomeOpt = options.m2Home + .filter(_.trim.nonEmpty) + .map(os.Path(_, os.pwd)) + Publish.doRun( inputs = inputs, logger = logger, @@ -81,6 +90,8 @@ object PublishLocal extends ScalaCommand[PublishLocalOptions] { workingDir = workingDir, ivy2HomeOpt = ivy2HomeOpt, publishLocal = true, + m2Local = options.m2, + m2HomeOpt = m2HomeOpt, forceSigningExternally = options.scalaSigning.forceSigningExternally.getOrElse(false), parallelUpload = Some(true), watch = options.watch.watch, diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala index e41bca8793..dc4409ee0d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala @@ -4,6 +4,7 @@ import caseapp.* import scala.cli.commands.pgp.PgpScalaSigningOptions import scala.cli.commands.shared.* +import scala.cli.commands.tags // format: off @HelpMessage(PublishLocalOptions.helpMessage, "", PublishLocalOptions.detailedHelpMessage) @@ -22,6 +23,19 @@ final case class PublishLocalOptions( sharedPublish: SharedPublishOptions = SharedPublishOptions(), @Recurse scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions(), + + @Group(HelpGroup.Publishing.toString) + @HelpMessage("Publish to the local Maven repository (defaults to ~/.m2/repository) instead of Ivy2 local") + @Name("mavenLocal") + @Tag(tags.experimental) + @Tag(tags.inShortHelp) + m2: Boolean = false, + + @Group(HelpGroup.Publishing.toString) + @HelpMessage("Set the local Maven repository path (defaults to ~/.m2/repository)") + @ValueDescription("path") + @Tag(tags.experimental) + m2Home: Option[String] = None, ) extends HasSharedOptions with HasSharedWatchOptions // format: on @@ -29,7 +43,7 @@ object PublishLocalOptions { implicit lazy val parser: Parser[PublishLocalOptions] = Parser.derive implicit lazy val help: Help[PublishLocalOptions] = Help.derive val cmdName = "publish local" - private val helpHeader = "Publishes build artifacts to the local Ivy2 repository." + private val helpHeader = "Publishes build artifacts to the local Ivy2 or Maven repository." private val docWebsiteSuffix = "publishing/publish-local" val helpMessage: String = s"""$helpHeader diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala index 3056716e2e..e49aa9d8c7 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala @@ -80,6 +80,8 @@ object RepoParams { repo match { case "ivy2-local" => RepoParams.ivy2Local(ivy2HomeOpt) + case "m2-local" | "maven-local" => + RepoParams.m2Local(None) case "sonatype" | "central" | "maven-central" | "mvn-central" => logger.message(s"Using Portal OSSRH Staging API: $sonatypeOssrhStagingApiBase") RepoParams.centralRepo( @@ -245,4 +247,19 @@ object RepoParams { ) } + def m2Local(m2HomeOpt: Option[os.Path]): RepoParams = { + val base = m2HomeOpt.getOrElse(os.home / ".m2" / "repository") + RepoParams( + repo = PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)), + targetRepoOpt = None, + hooks = Hooks.dummy, + isIvy2LocalLike = false, + defaultParallelUpload = true, + supportsSig = true, + acceptsChecksums = true, + shouldSign = false, + shouldAuthenticate = false + ) + } + } diff --git a/modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala index d26469eeb2..94fbc8f9af 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala @@ -349,6 +349,116 @@ abstract class PublishLocalTestDefinitions extends ScalaCliSuite with TestScalaV } } + test("publish local --m2") { + val expectedFiles = { + val modName = s"${PublishTestInputs.testName}_$testedPublishedScalaVersion" + val base = + os.rel / PublishTestInputs.testOrg.split('.').toSeq / modName / testPublishVersion + val baseFiles = Seq( + base / s"$modName-$testPublishVersion.jar", + base / s"$modName-$testPublishVersion.pom", + base / s"$modName-$testPublishVersion-sources.jar", + base / s"$modName-$testPublishVersion-javadoc.jar" + ) + baseFiles + .flatMap { f => + val md5 = f / os.up / s"${f.last}.md5" + val sha1 = f / os.up / s"${f.last}.sha1" + Seq(f, md5, sha1) + } + .toSet + } + + PublishTestInputs.inputs() + .fromRoot { root => + os.proc( + TestUtil.cli, + "--power", + "publish", + "local", + ".", + "--m2", + "--m2-home", + (root / "m2repo").toString, + extraOptions + ) + .call(cwd = root) + val m2Local = root / "m2repo" + val foundFiles = os.walk(m2Local) + .filter(os.isFile(_)) + .map(_.relativeTo(m2Local)) + .toSet + val missingFiles = expectedFiles -- foundFiles + val unexpectedFiles = foundFiles -- expectedFiles + if (missingFiles.nonEmpty) + pprint.err.log(missingFiles) + if (unexpectedFiles.nonEmpty) + pprint.err.log(unexpectedFiles) + expect(missingFiles.isEmpty) + expect(unexpectedFiles.isEmpty) + } + } + + test("publish local --m2 twice") { + PublishTestInputs.inputs().fromRoot { root => + val m2Repo = root / "m2repo" + val modName = s"${PublishTestInputs.testName}_$testedPublishedScalaVersion" + val jarPath = m2Repo / + PublishTestInputs.testOrg.split('.').toSeq / + modName / testPublishVersion / s"$modName-$testPublishVersion.jar" + + def publishLocal(): os.CommandResult = + os.proc( + TestUtil.cli, + "--power", + "publish", + "local", + ".", + "--m2", + "--m2-home", + m2Repo.toString, + "--working-dir", + os.rel / "work-dir", + extraOptions + ) + .call(cwd = root) + + lazy val depsCp: String = + os.proc( + TestUtil.cs, + "fetch", + "--classpath", + s"com.lihaoyi:os-lib_$testedPublishedScalaVersion:0.11.3" + ) + .call(cwd = root) + .out.trim() + + def output(): String = + os.proc( + "java", + "-cp", + s"$jarPath${java.io.File.pathSeparator}$depsCp", + "Project" + ) + .call(cwd = root) + .out.trim() + + val expectedMessage1 = "Hello" + val expectedMessage2 = "olleH" + publishLocal() + val output1 = output() + expect(output1 == expectedMessage1) + + os.write.over( + root / PublishTestInputs.projectFilePath, + PublishTestInputs.projFile(expectedMessage2) + ) + publishLocal() + val output2 = output() + expect(output2 == expectedMessage2) + } + } + if actualScalaVersion.startsWith("3") then test("publish local with compileOnly.dep") { TestInputs( diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 5a764875ef..6d0ec34942 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -1088,6 +1088,24 @@ Proceed as if publishing, but do not upload / write artifacts to the remote repo ### `--parallel-upload` [Internal] +## Publish local options + +Available in commands: + +[`publish local`](./commands.md#publish-local) + + + +### `--m2` + +Aliases: `--maven-local` + +Publish to the local Maven repository (defaults to ~/.m2/repository) instead of Ivy2 local + +### `--m2-home` + +Set the local Maven repository path (defaults to ~/.m2/repository) + ## Publish params options Available in commands: diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index 3b375f4054..64ef81f38d 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -261,7 +261,7 @@ Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [c ## publish local -Publishes build artifacts to the local Ivy2 repository. +Publishes build artifacts to the local Ivy2 or Maven repository. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/publishing/publish-local @@ -269,7 +269,7 @@ The `publish-local` sub-command is experimental. Please bear in mind that non-ideal user experience should be expected. If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [pgp scala signing](./cli-options.md#pgp-scala-signing-options), [power](./cli-options.md#power-options), [publish](./cli-options.md#publish-options), [publish params](./cli-options.md#publish-params-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [pgp scala signing](./cli-options.md#pgp-scala-signing-options), [power](./cli-options.md#power-options), [publish](./cli-options.md#publish-options), [publish local](./cli-options.md#publish-local-options), [publish params](./cli-options.md#publish-params-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## publish setup From 039e6e1a37c8685e86317116906afc47f3f71f72 Mon Sep 17 00:00:00 2001 From: "v.karamyshev" Date: Fri, 6 Mar 2026 11:51:17 +0500 Subject: [PATCH 19/20] Add WASM support: --wasm flag with Node.js and Deno runtimes Implements scala-cli issue #3316: integrate WebAssembly with Scala CLI. - `--wasm` CLI flag and `//> using wasm` directive to enable WASM output - `--wasm-runtime ` option and `//> using wasmRuntime` directive Supported values: node (default), deno - `--deno-version`, `--wasmtime-version`, `--wasmer-version` options and corresponding directives for pinning runtime versions - **Node.js** (default): runs Scala.js WASM output with `--experimental-wasm-exnref` flag, requires Node.js >= 22 - **Deno**: runs Scala.js WASM output --- build.mill | 2 + .../scala/scala/build/internal/Runner.scala | 146 ++++- .../DirectivesPreprocessingUtils.scala | 3 +- .../scala/cli/commands/fix/BuiltInRules.scala | 1 + .../scala/scala/cli/commands/run/Run.scala | 514 +++++++++++------- .../cli/commands/shared/HelpGroups.scala | 3 +- .../cli/commands/shared/SharedOptions.scala | 41 +- .../cli/commands/shared/WasmOptions.scala | 32 ++ .../cli/internal/WasmRuntimeDownloader.scala | 104 ++++ .../build/errors/DenoNotFoundError.scala | 5 + .../errors/UnsupportedWasmRuntimeError.scala | 3 + .../build/preprocessing/directives/Wasm.scala | 52 ++ .../RunScalaJsTestDefinitions.scala | 277 +++++++++- .../scala/build/options/BuildOptions.scala | 1 + .../scala/build/options/WasmOptions.scala | 26 + .../scala/build/options/WasmRuntime.scala | 54 ++ website/docs/reference/cli-options.md | 26 + website/docs/reference/directives.md | 21 + 18 files changed, 1086 insertions(+), 225 deletions(-) create mode 100644 modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala create mode 100644 modules/cli/src/main/scala/scala/cli/internal/WasmRuntimeDownloader.scala create mode 100644 modules/core/src/main/scala/scala/build/errors/DenoNotFoundError.scala create mode 100644 modules/core/src/main/scala/scala/build/errors/UnsupportedWasmRuntimeError.scala create mode 100644 modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala create mode 100644 modules/options/src/main/scala/scala/build/options/WasmOptions.scala create mode 100644 modules/options/src/main/scala/scala/build/options/WasmRuntime.scala diff --git a/build.mill b/build.mill index 83484c3460..db75c17794 100644 --- a/build.mill +++ b/build.mill @@ -520,6 +520,8 @@ trait Core extends ScalaCliCrossSbtModule | def toolkitVersionForNative04 = "${Deps.toolkitVersionForNative04}" | def toolkitVersionForNative05 = "${Deps.toolkitVersionForNative05}" | + | def defaultDenoVersion = "2.1.4" + | | def typelevelOrganization = "${Deps.typelevelToolkit.dep.module.organization.value}" | def typelevelToolkitDefaultVersion = "${Deps.typelevelToolkitVersion}" | def typelevelToolkitMaxScalaNative = "${Deps.Versions.maxScalaNativeForTypelevelToolkit}" diff --git a/modules/build/src/main/scala/scala/build/internal/Runner.scala b/modules/build/src/main/scala/scala/build/internal/Runner.scala index c02df7ee8b..ee774179f3 100644 --- a/modules/build/src/main/scala/scala/build/internal/Runner.scala +++ b/modules/build/src/main/scala/scala/build/internal/Runner.scala @@ -186,6 +186,60 @@ object Runner { run(command, logger, cwd = cwd, extraEnv = extraEnv) } + // Detects the major version of Node.js on PATH; cached for the JVM lifetime (lazy val). + // Returns None if node is not found or version cannot be parsed. + private lazy val nodeMajorVersion: Option[Int] = + try { + val process = new ProcessBuilder("node", "--version") + .redirectErrorStream(true) + .start() + val output = new String(process.getInputStream.readAllBytes()).trim + process.waitFor() + // Node version format: "v22.5.0" -> extract 22 + if (output.startsWith("v")) + output.drop(1).takeWhile(_.isDigit) match { + case s if s.nonEmpty => Some(s.toInt) + case _ => None + } + else None + } + catch { + case _: Exception => None + } + + // Node 24+ (V8 13+) has wasm-exnref enabled by default; older versions need --experimental-wasm-exnref. + private def nodeNeedsWasmFlag: Boolean = + nodeMajorVersion.forall(_ < 24) // true if unknown or < 24 + + // Detects the major version of Deno on PATH; cached for the JVM lifetime (lazy val). + // Returns None if deno is not found or version cannot be parsed. + private lazy val denoMajorVersion: Option[Int] = + try { + val process = new ProcessBuilder("deno", "--version") + .redirectErrorStream(true) + .start() + val output = new String(process.getInputStream.readAllBytes()).trim + process.waitFor() + // Deno version format: "deno 2.1.0 (release, aarch64-apple-darwin)\nv8 13.x\ntypescript 5.x" + // Extract major from first line + val firstLine = output.linesIterator.nextOption().getOrElse("") + val versionStr = firstLine.stripPrefix("deno ").takeWhile(c => c.isDigit || c == '.') + versionStr.takeWhile(_.isDigit) match { + case s if s.nonEmpty => Some(s.toInt) + case _ => None + } + } + catch { + case _: Exception => None + } + + // Deno 2.x+ bundles V8 13+ which has wasm-exnref enabled by default; no flag needed. + private def denoNeedsWasmFlag: Boolean = + denoMajorVersion.flatMap { major => + if (major >= 2) Some(false) // Deno 2.x+ has V8 13+ with wasm-exnref by default + else Some(true) + }.getOrElse(true) // true if unknown + private def endsWithCaseInsensitive(s: String, suffix: String): Boolean = s.length >= suffix.length && s.regionMatches(true, s.length - suffix.length, suffix, 0, suffix.length) @@ -218,11 +272,13 @@ object Runner { def jsCommand( entrypoint: File, args: Seq[String], - jsDom: Boolean = false + jsDom: Boolean = false, + emitWasm: Boolean = false ): Seq[String] = { - val nodePath = findInPath("node").fold("node")(_.toString) - val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args + val nodePath = findInPath("node").fold("node")(_.toString) + val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List("--experimental-wasm-exnref") else Nil + val command = Seq(nodePath) ++ nodeFlags ++ Seq(entrypoint.getAbsolutePath) ++ args if (jsDom) // FIXME We'd need to replicate what JSDOMNodeJSEnv does under-the-hood to get the command in that case. @@ -239,14 +295,16 @@ object Runner { allowExecve: Boolean = false, jsDom: Boolean = false, sourceMap: Boolean = false, - esModule: Boolean = false + esModule: Boolean = false, + emitWasm: Boolean = false ): Either[BuildException, Process] = either { val nodePath: String = value(findInPath("node") .map(_.toString) .toRight(NodeNotFoundError())) + val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List("--experimental-wasm-exnref") else Nil if !jsDom && allowExecve && Execve.available() then { - val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args + val command = Seq(nodePath) ++ nodeFlags ++ Seq(entrypoint.getAbsolutePath) ++ args logger.log( s"Running ${command.mkString(" ")}", @@ -262,12 +320,25 @@ object Runner { ) sys.error("should not happen") } + else if (emitWasm) { + // For WASM mode with ES modules, run node directly instead of NodeJSEnv. + // NodeJSEnv's stdin piping with "-" doesn't work with Input.ESModule. + val command = Seq(nodePath) ++ nodeFlags ++ Seq(entrypoint.getAbsolutePath) ++ args + + logger.log( + s"Running ${command.mkString(" ")}", + " Running" + System.lineSeparator() + + command.iterator.map(_ + System.lineSeparator()).mkString + ) + + new ProcessBuilder(command: _*).inheritIO().start() + } else { val nodeArgs = // Scala.js runs apps by piping JS to node. // If we need to pass arguments, we must first make the piped input explicit // with "-", and we pass the user's arguments after that. - if args.isEmpty then Nil else "-" :: args.toList + nodeFlags ++ (if args.isEmpty then Nil else "-" :: args.toList) val envJs = if jsDom then new JSDOMNodeJSEnv( @@ -304,6 +375,69 @@ object Runner { } } + def denoCommand( + entrypoint: File, + args: Seq[String], + denoPathOpt: Option[String] = None + ): Seq[String] = { + val denoPath = denoPathOpt.getOrElse(findInPath("deno").fold("deno")(_.toString)) + val denoFlags = Seq("run", "--allow-read") + Seq(denoPath) ++ denoFlags ++ Seq(entrypoint.getAbsolutePath) ++ args + } + + def runDeno( + entrypoint: File, + args: Seq[String], + logger: Logger, + allowExecve: Boolean = false, + emitWasm: Boolean = false, + denoPathOpt: Option[String] = None + ): Either[BuildException, Process] = either { + val denoPath: String = denoPathOpt.getOrElse { + value(findInPath("deno") + .map(_.toString) + .toRight(DenoNotFoundError())) + } + val denoFlags = Seq("run", "--allow-read") + val extraEnv = + if (emitWasm && denoNeedsWasmFlag) Map("DENO_V8_FLAGS" -> "--experimental-wasm-exnref") + else Map.empty + + if (allowExecve && Execve.available()) { + val command = Seq(denoPath) ++ denoFlags ++ Seq(entrypoint.getAbsolutePath) ++ args + + logger.log( + s"Running ${command.mkString(" ")}", + " Running" + System.lineSeparator() + + command.iterator.map(_ + System.lineSeparator()).mkString + ) + + logger.debug("execve available") + Execve.execve( + command.head, + "deno" +: command.tail.toArray, + (sys.env ++ extraEnv).toArray.sorted.map { case (k, v) => s"$k=$v" } + ) + sys.error("should not happen") + } + else { + val command = Seq(denoPath) ++ denoFlags ++ Seq(entrypoint.getAbsolutePath) ++ args + + logger.log( + s"Running ${command.mkString(" ")}", + " Running" + System.lineSeparator() + + command.iterator.map(_ + System.lineSeparator()).mkString + ) + + val builder = new ProcessBuilder(command*) + .inheritIO() + val env = builder.environment() + for ((k, v) <- extraEnv) + env.put(k, v) + builder.start() + } + } + def runNative( launcher: File, args: Seq[String], diff --git a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala index 5b439b07fc..dfacd593fa 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala @@ -32,7 +32,8 @@ object DirectivesPreprocessingUtils { directives.ScalaVersion.handler, directives.Sources.handler, directives.Watching.handler, - directives.Tests.handler + directives.Tests.handler, + directives.Wasm.handler ).map(_.mapE(_.buildOptions)) val usingDirectiveWithReqsHandlers diff --git a/modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala b/modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala index 5c34b3368b..a2f92d2e42 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala @@ -387,6 +387,7 @@ object BuiltInRules extends CommandHelpers { JavaHome.handler.keys, ScalaNative.handler.keys, ScalaJs.handler.keys, + Wasm.handler.keys, ScalacOptions.handler.keys, JavaOptions.handler.keys, JavacOptions.handler.keys, diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index 78b53b5bfb..a9fe00ad2e 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -12,13 +12,13 @@ import java.util.concurrent.atomic.AtomicReference import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* -import scala.build.errors.{BuildException, CompositeBuildException} +import scala.build.errors.{BuildException, CompositeBuildException, UnsupportedWasmRuntimeError} import scala.build.input.* import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig} import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.internals.EnvVar -import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, Scope} +import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, Scope, WasmRuntime} import scala.cli.CurrentParams import scala.cli.commands.package0.Package import scala.cli.commands.setupide.SetupIde @@ -28,7 +28,7 @@ import scala.cli.commands.util.BuildCommandHelpers.* import scala.cli.commands.util.{BuildCommandHelpers, RunHadoop, RunSpark} import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil} import scala.cli.config.Keys -import scala.cli.internal.ProcUtil +import scala.cli.internal.{ProcUtil, WasmRuntimeDownloader} import scala.cli.packaging.Library.fullClassPathMaybeAsJar import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils @@ -474,228 +474,332 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { if shouldLogCrossInfo then logger.debug(s"Running build for ${crossBuildParams.asString}") val build = builds.head either { - build.options.platform.value match { - case Platform.JS => - val esModule = - build.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule") - - val linkerConfig = builds.head.options.scalaJsOptions.linkerConfig(logger) - val jsDest = { - val delete = scratchDirOpt.isEmpty - scratchDirOpt.foreach(os.makeDir.all(_)) - os.temp( - dir = scratchDirOpt.orNull, - prefix = "main", - suffix = if esModule then ".mjs" else ".js", - deleteOnExit = delete - ) + val wasmOpts = build.options.wasmOptions + + // Check if WASM mode is requested + if wasmOpts.enabled then { + val runtime = wasmOpts.runtime + + if runtime.isJsBased then { + // JS-based WASM path - uses Scala.js WASM with JavaScript helpers (Node.js or Deno) + val esModule = true // WASM backend uses ES modules + scratchDirOpt.foreach(os.makeDir.all(_)) + val jsDest = os.temp( + dir = scratchDirOpt.orNull, + prefix = "main", + suffix = ".mjs", + deleteOnExit = scratchDirOpt.isEmpty + ) + + // Resolve Deno binary: check PATH first, download if needed + val denoPathOpt: Option[String] = runtime match { + case WasmRuntime.Deno => + val denoCmd = value(WasmRuntimeDownloader.denoCommand( + wasmOpts.finalDenoVersion, + build.options.archiveCache, + logger + )) + Some(denoCmd.head) + case _ => None } - val res = - Package.linkJs( - builds = builds, - dest = jsDest, - mainClassOpt = Some(mainClass), - addTestInitializer = false, - config = linkerConfig, - fullOpt = value(build.options.scalaJsOptions.fullOpt), - noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false), - logger = logger, - scratchDirOpt = scratchDirOpt - ).map { outputPath => - val jsDom = build.options.scalaJsOptions.dom.getOrElse(false) - if showCommand then Left(Runner.jsCommand(outputPath.toIO, args, jsDom = jsDom)) - else { - val process = value { - Runner.runJs( - outputPath.toIO, - args, - logger, - allowExecve = effectiveAllowExecve, - jsDom = jsDom, - sourceMap = build.options.scalaJsOptions.emitSourceMaps, - esModule = esModule - ) - } - process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) - Right((process, None)) - } - } - value(res) - case Platform.Native => - val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false) - val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) = - if setupPython then { - val (exec, libPaths) = value { - val python = value(createPythonInstance().orPythonDetectionError) - val pythonPropertiesOrError = for { - paths <- python.nativeLibraryPaths - executable <- python.executable - } yield (Some(executable), paths) - logger.debug( - s"Python executable and native library paths: $pythonPropertiesOrError" - ) - pythonPropertiesOrError.orPythonDetectionError + + val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) + .copy(emitWasm = true, moduleKind = ScalaJsLinkerConfig.ModuleKind.ESModule) + + val res = Package.linkJs( + builds = builds, + dest = jsDest, + mainClassOpt = Some(mainClass), + addTestInitializer = false, + config = linkerConfig, + fullOpt = value(build.options.scalaJsOptions.fullOpt), + noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false), + logger = logger, + scratchDirOpt = scratchDirOpt + ).map { outputPath => + if showCommand then + runtime match { + case WasmRuntime.Deno => + Left(Runner.denoCommand(outputPath.toIO, args, denoPathOpt = denoPathOpt)) + case _ => + Left(Runner.jsCommand(outputPath.toIO, args, jsDom = false, emitWasm = true)) } - // Putting the workspace in PYTHONPATH, see - // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 - // for context. - (exec, libPaths, pythonPathEnv(builds.head.inputs.workspace)) - } - else - (None, Nil, Map()) - // seems conda doesn't add the lib directory to LD_LIBRARY_PATH (see conda/conda#308), - // which prevents apps from finding libpython for example, so we update it manually here - val libraryPathsEnv = - if pythonLibraryPaths.isEmpty then Map.empty else { - val prependTo = - if Properties.isWin then EnvVar.Misc.path.name - else if Properties.isMac then EnvVar.Misc.dyldLibraryPath.name - else EnvVar.Misc.ldLibraryPath.name - val currentOpt = Option(System.getenv(prependTo)) - val currentEntries = currentOpt - .map(_.split(File.pathSeparator).toSet) - .getOrElse(Set.empty) - val additionalEntries = pythonLibraryPaths.filter(!currentEntries.contains(_)) - if additionalEntries.isEmpty then Map.empty - else { - val newValue = (additionalEntries.iterator ++ currentOpt.iterator).mkString( - File.pathSeparator - ) - Map(prependTo -> newValue) + val process = value { + runtime match { + case WasmRuntime.Deno => + Runner.runDeno( + outputPath.toIO, + args, + logger, + allowExecve = effectiveAllowExecve, + emitWasm = true, + denoPathOpt = denoPathOpt + ) + case _ => + Runner.runJs( + outputPath.toIO, + args, + logger, + allowExecve = effectiveAllowExecve, + jsDom = false, + sourceMap = build.options.scalaJsOptions.emitSourceMaps, + esModule = esModule, + emitWasm = true + ) + } } + process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) + Right((process, None)) } - val programNameEnv = - pythonExecutable.fold(Map.empty)(py => Map("SCALAPY_PYTHON_PROGRAMNAME" -> py)) - val extraEnv = libraryPathsEnv ++ programNameEnv ++ pythonExtraEnv - val maybeResult = withNativeLauncher( - builds, - mainClass, - logger - ) { launcher => - if showCommand then - Left( - extraEnv.toVector.sorted.map { case (k, v) => s"$k=$v" } ++ - Seq(launcher.toString) ++ - args + } + value(res) + } + else { + // Standalone WASM runtimes - not yet supported. + // Scala.js currently produces JS-dependent WASM output. + // Standalone support requires upstream Scala.js changes (scala-js/scala-js#4991). + val runtimeName = runtime.name + val extraNote = runtime match { + case WasmRuntime.Wasmer => + " Note: Wasmer does not yet support WasmGC, which is required for Scala WASM output." + case _ => "" + } + value(Left(new UnsupportedWasmRuntimeError( + s"Standalone WASM runtime '$runtimeName' is not yet supported." + + s"$extraNote" + + " Scala.js currently produces JavaScript-dependent WASM output." + + " Standalone WASM support is tracked at: https://github.com/scala-js/scala-js/issues/4991" + + " Use --wasm-runtime node (default) or --wasm-runtime deno for JS-based WASM execution." + ))) + } + } + else + build.options.platform.value match { + case Platform.JS => + val esModule = + build.options.scalaJsOptions.moduleKindStr.exists(m => + m == "es" || m == "esmodule" ) - else { - val proc = Runner.runNative( - launcher = launcher.toIO, - args = args, - logger = logger, - allowExecve = effectiveAllowExecve, - extraEnv = extraEnv + + val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) + val jsDest = { + val delete = scratchDirOpt.isEmpty + scratchDirOpt.foreach(os.makeDir.all(_)) + os.temp( + dir = scratchDirOpt.orNull, + prefix = "main", + suffix = if esModule then ".mjs" else ".js", + deleteOnExit = delete ) - Right((proc, None)) } - } - value(maybeResult) - case Platform.JVM => - def fwd(s: String): String = s.replace('\\', '/') - def base(s: String): String = fwd(s).replaceAll(".*/", "") - runMode match { - case RunMode.Default => - val sourceFiles = builds.head.inputs.sourceFiles().map { - case s: ScalaFile => fwd(s.path.toString) - case s: Script => fwd(s.path.toString) - case s: MarkdownFile => fwd(s.path.toString) - case s: OnDisk => fwd(s.path.toString) - case s => s.getClass.getName - }.filter(_.nonEmpty).distinct - val sources = sourceFiles.mkString(File.pathSeparator) - val sourceNames = sourceFiles.map(base).mkString(File.pathSeparator) - - val baseJavaProps = build.options.javaOptions.javaOpts.toSeq.map(_.value.value) - ++ Seq(s"-Dscala.sources=$sources", s"-Dscala.source.names=$sourceNames") - val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false) - val (pythonJavaProps, pythonExtraEnv) = - if setupPython then { - val scalapyProps = value { - val python = value(createPythonInstance().orPythonDetectionError) - val propsOrError = python.scalapyProperties - logger.debug(s"Python Java properties: $propsOrError") - propsOrError.orPythonDetectionError - } - val props = scalapyProps.toVector.sorted.map { - case (k, v) => s"-D$k=$v" + val res = + Package.linkJs( + builds = builds, + dest = jsDest, + mainClassOpt = Some(mainClass), + addTestInitializer = false, + config = linkerConfig, + fullOpt = value(build.options.scalaJsOptions.fullOpt), + noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false), + logger = logger, + scratchDirOpt = scratchDirOpt + ).map { outputPath => + val jsDom = build.options.scalaJsOptions.dom.getOrElse(false) + if showCommand then Left(Runner.jsCommand(outputPath.toIO, args, jsDom = jsDom)) + else { + val process = value { + Runner.runJs( + outputPath.toIO, + args, + logger, + allowExecve = effectiveAllowExecve, + jsDom = jsDom, + sourceMap = build.options.scalaJsOptions.emitSourceMaps, + esModule = esModule + ) } - // Putting the workspace in PYTHONPATH, see - // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 - // for context. - (props, pythonPathEnv(build.inputs.workspace)) + process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) + Right((process, None)) } - else - (Nil, Map.empty[String, String]) - val allJavaOpts = pythonJavaProps ++ baseJavaProps - if showCommand then - Left { - Runner.jvmCommand( - build.options.javaHome().value.javaCommand, - allJavaOpts, - builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, - mainClass, - args, - extraEnv = pythonExtraEnv, - useManifest = build.options.notForBloopOptions.runWithManifest, - scratchDirOpt = scratchDirOpt + } + value(res) + case Platform.Native => + val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false) + val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) = + if setupPython then { + val (exec, libPaths) = value { + val python = value(createPythonInstance().orPythonDetectionError) + val pythonPropertiesOrError = for { + paths <- python.nativeLibraryPaths + executable <- python.executable + } yield (Some(executable), paths) + logger.debug( + s"Python executable and native library paths: $pythonPropertiesOrError" ) + pythonPropertiesOrError.orPythonDetectionError } - else { - val proc = Runner.runJvm( - javaCommand = build.options.javaHome().value.javaCommand, - javaArgs = allJavaOpts, - classPath = builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, - mainClass = mainClass, - args = args, - logger = logger, - allowExecve = effectiveAllowExecve, - extraEnv = pythonExtraEnv, - useManifest = build.options.notForBloopOptions.runWithManifest, - scratchDirOpt = scratchDirOpt - ) - Right((proc, None)) + // Putting the workspace in PYTHONPATH, see + // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 + // for context. + (exec, libPaths, pythonPathEnv(builds.head.inputs.workspace)) } - case mode: RunMode.SparkSubmit => - value { - RunSpark.run( - builds = builds, - mainClass = mainClass, - args = args, - submitArgs = mode.submitArgs, - logger = logger, - allowExecve = effectiveAllowExecve, - showCommand = showCommand, - scratchDirOpt = scratchDirOpt - ) + else + (None, Nil, Map()) + // seems conda doesn't add the lib directory to LD_LIBRARY_PATH (see conda/conda#308), + // which prevents apps from finding libpython for example, so we update it manually here + val libraryPathsEnv = + if pythonLibraryPaths.isEmpty then Map.empty + else { + val prependTo = + if Properties.isWin then EnvVar.Misc.path.name + else if Properties.isMac then EnvVar.Misc.dyldLibraryPath.name + else EnvVar.Misc.ldLibraryPath.name + val currentOpt = Option(System.getenv(prependTo)) + val currentEntries = currentOpt + .map(_.split(File.pathSeparator).toSet) + .getOrElse(Set.empty) + val additionalEntries = pythonLibraryPaths.filter(!currentEntries.contains(_)) + if additionalEntries.isEmpty then Map.empty + else { + val newValue = (additionalEntries.iterator ++ currentOpt.iterator).mkString( + File.pathSeparator + ) + Map(prependTo -> newValue) + } } - case mode: RunMode.StandaloneSparkSubmit => - value { - RunSpark.runStandalone( - builds = builds, - mainClass = mainClass, - args = args, - submitArgs = mode.submitArgs, - logger = logger, - allowExecve = effectiveAllowExecve, - showCommand = showCommand, - scratchDirOpt = scratchDirOpt + val programNameEnv = + pythonExecutable.fold(Map.empty)(py => Map("SCALAPY_PYTHON_PROGRAMNAME" -> py)) + val extraEnv = libraryPathsEnv ++ programNameEnv ++ pythonExtraEnv + val maybeResult = withNativeLauncher( + builds, + mainClass, + logger + ) { launcher => + if showCommand then + Left( + extraEnv.toVector.sorted.map { case (k, v) => s"$k=$v" } ++ + Seq(launcher.toString) ++ + args ) - } - case RunMode.HadoopJar => - value { - RunHadoop.run( - builds = builds, - mainClass = mainClass, + else { + val proc = Runner.runNative( + launcher = launcher.toIO, args = args, logger = logger, allowExecve = effectiveAllowExecve, - showCommand = showCommand, - scratchDirOpt = scratchDirOpt + extraEnv = extraEnv ) + Right((proc, None)) } - } - } + } + value(maybeResult) + case Platform.JVM => + def fwd(s: String): String = s.replace('\\', '/') + def base(s: String): String = fwd(s).replaceAll(".*/", "") + runMode match { + case RunMode.Default => + val sourceFiles = builds.head.inputs.sourceFiles().map { + case s: ScalaFile => fwd(s.path.toString) + case s: Script => fwd(s.path.toString) + case s: MarkdownFile => fwd(s.path.toString) + case s: OnDisk => fwd(s.path.toString) + case s => s.getClass.getName + }.filter(_.nonEmpty).distinct + val sources = sourceFiles.mkString(File.pathSeparator) + val sourceNames = sourceFiles.map(base).mkString(File.pathSeparator) + + val baseJavaProps = build.options.javaOptions.javaOpts.toSeq.map(_.value.value) + ++ Seq(s"-Dscala.sources=$sources", s"-Dscala.source.names=$sourceNames") + val setupPython = + build.options.notForBloopOptions.doSetupPython.getOrElse(false) + val (pythonJavaProps, pythonExtraEnv) = + if setupPython then { + val scalapyProps = value { + val python = value(createPythonInstance().orPythonDetectionError) + val propsOrError = python.scalapyProperties + logger.debug(s"Python Java properties: $propsOrError") + propsOrError.orPythonDetectionError + } + val props = scalapyProps.toVector.sorted.map { + case (k, v) => s"-D$k=$v" + } + // Putting the workspace in PYTHONPATH, see + // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 + // for context. + (props, pythonPathEnv(build.inputs.workspace)) + } + else + (Nil, Map.empty[String, String]) + val allJavaOpts = pythonJavaProps ++ baseJavaProps + if showCommand then + Left { + Runner.jvmCommand( + build.options.javaHome().value.javaCommand, + allJavaOpts, + builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, + mainClass, + args, + extraEnv = pythonExtraEnv, + useManifest = build.options.notForBloopOptions.runWithManifest, + scratchDirOpt = scratchDirOpt + ) + } + else { + val proc = Runner.runJvm( + javaCommand = build.options.javaHome().value.javaCommand, + javaArgs = allJavaOpts, + classPath = builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, + mainClass = mainClass, + args = args, + logger = logger, + allowExecve = effectiveAllowExecve, + extraEnv = pythonExtraEnv, + useManifest = build.options.notForBloopOptions.runWithManifest, + scratchDirOpt = scratchDirOpt + ) + Right((proc, None)) + } + case mode: RunMode.SparkSubmit => + value { + RunSpark.run( + builds = builds, + mainClass = mainClass, + args = args, + submitArgs = mode.submitArgs, + logger = logger, + allowExecve = effectiveAllowExecve, + showCommand = showCommand, + scratchDirOpt = scratchDirOpt + ) + } + case mode: RunMode.StandaloneSparkSubmit => + value { + RunSpark.runStandalone( + builds = builds, + mainClass = mainClass, + args = args, + submitArgs = mode.submitArgs, + logger = logger, + allowExecve = effectiveAllowExecve, + showCommand = showCommand, + scratchDirOpt = scratchDirOpt + ) + } + case RunMode.HadoopJar => + value { + RunHadoop.run( + builds = builds, + mainClass = mainClass, + args = args, + logger = logger, + allowExecve = effectiveAllowExecve, + showCommand = showCommand, + scratchDirOpt = scratchDirOpt + ) + } + } + } } } .sequence diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroups.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroups.scala index 8f6099a324..c943c3c904 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroups.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroups.scala @@ -17,7 +17,7 @@ enum HelpGroup: Scala, ScalaJs, ScalaNative, Secret, Signing, SuppressWarnings, SourceGenerator, Test, Uninstall, Update, - Watch, Windows, + Wasm, Watch, Windows, Version override def toString: String = this match @@ -30,6 +30,7 @@ enum HelpGroup: case SuppressWarnings => "Suppress warnings" case SourceGenerator => "Source generator" case ProjectVersion => "Project version" + case Wasm => "WebAssembly" case e => e.productPrefix enum HelpCommandGroup: diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index 6be30799ea..9ddcaa83b4 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -57,6 +57,8 @@ final case class SharedOptions( js: ScalaJsOptions = ScalaJsOptions(), @Recurse native: ScalaNativeOptions = ScalaNativeOptions(), + @Recurse + wasmOptions: WasmOptions = WasmOptions(), @Recurse compilationServer: SharedCompilationServerOptions = SharedCompilationServerOptions(), @Recurse @@ -283,6 +285,16 @@ final case class SharedOptions( ) } + private def buildWasmOptions(opts: WasmOptions): options.WasmOptions = { + import opts._ + options.WasmOptions( + enabled = wasm, + runtime = + wasmRuntime.flatMap(options.WasmRuntime.parse).getOrElse(options.WasmRuntime.default), + denoVersion = denoVersion + ) + } + lazy val scalacOptionsFromFiles: List[String] = scalac.argsFiles.flatMap(argFile => ArgSplitter.splitToArgs(os.read(os.Path(argFile.file, os.pwd))) @@ -307,21 +319,27 @@ final case class SharedOptions( case _ => } val parsedPlatform = platform.map(Platform.normalize).flatMap(Platform.parse) - val platformOpt = value { - (parsedPlatform, js.js, native.native) match { - case (Some(p: Platform.JS.type), _, false) => Right(Some(p)) - case (Some(p: Platform.Native.type), false, _) => Right(Some(p)) - case (Some(p: Platform.JVM.type), false, false) => Right(Some(p)) - case (Some(p), _, _) => - val jsSeq = if (js.js) Seq(Platform.JS) else Seq.empty + // WASM mode requires Scala.js platform for compilation + val wasmEnabled = wasmOptions.wasm + val platformOpt = value { + (parsedPlatform, js.js, native.native, wasmEnabled) match { + case (Some(p: Platform.JS.type), _, false, _) => Right(Some(p)) + case (Some(p: Platform.Native.type), false, _, false) => Right(Some(p)) + case (Some(p: Platform.JVM.type), false, false, false) => Right(Some(p)) + case (Some(p), _, _, _) => + val jsSeq = if (js.js || wasmEnabled) Seq(Platform.JS) else Seq.empty val nativeSeq = if (native.native) Seq(Platform.Native) else Seq.empty val platformsSeq = Seq(p) ++ jsSeq ++ nativeSeq Left(new AmbiguousPlatformError(platformsSeq.distinct.map(_.toString))) - case (_, true, true) => + case (_, true, true, _) => Left(new AmbiguousPlatformError(Seq(Platform.JS.toString, Platform.Native.toString))) - case (_, true, _) => Right(Some(Platform.JS)) - case (_, _, true) => Right(Some(Platform.Native)) - case _ => Right(None) + case (_, _, true, true) => + Left(new AmbiguousPlatformError(Seq(Platform.Native.toString, "WASM (requires JS)"))) + case (_, true, _, _) => Right(Some(Platform.JS)) + case (_, _, _, true) => + Right(Some(Platform.JS)) // WASM requires JS compilation (Scala.js WASM backend) + case (_, _, true, _) => Right(Some(Platform.Native)) + case _ => Right(None) } } val (assumedSourceJars, extraRegularJarsAndClasspath) = @@ -408,6 +426,7 @@ final case class SharedOptions( ), scalaJsOptions = scalaJsOptions(js), scalaNativeOptions = snOpts, + wasmOptions = buildWasmOptions(wasmOptions), javaOptions = value(scala.cli.commands.util.JvmUtils.javaOptions(jvm)), jmhOptions = scala.build.options.JmhOptions( jmhVersion = benchmarking.jmhVersion, diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala new file mode 100644 index 0000000000..d8793afaa2 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala @@ -0,0 +1,32 @@ +package scala.cli.commands.shared + +import caseapp.* +import com.github.plokhotnyuk.jsoniter_scala.core.* +import com.github.plokhotnyuk.jsoniter_scala.macros.* + +import scala.cli.commands.tags + +// format: off +final case class WasmOptions( + @Group(HelpGroup.Scala.toString) + @Tag(tags.experimental) + @HelpMessage("Enable WebAssembly output (Scala.js WASM backend). Uses Node.js by default. To show more options for WASM pass `--help-wasm`") + wasm: Boolean = false, + + @Group(HelpGroup.Wasm.toString) + @Tag(tags.experimental) + @HelpMessage("WASM runtime to use: node (default), deno. Standalone runtimes (wasmtime, wasmedge) planned for future releases.") + wasmRuntime: Option[String] = None, + + @Group(HelpGroup.Wasm.toString) + @Tag(tags.experimental) + @HelpMessage("Version of Deno to use. If Deno is not found on PATH, it will be downloaded automatically.") + denoVersion: Option[String] = None +) +// format: on + +object WasmOptions { + implicit lazy val parser: Parser[WasmOptions] = Parser.derive + implicit lazy val help: Help[WasmOptions] = Help.derive + implicit lazy val jsonCodec: JsonValueCodec[WasmOptions] = JsonCodecMaker.make +} diff --git a/modules/cli/src/main/scala/scala/cli/internal/WasmRuntimeDownloader.scala b/modules/cli/src/main/scala/scala/cli/internal/WasmRuntimeDownloader.scala new file mode 100644 index 0000000000..751e087034 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/internal/WasmRuntimeDownloader.scala @@ -0,0 +1,104 @@ +package scala.cli.internal + +import coursier.cache.ArchiveCache +import coursier.util.Task + +import java.util.Locale + +import scala.build.EitherCps.{either, value} +import scala.build.Logger +import scala.build.errors.BuildException +import scala.build.internal.FetchExternalBinary +import scala.util.Properties + +/** Resolves Deno binary for WASM execution. + * + * Deno is first looked up on the system PATH. If not found, it is downloaded from GitHub releases + * and cached via Coursier's ArchiveCache. + */ +object WasmRuntimeDownloader { + + /** Returns the command to run Deno. + * + * First checks system PATH, otherwise downloads the binary. + */ + def denoCommand( + version: String, + archiveCache: ArchiveCache[Task], + logger: Logger + ): Either[BuildException, Seq[String]] = either { + findOnPath("deno") match { + case Some(path) => + logger.debug(s"Using system deno at: $path") + Seq(path) + case None => + logger.message(s"Deno not found on PATH, downloading v$version...") + val binary = value(fetchDeno(version, archiveCache, logger)) + Seq(binary.toString) + } + } + + /** Find an executable on the system PATH */ + private def findOnPath(name: String): Option[String] = { + val exeName = if (Properties.isWin) s"$name.exe" else name + sys.env.get("PATH").flatMap { pathEnv => + pathEnv.split(java.io.File.pathSeparator).view.map { dir => + val file = new java.io.File(dir, exeName) + if (file.exists() && file.canExecute) Some(file.getAbsolutePath) + else None + }.find(_.isDefined).flatten + } + } + + private def detectOs(win: String, linux: String, mac: String): Either[BuildException, String] = + if (Properties.isWin) Right(win) + else if (Properties.isLinux) Right(linux) + else if (Properties.isMac) Right(mac) + else Left(new WasmRuntimeDownloadError(s"Unsupported OS: ${sys.props("os.name")}")) + + private def detectArch64(x86_64: String, aarch64: String): Either[BuildException, String] = + sys.props("os.arch").toLowerCase(Locale.ROOT) match { + case "amd64" | "x86_64" => Right(x86_64) + case "aarch64" | "arm64" => Right(aarch64) + case other => Left(new WasmRuntimeDownloadError(s"Unsupported architecture: $other")) + } + + /** Fetches Deno binary for the current platform. + * + * Deno releases are at: + * https://github.com/denoland/deno/releases/download/v{version}/deno-{platform}.zip + */ + private def fetchDeno( + version: String, + archiveCache: ArchiveCache[Task], + logger: Logger + ): Either[BuildException, os.Path] = either { + val platform = value(denoPlatform) + val url = s"https://github.com/denoland/deno/releases/download/v$version/deno-$platform.zip" + + val binaryOpt = value { + FetchExternalBinary.fetchLauncher( + url = url, + changing = false, + archiveCache = archiveCache, + logger = logger, + launcherPrefix = "deno", + launcherPathOpt = None, + makeExecutable = true + ) + } + + binaryOpt.getOrElse { + value(Left(new WasmRuntimeDownloadError(s"Could not download Deno v$version for $platform"))) + } + } + + /** Platform suffix for Deno downloads */ + private def denoPlatform: Either[BuildException, String] = either { + val arch = value(detectArch64("x86_64", "aarch64")) + val os = value(detectOs("pc-windows-msvc", "unknown-linux-gnu", "apple-darwin")) + s"$arch-$os" + } +} + +class WasmRuntimeDownloadError(message: String) extends BuildException(message) diff --git a/modules/core/src/main/scala/scala/build/errors/DenoNotFoundError.scala b/modules/core/src/main/scala/scala/build/errors/DenoNotFoundError.scala new file mode 100644 index 0000000000..4566e346a4 --- /dev/null +++ b/modules/core/src/main/scala/scala/build/errors/DenoNotFoundError.scala @@ -0,0 +1,5 @@ +package scala.build.errors + +final class DenoNotFoundError extends BuildException( + "Deno was not found on the PATH. Install Deno from https://deno.land/ or use --wasm-runtime node" + ) diff --git a/modules/core/src/main/scala/scala/build/errors/UnsupportedWasmRuntimeError.scala b/modules/core/src/main/scala/scala/build/errors/UnsupportedWasmRuntimeError.scala new file mode 100644 index 0000000000..b663b6a956 --- /dev/null +++ b/modules/core/src/main/scala/scala/build/errors/UnsupportedWasmRuntimeError.scala @@ -0,0 +1,3 @@ +package scala.build.errors + +final class UnsupportedWasmRuntimeError(message: String) extends BuildException(message) diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala new file mode 100644 index 0000000000..361fcc32ab --- /dev/null +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala @@ -0,0 +1,52 @@ +package scala.build.preprocessing.directives + +import scala.build.Positioned +import scala.build.directives.* +import scala.build.errors.BuildException +import scala.build.options.{BuildOptions, Platform, ScalaOptions, WasmOptions, WasmRuntime} +import scala.cli.commands.SpecificationLevel + +@DirectiveGroupName("WASM options") +@DirectiveExamples("//> using wasm") +@DirectiveExamples("//> using wasmRuntime node") +@DirectiveExamples("//> using wasmRuntime deno") +@DirectiveExamples("//> using denoVersion 2.1.4") +@DirectiveUsage( + "//> using wasm|wasmRuntime|denoVersion _value_", + """ + |`//> using wasm` _true|false_ + | + |`//> using wasm` + | + |`//> using wasmRuntime` _node|deno|wasmtime|wasmedge|wasmer_ + | + |`//> using denoVersion` _value_ + |""".stripMargin +) +@DirectiveDescription("Add WebAssembly options") +@DirectiveLevel(SpecificationLevel.EXPERIMENTAL) +final case class Wasm( + wasm: Option[Boolean] = None, + wasmRuntime: Option[String] = None, + denoVersion: Option[String] = None +) extends HasBuildOptions { + def buildOptions: Either[BuildException, BuildOptions] = { + val parsedRuntime = wasmRuntime.flatMap(WasmRuntime.parse) + val wasmOptions = WasmOptions( + enabled = wasm.getOrElse(false), + runtime = parsedRuntime.getOrElse(WasmRuntime.default), + denoVersion = denoVersion + ) + // When WASM is enabled, force Platform.JS (Scala.js WASM backend requires JS compilation) + val scalaOptions = + if (wasm.getOrElse(false)) + ScalaOptions(platform = Some(Positioned.none(Platform.JS))) + else + ScalaOptions() + Right(BuildOptions(scalaOptions = scalaOptions, wasmOptions = wasmOptions)) + } +} + +object Wasm { + val handler: DirectiveHandler[Wasm] = DirectiveHandler.derive +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala index 4745787924..8436bc2e0f 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala @@ -325,11 +325,286 @@ trait RunScalaJsTestDefinitions { this: RunTestDefinitions => .call(cwd = root).out.trim() val path = absOutDir / "main.wasm" expect(os.exists(path)) + } + } + + test("Run with --wasm flag") { + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """object Hello { + | def main(args: Array[String]): Unit = println("Hello from WASM!") + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + "--wasm", + "--wasm-runtime", + "node", + extraOptions + ).call(cwd = root).out.trim() + expect(output == "Hello from WASM!") + } + } - // TODO : Run WASM using node. Requires node 22. + test("Run with --wasm uses Node.js by default") { + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """object Hello { + | def main(args: Array[String]): Unit = println("Hello default WASM!") + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + "--wasm", + extraOptions + ).call(cwd = root).out.trim() + expect(output == "Hello default WASM!") } } + test("Run with //> using wasm directive") { + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """//> using wasm + |//> using wasmRuntime node + |object Hello { + | def main(args: Array[String]): Unit = println("Hello from WASM directive!") + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + extraOptions + ).call(cwd = root).out.trim() + expect(output == "Hello from WASM directive!") + } + } + + test("WASM passes arguments to program") { + // Scala.js always passes an empty Array[String] to main(args), + // so we must read process.argv directly via JS interop. + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """import scala.scalajs.js + |import scala.scalajs.js.Dynamic.global + |object Hello { + | def main(args: Array[String]): Unit = { + | val argv = global.process.argv.asInstanceOf[js.Array[String]].drop(2).toSeq + | println(argv.mkString(" ")) + | } + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + "--wasm", + "--wasm-runtime", + "node", + extraOptions, + "--", + "foo", + "bar", + "baz" + ).call(cwd = root).out.trim() + expect(output == "foo bar baz") + } + } + + for (runtime <- Seq("wasmtime", "wasmedge", "wasmer")) + test(s"Unsupported WASM runtime '$runtime' gives clear error") { + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """object Hello { + | def main(args: Array[String]): Unit = println("Hello!") + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val res = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + "--wasm", + "--wasm-runtime", + runtime, + extraOptions + ).call(cwd = root, check = false, mergeErrIntoOut = true) + expect(res.exitCode != 0) + expect(res.out.trim().contains("not yet supported")) + expect(res.out.trim().contains("scala-js/scala-js/issues/4991")) + } + } + + if (TestUtil.fromPath("deno").isDefined) + test("Run with --wasm-runtime deno") { + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """object Hello { + | def main(args: Array[String]): Unit = println("Hello from Deno WASM!") + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + "--wasm", + "--wasm-runtime", + "deno", + extraOptions + ).call(cwd = root).out.trim() + expect(output == "Hello from Deno WASM!") + } + } + + test("WASM multiple source files") { + val inputs = TestInputs( + os.rel / "Greeter.scala" -> + """trait Greeter { + | def greet(name: String): String + |} + | + |object EnthusiasticGreeter extends Greeter { + | def greet(name: String): String = s"Hello, $name!" + |} + |""".stripMargin, + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = { + | println(EnthusiasticGreeter.greet("WASM")) + | } + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Main.scala", + "Greeter.scala", + "--wasm", + "--wasm-runtime", + "node", + extraOptions + ).call(cwd = root).out.trim() + expect(output == "Hello, WASM!") + } + } + + test("WASM exception handling") { + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """object Hello { + | def riskyOp(x: Int): Int = + | if (x == 0) throw new IllegalArgumentException("zero!") + | else 100 / x + | + | def main(args: Array[String]): Unit = { + | val ok = try riskyOp(5).toString catch { case e: Exception => s"err: ${e.getMessage}" } + | val caught = try riskyOp(0).toString catch { case e: Exception => s"caught: ${e.getMessage}" } + | println(ok) + | println(caught) + | } + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + "--wasm", + "--wasm-runtime", + "node", + extraOptions + ).call(cwd = root).out.trim() + val lines = output.linesIterator.toSeq + expect(lines.contains("20")) + expect(lines.contains("caught: zero!")) + } + } + + test("WASM collections and higher-order functions") { + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """object Hello { + | def fib(n: Int): Int = if (n <= 1) n else fib(n - 1) + fib(n - 2) + | + | def main(args: Array[String]): Unit = { + | val fibs = (0 to 7).map(fib).toList + | println(fibs.mkString(", ")) + | println(fibs.filter(_ % 2 == 0).sum) + | println(fibs.foldLeft(0)(_ + _)) + | } + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + "--wasm", + "--wasm-runtime", + "node", + extraOptions + ).call(cwd = root).out.trim() + val lines = output.linesIterator.toSeq + expect(lines.contains("0, 1, 1, 2, 3, 5, 8, 13")) + expect(lines.contains("10")) // 0 + 2 + 8 = 10 + expect(lines.contains("33")) // sum of first 8 fibs + } + } + + if (!actualScalaVersion.startsWith("2")) + test("WASM @main annotation (Scala 3)") { + // Scala.js always passes empty args to main, so @main with parameters won't work. + // Test @main without parameters instead. + val inputs = TestInputs( + os.rel / "Hello.scala" -> + """@main def hello(): Unit = + | println("Hello, Scala3!") + |""".stripMargin + ) + inputs.fromRoot { root => + val output = os.proc( + TestUtil.cli, + "--power", + "run", + "Hello.scala", + "--wasm", + "--wasm-runtime", + "node", + extraOptions + ).call(cwd = root).out.trim() + expect(output == "Hello, Scala3!") + } + } + test("remap imports directive") { val importmapFile = "importmap.json" val outDir = "out" diff --git a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala index 49b9a4cf5c..05edffd69c 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -37,6 +37,7 @@ final case class BuildOptions( scalaOptions: ScalaOptions = ScalaOptions(), scalaJsOptions: ScalaJsOptions = ScalaJsOptions(), scalaNativeOptions: ScalaNativeOptions = ScalaNativeOptions(), + wasmOptions: WasmOptions = WasmOptions(), internalDependencies: InternalDependenciesOptions = InternalDependenciesOptions(), javaOptions: JavaOptions = JavaOptions(), jmhOptions: JmhOptions = JmhOptions(), diff --git a/modules/options/src/main/scala/scala/build/options/WasmOptions.scala b/modules/options/src/main/scala/scala/build/options/WasmOptions.scala new file mode 100644 index 0000000000..f96d697803 --- /dev/null +++ b/modules/options/src/main/scala/scala/build/options/WasmOptions.scala @@ -0,0 +1,26 @@ +package scala.build.options + +import scala.build.internal.Constants + +/** Options for WebAssembly compilation and execution. + * + * @param enabled + * If true, enable WASM output (Scala.js WASM backend) + * @param runtime + * The WASM runtime to use for execution (node, deno, wasmtime, wasmedge, wasmer) + * @param denoVersion + * Version of Deno to download (if not found on PATH) + */ +final case class WasmOptions( + enabled: Boolean = false, + runtime: WasmRuntime = WasmRuntime.default, + denoVersion: Option[String] = None +) { + def finalDenoVersion: String = + denoVersion.filter(_.nonEmpty).getOrElse(Constants.defaultDenoVersion) +} + +object WasmOptions { + implicit val hasHashData: HasHashData[WasmOptions] = HasHashData.derive + implicit val monoid: ConfigMonoid[WasmOptions] = ConfigMonoid.derive +} diff --git a/modules/options/src/main/scala/scala/build/options/WasmRuntime.scala b/modules/options/src/main/scala/scala/build/options/WasmRuntime.scala new file mode 100644 index 0000000000..f88f3028ab --- /dev/null +++ b/modules/options/src/main/scala/scala/build/options/WasmRuntime.scala @@ -0,0 +1,54 @@ +package scala.build.options + +import java.util.Locale + +/** Represents available WebAssembly runtimes for execution. + * + * JS-based runtimes (work now with Scala.js WASM backend): + * - Node: Uses Node.js (V8 engine) with JavaScript loader + * - Deno: Uses Deno (V8 engine) with ES module support + * + * Standalone runtimes (future, requires upstream Scala.js standalone WASM support): + * - Wasmtime: Primary standalone target, full WasmGC + Component Model + * - WasmEdge: Secondary standalone target, CNCF cloud-native runtime + * - Wasmer: Placeholder, no WasmGC support yet + */ +sealed abstract class WasmRuntime(val name: String) { + def isJsBased: Boolean = this match { + case WasmRuntime.Node | WasmRuntime.Deno => true + case _ => false + } + def isStandalone: Boolean = !isJsBased +} + +object WasmRuntime { + // JS-based runtimes (work now) + case object Node extends WasmRuntime("node") + case object Deno extends WasmRuntime("deno") + // Standalone runtimes (future - requires upstream Scala.js standalone WASM support) + case object Wasmtime extends WasmRuntime("wasmtime") + case object WasmEdge extends WasmRuntime("wasmedge") + case object Wasmer extends WasmRuntime("wasmer") + + val all: Seq[WasmRuntime] = Seq(Node, Deno, Wasmtime, WasmEdge, Wasmer) + + def default: WasmRuntime = Node + + def parse(s: String): Option[WasmRuntime] = + s.trim.toLowerCase(Locale.ROOT) match { + case "node" | "nodejs" => Some(Node) + case "deno" => Some(Deno) + case "wasmtime" => Some(Wasmtime) + case "wasmedge" => Some(WasmEdge) + case "wasmer" => Some(Wasmer) + case _ => None + } + + implicit val hashedType: HashedType[WasmRuntime] = runtime => runtime.name + + implicit val hasHashData: HasHashData[WasmRuntime] = HasHashData.asIs + + implicit val monoid: ConfigMonoid[WasmRuntime] = ConfigMonoid.instance[WasmRuntime](default) { + (a, b) => if (b == default) a else b + } +} diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 6d0ec34942..21d25a5720 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -1955,6 +1955,32 @@ A github token used to access GitHub. Not needed in most cases. Don't check for the newest available Scala CLI version upstream +## WebAssembly options + +Available in commands: + +[`run`](./commands.md#run), [`shebang`](./commands.md#shebang) + + + +### `--wasm` + +[Experimental] + +Enable WebAssembly output (Scala.js WASM backend). Uses Node.js by default. To show more options for WASM pass `--help-wasm` + +### `--wasm-runtime` + +[Experimental] + +WASM runtime to use: node (default), deno. Standalone runtimes (wasmtime, wasmedge) planned for future releases. + +### `--deno-version` + +[Experimental] + +Version of Deno to use. If Deno is not found on PATH, it will be downloaded automatically. + ## Watch options Available in commands: diff --git a/website/docs/reference/directives.md b/website/docs/reference/directives.md index 8bd2d05a20..0b385f9779 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -668,6 +668,27 @@ Add Scala.js options `//> using jsEmitWasm` +### WebAssembly + +Add WebAssembly options + +`//> using wasm` _true|false_ + +`//> using wasm` + +`//> using wasmRuntime` _node|deno|wasmtime|wasmedge|wasmer_ + +`//> using denoVersion` _value_ + +#### Examples +`//> using wasm` + +`//> using wasmRuntime node` + +`//> using wasmRuntime deno` + +`//> using denoVersion 2.1.4` + ### Test framework Set the test framework From f138e4cda59d0cbfc03aa0ff1842e2ab21e9242c Mon Sep 17 00:00:00 2001 From: "v.karamyshev" Date: Tue, 17 Mar 2026 00:05:27 +0500 Subject: [PATCH 20/20] Review fixes: remove runtime download and unsupported standalone runtimes - Move --wasm flag to dedicated Wasm help group with --help-wasm option - Simplify wasmOptions parsing with fold/toRight pattern - Add runtime validation with UnrecognizedWasmRuntimeError in directives - Auto-enable WASM when wasmRuntime directive is set - Update reference documentation --- build.mill | 2 - .../scala/scala/build/internal/Runner.scala | 11 +- .../scala/scala/cli/commands/run/Run.scala | 143 +++++++----------- .../commands/shared/HelpGroupOptions.scala | 9 +- .../cli/commands/shared/SharedOptions.scala | 28 +++- .../cli/commands/shared/WasmOptions.scala | 11 +- .../cli/internal/WasmRuntimeDownloader.scala | 104 ------------- .../errors/UnrecognizedWasmRuntimeError.scala | 4 + .../errors/UnsupportedWasmRuntimeError.scala | 3 - .../build/preprocessing/directives/Wasm.scala | 46 +++--- .../RunScalaJsTestDefinitions.scala | 26 ---- .../scala/build/options/WasmOptions.scala | 14 +- .../scala/build/options/WasmRuntime.scala | 23 +-- website/docs/reference/cli-options.md | 22 ++- website/docs/reference/commands.md | 30 ++-- website/docs/reference/directives.md | 40 +++-- .../reference/scala-command/cli-options.md | 18 +++ .../docs/reference/scala-command/commands.md | 18 +-- .../scala-command/runner-specification.md | 54 +++++++ 19 files changed, 247 insertions(+), 359 deletions(-) delete mode 100644 modules/cli/src/main/scala/scala/cli/internal/WasmRuntimeDownloader.scala create mode 100644 modules/core/src/main/scala/scala/build/errors/UnrecognizedWasmRuntimeError.scala delete mode 100644 modules/core/src/main/scala/scala/build/errors/UnsupportedWasmRuntimeError.scala diff --git a/build.mill b/build.mill index db75c17794..83484c3460 100644 --- a/build.mill +++ b/build.mill @@ -520,8 +520,6 @@ trait Core extends ScalaCliCrossSbtModule | def toolkitVersionForNative04 = "${Deps.toolkitVersionForNative04}" | def toolkitVersionForNative05 = "${Deps.toolkitVersionForNative05}" | - | def defaultDenoVersion = "2.1.4" - | | def typelevelOrganization = "${Deps.typelevelToolkit.dep.module.organization.value}" | def typelevelToolkitDefaultVersion = "${Deps.typelevelToolkitVersion}" | def typelevelToolkitMaxScalaNative = "${Deps.Versions.maxScalaNativeForTypelevelToolkit}" diff --git a/modules/build/src/main/scala/scala/build/internal/Runner.scala b/modules/build/src/main/scala/scala/build/internal/Runner.scala index ee774179f3..44a74904a0 100644 --- a/modules/build/src/main/scala/scala/build/internal/Runner.scala +++ b/modules/build/src/main/scala/scala/build/internal/Runner.scala @@ -377,10 +377,9 @@ object Runner { def denoCommand( entrypoint: File, - args: Seq[String], - denoPathOpt: Option[String] = None + args: Seq[String] ): Seq[String] = { - val denoPath = denoPathOpt.getOrElse(findInPath("deno").fold("deno")(_.toString)) + val denoPath = findInPath("deno").fold("deno")(_.toString) val denoFlags = Seq("run", "--allow-read") Seq(denoPath) ++ denoFlags ++ Seq(entrypoint.getAbsolutePath) ++ args } @@ -390,14 +389,12 @@ object Runner { args: Seq[String], logger: Logger, allowExecve: Boolean = false, - emitWasm: Boolean = false, - denoPathOpt: Option[String] = None + emitWasm: Boolean = false ): Either[BuildException, Process] = either { - val denoPath: String = denoPathOpt.getOrElse { + val denoPath: String = value(findInPath("deno") .map(_.toString) .toRight(DenoNotFoundError())) - } val denoFlags = Seq("run", "--allow-read") val extraEnv = if (emitWasm && denoNeedsWasmFlag) Map("DENO_V8_FLAGS" -> "--experimental-wasm-exnref") diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index a9fe00ad2e..dceaaa76a4 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -12,7 +12,7 @@ import java.util.concurrent.atomic.AtomicReference import scala.build.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* -import scala.build.errors.{BuildException, CompositeBuildException, UnsupportedWasmRuntimeError} +import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.input.* import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig} import scala.build.internals.ConsoleUtils.ScalaCliConsole @@ -28,7 +28,7 @@ import scala.cli.commands.util.BuildCommandHelpers.* import scala.cli.commands.util.{BuildCommandHelpers, RunHadoop, RunSpark} import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil} import scala.cli.config.Keys -import scala.cli.internal.{ProcUtil, WasmRuntimeDownloader} +import scala.cli.internal.ProcUtil import scala.cli.packaging.Library.fullClassPathMaybeAsJar import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils @@ -478,101 +478,66 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { // Check if WASM mode is requested if wasmOpts.enabled then { - val runtime = wasmOpts.runtime - - if runtime.isJsBased then { - // JS-based WASM path - uses Scala.js WASM with JavaScript helpers (Node.js or Deno) - val esModule = true // WASM backend uses ES modules - scratchDirOpt.foreach(os.makeDir.all(_)) - val jsDest = os.temp( - dir = scratchDirOpt.orNull, - prefix = "main", - suffix = ".mjs", - deleteOnExit = scratchDirOpt.isEmpty - ) - - // Resolve Deno binary: check PATH first, download if needed - val denoPathOpt: Option[String] = runtime match { - case WasmRuntime.Deno => - val denoCmd = value(WasmRuntimeDownloader.denoCommand( - wasmOpts.finalDenoVersion, - build.options.archiveCache, - logger - )) - Some(denoCmd.head) - case _ => None - } + val runtime = wasmOpts.runtime + val esModule = true // WASM backend uses ES modules + scratchDirOpt.foreach(os.makeDir.all(_)) + val jsDest = os.temp( + dir = scratchDirOpt.orNull, + prefix = "main", + suffix = ".mjs", + deleteOnExit = scratchDirOpt.isEmpty + ) - val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) - .copy(emitWasm = true, moduleKind = ScalaJsLinkerConfig.ModuleKind.ESModule) - - val res = Package.linkJs( - builds = builds, - dest = jsDest, - mainClassOpt = Some(mainClass), - addTestInitializer = false, - config = linkerConfig, - fullOpt = value(build.options.scalaJsOptions.fullOpt), - noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false), - logger = logger, - scratchDirOpt = scratchDirOpt - ).map { outputPath => - if showCommand then + val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) + .copy(emitWasm = true, moduleKind = ScalaJsLinkerConfig.ModuleKind.ESModule) + + val res = Package.linkJs( + builds = builds, + dest = jsDest, + mainClassOpt = Some(mainClass), + addTestInitializer = false, + config = linkerConfig, + fullOpt = value(build.options.scalaJsOptions.fullOpt), + noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false), + logger = logger, + scratchDirOpt = scratchDirOpt + ).map { outputPath => + if showCommand then + runtime match { + case WasmRuntime.Deno => + Left(Runner.denoCommand(outputPath.toIO, args)) + case _ => + Left(Runner.jsCommand(outputPath.toIO, args, jsDom = false, emitWasm = true)) + } + else { + val process = value { runtime match { case WasmRuntime.Deno => - Left(Runner.denoCommand(outputPath.toIO, args, denoPathOpt = denoPathOpt)) + Runner.runDeno( + outputPath.toIO, + args, + logger, + allowExecve = effectiveAllowExecve, + emitWasm = true + ) case _ => - Left(Runner.jsCommand(outputPath.toIO, args, jsDom = false, emitWasm = true)) - } - else { - val process = value { - runtime match { - case WasmRuntime.Deno => - Runner.runDeno( - outputPath.toIO, - args, - logger, - allowExecve = effectiveAllowExecve, - emitWasm = true, - denoPathOpt = denoPathOpt - ) - case _ => - Runner.runJs( - outputPath.toIO, - args, - logger, - allowExecve = effectiveAllowExecve, - jsDom = false, - sourceMap = build.options.scalaJsOptions.emitSourceMaps, - esModule = esModule, - emitWasm = true - ) - } + Runner.runJs( + outputPath.toIO, + args, + logger, + allowExecve = effectiveAllowExecve, + jsDom = false, + sourceMap = build.options.scalaJsOptions.emitSourceMaps, + esModule = esModule, + emitWasm = true + ) } - process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) - Right((process, None)) } + process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) + Right((process, None)) } - value(res) - } - else { - // Standalone WASM runtimes - not yet supported. - // Scala.js currently produces JS-dependent WASM output. - // Standalone support requires upstream Scala.js changes (scala-js/scala-js#4991). - val runtimeName = runtime.name - val extraNote = runtime match { - case WasmRuntime.Wasmer => - " Note: Wasmer does not yet support WasmGC, which is required for Scala WASM output." - case _ => "" - } - value(Left(new UnsupportedWasmRuntimeError( - s"Standalone WASM runtime '$runtimeName' is not yet supported." + - s"$extraNote" + - " Scala.js currently produces JavaScript-dependent WASM output." + - " Standalone WASM support is tracked at: https://github.com/scala-js/scala-js/issues/4991" + - " Use --wasm-runtime node (default) or --wasm-runtime deno for JS-based WASM execution." - ))) } + value(res) } else build.options.platform.value match { diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroupOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroupOptions.scala index ef012e22f0..76d78dcb19 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroupOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroupOptions.scala @@ -49,7 +49,13 @@ case class HelpGroupOptions( @Name("fmtHelp") @Tag(tags.implementation) @Tag(tags.inShortHelp) - helpScalafmt: Boolean = false + helpScalafmt: Boolean = false, + @Group(HelpGroup.Help.toString) + @HelpMessage("Show options for WebAssembly") + @Name("wasmHelp") + @Tag(tags.implementation) + @Tag(tags.inShortHelp) + helpWasm: Boolean = false ) { private def printHelpWithGroup(help: Help[?], helpFormat: HelpFormat, group: String): Nothing = { @@ -68,6 +74,7 @@ case class HelpGroupOptions( def maybePrintGroupHelp(help: Help[?], helpFormat: HelpFormat): Unit = { if (helpJs) printHelpWithGroup(help, helpFormat, HelpGroup.ScalaJs.toString) else if (helpNative) printHelpWithGroup(help, helpFormat, HelpGroup.ScalaNative.toString) + else if (helpWasm) printHelpWithGroup(help, helpFormat, HelpGroup.Wasm.toString) } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index 9ddcaa83b4..2bb1a09f9f 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -285,13 +285,25 @@ final case class SharedOptions( ) } - private def buildWasmOptions(opts: WasmOptions): options.WasmOptions = { + private def buildWasmOptions( + opts: WasmOptions + ): Either[BuildException, options.WasmOptions] = { import opts._ - options.WasmOptions( - enabled = wasm, - runtime = - wasmRuntime.flatMap(options.WasmRuntime.parse).getOrElse(options.WasmRuntime.default), - denoVersion = denoVersion + val wasmEnabled = wasm || wasmRuntime.isDefined + val parsedRuntime = wasmRuntime.fold(Right(options.WasmRuntime.default): Either[ + BuildException, + options.WasmRuntime + ]) { rt => + options.WasmRuntime.parse(rt).toRight { + val validValues = options.WasmRuntime.all.map(_.name).mkString(", ") + new scala.build.errors.UnrecognizedWasmRuntimeError(rt, validValues) + } + } + parsedRuntime.map(runtime => + options.WasmOptions( + enabled = wasmEnabled, + runtime = runtime + ) ) } @@ -320,7 +332,7 @@ final case class SharedOptions( } val parsedPlatform = platform.map(Platform.normalize).flatMap(Platform.parse) // WASM mode requires Scala.js platform for compilation - val wasmEnabled = wasmOptions.wasm + val wasmEnabled = wasmOptions.wasm || wasmOptions.wasmRuntime.isDefined val platformOpt = value { (parsedPlatform, js.js, native.native, wasmEnabled) match { case (Some(p: Platform.JS.type), _, false, _) => Right(Some(p)) @@ -426,7 +438,7 @@ final case class SharedOptions( ), scalaJsOptions = scalaJsOptions(js), scalaNativeOptions = snOpts, - wasmOptions = buildWasmOptions(wasmOptions), + wasmOptions = value(buildWasmOptions(wasmOptions)), javaOptions = value(scala.cli.commands.util.JvmUtils.javaOptions(jvm)), jmhOptions = scala.build.options.JmhOptions( jmhVersion = benchmarking.jmhVersion, diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala index d8793afaa2..a2e6251fdc 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala @@ -8,20 +8,15 @@ import scala.cli.commands.tags // format: off final case class WasmOptions( - @Group(HelpGroup.Scala.toString) + @Group(HelpGroup.Wasm.toString) @Tag(tags.experimental) @HelpMessage("Enable WebAssembly output (Scala.js WASM backend). Uses Node.js by default. To show more options for WASM pass `--help-wasm`") wasm: Boolean = false, @Group(HelpGroup.Wasm.toString) @Tag(tags.experimental) - @HelpMessage("WASM runtime to use: node (default), deno. Standalone runtimes (wasmtime, wasmedge) planned for future releases.") - wasmRuntime: Option[String] = None, - - @Group(HelpGroup.Wasm.toString) - @Tag(tags.experimental) - @HelpMessage("Version of Deno to use. If Deno is not found on PATH, it will be downloaded automatically.") - denoVersion: Option[String] = None + @HelpMessage("WASM runtime to use: node (default), deno") + wasmRuntime: Option[String] = None ) // format: on diff --git a/modules/cli/src/main/scala/scala/cli/internal/WasmRuntimeDownloader.scala b/modules/cli/src/main/scala/scala/cli/internal/WasmRuntimeDownloader.scala deleted file mode 100644 index 751e087034..0000000000 --- a/modules/cli/src/main/scala/scala/cli/internal/WasmRuntimeDownloader.scala +++ /dev/null @@ -1,104 +0,0 @@ -package scala.cli.internal - -import coursier.cache.ArchiveCache -import coursier.util.Task - -import java.util.Locale - -import scala.build.EitherCps.{either, value} -import scala.build.Logger -import scala.build.errors.BuildException -import scala.build.internal.FetchExternalBinary -import scala.util.Properties - -/** Resolves Deno binary for WASM execution. - * - * Deno is first looked up on the system PATH. If not found, it is downloaded from GitHub releases - * and cached via Coursier's ArchiveCache. - */ -object WasmRuntimeDownloader { - - /** Returns the command to run Deno. - * - * First checks system PATH, otherwise downloads the binary. - */ - def denoCommand( - version: String, - archiveCache: ArchiveCache[Task], - logger: Logger - ): Either[BuildException, Seq[String]] = either { - findOnPath("deno") match { - case Some(path) => - logger.debug(s"Using system deno at: $path") - Seq(path) - case None => - logger.message(s"Deno not found on PATH, downloading v$version...") - val binary = value(fetchDeno(version, archiveCache, logger)) - Seq(binary.toString) - } - } - - /** Find an executable on the system PATH */ - private def findOnPath(name: String): Option[String] = { - val exeName = if (Properties.isWin) s"$name.exe" else name - sys.env.get("PATH").flatMap { pathEnv => - pathEnv.split(java.io.File.pathSeparator).view.map { dir => - val file = new java.io.File(dir, exeName) - if (file.exists() && file.canExecute) Some(file.getAbsolutePath) - else None - }.find(_.isDefined).flatten - } - } - - private def detectOs(win: String, linux: String, mac: String): Either[BuildException, String] = - if (Properties.isWin) Right(win) - else if (Properties.isLinux) Right(linux) - else if (Properties.isMac) Right(mac) - else Left(new WasmRuntimeDownloadError(s"Unsupported OS: ${sys.props("os.name")}")) - - private def detectArch64(x86_64: String, aarch64: String): Either[BuildException, String] = - sys.props("os.arch").toLowerCase(Locale.ROOT) match { - case "amd64" | "x86_64" => Right(x86_64) - case "aarch64" | "arm64" => Right(aarch64) - case other => Left(new WasmRuntimeDownloadError(s"Unsupported architecture: $other")) - } - - /** Fetches Deno binary for the current platform. - * - * Deno releases are at: - * https://github.com/denoland/deno/releases/download/v{version}/deno-{platform}.zip - */ - private def fetchDeno( - version: String, - archiveCache: ArchiveCache[Task], - logger: Logger - ): Either[BuildException, os.Path] = either { - val platform = value(denoPlatform) - val url = s"https://github.com/denoland/deno/releases/download/v$version/deno-$platform.zip" - - val binaryOpt = value { - FetchExternalBinary.fetchLauncher( - url = url, - changing = false, - archiveCache = archiveCache, - logger = logger, - launcherPrefix = "deno", - launcherPathOpt = None, - makeExecutable = true - ) - } - - binaryOpt.getOrElse { - value(Left(new WasmRuntimeDownloadError(s"Could not download Deno v$version for $platform"))) - } - } - - /** Platform suffix for Deno downloads */ - private def denoPlatform: Either[BuildException, String] = either { - val arch = value(detectArch64("x86_64", "aarch64")) - val os = value(detectOs("pc-windows-msvc", "unknown-linux-gnu", "apple-darwin")) - s"$arch-$os" - } -} - -class WasmRuntimeDownloadError(message: String) extends BuildException(message) diff --git a/modules/core/src/main/scala/scala/build/errors/UnrecognizedWasmRuntimeError.scala b/modules/core/src/main/scala/scala/build/errors/UnrecognizedWasmRuntimeError.scala new file mode 100644 index 0000000000..46e2f43b6c --- /dev/null +++ b/modules/core/src/main/scala/scala/build/errors/UnrecognizedWasmRuntimeError.scala @@ -0,0 +1,4 @@ +package scala.build.errors + +class UnrecognizedWasmRuntimeError(runtime: String, validValues: String) + extends BuildException(s"Unrecognized WASM runtime: '$runtime'. Valid values: $validValues") diff --git a/modules/core/src/main/scala/scala/build/errors/UnsupportedWasmRuntimeError.scala b/modules/core/src/main/scala/scala/build/errors/UnsupportedWasmRuntimeError.scala deleted file mode 100644 index b663b6a956..0000000000 --- a/modules/core/src/main/scala/scala/build/errors/UnsupportedWasmRuntimeError.scala +++ /dev/null @@ -1,3 +0,0 @@ -package scala.build.errors - -final class UnsupportedWasmRuntimeError(message: String) extends BuildException(message) diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala index 361fcc32ab..9a1b7f67f9 100644 --- a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala @@ -2,7 +2,7 @@ package scala.build.preprocessing.directives import scala.build.Positioned import scala.build.directives.* -import scala.build.errors.BuildException +import scala.build.errors.{BuildException, UnrecognizedWasmRuntimeError} import scala.build.options.{BuildOptions, Platform, ScalaOptions, WasmOptions, WasmRuntime} import scala.cli.commands.SpecificationLevel @@ -10,40 +10,44 @@ import scala.cli.commands.SpecificationLevel @DirectiveExamples("//> using wasm") @DirectiveExamples("//> using wasmRuntime node") @DirectiveExamples("//> using wasmRuntime deno") -@DirectiveExamples("//> using denoVersion 2.1.4") @DirectiveUsage( - "//> using wasm|wasmRuntime|denoVersion _value_", + "//> using wasm|wasmRuntime _value_", """ |`//> using wasm` _true|false_ | |`//> using wasm` | - |`//> using wasmRuntime` _node|deno|wasmtime|wasmedge|wasmer_ - | - |`//> using denoVersion` _value_ + |`//> using wasmRuntime` _node|deno_ |""".stripMargin ) @DirectiveDescription("Add WebAssembly options") @DirectiveLevel(SpecificationLevel.EXPERIMENTAL) final case class Wasm( wasm: Option[Boolean] = None, - wasmRuntime: Option[String] = None, - denoVersion: Option[String] = None + wasmRuntime: Option[String] = None ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = { - val parsedRuntime = wasmRuntime.flatMap(WasmRuntime.parse) - val wasmOptions = WasmOptions( - enabled = wasm.getOrElse(false), - runtime = parsedRuntime.getOrElse(WasmRuntime.default), - denoVersion = denoVersion - ) - // When WASM is enabled, force Platform.JS (Scala.js WASM backend requires JS compilation) - val scalaOptions = - if (wasm.getOrElse(false)) - ScalaOptions(platform = Some(Positioned.none(Platform.JS))) - else - ScalaOptions() - Right(BuildOptions(scalaOptions = scalaOptions, wasmOptions = wasmOptions)) + val parsedRuntime = + wasmRuntime.fold(Right(WasmRuntime.default): Either[BuildException, WasmRuntime]) { rt => + WasmRuntime.parse(rt).toRight { + val validValues = WasmRuntime.all.map(_.name).mkString(", ") + new UnrecognizedWasmRuntimeError(rt, validValues) + } + } + parsedRuntime.map { runtime => + val wasmEnabled = wasm.getOrElse(false) || wasmRuntime.isDefined + val wasmOptions = WasmOptions( + enabled = wasmEnabled, + runtime = runtime + ) + // When WASM is enabled, force Platform.JS (Scala.js WASM backend requires JS compilation) + val scalaOptions = + if (wasmEnabled) + ScalaOptions(platform = Some(Positioned.none(Platform.JS))) + else + ScalaOptions() + BuildOptions(scalaOptions = scalaOptions, wasmOptions = wasmOptions) + } } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala index 8436bc2e0f..7ff3972f7a 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala @@ -428,32 +428,6 @@ trait RunScalaJsTestDefinitions { this: RunTestDefinitions => } } - for (runtime <- Seq("wasmtime", "wasmedge", "wasmer")) - test(s"Unsupported WASM runtime '$runtime' gives clear error") { - val inputs = TestInputs( - os.rel / "Hello.scala" -> - """object Hello { - | def main(args: Array[String]): Unit = println("Hello!") - |} - |""".stripMargin - ) - inputs.fromRoot { root => - val res = os.proc( - TestUtil.cli, - "--power", - "run", - "Hello.scala", - "--wasm", - "--wasm-runtime", - runtime, - extraOptions - ).call(cwd = root, check = false, mergeErrIntoOut = true) - expect(res.exitCode != 0) - expect(res.out.trim().contains("not yet supported")) - expect(res.out.trim().contains("scala-js/scala-js/issues/4991")) - } - } - if (TestUtil.fromPath("deno").isDefined) test("Run with --wasm-runtime deno") { val inputs = TestInputs( diff --git a/modules/options/src/main/scala/scala/build/options/WasmOptions.scala b/modules/options/src/main/scala/scala/build/options/WasmOptions.scala index f96d697803..34450e2385 100644 --- a/modules/options/src/main/scala/scala/build/options/WasmOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/WasmOptions.scala @@ -1,24 +1,16 @@ package scala.build.options -import scala.build.internal.Constants - /** Options for WebAssembly compilation and execution. * * @param enabled * If true, enable WASM output (Scala.js WASM backend) * @param runtime - * The WASM runtime to use for execution (node, deno, wasmtime, wasmedge, wasmer) - * @param denoVersion - * Version of Deno to download (if not found on PATH) + * The WASM runtime to use for execution (node, deno) */ final case class WasmOptions( enabled: Boolean = false, - runtime: WasmRuntime = WasmRuntime.default, - denoVersion: Option[String] = None -) { - def finalDenoVersion: String = - denoVersion.filter(_.nonEmpty).getOrElse(Constants.defaultDenoVersion) -} + runtime: WasmRuntime = WasmRuntime.default +) object WasmOptions { implicit val hasHashData: HasHashData[WasmOptions] = HasHashData.derive diff --git a/modules/options/src/main/scala/scala/build/options/WasmRuntime.scala b/modules/options/src/main/scala/scala/build/options/WasmRuntime.scala index f88f3028ab..a2e68d63f8 100644 --- a/modules/options/src/main/scala/scala/build/options/WasmRuntime.scala +++ b/modules/options/src/main/scala/scala/build/options/WasmRuntime.scala @@ -7,30 +7,14 @@ import java.util.Locale * JS-based runtimes (work now with Scala.js WASM backend): * - Node: Uses Node.js (V8 engine) with JavaScript loader * - Deno: Uses Deno (V8 engine) with ES module support - * - * Standalone runtimes (future, requires upstream Scala.js standalone WASM support): - * - Wasmtime: Primary standalone target, full WasmGC + Component Model - * - WasmEdge: Secondary standalone target, CNCF cloud-native runtime - * - Wasmer: Placeholder, no WasmGC support yet */ -sealed abstract class WasmRuntime(val name: String) { - def isJsBased: Boolean = this match { - case WasmRuntime.Node | WasmRuntime.Deno => true - case _ => false - } - def isStandalone: Boolean = !isJsBased -} +sealed abstract class WasmRuntime(val name: String) object WasmRuntime { - // JS-based runtimes (work now) case object Node extends WasmRuntime("node") case object Deno extends WasmRuntime("deno") - // Standalone runtimes (future - requires upstream Scala.js standalone WASM support) - case object Wasmtime extends WasmRuntime("wasmtime") - case object WasmEdge extends WasmRuntime("wasmedge") - case object Wasmer extends WasmRuntime("wasmer") - val all: Seq[WasmRuntime] = Seq(Node, Deno, Wasmtime, WasmEdge, Wasmer) + val all: Seq[WasmRuntime] = Seq(Node, Deno) def default: WasmRuntime = Node @@ -38,9 +22,6 @@ object WasmRuntime { s.trim.toLowerCase(Locale.ROOT) match { case "node" | "nodejs" => Some(Node) case "deno" => Some(Deno) - case "wasmtime" => Some(Wasmtime) - case "wasmedge" => Some(WasmEdge) - case "wasmer" => Some(Wasmer) case _ => None } diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 21d25a5720..d47030620e 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -591,6 +591,12 @@ Aliases: `--fmt-help`, `--help-fmt`, `--scalafmt-help` Show options for Scalafmt +### `--help-wasm` + +Aliases: `--wasm-help` + +Show options for WebAssembly + ## Install completions options Available in commands: @@ -1955,31 +1961,21 @@ A github token used to access GitHub. Not needed in most cases. Don't check for the newest available Scala CLI version upstream -## WebAssembly options +## Wasm options Available in commands: -[`run`](./commands.md#run), [`shebang`](./commands.md#shebang) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) ### `--wasm` -[Experimental] - Enable WebAssembly output (Scala.js WASM backend). Uses Node.js by default. To show more options for WASM pass `--help-wasm` ### `--wasm-runtime` -[Experimental] - -WASM runtime to use: node (default), deno. Standalone runtimes (wasmtime, wasmedge) planned for future releases. - -### `--deno-version` - -[Experimental] - -Version of Deno to use. If Deno is not found on PATH, it will be downloaded automatically. +WASM runtime to use: node (default), deno ## Watch options diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index 64ef81f38d..03d0869f5c 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -32,7 +32,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/compile -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [compile](./cli-options.md#compile-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [compile](./cli-options.md#compile-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## config @@ -81,7 +81,7 @@ Accepts option groups: [config](./cli-options.md#config-options), [coursier](./c Update dependency directives in the project -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [dependency update](./cli-options.md#dependency-update-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [dependency update](./cli-options.md#dependency-update-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ## doc @@ -95,7 +95,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/doc -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [doc](./cli-options.md#doc-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [doc](./cli-options.md#doc-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ## export @@ -117,7 +117,7 @@ The `export` sub-command is experimental. Please bear in mind that non-ideal user experience should be expected. If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [export](./cli-options.md#export-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [export](./cli-options.md#export-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ## fix @@ -143,7 +143,7 @@ The `fix` sub-command is experimental. Please bear in mind that non-ideal user experience should be expected. If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [fix](./cli-options.md#fix-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [Scalafix](./cli-options.md#scalafix-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [fix](./cli-options.md#fix-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [Scalafix](./cli-options.md#scalafix-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ## fmt @@ -160,7 +160,7 @@ All standard Scala CLI inputs are accepted, but only Scala sources will be forma For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/fmt -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [fmt](./cli-options.md#fmt-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [fmt](./cli-options.md#fmt-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ## help @@ -212,7 +212,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/repl -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [repl](./cli-options.md#repl-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [repl](./cli-options.md#repl-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## package @@ -230,7 +230,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/package -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [package](./cli-options.md#package-options), [packager](./cli-options.md#packager-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [package](./cli-options.md#package-options), [packager](./cli-options.md#packager-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## publish @@ -257,7 +257,7 @@ The `publish` sub-command is experimental. Please bear in mind that non-ideal user experience should be expected. If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [pgp scala signing](./cli-options.md#pgp-scala-signing-options), [power](./cli-options.md#power-options), [publish](./cli-options.md#publish-options), [publish connection](./cli-options.md#publish-connection-options), [publish params](./cli-options.md#publish-params-options), [publish repository](./cli-options.md#publish-repository-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [pgp scala signing](./cli-options.md#pgp-scala-signing-options), [power](./cli-options.md#power-options), [publish](./cli-options.md#publish-options), [publish connection](./cli-options.md#publish-connection-options), [publish params](./cli-options.md#publish-params-options), [publish repository](./cli-options.md#publish-repository-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## publish local @@ -269,7 +269,7 @@ The `publish-local` sub-command is experimental. Please bear in mind that non-ideal user experience should be expected. If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [pgp scala signing](./cli-options.md#pgp-scala-signing-options), [power](./cli-options.md#power-options), [publish](./cli-options.md#publish-options), [publish local](./cli-options.md#publish-local-options), [publish params](./cli-options.md#publish-params-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [pgp scala signing](./cli-options.md#pgp-scala-signing-options), [power](./cli-options.md#power-options), [publish](./cli-options.md#publish-options), [publish local](./cli-options.md#publish-local-options), [publish params](./cli-options.md#publish-params-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## publish setup @@ -307,7 +307,7 @@ To pass arguments to the actual application, just add them after `--`, like: For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/run -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## github secret create @@ -354,7 +354,7 @@ Using directives can be defined in all supported input source file types. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/setup-ide -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [bsp file](./cli-options.md#bsp-file-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [setup IDE](./cli-options.md#setup-ide-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [bsp file](./cli-options.md#bsp-file-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [setup IDE](./cli-options.md#setup-ide-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ## shebang @@ -385,7 +385,7 @@ Using this, it is possible to conveniently set up Unix shebang scripts. For exam For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/shebang -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## test @@ -409,7 +409,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/test -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [test](./cli-options.md#test-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [test](./cli-options.md#test-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## uninstall @@ -512,7 +512,7 @@ It is normally supposed to be invoked by your IDE when a Scala CLI project is im Detailed documentation can be found on our website: https://scala-cli.virtuslab.org -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [bsp](./cli-options.md#bsp-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [bsp](./cli-options.md#bsp-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ### default-file diff --git a/website/docs/reference/directives.md b/website/docs/reference/directives.md index 0b385f9779..ec95953781 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -668,27 +668,6 @@ Add Scala.js options `//> using jsEmitWasm` -### WebAssembly - -Add WebAssembly options - -`//> using wasm` _true|false_ - -`//> using wasm` - -`//> using wasmRuntime` _node|deno|wasmtime|wasmedge|wasmer_ - -`//> using denoVersion` _value_ - -#### Examples -`//> using wasm` - -`//> using wasmRuntime node` - -`//> using wasmRuntime deno` - -`//> using denoVersion 2.1.4` - ### Test framework Set the test framework @@ -716,6 +695,25 @@ Use a toolkit as dependency (not supported in Scala 2.12), 'default' version for `//> using test.toolkit default` +### WASM options + +Add WebAssembly options + + +`//> using wasm` _true|false_ + +`//> using wasm` + +`//> using wasmRuntime` _node|deno_ + + +#### Examples +`//> using wasm` + +`//> using wasmRuntime node` + +`//> using wasmRuntime deno` + ### Watch additional inputs Watch additional files or directories when using watch mode diff --git a/website/docs/reference/scala-command/cli-options.md b/website/docs/reference/scala-command/cli-options.md index 2c2f7e14e4..f6ae74043e 100644 --- a/website/docs/reference/scala-command/cli-options.md +++ b/website/docs/reference/scala-command/cli-options.md @@ -498,6 +498,14 @@ Aliases: `--fmt-help`, `--help-fmt`, `--scalafmt-help` Show options for Scalafmt +### `--help-wasm` + +Aliases: `--wasm-help` + +`IMPLEMENTATION specific` per Scala Runner specification + +Show options for WebAssembly + ## Install completions options Available in commands: @@ -1435,6 +1443,16 @@ A github token used to access GitHub. Not needed in most cases. Don't check for the newest available Scala CLI version upstream +## Wasm options + +Available in commands: + +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`doc`](./commands.md#doc), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) + + + +*This section was automatically generated and may be empty if no options were available.* + ## Watch options Available in commands: diff --git a/website/docs/reference/scala-command/commands.md b/website/docs/reference/scala-command/commands.md index 0d8583fa93..1d9cf8ef26 100644 --- a/website/docs/reference/scala-command/commands.md +++ b/website/docs/reference/scala-command/commands.md @@ -31,7 +31,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/compile -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [compile](./cli-options.md#compile-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [compile](./cli-options.md#compile-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ### config @@ -88,7 +88,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/doc -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [doc](./cli-options.md#doc-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [doc](./cli-options.md#doc-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ### repl @@ -110,7 +110,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/repl -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [repl](./cli-options.md#repl-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [repl](./cli-options.md#repl-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ### run @@ -136,7 +136,7 @@ To pass arguments to the actual application, just add them after `--`, like: For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/run -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ### shebang @@ -167,7 +167,7 @@ Using this, it is possible to conveniently set up Unix shebang scripts. For exam For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/shebang -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## SHOULD have commands: @@ -186,7 +186,7 @@ All standard Scala CLI inputs are accepted, but only Scala sources will be forma For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/fmt -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [fmt](./cli-options.md#fmt-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [fmt](./cli-options.md#fmt-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ### test @@ -210,7 +210,7 @@ All supported types of inputs can be mixed with each other. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/test -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [test](./cli-options.md#test-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [test](./cli-options.md#test-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ### version @@ -242,7 +242,7 @@ It is normally supposed to be invoked by your IDE when a Scala CLI project is im Detailed documentation can be found on our website: https://scala-cli.virtuslab.org -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [bsp](./cli-options.md#bsp-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [bsp](./cli-options.md#bsp-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ### clean @@ -294,7 +294,7 @@ Using directives can be defined in all supported input source file types. For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/setup-ide -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [bsp file](./cli-options.md#bsp-file-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [setup IDE](./cli-options.md#setup-ide-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [bsp file](./cli-options.md#bsp-file-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [setup IDE](./cli-options.md#setup-ide-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [wasm](./cli-options.md#wasm-options), [workspace](./cli-options.md#workspace-options) ### uninstall diff --git a/website/docs/reference/scala-command/runner-specification.md b/website/docs/reference/scala-command/runner-specification.md index 65024b3e0a..abf7c5a582 100644 --- a/website/docs/reference/scala-command/runner-specification.md +++ b/website/docs/reference/scala-command/runner-specification.md @@ -636,6 +636,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check** @@ -1427,6 +1433,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check** @@ -2036,6 +2048,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check** @@ -2675,6 +2693,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check** @@ -3323,6 +3347,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check** @@ -3929,6 +3959,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check** @@ -4613,6 +4649,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check** @@ -5307,6 +5349,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check** @@ -6284,6 +6332,12 @@ Show options for Scalafmt Aliases: `--help-fmt` ,`--scalafmt-help` ,`--fmt-help` +**--help-wasm** + +Show options for WebAssembly + +Aliases: `--wasm-help` + **--strict-bloop-json-check**