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/.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/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc653938c1..54100be807 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 }} @@ -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 }} 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 }} diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml index db7bb8da78..e70e63aa25 100644 --- a/.github/workflows/test-report.yml +++ b/.github/workflows/test-report.yml @@ -14,7 +14,7 @@ jobs: report: runs-on: ubuntu-latest steps: - - uses: dorny/test-reporter@v2 + - uses: dorny/test-reporter@v3 with: artifact: /test-results-(.*)/ name: 'Test report $1' 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/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/CONTRIBUTING.md b/CONTRIBUTING.md index 08fa9c9af0..9f297b2938 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 @@ -54,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; 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 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. diff --git a/build.mill b/build.mill index f98bd81673..17bbedff6d 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 @@ -112,6 +112,8 @@ object runner extends Cross[Runner](Scala.runnerScalaVersions) with CrossScalaDefaultToRunner object `test-runner` extends Cross[TestRunner](Scala.runnerScalaVersions) with CrossScalaDefaultToRunner +object `java-test-runner` extends JavaTestRunner + with LocatedInModules object `tasty-lib` extends Cross[TastyLib](Scala.scala3MainVersions) with CrossScalaDefaultToInternal @@ -452,12 +454,18 @@ trait Core extends ScalaCliCrossSbtModule val runnerMainClass = build.runner(crossScalaVersion) .mainClass() .getOrElse(sys.error("No main class defined for runner")) + val javaTestRunnerMainClass = `java-test-runner` + .mainClass() + .getOrElse(sys.error("No main class defined for java-test-runner")) val detailedVersionValue = if (`local-repo`.developingOnStubModules) s"""Some("${vcsState()}")""" else "None" val testRunnerOrganization = `test-runner`(crossScalaVersion) .pomSettings() .organization + val javaTestRunnerOrganization = `java-test-runner` + .pomSettings() + .organization val code = s"""package scala.build.internal | @@ -479,6 +487,11 @@ trait Core extends ScalaCliCrossSbtModule | def testRunnerVersion = "${`test-runner`(crossScalaVersion).publishVersion()}" | def testRunnerMainClass = "$testRunnerMainClass" | + | def javaTestRunnerOrganization = "$javaTestRunnerOrganization" + | def javaTestRunnerModuleName = "${`java-test-runner`.artifactName()}" + | def javaTestRunnerVersion = "${`java-test-runner`.publishVersion()}" + | def javaTestRunnerMainClass = "$javaTestRunnerMainClass" + | | def runnerOrganization = "${build.runner(crossScalaVersion).pomSettings().organization}" | def runnerModuleName = "${build.runner(crossScalaVersion).artifactName()}" | def runnerVersion = "${build.runner(crossScalaVersion).publishVersion()}" @@ -528,6 +541,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}" @@ -1322,6 +1336,16 @@ trait TestRunner extends CrossSbtModule override def mainClass: T[Option[String]] = Some("scala.build.testrunner.DynamicTestRunner") } +trait JavaTestRunner extends JavaModule + with ScalaCliPublishModule + with LocatedInModules { + override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq( + Deps.asm, + Deps.testInterface + ) + override def mainClass: T[Option[String]] = Some("scala.build.testrunner.JavaDynamicTestRunner") +} + trait TastyLib extends ScalaCliCrossSbtModule with ScalaCliPublishModule with ScalaCliScalafixModule @@ -1356,7 +1380,7 @@ object `local-repo` extends LocalRepo { def developingOnStubModules = false override def stubsModules: Seq[PublishLocalNoFluff] = - Seq(runner(Scala.runnerScala3), `test-runner`(Scala.runnerScala3)) + Seq(runner(Scala.runnerScala3), `test-runner`(Scala.runnerScala3), `java-test-runner`) override def version: T[String] = runner(Scala.runnerScala3).publishVersion() } 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/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/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index c631c88ea0..1a52b9bc6d 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() @@ -1092,8 +1105,7 @@ object Build { either { val options0 = - // FIXME: don't add Scala to pure Java test builds (need to add pure Java test runner) - if sources.hasJava && !sources.hasScala && scope != Scope.Test + if sources.hasJava && !sources.hasScala then options.copy( scalaOptions = options.scalaOptions.copy( @@ -1186,6 +1198,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/main/scala/scala/build/input/Element.scala b/modules/build/src/main/scala/scala/build/input/Element.scala index 5f2c941706..f89ea71f52 100644 --- a/modules/build/src/main/scala/scala/build/input/Element.scala +++ b/modules/build/src/main/scala/scala/build/input/Element.scala @@ -107,6 +107,11 @@ final case class MarkdownFile(base: os.Path, subPath: os.SubPath) lazy val path: os.Path = base / subPath } +final case class SbtFile(base: os.Path, subPath: os.SubPath) + extends OnDisk with SourceFile { + lazy val path: os.Path = base / subPath +} + final case class Directory(path: os.Path) extends OnDisk with Compiled final case class ResourceDirectory(path: os.Path) extends OnDisk diff --git a/modules/build/src/main/scala/scala/build/input/ElementsUtils.scala b/modules/build/src/main/scala/scala/build/input/ElementsUtils.scala index 2c79db3284..4e49678376 100644 --- a/modules/build/src/main/scala/scala/build/input/ElementsUtils.scala +++ b/modules/build/src/main/scala/scala/build/input/ElementsUtils.scala @@ -34,6 +34,8 @@ object ElementsUtils { case p if p.last.endsWith(".sc") => // TODO: hasShebang test without consuming 1st 2 bytes of Stream Script(d.path, p.subRelativeTo(d.path), None) + case p if p.last.endsWith(".sbt") => + SbtFile(d.path, p.subRelativeTo(d.path)) } .toVector .sortBy(_.subPath.segments) @@ -68,6 +70,7 @@ object ElementsUtils { case _: Script => "sc:" case _: MarkdownFile => "md:" case _: JarFile => "jar:" + case _: SbtFile => "sbt:" } Iterator(prefix, elem.path.toString, "\n").map(bytes) case v: Virtual => diff --git a/modules/build/src/main/scala/scala/build/input/Inputs.scala b/modules/build/src/main/scala/scala/build/input/Inputs.scala index 055be9baa0..2e608b28db 100644 --- a/modules/build/src/main/scala/scala/build/input/Inputs.scala +++ b/modules/build/src/main/scala/scala/build/input/Inputs.scala @@ -104,6 +104,7 @@ final case class Inputs( Seq("dir:") ++ dirInput.singleFilesFromDirectory(enableMarkdown) .map(file => s"${file.path}:" + os.read(file.path)) case _: ResourceDirectory => Nil + case _: SbtFile => Nil case _ => Seq(os.read(elem.path)) } (Iterator(elem.path.toString) ++ content.iterator ++ Iterator("\n")).map(bytes) @@ -282,6 +283,7 @@ object Inputs { else if arg.endsWith(".java") then Right(Seq(JavaFile(dir, subPath))) else if arg.endsWith(".jar") then Right(Seq(JarFile(dir, subPath))) else if arg.endsWith(".c") || arg.endsWith(".h") then Right(Seq(CFile(dir, subPath))) + else if arg.endsWith(".sbt") then Right(Seq(SbtFile(dir, subPath))) else if arg.endsWith(".md") then Right(Seq(MarkdownFile(dir, subPath))) else if acceptFds && arg.startsWith("/dev/fd/") then Right(Seq(VirtualScript(content, arg, os.sub / s"input-${idx + 1}.sc"))) 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..fca8c3d63c 100644 --- a/modules/build/src/main/scala/scala/build/internal/Runner.scala +++ b/modules/build/src/main/scala/scala/build/internal/Runner.scala @@ -15,12 +15,15 @@ import scala.build.Logger import scala.build.errors.* import scala.build.internals.EnvVar import scala.build.testrunner.FrameworkUtils.* -import scala.build.testrunner.{AsmTestRunner, TestRunner} +import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger, TestRunner} import scala.scalanative.testinterface.adapter.TestAdapter as ScalaNativeTestAdapter import scala.util.{Failure, Properties, Success} object Runner { + private def toTestRunnerLogger(logger: Logger): TestRunnerLogger = + TestRunnerLogger(logger.verbosity) + def maybeExec( commandName: String, command: Seq[String], @@ -186,6 +189,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 +275,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 +298,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 +323,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 +378,66 @@ object Runner { } } + def denoCommand( + entrypoint: File, + args: Seq[String] + ): Seq[String] = { + val denoPath = 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 + ): Either[BuildException, Process] = either { + 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") + 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], @@ -346,15 +480,18 @@ object Runner { frameworks: Seq[Framework], requireTests: Boolean, args: Seq[String], - parentInspector: AsmTestRunner.ParentInspector + parentInspector: AsmTestRunner.ParentInspector, + logger: Logger ): Either[NoTestsRun, Boolean] = frameworks .flatMap { framework => + val trLogger = toTestRunnerLogger(logger) val taskDefs = AsmTestRunner.taskDefs( classPath, keepJars = false, framework.fingerprints().toIndexedSeq, - parentInspector + parentInspector, + trLogger ).toArray val runner = framework.runner(args.toArray, Array(), null) @@ -380,16 +517,22 @@ object Runner { parentInspector: AsmTestRunner.ParentInspector, logger: Logger ): Either[NoTestFrameworkFoundError, Seq[String]] = { + val trLogger = toTestRunnerLogger(logger) logger.debug("Looking for test framework services on the classpath...") val foundFrameworkServices = - AsmTestRunner.findFrameworkServices(classPath) + AsmTestRunner.findFrameworkServices(classPath, trLogger) .map(_.replace('/', '.').replace('\\', '.')) logger.debug(s"Found ${foundFrameworkServices.length} test framework services.") if foundFrameworkServices.nonEmpty then logger.debug(s" - ${foundFrameworkServices.mkString("\n - ")}") logger.debug("Looking for more test frameworks on the classpath...") val foundFrameworks = - AsmTestRunner.findFrameworks(classPath, TestRunner.commonTestFrameworks, parentInspector) + AsmTestRunner.findFrameworks( + classPath, + TestRunner.commonTestFrameworks, + parentInspector, + trLogger + ) .map(_.replace('/', '.').replace('\\', '.')) logger.debug(s"Found ${foundFrameworks.length} additional test frameworks") if foundFrameworks.nonEmpty then @@ -444,7 +587,7 @@ object Runner { logger.debug(s"JS tests class path: $classPath") - val parentInspector = new AsmTestRunner.ParentInspector(classPath) + val parentInspector = new AsmTestRunner.ParentInspector(classPath, toTestRunnerLogger(logger)) val foundFrameworkNames: List[String] = predefinedTestFrameworks match { case f if f.nonEmpty => f.toList case Nil => value(frameworkNames(classPath, parentInspector, logger)).toList @@ -474,7 +617,7 @@ object Runner { ) if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError) - else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector) + else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector, logger) } finally if adapter != null then adapter.close() @@ -492,7 +635,7 @@ object Runner { logger.debug("Preparing to run tests with Scala Native...") logger.debug(s"Native tests class path: $classPath") - val parentInspector = new AsmTestRunner.ParentInspector(classPath) + val parentInspector = new AsmTestRunner.ParentInspector(classPath, toTestRunnerLogger(logger)) val foundFrameworkNames: List[String] = predefinedTestFrameworks match { case f if f.nonEmpty => f.toList case Nil => value(frameworkNames(classPath, parentInspector, logger)).toList @@ -539,8 +682,8 @@ object Runner { |""".stripMargin ) - if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByBridgeError) - else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector) + if finalTestFrameworks.isEmpty then Left(new NoFrameworkFoundByNativeBridgeError) + else runTests(classPath, finalTestFrameworks, requireTests, args, parentInspector, logger) } finally if adapter != null then adapter.close() 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..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 @@ -31,7 +31,9 @@ object DirectivesPreprocessingUtils { directives.ScalaNative.handler, directives.ScalaVersion.handler, directives.Sources.handler, - directives.Tests.handler + directives.Watching.handler, + directives.Tests.handler, + directives.Wasm.handler ).map(_.mapE(_.buildOptions)) val usingDirectiveWithReqsHandlers 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/FrameworkDiscoveryTests.scala b/modules/build/src/test/scala/scala/build/tests/FrameworkDiscoveryTests.scala new file mode 100644 index 0000000000..007c35341d --- /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, Logger as TestRunnerLogger} + +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), TestRunnerLogger(0)) + 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/build/src/test/scala/scala/build/tests/InputsTests.scala b/modules/build/src/test/scala/scala/build/tests/InputsTests.scala index 5cf10bdd3e..1707975241 100644 --- a/modules/build/src/test/scala/scala/build/tests/InputsTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/InputsTests.scala @@ -3,14 +3,8 @@ package scala.build.tests import bloop.rifle.BloopRifleConfig import com.eed3si9n.expecty.Expecty.expect +import scala.build.input.* import scala.build.input.ElementsUtils.* -import scala.build.input.{ - Inputs, - ScalaCliInvokeData, - VirtualJavaFile, - VirtualScalaFile, - VirtualScript -} import scala.build.internal.Constants import scala.build.options.{BuildOptions, InternalOptions} import scala.build.tests.util.BloopServer @@ -127,6 +121,34 @@ class InputsTests extends TestUtil.ScalaCliBuildSuite { } } + test("sbt file is recognized as SbtFile when passed explicitly") { + TestInputs(os.rel / "build.sbt" -> "").fromRoot { root => + val elements = Inputs.validateArgs( + Seq((root / "build.sbt").toString), + root, + download = _ => Right(Array.emptyByteArray), + stdinOpt = None, + acceptFds = false, + enableMarkdown = false + )(using ScalaCliInvokeData.dummy) + elements match { + case Seq(Right(Seq(f: SbtFile))) => + assert(f.path == root / "build.sbt") + case _ => fail(s"Unexpected elements: $elements") + } + } + } + + test("sbt file is picked up from directory scan") { + TestInputs(os.rel / "build.sbt" -> "").fromRoot { root => + val dir = Directory(root) + val singles = dir.singleFilesFromDirectory(enableMarkdown = false) + val sbtFiles = singles.collect { case f: SbtFile => f } + assert(sbtFiles.nonEmpty) + assert(sbtFiles.head.path == root / "build.sbt") + } + } + test("URLs with query parameters") { val urlBase = "https://gist.githubusercontent.com/USER/hash/raw/hash" diff --git a/modules/build/src/test/scala/scala/build/tests/JavaTestRunnerTests.scala b/modules/build/src/test/scala/scala/build/tests/JavaTestRunnerTests.scala new file mode 100644 index 0000000000..3cfc8f155e --- /dev/null +++ b/modules/build/src/test/scala/scala/build/tests/JavaTestRunnerTests.scala @@ -0,0 +1,51 @@ +package scala.build.tests + +import com.eed3si9n.expecty.Expecty.assert as expect + +import scala.build.options.* + +class JavaTestRunnerTests extends TestUtil.ScalaCliBuildSuite { + + private def makeOptions( + scalaVersionOpt: Option[MaybeScalaVersion], + addTestRunner: Boolean + ): BuildOptions = + BuildOptions( + scalaOptions = ScalaOptions( + scalaVersion = scalaVersionOpt + ), + internalDependencies = InternalDependenciesOptions( + addTestRunnerDependencyOpt = Some(addTestRunner) + ) + ) + + test("pure Java build has no scalaParams") { + val opts = makeOptions(Some(MaybeScalaVersion.none), addTestRunner = false) + val params = opts.scalaParams.toOption.flatten + expect(params.isEmpty, "Pure Java build should have no scalaParams") + } + + test("Scala build has scalaParams") { + val opts = makeOptions(None, addTestRunner = false) + val params = opts.scalaParams.toOption.flatten + expect(params.isDefined, "Scala build should have scalaParams") + } + + test("pure Java test build gets addJvmJavaTestRunner=true in Artifacts params") { + val opts = makeOptions(Some(MaybeScalaVersion.none), addTestRunner = true) + val isJava = opts.scalaParams.toOption.flatten.isEmpty + expect(isJava, "Expected pure Java build to have no scalaParams") + } + + test("Scala test build gets addJvmTestRunner=true in Artifacts params") { + val opts = makeOptions(None, addTestRunner = true) + val isJava = opts.scalaParams.toOption.flatten.isEmpty + expect(!isJava, "Expected Scala build to have scalaParams") + } + + test("mixed Scala+Java build still gets Scala test runner") { + val opts = makeOptions(None, addTestRunner = true) + val isJava = opts.scalaParams.toOption.flatten.isEmpty + expect(!isJava, "Mixed Scala+Java build should still use Scala test runner") + } +} 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/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/doc/Doc.scala b/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala index 521c53c72a..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) @@ -125,16 +193,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 +249,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/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/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/fmt/Fmt.scala b/modules/cli/src/main/scala/scala/cli/commands/fmt/Fmt.scala index 96c24b96db..f08c247048 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/fmt/Fmt.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/fmt/Fmt.scala @@ -5,7 +5,7 @@ import caseapp.core.help.HelpFormat import dependency.* import scala.build.Logger -import scala.build.input.{ProjectScalaFile, Script, SourceScalaFile} +import scala.build.input.{ProjectScalaFile, SbtFile, Script, SourceScalaFile} import scala.build.internal.{Constants, ExternalBinaryParams, FetchExternalBinary, Runner} import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.cli.CurrentParams @@ -53,7 +53,7 @@ object Fmt extends ScalaCommand[FmtOptions] { if args.all.isEmpty then (Seq(os.pwd), os.pwd, None) else { val i = options.shared.inputs(args.all).orExit(logger) - type FormattableSourceFile = Script | SourceScalaFile | ProjectScalaFile + type FormattableSourceFile = Script | SourceScalaFile | ProjectScalaFile | SbtFile val s = i.sourceFiles().collect { case sc: FormattableSourceFile => sc.path } (s, i.workspace, Some(i)) } 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/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..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 @@ -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, @@ -253,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, @@ -276,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, @@ -306,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, @@ -339,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, @@ -360,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, @@ -416,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, @@ -684,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], @@ -738,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 8355c2df8c..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 @@ -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") ) @@ -32,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) @@ -68,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, @@ -78,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 f131f33a73..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,14 +23,27 @@ final case class PublishLocalOptions( sharedPublish: SharedPublishOptions = SharedPublishOptions(), @Recurse scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions(), -) extends HasSharedOptions + + @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 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/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/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/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..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 @@ -18,7 +18,7 @@ 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 @@ -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( @@ -474,228 +474,297 @@ 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 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 { + val wasmOpts = build.options.wasmOptions + + // Check if WASM mode is requested + if wasmOpts.enabled then { + 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 + 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 => + Runner.runDeno( + outputPath.toIO, + args, + logger, + allowExecve = effectiveAllowExecve, + emitWasm = true + ) + case _ => Runner.runJs( outputPath.toIO, args, logger, allowExecve = effectiveAllowExecve, - jsDom = jsDom, + jsDom = false, sourceMap = build.options.scalaJsOptions.emitSourceMaps, - esModule = esModule + esModule = esModule, + emitWasm = true ) - } - 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 - } - // 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 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 + process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) + Right((process, None)) + } + } + value(res) + } + 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/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/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/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 fe4b8903d9..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 @@ -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,28 @@ final case class SharedOptions( ) } + private def buildWasmOptions( + opts: WasmOptions + ): Either[BuildException, options.WasmOptions] = { + import opts._ + 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 + ) + ) + } + lazy val scalacOptionsFromFiles: List[String] = scalac.argsFiles.flatMap(argFile => ArgSplitter.splitToArgs(os.read(os.Path(argFile.file, os.pwd))) @@ -290,7 +314,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") @@ -304,21 +331,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 || wasmOptions.wasmRuntime.isDefined + 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) = @@ -405,6 +438,7 @@ final case class SharedOptions( ), scalaJsOptions = scalaJsOptions(js), scalaNativeOptions = snOpts, + wasmOptions = value(buildWasmOptions(wasmOptions)), javaOptions = value(scala.cli.commands.util.JvmUtils.javaOptions(jvm)), jmhOptions = scala.build.options.JmhOptions( jmhVersion = benchmarking.jmhVersion, @@ -441,7 +475,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/shared/WasmOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala new file mode 100644 index 0000000000..a2e6251fdc --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala @@ -0,0 +1,27 @@ +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.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") + wasmRuntime: 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/commands/test/Test.scala b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala index c1d528a530..7342d8c074 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 @@ -12,7 +12,7 @@ import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.internal.{Constants, Runner} import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.build.options.{BuildOptions, JavaOpt, Platform, Scope} -import scala.build.testrunner.AsmTestRunner +import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger} import scala.cli.CurrentParams import scala.cli.commands.run.Run import scala.cli.commands.setupide.SetupIde @@ -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 = @@ -256,11 +256,16 @@ object Test extends ScalaCommand[TestOptions] { testOnly.map(to => s"--test-only=$to").toSeq ++ Seq("--") ++ args + val testRunnerMainClass = + if build.artifacts.hasJavaTestRunner + then Constants.javaTestRunnerMainClass + else Constants.testRunnerMainClass + Runner.runJvm( build.options.javaHome().value.javaCommand, build.options.javaOptions.javaOpts.toSeq.map(_.value.value), classPath, - Constants.testRunnerMainClass, + testRunnerMainClass, extraArgs, logger, allowExecve = allowExecve @@ -274,7 +279,8 @@ object Test extends ScalaCommand[TestOptions] { // https://github.com/VirtusLab/scala-cli/issues/426 if classPath0.exists(_.contains("zio-test")) && !classPath0.exists(_.contains("zio-test-sbt")) then { - val parentInspector = new AsmTestRunner.ParentInspector(classPath) + val parentInspector = + new AsmTestRunner.ParentInspector(classPath, TestRunnerLogger(logger.verbosity)) Runner.frameworkNames(classPath, parentInspector, logger) match { case Right(f) => f.headOption case Left(_) => diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala index f0f08646cd..82aa32653f 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/MillProjectDescriptor.scala @@ -9,7 +9,7 @@ import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.internal.Runner.frameworkNames import scala.build.options.{BuildOptions, Platform, ScalaJsOptions, ScalaNativeOptions, Scope} -import scala.build.testrunner.AsmTestRunner +import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger} import scala.build.{Logger, Sources} import scala.cli.ScalaCli @@ -137,8 +137,9 @@ final case class MillProjectDescriptor( logger.debug(exception.message) Seq.empty } - val parentInspector = new AsmTestRunner.ParentInspector(testClassPath) - val frameworkName0 = options.testOptions.frameworks.headOption.orElse { + val parentInspector = + new AsmTestRunner.ParentInspector(testClassPath, TestRunnerLogger(logger.verbosity)) + val frameworkName0 = options.testOptions.frameworks.headOption.orElse { frameworkNames(testClassPath, parentInspector, logger).toOption .flatMap(_.headOption) // TODO: handle multiple frameworks here } diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala index 0637903252..c56f0bc0a5 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/SbtProjectDescriptor.scala @@ -17,7 +17,7 @@ import scala.build.options.{ Scope, ShadowingSeq } -import scala.build.testrunner.AsmTestRunner +import scala.build.testrunner.{AsmTestRunner, Logger as TestRunnerLogger} import scala.build.{Logger, Positioned, Sources} import scala.cli.ScalaCli @@ -258,8 +258,9 @@ final case class SbtProjectDescriptor( Seq.empty } - val parentInspector = new AsmTestRunner.ParentInspector(testClassPath) - val frameworkName0 = options.testOptions.frameworks.headOption.orElse { + val parentInspector = + new AsmTestRunner.ParentInspector(testClassPath, TestRunnerLogger(logger.verbosity)) + val frameworkName0 = options.testOptions.frameworks.headOption.orElse { frameworkNames(testClassPath, parentInspector, logger).toOption .flatMap(_.headOption) // TODO: handle multiple frameworks here } 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..62b8fffb15 --- /dev/null +++ b/modules/cli/src/test/scala/cli/commands/tests/DocTests.scala @@ -0,0 +1,81 @@ +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) + 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/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/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/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/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..9a1b7f67f9 --- /dev/null +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Wasm.scala @@ -0,0 +1,56 @@ +package scala.build.preprocessing.directives + +import scala.build.Positioned +import scala.build.directives.* +import scala.build.errors.{BuildException, UnrecognizedWasmRuntimeError} +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") +@DirectiveUsage( + "//> using wasm|wasmRuntime _value_", + """ + |`//> using wasm` _true|false_ + | + |`//> using wasm` + | + |`//> using wasmRuntime` _node|deno_ + |""".stripMargin +) +@DirectiveDescription("Add WebAssembly options") +@DirectiveLevel(SpecificationLevel.EXPERIMENTAL) +final case class Wasm( + wasm: Option[Boolean] = None, + wasmRuntime: Option[String] = None +) extends HasBuildOptions { + def buildOptions: Either[BuildException, BuildOptions] = { + 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) + } + } +} + +object Wasm { + val handler: DirectiveHandler[Wasm] = DirectiveHandler.derive +} 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..b4e5fdfeaa 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 @@ -60,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 {} @@ -71,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)) + } } } @@ -138,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" -> @@ -158,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, @@ -174,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)) + } } } @@ -697,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 { @@ -719,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")) } } @@ -907,4 +933,56 @@ 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) + } + } + } + + test("sbt file in directory does not break compile") { + TestInputs( + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = println("Hello") + |} + |""".stripMargin, + os.rel / "build.sbt" -> """name := "my-project"""" + ).fromRoot { root => + os.proc(TestUtil.cli, "compile", extraOptions, ".").call(cwd = root) + } + } } 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..2cfdb38fd4 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,96 @@ 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/")) + } + } + } + + 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/modules/integration/src/test/scala/scala/cli/integration/FixTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/FixTestDefinitions.scala index b7e060246f..c0355bd4cb 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/FixTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/FixTestDefinitions.scala @@ -68,6 +68,30 @@ abstract class FixTestDefinitions } } + test("sbt file in directory does not break fix") { + TestInputs( + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = println("Hello") + |} + |""".stripMargin, + os.rel / "build.sbt" -> """name := "my-project"""", + os.rel / scalafixConfFileName -> + """rules = [ + | RedundantSyntax + |] + |""".stripMargin + ).fromRoot { root => + os.proc( + TestUtil.cli, + "--power", + "fix", + ".", + extraOptions + ).call(cwd = root) + } + } + def filterDebugOutputs(output: String): String = output .linesIterator diff --git a/modules/integration/src/test/scala/scala/cli/integration/FmtTests.scala b/modules/integration/src/test/scala/scala/cli/integration/FmtTests.scala index ace4fef4b4..afd439f1bc 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/FmtTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/FmtTests.scala @@ -232,4 +232,35 @@ class FmtTests extends ScalaCliSuite { expect(updatedContent == expectedSimpleInputsFormattedContent) } } + + val sbtUnformattedContent: String = + """val message = "hello" + |""".stripMargin + val expectedSbtFormattedContent: String = noCrLf { + """val message = "hello" + |""".stripMargin + } + val sbtInputs: TestInputs = TestInputs( + os.rel / confFileName -> + s"""|version = "${Constants.defaultScalafmtVersion}" + |runner.dialect = scala213 + |""".stripMargin, + os.rel / "build.sbt" -> sbtUnformattedContent + ) + + test("sbt file is formatted when passed explicitly") { + sbtInputs.fromRoot { root => + os.proc(TestUtil.cli, "fmt", "build.sbt").call(cwd = root) + val updatedContent = noCrLf(os.read(root / "build.sbt")) + expect(updatedContent == expectedSbtFormattedContent) + } + } + + test("sbt file is formatted when directory is passed") { + sbtInputs.fromRoot { root => + os.proc(TestUtil.cli, "fmt", ".").call(cwd = root) + val updatedContent = noCrLf(os.read(root / "build.sbt")) + expect(updatedContent == expectedSbtFormattedContent) + } + } } 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..97c20ec286 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} @@ -1498,15 +1499,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 +1527,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)) + } } } } + } } } @@ -1563,4 +1560,67 @@ 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) + } + } + } + + test("sbt file in directory does not break package") { + val message = "Hello from package" + TestInputs( + os.rel / "Main.scala" -> + s"""object Main { + | def main(args: Array[String]): Unit = println("$message") + |} + |""".stripMargin, + os.rel / "build.sbt" -> """name := "my-project"""" + ).fromRoot { root => + os.proc(TestUtil.cli, "--power", "package", extraOptions, ".").call( + cwd = root, + stdin = os.Inherit, + stdout = os.Inherit + ) + val launcher = root / (if Properties.isWin then "Main.bat" else "Main") + expect(os.isFile(launcher)) + val output = TestUtil.maybeUseBash(launcher)(cwd = root).out.trim() + expect(output == message) + } + } } 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")) + } + } + } + } + } 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/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunScalaJsTestDefinitions.scala index 4745787924..7ff3972f7a 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,260 @@ 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!") + } + } + + 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!") + } + } - // TODO : Run WASM using node. Requires node 22. + 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") } } + 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/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index 1d3517e2d6..6bb62f249e 100755 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -2510,4 +2510,19 @@ abstract class RunTestDefinitions processes.foreach { case (p, _) => expect(p.exitCode() == 0) } } } + + test("sbt file in directory does not break run") { + val message = "Hello from run" + TestInputs( + os.rel / "Main.scala" -> + s"""object Main { + | def main(args: Array[String]): Unit = println("$message") + |} + |""".stripMargin, + os.rel / "build.sbt" -> """name := "my-project"""" + ).fromRoot { root => + val output = os.proc(TestUtil.cli, extraOptions, ".").call(cwd = root).out.trim() + expect(output == message) + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala index 2464a67e19..aff40fa39a 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala @@ -236,4 +236,30 @@ class RunTestsDefault extends RunTestDefinitions expect(res.err.trim().contains(expectedWarning)) } } + + for { + buildServerOptions <- Seq(Nil, Seq("--server=false")) + buildServerDesc = + if buildServerOptions.isEmpty then "with build server" else "without build server" + } + test(s"pure Java run has no Scala on classpath $buildServerDesc") { + TestInputs( + os.rel / "Main.java" -> + """public class Main { + | public static void main(String[] args) { + | try { + | Class.forName("scala.Predef"); + | throw new RuntimeException("Scala should not be on the classpath"); + | } catch (ClassNotFoundException e) { + | System.out.println("No Scala on classpath!"); + | } + | } + |} + |""".stripMargin + ).fromRoot { root => + val res = + os.proc(TestUtil.cli, "run", buildServerOptions, extraOptions, ".").call(cwd = root) + expect(res.out.text().contains("No Scala on classpath!")) + } + } } 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/TestTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/TestTestsDefault.scala index 391494c909..c38e260b44 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestTestsDefault.scala @@ -98,4 +98,38 @@ class TestTestsDefault extends TestTestDefinitions with TestDefault { expect(err.countOccurrences(expectedWarning) == 1) } } + + for { + buildServerOptions <- Seq(Nil, Seq("--server=false")) + buildServerDesc = + if buildServerOptions.isEmpty then "with build server" else "without build server" + } + test(s"pure Java test with JUnit has no Scala on classpath $buildServerDesc") { + TestInputs( + os.rel / "test" / "MyTests.java" -> + """//> using test.dep junit:junit:4.13.2 + |//> using test.dep com.novocode:junit-interface:0.11 + |import org.junit.Test; + |import static org.junit.Assert.assertEquals; + | + |public class MyTests { + | @Test + | public void foo() { + | try { + | Class.forName("scala.Predef"); + | throw new AssertionError("Scala should not be on the classpath"); + | } catch (ClassNotFoundException e) { + | // expected + | } + | assertEquals(4, 2 + 2); + | System.out.println("No Scala on classpath!"); + | } + |} + |""".stripMargin + ).fromRoot { root => + val res = + os.proc(TestUtil.cli, "test", extraOptions, buildServerOptions, ".").call(cwd = root) + expect(res.out.text().contains("No Scala on classpath!")) + } + } } 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/java-test-runner/src/main/java/scala/build/testrunner/JavaAsmTestRunner.java b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaAsmTestRunner.java new file mode 100644 index 0000000000..8c3df378e1 --- /dev/null +++ b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaAsmTestRunner.java @@ -0,0 +1,329 @@ +package scala.build.testrunner; + +import org.objectweb.asm.*; +import sbt.testing.*; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; +import java.util.zip.*; + +public class JavaAsmTestRunner { + + public static class ParentInspector { + private final List classPath; + private final JavaTestLogger logger; + private final ConcurrentHashMap> cache = new ConcurrentHashMap<>(); + + public ParentInspector(List classPath, JavaTestLogger logger) { + this.classPath = classPath; + this.logger = logger; + } + + private List parents(String className) { + return cache.computeIfAbsent(className, name -> { + byte[] byteCode = findInClassPath(classPath, name + ".class", logger); + if (byteCode == null) return Collections.emptyList(); + TestClassChecker checker = new TestClassChecker(); + ClassReader reader = new ClassReader(byteCode); + reader.accept(checker, 0); + return checker.getImplements(); + }); + } + + public List allParents(String className) { + List result = new ArrayList<>(); + Set done = new HashSet<>(); + Deque todo = new ArrayDeque<>(); + todo.add(className); + while (!todo.isEmpty()) { + String current = todo.poll(); + if (!done.add(current)) continue; + result.add(current); + todo.addAll(parents(current)); + } + return result; + } + } + + public static Optional matchFingerprints( + String className, + InputStream byteCodeStream, + List fingerprints, + ParentInspector parentInspector, + ClassLoader loader, + JavaTestLogger logger + ) throws IOException { + TestClassChecker checker = new TestClassChecker(); + ClassReader reader = new ClassReader(byteCodeStream); + reader.accept(checker, 0); + + boolean isModule = className.endsWith("$"); + boolean hasPublicConstructors = checker.getPublicConstructorCount() > 0; + boolean definitelyNoTests = checker.isAbstract() || + checker.isInterface() || + checker.getPublicConstructorCount() > 1 || + isModule == hasPublicConstructors; + + if (definitelyNoTests) return Optional.empty(); + + for (Fingerprint fp : fingerprints) { + if (fp instanceof SubclassFingerprint) { + SubclassFingerprint sf = (SubclassFingerprint) fp; + if (sf.isModule() != isModule) continue; + String superName = sf.superclassName().replace('.', '/'); + if (parentInspector.allParents(checker.getName()).contains(superName)) { + return Optional.of(fp); + } + } else if (fp instanceof AnnotatedFingerprint) { + AnnotatedFingerprint af = (AnnotatedFingerprint) fp; + if (af.isModule() != isModule) continue; + // Use classloader-based reflection for annotation matching (proven approach) + if (loader != null) { + try { + String rawName = className.replace('/', '.').replace('\\', '.'); + String clsNameForLoad = rawName.endsWith("$") ? rawName.substring(0, rawName.length() - 1) : rawName; + Class cls = loader.loadClass(clsNameForLoad); + Optional result = + JavaFrameworkUtils.matchFingerprints(loader, cls, new Fingerprint[]{fp}, logger); + if (result.isPresent()) return Optional.of(fp); + } catch (ClassNotFoundException | NoClassDefFoundError | + UnsupportedClassVersionError | IncompatibleClassChangeError e) { + // Expected: class may not be loadable during scanning + logger.debug( + "Could not load class for annotation matching: " + className + " (" + e + ")"); + } + } + } + } + return Optional.empty(); + } + + public static List findFrameworkServices(List classPath, JavaTestLogger logger) { + List result = new ArrayList<>(); + byte[] content = findInClassPath(classPath, "META-INF/services/sbt.testing.Framework", logger); + if (content != null) { + parseServiceFileContent(new String(content, StandardCharsets.UTF_8), result); + } + return result; + } + + private static void parseServiceFileContent(String content, List result) { + for (String line : content.split("[\r\n]+")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + result.add(trimmed); + } + } + } + + public static List findFrameworks( + List classPath, + List preferredClasses, + ParentInspector parentInspector, + JavaTestLogger logger + ) { + List result = new ArrayList<>(); + // first check preferred classes + for (String preferred : preferredClasses) { + String resourceName = preferred.replace('.', '/') + ".class"; + byte[] bytes = findInClassPath(classPath, resourceName, logger); + if (bytes != null) { + TestClassChecker checker = new TestClassChecker(); + new ClassReader(bytes).accept(checker, 0); + if (!checker.isAbstract() && checker.getPublicConstructorCount() == 1) { + String internalName = preferred.replace('.', '/'); + if (parentInspector.allParents(internalName).contains("sbt/testing/Framework")) { + result.add(internalName); + } + } + } + } + if (!result.isEmpty()) return result; + + // scan all classes in classpath + for (Map.Entry entry : listClassesByteCode(classPath, true, logger).entrySet()) { + String name = entry.getKey(); + if (name.contains("module-info")) continue; + TestClassChecker checker = new TestClassChecker(); + new ClassReader(entry.getValue()).accept(checker, 0); + if (!checker.isAbstract() && checker.getPublicConstructorCount() == 1) { + if (parentInspector.allParents(name).contains("sbt/testing/Framework")) { + result.add(name); + } + } + } + return result; + } + + public static List taskDefs( + List classPath, + boolean keepJars, + List fingerprints, + ParentInspector parentInspector, + ClassLoader loader, + JavaTestLogger logger + ) { + List result = new ArrayList<>(); + for (Map.Entry entry : listClassesByteCode(classPath, keepJars, logger).entrySet()) { + String name = entry.getKey(); + if (name.contains("module-info")) continue; + try { + Optional fp = matchFingerprints( + name, + new ByteArrayInputStream(entry.getValue()), + fingerprints, + parentInspector, + loader, + logger + ); + if (fp.isPresent()) { + String stripped = name.endsWith("$") ? name.substring(0, name.length() - 1) : name; + String clsName = stripped.replace('/', '.').replace('\\', '.'); + result.add(new TaskDef(clsName, fp.get(), false, new Selector[]{new SuiteSelector()})); + } + } catch (IOException e) { + logger.debug("Could not read bytecode for " + name + ": " + e.getMessage()); + } + } + return result; + } + + private static Map listClassesByteCode( + List classPath, boolean keepJars, JavaTestLogger logger + ) { + Map result = new LinkedHashMap<>(); + for (Path entry : classPath) { + result.putAll(listClassesByteCode(entry, keepJars, logger)); + } + return result; + } + + private static Map listClassesByteCode( + Path entry, boolean keepJars, JavaTestLogger logger + ) { + Map result = new LinkedHashMap<>(); + if (Files.isDirectory(entry)) { + try (Stream stream = Files.walk(entry, Integer.MAX_VALUE)) { + stream.filter(p -> p.getFileName().toString().endsWith(".class")) + .forEach(p -> { + String rel = entry.relativize(p).toString().replace('\\', '/'); + String name = rel.endsWith(".class") ? rel.substring(0, rel.length() - 6) : rel; + try { + result.put(name, Files.readAllBytes(p)); + } catch (IOException e) { + logger.debug("Could not read class file " + p + ": " + e.getMessage()); + } + }); + } catch (IOException e) { + logger.log("Could not walk directory " + entry + ": " + e.getMessage()); + } + } else if (keepJars && Files.isRegularFile(entry)) { + byte[] buf = new byte[16384]; + try (ZipFile zf = new ZipFile(entry.toFile())) { + Enumeration entries = zf.entries(); + while (entries.hasMoreElements()) { + ZipEntry ze = entries.nextElement(); + if (!ze.getName().endsWith(".class")) continue; + String name = ze.getName(); + name = name.substring(0, name.length() - 6); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (InputStream is = zf.getInputStream(ze)) { + int read; + while ((read = is.read(buf)) >= 0) { + baos.write(buf, 0, read); + } + } + result.put(name, baos.toByteArray()); + } + } catch (IOException e) { + logger.log("Could not read JAR " + entry + ": " + e.getMessage()); + } + } + return result; + } + + static byte[] findInClassPath(List classPath, String name, JavaTestLogger logger) { + for (Path entry : classPath) { + byte[] found = findInClassPathEntry(entry, name, logger); + if (found != null) return found; + } + return null; + } + + private static byte[] findInClassPathEntry(Path entry, String name, JavaTestLogger logger) { + if (Files.isDirectory(entry)) { + Path p = entry.resolve(name); + if (Files.isRegularFile(p)) { + try { + return Files.readAllBytes(p); + } catch (IOException e) { + logger.debug("Could not read " + p + ": " + e.getMessage()); + return null; + } + } + } else if (Files.isRegularFile(entry)) { + byte[] buf = new byte[16384]; + try (ZipFile zf = new ZipFile(entry.toFile())) { + ZipEntry ze = zf.getEntry(name); + if (ze == null) return null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (InputStream is = zf.getInputStream(ze)) { + int read; + while ((read = is.read(buf)) >= 0) { + baos.write(buf, 0, read); + } + } + return baos.toByteArray(); + } catch (IOException e) { + logger.debug("Could not read " + name + " from " + entry + ": " + e.getMessage()); + return null; + } + } + return null; + } + + public static class TestClassChecker extends ClassVisitor { + private String name; + private int publicConstructorCount = 0; + private boolean isInterface = false; + private boolean isAbstract = false; + private List implementsList = new ArrayList<>(); + + public TestClassChecker() { + super(Opcodes.ASM9); + } + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + this.name = name; + this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; + this.isAbstract = (access & Opcodes.ACC_ABSTRACT) != 0; + if (superName != null) implementsList.add(superName); + if (interfaces != null) { + for (String iface : interfaces) { + implementsList.add(iface); + } + } + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + if ("".equals(name) && (access & Opcodes.ACC_PUBLIC) != 0) { + publicConstructorCount++; + } + return null; + } + + public String getName() { return name; } + public int getPublicConstructorCount() { return publicConstructorCount; } + public boolean isInterface() { return isInterface; } + public boolean isAbstract() { return isAbstract; } + public List getImplements() { return implementsList; } + } +} diff --git a/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaDynamicTestRunner.java b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaDynamicTestRunner.java new file mode 100644 index 0000000000..5de3b325d9 --- /dev/null +++ b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaDynamicTestRunner.java @@ -0,0 +1,162 @@ +package scala.build.testrunner; + +import sbt.testing.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class JavaDynamicTestRunner { + + /** + * Based on junit-interface GlobFilter.compileGlobPattern: + * https://github.com/sbt/junit-interface/blob/f8c6372ed01ce86f15393b890323d96afbe6d594/src/main/java/com/novocode/junit/GlobFilter.java#L37 + * + * Converts a glob expression (only * supported) into a regex Pattern. + */ + private static Pattern globPattern(String expr) { + String[] parts = expr.split("\\*", -1); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < parts.length; i++) { + if (i != 0) sb.append(".*"); + if (!parts[i].isEmpty()) sb.append(Pattern.quote(parts[i].replace("\n", "\\n"))); + } + return Pattern.compile(sb.toString()); + } + + public static void main(String[] args) { + List testFrameworks = new ArrayList<>(); + List remainingArgs = new ArrayList<>(); + boolean requireTests = false; + int verbosity = 0; + Optional testOnly = Optional.empty(); + + boolean pastDashDash = false; + for (String arg : args) { + if (pastDashDash) { + remainingArgs.add(arg); + } else if ("--".equals(arg)) { + pastDashDash = true; + } else if (arg.startsWith("--test-framework=")) { + testFrameworks.add(arg.substring("--test-framework=".length())); + } else if (arg.startsWith("--test-only=")) { + testOnly = Optional.of(arg.substring("--test-only=".length())); + } else if (arg.startsWith("--verbosity=")) { + try { + verbosity = Integer.parseInt(arg.substring("--verbosity=".length())); + } catch (NumberFormatException e) { + System.err.println("Warning: malformed --verbosity value: " + arg); + } + } else if ("--require-tests".equals(arg)) { + requireTests = true; + } else { + remainingArgs.add(arg); + } + } + + JavaTestLogger logger = new JavaTestLogger(verbosity, System.err); + + if (!testFrameworks.isEmpty()) { + logger.debug("Directly passed " + testFrameworks.size() + " test frameworks:\n - " + + String.join("\n - ", testFrameworks)); + } + + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + java.util.List classPath0 = JavaTestRunner.classPath(classLoader, logger); + + List frameworks; + if (!testFrameworks.isEmpty()) { + frameworks = new ArrayList<>(); + for (String fw : testFrameworks) { + try { + frameworks.add(JavaFrameworkUtils.loadFramework(classLoader, fw)); + } catch (Exception e) { + logger.error("Could not load test framework: " + fw); + logger.error(e.toString()); + System.exit(1); + } + } + } else { + List frameworkServices = JavaFrameworkUtils.findFrameworkServices(classLoader); + List scannedFrameworks = JavaFrameworkUtils.findFrameworks( + classPath0, classLoader, JavaTestRunner.commonTestFrameworks(), logger + ); + List toRun = JavaFrameworkUtils.getFrameworksToRun( + frameworkServices, scannedFrameworks, logger + ); + if (toRun.isEmpty()) { + if (verbosity >= 2) { + throw new RuntimeException("No test framework found"); + } else { + System.err.println("No test framework found"); + System.exit(1); + } + } + frameworks = toRun; + } + + String[] runnerArgs = remainingArgs.toArray(new String[0]); + final Optional testOnlyFinal = testOnly; + final boolean requireTestsFinal = requireTests; + + boolean anyFailed = false; + for (Framework framework : frameworks) { + logger.log("Running test framework: " + framework.name()); + Fingerprint[] fingerprints = framework.fingerprints(); + Runner runner = framework.runner(runnerArgs, new String[0], classLoader); + + List> classes = new ArrayList<>(); + for (String name : JavaFrameworkUtils.listClasses(classPath0, false, logger)) { + try { + classes.add(classLoader.loadClass(name)); + } catch (ClassNotFoundException | NoClassDefFoundError | + UnsupportedClassVersionError | IncompatibleClassChangeError e) { + // Expected: not every .class file on the classpath is loadable + logger.debug("Could not load class " + name + ": " + e); + } + } + + List taskDefs = new ArrayList<>(); + for (Class cls : classes) { + Optional fp = JavaFrameworkUtils.matchFingerprints( + classLoader, cls, fingerprints, logger + ); + if (!fp.isPresent()) continue; + String clsName = cls.getName().endsWith("$") + ? cls.getName().substring(0, cls.getName().length() - 1) + : cls.getName(); + if (testOnlyFinal.isPresent()) { + Pattern pat = globPattern(testOnlyFinal.get()); + if (!pat.matcher(clsName).matches()) continue; + } + taskDefs.add(new TaskDef(clsName, fp.get(), false, new Selector[]{new SuiteSelector()})); + } + + Task[] initialTasks = runner.tasks(taskDefs.toArray(new TaskDef[0])); + List events = JavaTestRunner.runTasks(Arrays.asList(initialTasks), System.out); + + boolean failed = events.stream().anyMatch(ev -> + ev.status() == Status.Error || + ev.status() == Status.Failure || + ev.status() == Status.Canceled + ); + + String doneMsg = runner.done(); + if (doneMsg != null && !doneMsg.isEmpty()) System.out.println(doneMsg); + + if (requireTestsFinal && events.isEmpty()) { + logger.error("Error: no tests were run for " + framework.name() + "."); + anyFailed = true; + } else if (failed) { + logger.error("Error: " + framework.name() + " tests failed."); + anyFailed = true; + } else { + logger.log(framework.name() + " tests ran successfully."); + } + } + + System.exit(anyFailed ? 1 : 0); + } +} diff --git a/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaFrameworkUtils.java b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaFrameworkUtils.java new file mode 100644 index 0000000000..eb3b516b7e --- /dev/null +++ b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaFrameworkUtils.java @@ -0,0 +1,201 @@ +package scala.build.testrunner; + +import sbt.testing.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.stream.Stream; + +public class JavaFrameworkUtils { + + public static List findFrameworkServices(ClassLoader loader) { + List result = new ArrayList<>(); + ServiceLoader serviceLoader = ServiceLoader.load(Framework.class, loader); + for (Framework f : serviceLoader) { + result.add(f); + } + return result; + } + + public static Framework loadFramework(ClassLoader loader, String className) throws Exception { + Class cls = loader.loadClass(className); + return (Framework) cls.getConstructor().newInstance(); + } + + public static List findFrameworks( + List classPath, + ClassLoader loader, + List preferredClasses, + JavaTestLogger logger + ) { + Class frameworkCls = Framework.class; + List result = new ArrayList<>(); + Set seen = new LinkedHashSet<>(); + + // first try preferred classes, then scan classpath + List candidates = new ArrayList<>(preferredClasses); + for (String name : listClasses(classPath, true, logger)) { + if (!seen.contains(name)) { + candidates.add(name); + } + } + + for (String name : candidates) { + if (!seen.add(name)) continue; + Class cls; + try { + cls = loader.loadClass(name); + } catch (ClassNotFoundException | UnsupportedClassVersionError | + NoClassDefFoundError | IncompatibleClassChangeError e) { + // Expected: most classpath entries aren't test frameworks + continue; + } + if (!frameworkCls.isAssignableFrom(cls)) continue; + if (Modifier.isAbstract(cls.getModifiers())) continue; + long publicNoArgCtors = Arrays.stream(cls.getConstructors()) + .filter(c -> Modifier.isPublic(c.getModifiers()) && c.getParameterCount() == 0) + .count(); + if (publicNoArgCtors != 1) continue; + try { + Framework instance = (Framework) cls.getConstructor().newInstance(); + result.add(instance); + } catch (Exception e) { + logger.log("Could not instantiate framework " + name + ": " + e); + } + } + return result; + } + + public static Optional matchFingerprints( + ClassLoader loader, + Class cls, + Fingerprint[] fingerprints, + JavaTestLogger logger + ) { + boolean isModule = cls.getName().endsWith("$"); + long publicCtorCount = Arrays.stream(cls.getConstructors()) + .filter(c -> Modifier.isPublic(c.getModifiers())) + .count(); + boolean noPublicConstructors = publicCtorCount == 0; + boolean definitelyNoTests = Modifier.isAbstract(cls.getModifiers()) || + cls.isInterface() || + publicCtorCount > 1 || + isModule != noPublicConstructors; + if (definitelyNoTests) return Optional.empty(); + + for (Fingerprint fp : fingerprints) { + if (fp instanceof SubclassFingerprint) { + SubclassFingerprint sf = (SubclassFingerprint) fp; + if (sf.isModule() != isModule) continue; + try { + Class superCls = loader.loadClass(sf.superclassName()); + if (superCls.isAssignableFrom(cls)) return Optional.of(fp); + } catch (ClassNotFoundException e) { + logger.debug( + "Superclass not found for fingerprint matching: " + sf.superclassName()); + } + } else if (fp instanceof AnnotatedFingerprint) { + AnnotatedFingerprint af = (AnnotatedFingerprint) fp; + if (af.isModule() != isModule) continue; + try { + @SuppressWarnings("unchecked") + Class annotationCls = + (Class) loader.loadClass(af.annotationName()); + boolean matches = + cls.isAnnotationPresent(annotationCls) || + Arrays.stream(cls.getDeclaredMethods()) + .anyMatch(m -> m.isAnnotationPresent(annotationCls)) || + Arrays.stream(cls.getMethods()) + .anyMatch(m -> m.isAnnotationPresent(annotationCls) && + Modifier.isPublic(m.getModifiers())); + if (matches) return Optional.of(fp); + } catch (ClassNotFoundException e) { + logger.debug( + "Annotation class not found for fingerprint matching: " + af.annotationName()); + } + } + } + return Optional.empty(); + } + + public static List getFrameworksToRun( + List frameworkServices, + List frameworks, + JavaTestLogger logger + ) { + List all = new ArrayList<>(frameworkServices); + all.addAll(frameworks); + return getFrameworksToRun(all, logger); + } + + public static List getFrameworksToRun( + List allFrameworks, + JavaTestLogger logger + ) { + // dedup by name + Map byName = new LinkedHashMap<>(); + for (Framework f : allFrameworks) { + byName.putIfAbsent(f.name(), f); + } + List distinct = new ArrayList<>(byName.values()); + + // filter out frameworks that are superclasses of another framework in the list + List finalFrameworks = new ArrayList<>(); + for (Framework f1 : distinct) { + boolean isInherited = distinct.stream() + .filter(f2 -> f2 != f1) + .anyMatch(f2 -> f1.getClass().isAssignableFrom(f2.getClass())); + if (!isInherited) finalFrameworks.add(f1); + } + return finalFrameworks; + } + + public static List listClasses(List classPath, boolean keepJars, JavaTestLogger logger) { + List result = new ArrayList<>(); + for (Path entry : classPath) { + result.addAll(listClasses(entry, keepJars, logger)); + } + return result; + } + + public static List listClasses(Path entry, boolean keepJars, JavaTestLogger logger) { + List result = new ArrayList<>(); + if (Files.isDirectory(entry)) { + try (Stream stream = Files.walk(entry, Integer.MAX_VALUE)) { + stream.filter(p -> p.getFileName().toString().endsWith(".class")) + .map(entry::relativize) + .map(p -> { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < p.getNameCount(); i++) { + if (i > 0) sb.append("."); + sb.append(p.getName(i).toString()); + } + String name = sb.toString(); + return name.endsWith(".class") ? name.substring(0, name.length() - 6) : name; + }) + .forEach(result::add); + } catch (Exception e) { + logger.log("Could not walk directory " + entry + ": " + e.getMessage()); + } + } else if (keepJars && Files.isRegularFile(entry)) { + try (ZipFile zf = new ZipFile(entry.toFile())) { + Enumeration entries = zf.entries(); + while (entries.hasMoreElements()) { + ZipEntry ze = entries.nextElement(); + String name = ze.getName(); + if (name.endsWith(".class")) { + result.add(name.substring(0, name.length() - 6).replace("/", ".")); + } + } + } catch (Exception e) { + logger.log("Could not read JAR " + entry + ": " + e.getMessage()); + } + } + return result; + } +} diff --git a/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaTestLogger.java b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaTestLogger.java new file mode 100644 index 0000000000..a02166de3b --- /dev/null +++ b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaTestLogger.java @@ -0,0 +1,29 @@ +package scala.build.testrunner; + +import java.io.PrintStream; + +public class JavaTestLogger { + private final int verbosity; + private final PrintStream out; + + public JavaTestLogger(int verbosity, PrintStream out) { + this.verbosity = verbosity; + this.out = out; + } + + public void error(String message) { + out.println(message); + } + + public void message(String message) { + if (verbosity >= 0) out.println(message); + } + + public void log(String message) { + if (verbosity >= 1) out.println(message); + } + + public void debug(String message) { + if (verbosity >= 2) out.println(message); + } +} diff --git a/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaTestRunner.java b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaTestRunner.java new file mode 100644 index 0000000000..d09ecfdc3f --- /dev/null +++ b/modules/java-test-runner/src/main/java/scala/build/testrunner/JavaTestRunner.java @@ -0,0 +1,82 @@ +package scala.build.testrunner; + +import sbt.testing.*; + +import java.io.File; +import java.io.PrintStream; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; + +public class JavaTestRunner { + + public static List commonTestFrameworks() { + // Only pure-Java-compatible frameworks belong here. + // Scala-only frameworks (munit, utest, ScalaCheck, ZIO Test, ScalaTest, weaver) + // live in the Scala test-runner's TestRunner.commonTestFrameworks instead. + return Arrays.asList("com.novocode.junit.JUnitFramework"); + } + + public static List classPath(ClassLoader loader, JavaTestLogger logger) { + List result = new ArrayList<>(); + collectClassPath(loader, result, logger); + return result; + } + + private static void collectClassPath(ClassLoader loader, List result, JavaTestLogger logger) { + if (loader == null) return; + if (loader instanceof URLClassLoader) { + URLClassLoader urlLoader = (URLClassLoader) loader; + for (java.net.URL url : urlLoader.getURLs()) { + if ("file".equals(url.getProtocol())) { + try { + result.add(Paths.get(url.toURI()).toAbsolutePath()); + } catch (Exception e) { + logger.debug( + "Could not convert URL to path: " + url + " (" + e.getMessage() + ")"); + } + } + } + } else if (loader.getClass().getName().equals("jdk.internal.loader.ClassLoaders$AppClassLoader")) { + String cp = System.getProperty("java.class.path", ""); + for (String entry : cp.split(File.pathSeparator)) { + if (!entry.isEmpty()) { + result.add(Paths.get(entry)); + } + } + } + collectClassPath(loader.getParent(), result, logger); + } + + public static List runTasks(List initialTasks, PrintStream out) { + Deque tasks = new ArrayDeque<>(initialTasks); + List events = new ArrayList<>(); + + sbt.testing.Logger logger = new sbt.testing.Logger() { + public boolean ansiCodesSupported() { return true; } + public void error(String msg) { out.println(msg); } + public void warn(String msg) { out.println(msg); } + public void info(String msg) { out.println(msg); } + public void debug(String msg) { out.println(msg); } + public void trace(Throwable t) { t.printStackTrace(out); } + }; + + EventHandler eventHandler = event -> events.add(event); + sbt.testing.Logger[] loggers = new sbt.testing.Logger[]{logger}; + + while (!tasks.isEmpty()) { + Task task = tasks.poll(); + Task[] newTasks = task.execute(eventHandler, loggers); + for (Task t : newTasks) { + tasks.add(t); + } + } + + return events; + } +} diff --git a/modules/options/src/main/scala/scala/build/Artifacts.scala b/modules/options/src/main/scala/scala/build/Artifacts.scala index bb430a5d81..c3cfa4c8ea 100644 --- a/modules/options/src/main/scala/scala/build/Artifacts.scala +++ b/modules/options/src/main/scala/scala/build/Artifacts.scala @@ -49,6 +49,7 @@ final case class Artifacts( extraSourceJars: Seq[os.Path], scalaOpt: Option[ScalaArtifacts], hasJvmRunner: Boolean, + hasJavaTestRunner: Boolean, resolution: Option[Resolution] ) { @@ -131,6 +132,7 @@ object Artifacts { jvmVersion: Int, addJvmRunner: Option[Boolean], addJvmTestRunner: Boolean, + addJvmJavaTestRunner: Boolean, addJmhDependencies: Option[String], extraRepositories: Seq[Repository], keepResolution: Boolean, @@ -189,11 +191,19 @@ object Artifacts { } else Nil + val jvmJavaTestRunnerDependencies = + if addJvmJavaTestRunner then + Seq( + dep"${Constants.javaTestRunnerOrganization}:${Constants.javaTestRunnerModuleName}:${Constants.javaTestRunnerVersion}" + ) + else Nil + val jmhDependencies = addJmhDependencies.toSeq .map(version => dep"${Constants.jmhOrg}:${Constants.jmhGeneratorBytecodeModule}:$version") val maybeSnapshotRepo = { val hasSnapshots = jvmTestRunnerDependencies.exists(_.version.endsWith("SNAPSHOT")) || + jvmJavaTestRunnerDependencies.exists(_.version.endsWith("SNAPSHOT")) || scalaArtifactsParamsOpt.flatMap(_.scalaNativeCliVersion).exists(_.endsWith("SNAPSHOT")) val hasNightlies = scalaArtifactsParamsOpt.exists(a => a.params.scalaVersion.endsWith("-NIGHTLY") || @@ -409,6 +419,7 @@ object Artifacts { val internalDependencies = jvmTestRunnerDependencies.map(Positioned.none) ++ + jvmJavaTestRunnerDependencies.map(Positioned.none) ++ scalaOpt.toSeq.flatMap(_.internalDependencies).map(Positioned.none) ++ jmhDependencies.map(Positioned.none) val updatedDependencies = dependencies ++ @@ -582,6 +593,7 @@ object Artifacts { extraSourceJars, scalaOpt, hasRunner, + addJvmJavaTestRunner, if (keepResolution) Some(fetchRes.resolution) else None ) } 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..a8547fd86c 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(), @@ -46,6 +47,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 ) { @@ -221,6 +223,10 @@ final case class BuildOptions( private def addJvmTestRunner: Boolean = platform.value == Platform.JVM && internalDependencies.addTestRunnerDependency + + private def addJvmJavaTestRunner: Boolean = + platform.value == Platform.JVM && + internalDependencies.addTestRunnerDependency private def addJsTestBridge: Option[String] = if (platform.value == Platform.JS && internalDependencies.addTestRunnerDependency) Some(scalaJsOptions.finalVersion) @@ -475,6 +481,7 @@ final case class BuildOptions( if (scalaArtifactsParamsOpt.isDefined) None else Some(false) // no runner in pure Java mode } + val isJavaBuild = scalaArtifactsParamsOpt.isEmpty val extraRepositories: Seq[Repository] = value(finalRepositories) val maybeArtifacts = Artifacts( scalaArtifactsParamsOpt = scalaArtifactsParamsOpt, @@ -489,7 +496,8 @@ final case class BuildOptions( fetchSources = classPathOptions.fetchSources.getOrElse(false), jvmVersion = javaHome().value.version, addJvmRunner = addRunnerDependency0, - addJvmTestRunner = isTests && addJvmTestRunner, + addJvmTestRunner = isTests && addJvmTestRunner && !isJavaBuild, + addJvmJavaTestRunner = isTests && addJvmJavaTestRunner && isJavaBuild, addJmhDependencies = jmhOptions.finalJmhVersion, extraRepositories = extraRepositories, keepResolution = internal.keepResolution, 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..34450e2385 --- /dev/null +++ b/modules/options/src/main/scala/scala/build/options/WasmOptions.scala @@ -0,0 +1,18 @@ +package scala.build.options + +/** 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) + */ +final case class WasmOptions( + enabled: Boolean = false, + runtime: WasmRuntime = WasmRuntime.default +) + +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..a2e68d63f8 --- /dev/null +++ b/modules/options/src/main/scala/scala/build/options/WasmRuntime.scala @@ -0,0 +1,35 @@ +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 + */ +sealed abstract class WasmRuntime(val name: String) + +object WasmRuntime { + case object Node extends WasmRuntime("node") + case object Deno extends WasmRuntime("deno") + + val all: Seq[WasmRuntime] = Seq(Node, Deno) + + 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 _ => 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/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/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..0781a9285f 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 @@ -1,7 +1,7 @@ package scala.build.testrunner import org.objectweb.asm -import sbt.testing.* +import sbt.testing.{Logger as _, *} import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} import java.nio.charset.StandardCharsets @@ -12,7 +12,7 @@ import scala.jdk.CollectionConverters.* object AsmTestRunner { - class ParentInspector(classPath: Seq[Path]) { + class ParentInspector(classPath: Seq[Path], logger: Logger) { private val cache = new ConcurrentHashMap[String, Seq[String]] @@ -21,7 +21,7 @@ object AsmTestRunner { case Some(value) => value case None => val byteCodeOpt = - findInClassPath(classPath, className + ".class") + findInClassPath(classPath, className + ".class", logger) .take(1) .toList .headOption @@ -99,7 +99,8 @@ object AsmTestRunner { private def listClassesByteCode( classPathEntry: Path, - keepJars: Boolean + keepJars: Boolean, + logger: Logger ): Iterator[(String, () => InputStream)] = if (Files.isDirectory(classPathEntry)) { var stream: java.util.stream.Stream[Path] = null @@ -118,6 +119,11 @@ object AsmTestRunner { .toVector // fully consume stream before closing it .iterator } + catch { + case e: Exception => + logger.log(s"Could not walk directory $classPathEntry: ${e.getMessage}") + Iterator.empty + } finally if (stream != null) stream.close() } else if (keepJars && Files.isRegularFile(classPathEntry)) { @@ -149,20 +155,36 @@ object AsmTestRunner { .toVector // fully consume ZipFile before closing it .iterator } + catch { + case e: Exception => + logger.log(s"Could not read JAR $classPathEntry: ${e.getMessage}") + Iterator.empty + } finally if (zf != null) zf.close() } else Iterator.empty private def listClassesByteCode( classPath: Seq[Path], - keepJars: Boolean + keepJars: Boolean, + logger: Logger ): Iterator[(String, () => InputStream)] = - classPath.iterator.flatMap(listClassesByteCode(_, keepJars)) + classPath.iterator.flatMap(listClassesByteCode(_, keepJars, logger)) - private def findInClassPath(classPathEntry: Path, name: String): Option[Array[Byte]] = + private def findInClassPath( + classPathEntry: Path, + name: String, + logger: Logger + ): Option[Array[Byte]] = if (Files.isDirectory(classPathEntry)) { val p = classPathEntry.resolve(name) - if (Files.isRegularFile(p)) Some(Files.readAllBytes(p)) + if (Files.isRegularFile(p)) + try Some(Files.readAllBytes(p)) + catch { + case e: java.io.IOException => + logger.debug(s"Could not read $p: ${e.getMessage}") + None + } else None } else if (Files.isRegularFile(classPathEntry)) { @@ -186,35 +208,57 @@ object AsmTestRunner { finally if (is != null) is.close() } } + catch { + case e: java.io.IOException => + logger.debug(s"Could not read $name from $classPathEntry: ${e.getMessage}") + None + } finally if (zf != null) zf.close() } else None - private def findInClassPath(classPath: Seq[Path], name: String): Iterator[Array[Byte]] = + private def findInClassPath( + classPath: Seq[Path], + name: String, + logger: Logger + ): Iterator[Array[Byte]] = classPath .iterator - .flatMap(findInClassPath(_, name).iterator) + .flatMap(findInClassPath(_, name, logger).iterator) - def findFrameworkServices(classPath: Seq[Path]): Seq[String] = - findInClassPath(classPath, "META-INF/services/sbt.testing.Framework") - .map(b => new String(b, StandardCharsets.UTF_8)) + /** 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], logger: Logger): Seq[String] = + findInClassPath(classPath, "META-INF/services/sbt.testing.Framework", logger) + .flatMap(b => parseServiceFileContent(new String(b, StandardCharsets.UTF_8))) .toSeq def findFrameworks( classPath: Seq[Path], preferredClasses: Seq[String], - parentInspector: ParentInspector + parentInspector: ParentInspector, + logger: Logger ): List[String] = { + // first check preferred classes val preferredClassesByteCode = preferredClasses .map(_.replace('.', '/')) .flatMap { name => - findInClassPath(classPath, name + ".class") + findInClassPath(classPath, name + ".class", logger) .map { b => def openStream() = new ByteArrayInputStream(b) (name, () => openStream()) } } - (preferredClassesByteCode.iterator ++ listClassesByteCode(classPath, true)) + // scan all classes in classpath + (preferredClassesByteCode.iterator ++ listClassesByteCode(classPath, true, logger)) .flatMap { case (moduleInfo, _) if moduleInfo.contains("module-info") => Iterator.empty case (name, is) => @@ -280,14 +324,21 @@ object AsmTestRunner { classPath: Seq[Path], keepJars: Boolean, fingerprints: Seq[Fingerprint], - parentInspector: ParentInspector + parentInspector: ParentInspector, + logger: Logger ): Iterator[TaskDef] = - listClassesByteCode(classPath, keepJars = keepJars) + listClassesByteCode(classPath, keepJars = keepJars, logger) .flatMap { case (name, is) => - matchFingerprints(name, is, fingerprints, parentInspector) - .map((name.stripSuffix("$"), _)) - .iterator + try + matchFingerprints(name, is, fingerprints, parentInspector) + .map((name.stripSuffix("$"), _)) + .iterator + catch { + case e: java.io.IOException => + logger.debug(s"Could not read bytecode for $name: ${e.getMessage}") + Iterator.empty + } } .map { case (clsName, fp) => @@ -301,16 +352,24 @@ object AsmTestRunner { def main(args: Array[String]): Unit = { - val classLoader = Thread.currentThread().getContextClassLoader - val classPath = TestRunner.classPath(classLoader) - - val parentCache = new ParentInspector(classPath) + val logger = Logger(0) - val frameworkClassName = findFrameworkServices(classPath).headOption // TODO handle multiple - .orElse(findFrameworks(classPath, TestRunner.commonTestFrameworks, parentCache).headOption) - .getOrElse(sys.error("No test framework found")) - .replace('/', '.') - .replace('\\', '.') + val classLoader = Thread.currentThread().getContextClassLoader + val classPath = TestRunner.classPath(classLoader, logger) + + val parentCache = new ParentInspector(classPath, logger) + + val frameworkClassName = + findFrameworkServices(classPath, logger).headOption // TODO handle multiple + .orElse(findFrameworks( + classPath, + TestRunner.commonTestFrameworks, + parentCache, + logger + ).headOption) + .getOrElse(sys.error("No test framework found")) + .replace('/', '.') + .replace('\\', '.') val framework = classLoader .loadClass(frameworkClassName) @@ -325,7 +384,8 @@ object AsmTestRunner { classPath, keepJars = false, framework.fingerprints().toIndexedSeq, - parentCache + parentCache, + logger ).toArray val runner = framework.runner(Array(), Array(), classLoader) diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala index ae4b5b3f9b..4b43aea694 100644 --- a/modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala @@ -61,11 +61,18 @@ object DynamicTestRunner { t ) case h :: t if h.startsWith("--verbosity=") => + val v = + try h.stripPrefix("--verbosity=").toInt + catch { + case _: NumberFormatException => + System.err.println(s"Warning: malformed --verbosity value: $h") + 0 + } parse( testFrameworks, reverseTestArgs, requireTests, - h.stripPrefix("--verbosity=").toInt, + v, testOnly, t ) @@ -86,7 +93,7 @@ object DynamicTestRunner { ) val classLoader = Thread.currentThread().getContextClassLoader - val classPath0 = TestRunner.classPath(classLoader) + val classPath0 = TestRunner.classPath(classLoader, logger) val frameworks = Option(testFrameworks) .filter(_.nonEmpty) @@ -94,7 +101,8 @@ object DynamicTestRunner { .getOrElse { getFrameworksToRun( frameworkServices = findFrameworkServices(classLoader), - frameworks = findFrameworks(classPath0, classLoader, TestRunner.commonTestFrameworks) + frameworks = + findFrameworks(classPath0, classLoader, TestRunner.commonTestFrameworks, logger) )(logger) match { case f if f.nonEmpty => f case _ if verbosity >= 2 => sys.error("No test framework found") @@ -105,7 +113,16 @@ object DynamicTestRunner { } def classes = { val keepJars = false // look into dependencies, much slower - listClasses(classPath0, keepJars).map(name => classLoader.loadClass(name)) + listClasses(classPath0, keepJars, logger).flatMap { name => + try Iterator(classLoader.loadClass(name)) + catch { + case _: ClassNotFoundException | _: NoClassDefFoundError | + _: UnsupportedClassVersionError | _: IncompatibleClassChangeError => + // Expected: not every .class file on the classpath is loadable + logger.debug(s"Could not load class $name") + Iterator.empty + } + } } val out = System.out @@ -117,7 +134,7 @@ object DynamicTestRunner { val runner = framework.runner(args0.toArray, Array(), classLoader) def clsFingerprints = classes.flatMap { cls => - matchFingerprints(classLoader, cls, fingerprints) + matchFingerprints(classLoader, cls, fingerprints, logger) .map((cls, _)) .iterator } diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/FrameworkUtils.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/FrameworkUtils.scala index 9dd92ab14c..7bb0e0a67d 100644 --- a/modules/test-runner/src/main/scala/scala/build/testrunner/FrameworkUtils.scala +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/FrameworkUtils.scala @@ -87,46 +87,6 @@ object FrameworkUtils { getFrameworksToRun(allFrameworks = frameworkServices ++ frameworks)(logger) } - def listClasses(classPath: Seq[Path], keepJars: Boolean): Iterator[String] = - classPath.iterator.flatMap(listClasses(_, keepJars)) - - def listClasses(classPathEntry: Path, keepJars: Boolean): Iterator[String] = - if (Files.isDirectory(classPathEntry)) { - var stream: java.util.stream.Stream[Path] = null - try { - stream = Files.walk(classPathEntry, Int.MaxValue) - stream - .iterator - .asScala - .filter(_.getFileName.toString.endsWith(".class")) - .map(classPathEntry.relativize) - .map { p => - val count = p.getNameCount - (0 until count).map(p.getName).mkString(".") - } - .map(_.stripSuffix(".class")) - .toVector // fully consume stream before closing it - .iterator - } - finally if (stream != null) stream.close() - } - else if (keepJars && Files.isRegularFile(classPathEntry)) { - import java.util.zip._ - var zf: ZipFile = null - try { - zf = new ZipFile(classPathEntry.toFile) - zf.entries - .asScala - // FIXME Check if these are files too - .filter(_.getName.endsWith(".class")) - .map(ent => ent.getName.stripSuffix(".class").replace("/", ".")) - .toVector // full consume ZipFile before closing it - .iterator - } - finally if (zf != null) zf.close() - } - else Iterator.empty - def findFrameworkServices(loader: ClassLoader): Seq[Framework] = ServiceLoader.load(classOf[Framework], loader) .iterator() @@ -145,15 +105,18 @@ object FrameworkUtils { def findFrameworks( classPath: Seq[Path], loader: ClassLoader, - preferredClasses: Seq[String] + preferredClasses: Seq[String], + logger: Logger ): Seq[Framework] = { val frameworkCls = classOf[Framework] - (preferredClasses.iterator ++ listClasses(classPath, true)) + // first try preferred classes, then scan classpath + (preferredClasses.iterator ++ listClasses(classPath, true, logger)) .flatMap { name => val it: Iterator[Class[?]] = try Iterator(loader.loadClass(name)) catch { case _: ClassNotFoundException | _: UnsupportedClassVersionError | _: NoClassDefFoundError | _: IncompatibleClassChangeError => + // Expected: most classpath entries aren't test frameworks Iterator.empty } it @@ -179,17 +142,70 @@ object FrameworkUtils { Iterator(constructor.newInstance().asInstanceOf[Framework]) } catch { - case _: NoSuchMethodException => Iterator.empty + case e: Exception => + logger.log(s"Could not instantiate framework ${cls.getName}: $e") + Iterator.empty } } .toSeq } + def listClasses(classPath: Seq[Path], keepJars: Boolean, logger: Logger): Iterator[String] = + classPath.iterator.flatMap(listClasses(_, keepJars, logger)) + + def listClasses(classPathEntry: Path, keepJars: Boolean, logger: Logger): Iterator[String] = + if (Files.isDirectory(classPathEntry)) { + var stream: java.util.stream.Stream[Path] = null + try { + stream = Files.walk(classPathEntry, Int.MaxValue) + stream + .iterator + .asScala + .filter(_.getFileName.toString.endsWith(".class")) + .map(classPathEntry.relativize) + .map { p => + val count = p.getNameCount + (0 until count).map(p.getName).mkString(".") + } + .map(_.stripSuffix(".class")) + .toVector // fully consume stream before closing it + .iterator + } + catch { + case e: Exception => + logger.log(s"Could not walk directory $classPathEntry: ${e.getMessage}") + Iterator.empty + } + finally if (stream != null) stream.close() + } + else if (keepJars && Files.isRegularFile(classPathEntry)) { + import java.util.zip._ + var zf: ZipFile = null + try { + zf = new ZipFile(classPathEntry.toFile) + zf.entries + .asScala + // FIXME Check if these are files too + .filter(_.getName.endsWith(".class")) + .map(ent => ent.getName.stripSuffix(".class").replace("/", ".")) + .toVector // full consume ZipFile before closing it + .iterator + } + catch { + case e: Exception => + logger.log(s"Could not read JAR $classPathEntry: ${e.getMessage}") + Iterator.empty + } + finally if (zf != null) zf.close() + } + else Iterator.empty + // adapted from https://github.com/com-lihaoyi/mill/blob/ab4d61a50da24fb7fac97c4453dd8a770d8ac62b/scalalib/src/Lib.scala#L156-L172 def matchFingerprints( loader: ClassLoader, cls: Class[?], - fingerprints: Array[Fingerprint] + fingerprints: Array[Fingerprint], + logger: Logger ): Option[Fingerprint] = { val isModule = cls.getName.endsWith("$") val publicConstructorCount = cls.getConstructors.count(c => Modifier.isPublic(c.getModifiers)) @@ -203,21 +219,39 @@ object FrameworkUtils { else fingerprints.find { case f: SubclassFingerprint => - f.isModule == isModule && - loader.loadClass(f.superclassName()) - .isAssignableFrom(cls) + f.isModule == isModule && { + try + loader.loadClass(f.superclassName()) + .isAssignableFrom(cls) + catch { + case _: ClassNotFoundException => + logger.debug( + s"Superclass not found for fingerprint matching: ${f.superclassName()}" + ) + false + } + } case f: AnnotatedFingerprint => - val annotationCls = loader.loadClass(f.annotationName()) - .asInstanceOf[Class[Annotation]] - f.isModule == isModule && ( - cls.isAnnotationPresent(annotationCls) || - cls.getDeclaredMethods.exists(_.isAnnotationPresent(annotationCls)) || - cls.getMethods.exists { m => - m.isAnnotationPresent(annotationCls) && - Modifier.isPublic(m.getModifiers) + f.isModule == isModule && { + try { + val annotationCls = loader.loadClass(f.annotationName()) + .asInstanceOf[Class[Annotation]] + cls.isAnnotationPresent(annotationCls) || + cls.getDeclaredMethods.exists(_.isAnnotationPresent(annotationCls)) || + cls.getMethods.exists { m => + m.isAnnotationPresent(annotationCls) && + Modifier.isPublic(m.getModifiers) + } } - ) + catch { + case _: ClassNotFoundException => + logger.debug( + s"Annotation class not found for fingerprint matching: ${f.annotationName()}" + ) + false + } + } } } } diff --git a/modules/test-runner/src/main/scala/scala/build/testrunner/TestRunner.scala b/modules/test-runner/src/main/scala/scala/build/testrunner/TestRunner.scala index 33e9547bfb..10896c69e4 100644 --- a/modules/test-runner/src/main/scala/scala/build/testrunner/TestRunner.scala +++ b/modules/test-runner/src/main/scala/scala/build/testrunner/TestRunner.scala @@ -20,7 +20,7 @@ object TestRunner { "weaver.framework.CatsEffect" ) - def classPath(loader: ClassLoader): Seq[Path] = { + def classPath(loader: ClassLoader, logger: Logger): Seq[Path] = { def helper(loader: ClassLoader): LazyList[Path] = if (loader == null) LazyList.empty else { @@ -30,7 +30,9 @@ object TestRunner { .flatMap { case url if url.getProtocol == "file" => Seq(Paths.get(url.toURI).toAbsolutePath) - case _ => Nil // FIXME Warn about this + case url => + logger.debug(s"Skipping non-file URL in classloader: $url") + Nil } .to(LazyList) case cl if cl.getClass.getName == "jdk.internal.loader.ClassLoaders$AppClassLoader" => @@ -39,7 +41,9 @@ object TestRunner { .split(File.pathSeparator) .to(LazyList) .map(Paths.get(_)) - case _ => LazyList.empty // FIXME Warn about this + case cl => + logger.log(s"Unknown classloader type: ${cl.getClass.getName}") + LazyList.empty } paths #::: helper(loader.getParent) } diff --git a/project/deps/package.mill b/project/deps/package.mill index f12ef82f94..246354df80 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -19,10 +19,10 @@ object Scala { def scala3Lts = s"$scala3LtsPrefix.7" // the LTS version currently used in the build def runnerScala3 = scala3Lts 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 scala3Next = s"$scala3NextPrefix.3" // the newest/next version of Scala + def scala3NextAnnounced = s"$scala3NextPrefix.2" // the newest/next version of Scala that's been announced + def scala3NextRc = "3.8.3-RC3" // the latest RC version of Scala Next + def scala3NextRcAnnounced = "3.8.3-RC2" // 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) @@ -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" diff --git a/scala-cli.bat b/scala-cli.bat index b5a0861320..046d5b2a77 100644 --- a/scala-cli.bat +++ b/scala-cli.bat @@ -7,7 +7,7 @@ rem Download the latest version of this script at https://github.com/VirtusLab/s setlocal enabledelayedexpansion -set "SCALA_CLI_VERSION=1.12.4" +set "SCALA_CLI_VERSION=1.12.5" set SCALA_CLI_URL=https://github.com/VirtusLab/scala-cli/releases/download/v%SCALA_CLI_VERSION%/scala-cli.bat set CACHE_BASE=%localappdata%/Coursier/v1 diff --git a/scala-cli.sh b/scala-cli.sh index bbde4f901e..d4b68849fa 100755 --- a/scala-cli.sh +++ b/scala-cli.sh @@ -7,7 +7,7 @@ set -eu -SCALA_CLI_VERSION="1.12.4" +SCALA_CLI_VERSION="1.12.5" GH_ORG="VirtusLab" GH_NAME="scala-cli" 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/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/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 056df024c6..0bfa11a310 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) @@ -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` @@ -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: @@ -1088,6 +1094,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: @@ -1605,7 +1629,7 @@ Available in commands: Aliases: `-S`, `--scala` -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) ### `--scala-binary-version` @@ -1937,6 +1961,22 @@ 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), [`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` + +Enable WebAssembly output (Scala.js WASM backend). Uses Node.js by default. To show more options for WASM pass `--help-wasm` + +### `--wasm-runtime` + +WASM runtime to use: node (default), deno + ## Watch options Available in commands: @@ -1957,6 +1997,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/commands.md b/website/docs/reference/commands.md index 8b5b788835..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), [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,11 +257,11 @@ 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 -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), [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 9a9f0456bb..ec95953781 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -695,6 +695,38 @@ 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 + +`//> using watching` _path_ + +`//> using watching` _path1_ _path2_ … + + + +#### Examples +`//> using watching ./data` + ## target directives diff --git a/website/docs/reference/scala-command/cli-options.md b/website/docs/reference/scala-command/cli-options.md index 5c7e54b124..2b0e42ffc1 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) @@ -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: @@ -1066,7 +1074,7 @@ Aliases: `-S`, `--scala` `MUST have` per Scala Runner specification -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) ### `--scala-binary-version` @@ -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 9d190bca31..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), [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..7650774eff 100644 --- a/website/docs/reference/scala-command/runner-specification.md +++ b/website/docs/reference/scala-command/runner-specification.md @@ -92,7 +92,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** @@ -883,7 +889,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** @@ -1492,7 +1504,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** @@ -2115,7 +2133,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** @@ -2763,7 +2787,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** @@ -3399,7 +3429,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** @@ -4054,7 +4090,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** @@ -4781,7 +4823,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** @@ -5758,7 +5806,7 @@ Aliases: `-P` ,`--plugin` **--scala-version** -Set the Scala version (3.8.2 by default) +Set the Scala version (3.8.3 by default) Aliases: `-S` ,`--scala` @@ -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** diff --git a/website/docs/reference/scala-versions.md b/website/docs/reference/scala-versions.md index 1e2128a3b2..d7a2595a25 100644 --- a/website/docs/reference/scala-versions.md +++ b/website/docs/reference/scala-versions.md @@ -38,5 +38,6 @@ it is recommended to update scala-cli. | 1.11.0 | 3.7.4 | 2.13.18 | 2.12.21 | | 1.12.0 | 3.8.0 | 2.13.18 | 2.12.21 | | 1.12.1 - 1.12.2 | 3.8.1 | 2.13.18 | 2.12.21 | -| 1.12.3 - current | 3.8.2 | 2.13.18 | 2.12.21 | +| 1.12.3 - 1.12.5 | 3.8.2 | 2.13.18 | 2.12.21 | +| 1.13.0 - current | 3.8.3 | 2.13.18 | 2.12.21 | diff --git a/website/docs/release_notes.md b/website/docs/release_notes.md index aafe1d29da..1406e1074a 100644 --- a/website/docs/release_notes.md +++ b/website/docs/release_notes.md @@ -8,6 +8,98 @@ import ReactPlayer from 'react-player' # Release notes +## [v1.12.5](https://github.com/VirtusLab/scala-cli/releases/tag/v1.12.5) + +### `--cross` support for `run`, `package` and `doc` sub-commands (experimental ⚡️) +It is now possible to cross-`run`, cross-`package` and cross-generate docs (`doc`) with the `--cross` command line +option. +- `run` runs each configured combination of Scala version and platform (e.g. JVM, Native, JS) in sequence; +- `package` produces one artifact per cross build, with the Scala version and platform in the artifact name; +- `doc` generates Scaladoc for each cross target into separate output directories. + +```scala title=cross.scala +//> using scala 3.3 3.8 +@main def main() = println("Hello") +``` + +```bash +scala-cli run cross.scala --cross --power +scala-cli package cross.scala --cross --power +scala-cli doc cross.scala --cross -o doc-out --power +``` + +Added by [@Gedochao](https://github.com/Gedochao) in [#3808](https://github.com/VirtusLab/scala-cli/pull/3808), [#4171](https://github.com/VirtusLab/scala-cli/pull/4171) & [#4183](https://github.com/VirtusLab/scala-cli/pull/4183) + +### Global `--offline` config key +You can set offline mode globally with the `config` sub-command, so Scala CLI uses the cache and skips network access +without passing `--offline` every time. + +```bash ignore +scala-cli config offline true +``` + +Added by [@Gedochao](https://github.com/Gedochao) in [#3216](https://github.com/VirtusLab/scala-cli/pull/3216) + +### Watch extra paths with `--watching` (experimental ⚡️) +Use the `--watching` option or `//> using watching` to have `--watch` re-run when files or directories outside +your sources change (e.g. config or assets). + +```bash ignore +scala-cli run . --watch --power --watching ./config --watching ./assets +``` + +Or in source: + +```scala compile power +//> using watching ./config ./assets +``` + +Added by [@Gedochao](https://github.com/Gedochao) in [#4174](https://github.com/VirtusLab/scala-cli/pull/4174) + +### Local `.m2` in `publish local` (experimental ⚡️) +`publish local` now publishes to your local Maven repository (`~/.m2`), so other local projects can depend +on the published artifacts via Maven coordinates. + +```bash ignore +scala-cli publish local . --m2 --power +``` + +Added by [@Gedochao](https://github.com/Gedochao) in [#4179](https://github.com/VirtusLab/scala-cli/pull/4179) + +### Features +* Run all cross builds when `--cross` is passed by [@Gedochao](https://github.com/Gedochao) in [#3808](https://github.com/VirtusLab/scala-cli/pull/3808) +* Add a global `--offline` config key by [@Gedochao](https://github.com/Gedochao) in [#3216](https://github.com/VirtusLab/scala-cli/pull/3216) +* Support `--cross` with the `package` sub-command by [@Gedochao](https://github.com/Gedochao) in [#4171](https://github.com/VirtusLab/scala-cli/pull/4171) +* Allow to `--watch` extra paths with `--watching` by [@Gedochao](https://github.com/Gedochao) in [#4174](https://github.com/VirtusLab/scala-cli/pull/4174) +* Add support for `--cross` in the `doc` sub-command by [@Gedochao](https://github.com/Gedochao) in [#4183](https://github.com/VirtusLab/scala-cli/pull/4183) +* Add support for local `.m2` in `publish local` by [@Gedochao](https://github.com/Gedochao) in [#4179](https://github.com/VirtusLab/scala-cli/pull/4179) + +### Fixes +* Use Java 17 mapping when generating docs with Scala 3.8+ with `doc` by [@Gedochao](https://github.com/Gedochao) in [#4180](https://github.com/VirtusLab/scala-cli/pull/4180) +* Make test framework discovery on Native more resilient & with better errors by [@Gedochao](https://github.com/Gedochao) in [#4185](https://github.com/VirtusLab/scala-cli/pull/4185) +* Warn when `.java` & `.scala` sources are used in a mixed compilation with `--server=false` by [@Gedochao](https://github.com/Gedochao) in [#4181](https://github.com/VirtusLab/scala-cli/pull/4181) + +### Build and internal changes +* Add LLM policy & a PR template by [@Gedochao](https://github.com/Gedochao) in [#4177](https://github.com/VirtusLab/scala-cli/pull/4177) +* Add `AGENTS.md` by [@Gedochao](https://github.com/Gedochao) in [#4178](https://github.com/VirtusLab/scala-cli/pull/4178) + +### Updates +* Bump the npm-dependencies group in /website with 3 updates by @dependabot[bot] in [#4165](https://github.com/VirtusLab/scala-cli/pull/4165) +* Bump the github-actions group with 3 updates by @dependabot[bot] in [#4164](https://github.com/VirtusLab/scala-cli/pull/4164) +* Update scala-cli.sh launcher for 1.12.4 by @github-actions[bot] in [#4166](https://github.com/VirtusLab/scala-cli/pull/4166) +* Bump svgo from 3.3.2 to 3.3.3 in /website by @dependabot[bot] in [#4168](https://github.com/VirtusLab/scala-cli/pull/4168) +* Bump immutable from 5.1.4 to 5.1.5 in /website by @dependabot[bot] in [#4167](https://github.com/VirtusLab/scala-cli/pull/4167) +* Bump Mill to 1.1.3 (was 1.1.2) by [@Gedochao](https://github.com/Gedochao) in [#4169](https://github.com/VirtusLab/scala-cli/pull/4169) +* Bump @algolia/client-search from 5.49.1 to 5.49.2 in /website in the npm-dependencies group by @dependabot[bot] in [#4173](https://github.com/VirtusLab/scala-cli/pull/4173) +* Bump the github-actions group with 4 updates by @dependabot[bot] in [#4172](https://github.com/VirtusLab/scala-cli/pull/4172) +* Update Scala 3 Next RC to 3.8.3-RC2 by [@Gedochao](https://github.com/Gedochao) in [#4175](https://github.com/VirtusLab/scala-cli/pull/4175) +* Bump undici from 7.18.2 to 7.24.1 in /website by @dependabot[bot] in [#4182](https://github.com/VirtusLab/scala-cli/pull/4182) +* Bump webfactory/ssh-agent from 0.9.1 to 0.10.0 in the github-actions group by @dependabot[bot] in [#4187](https://github.com/VirtusLab/scala-cli/pull/4187) +* Bump `coursier` to 2.1.25-M24 by [@Gedochao](https://github.com/Gedochao) in [#4184](https://github.com/VirtusLab/scala-cli/pull/4184) +* Bump sass from 1.97.3 to 1.98.0 in /website in the npm-dependencies group by @dependabot[bot] in [#4188](https://github.com/VirtusLab/scala-cli/pull/4188) + +**Full Changelog**: https://github.com/VirtusLab/scala-cli/compare/v1.12.4...v1.12.5 + ## [v1.12.4](https://github.com/VirtusLab/scala-cli/releases/tag/v1.12.4) This is just a small patch fixing a bug ([#4152](https://github.com/VirtusLab/scala-cli/issues/4152)) breaking Metals support in Scala CLI v1.12.3. diff --git a/website/package.json b/website/package.json index 54557eaba2..8f893b5eaa 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.50.0", "@docusaurus/core": "^3.9.2", "@docusaurus/plugin-content-docs": "^3.9.2", "@docusaurus/preset-classic": "^3.9.2", @@ -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 a2bf5b1c23..459a6a1d25 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.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.50.0.tgz#aec2bbd20fae474fb29eca3f8c83775dbd5d47de" + integrity sha512-emtOvR6dl3rX3sBJXXbofMNHU1qMQqQSWu319RMrNL5BWoBqyiq7y0Zn6cjJm7aGHV/Qbf+KCCYeWNKEMPI3BQ== "@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.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.50.0.tgz#c68668a63afbf27f7c2eb281aa51e87ac11d6973" + integrity sha512-Jc360x4yqb3eEg4OY4KEIdGePBxZogivKI+OGIU8aLXgAYPTECvzeOBc90312yHA1hr3AeRlAFl0rIc8lQaIrQ== 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.50.0" + "@algolia/requester-browser-xhr" "5.50.0" + "@algolia/requester-fetch" "5.50.0" + "@algolia/requester-node-http" "5.50.0" "@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.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.50.0.tgz#ae222fab442e78c8709f35555519e3fd43890557" + integrity sha512-bffIbUljAWnh/Ctu5uScORajuUavqmZ0ACYd1fQQeSSYA9NNN83ynO26pSc2dZRXpSK0fkc1//qSSFXMKGu+aw== dependencies: - "@algolia/client-common" "5.49.1" + "@algolia/client-common" "5.50.0" "@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.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.50.0.tgz#cabe0e128122ed0f5493b4d1fb72bf26b5178fc5" + integrity sha512-y0EwNvPGvkM+yTAqqO6Gpt9wVGm3CLDtpLvNEiB3VGvN3WzfkjZGtLUsG/ru2kVJIIU7QcV0puuYgEpBeFxcJg== dependencies: - "@algolia/client-common" "5.49.1" + "@algolia/client-common" "5.50.0" "@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.50.0": + version "5.50.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.50.0.tgz#733bfe889e35baa03f7986dd46587741e035eec3" + integrity sha512-xpwefe4fCOWnZgXCbkGpqQY6jgBSCf2hmgnySbyzZIccrv3SoashHKGPE4x6vVG+gdHrGciMTAcDo9HOZwH22Q== dependencies: - "@algolia/client-common" "5.49.1" + "@algolia/client-common" "5.50.0" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1": version "7.27.1" @@ -3569,9 +3569,9 @@ boxen@^7.0.0: wrap-ansi "^8.1.0" brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + version "1.1.13" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6" + integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -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== @@ -7074,9 +7074,9 @@ node-emoji@^2.1.0: skin-tone "^2.0.0" node-forge@^1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751" - integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg== + version "1.4.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2" + integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ== node-releases@^2.0.27: version "2.0.27" @@ -7391,9 +7391,9 @@ picocolors@^1.0.0, picocolors@^1.1.1: integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== pkg-dir@^7.0.0: version "7.0.0" @@ -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" @@ -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"