Skip to content

Commit 7f43349

Browse files
committed
Review fixes: remove runtime download and unsupported standalone runtimes
- Move --wasm flag to dedicated Wasm help group with --help-wasm option - Simplify wasmOptions parsing with fold/toRight pattern - Add runtime validation with UnrecognizedWasmRuntimeError in directives - Auto-enable WASM when wasmRuntime directive is set - Update reference documentation Code style: simplify denoNeedsWasmFlag, explicit runtime match cases, clean type annotation, scalfmt
1 parent 2e97668 commit 7f43349

19 files changed

Lines changed: 247 additions & 364 deletions

File tree

build.mill

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,6 @@ trait Core extends ScalaCliCrossSbtModule
533533
| def toolkitVersionForNative04 = "${Deps.toolkitVersionForNative04}"
534534
| def toolkitVersionForNative05 = "${Deps.toolkitVersionForNative05}"
535535
|
536-
| def defaultDenoVersion = "2.1.4"
537-
|
538536
| def typelevelOrganization = "${Deps.typelevelToolkit.dep.module.organization.value}"
539537
| def typelevelToolkitDefaultVersion = "${Deps.typelevelToolkitVersion}"
540538
| def typelevelToolkitMaxScalaNative = "${Deps.Versions.maxScalaNativeForTypelevelToolkit}"

modules/build/src/main/scala/scala/build/internal/Runner.scala

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,7 @@ object Runner {
238238

239239
// Deno 2.x+ bundles V8 13+ which has wasm-exnref enabled by default; no flag needed.
240240
private def denoNeedsWasmFlag: Boolean =
241-
denoMajorVersion.flatMap { major =>
242-
if (major >= 2) Some(false) // Deno 2.x+ has V8 13+ with wasm-exnref by default
243-
else Some(true)
244-
}.getOrElse(true) // true if unknown
241+
denoMajorVersion.forall(_ < 2) // true if unknown or < 2
245242

246243
private def endsWithCaseInsensitive(s: String, suffix: String): Boolean =
247244
s.length >= suffix.length &&
@@ -380,10 +377,9 @@ object Runner {
380377

381378
def denoCommand(
382379
entrypoint: File,
383-
args: Seq[String],
384-
denoPathOpt: Option[String] = None
380+
args: Seq[String]
385381
): Seq[String] = {
386-
val denoPath = denoPathOpt.getOrElse(findInPath("deno").fold("deno")(_.toString))
382+
val denoPath = findInPath("deno").fold("deno")(_.toString)
387383
val denoFlags = Seq("run", "--allow-read")
388384
Seq(denoPath) ++ denoFlags ++ Seq(entrypoint.getAbsolutePath) ++ args
389385
}
@@ -393,14 +389,12 @@ object Runner {
393389
args: Seq[String],
394390
logger: Logger,
395391
allowExecve: Boolean = false,
396-
emitWasm: Boolean = false,
397-
denoPathOpt: Option[String] = None
392+
emitWasm: Boolean = false
398393
): Either[BuildException, Process] = either {
399-
val denoPath: String = denoPathOpt.getOrElse {
394+
val denoPath: String =
400395
value(findInPath("deno")
401396
.map(_.toString)
402397
.toRight(DenoNotFoundError()))
403-
}
404398
val denoFlags = Seq("run", "--allow-read")
405399
val extraEnv =
406400
if (emitWasm && denoNeedsWasmFlag) Map("DENO_V8_FLAGS" -> "--experimental-wasm-exnref")

modules/cli/src/main/scala/scala/cli/commands/run/Run.scala

Lines changed: 55 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import java.util.concurrent.atomic.AtomicReference
1212
import scala.build.*
1313
import scala.build.EitherCps.{either, value}
1414
import scala.build.Ops.*
15-
import scala.build.errors.{BuildException, CompositeBuildException, UnsupportedWasmRuntimeError}
15+
import scala.build.errors.{BuildException, CompositeBuildException}
1616
import scala.build.input.*
1717
import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig}
1818
import scala.build.internals.ConsoleUtils.ScalaCliConsole
@@ -28,7 +28,7 @@ import scala.cli.commands.util.BuildCommandHelpers.*
2828
import scala.cli.commands.util.{BuildCommandHelpers, RunHadoop, RunSpark}
2929
import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil}
3030
import scala.cli.config.Keys
31-
import scala.cli.internal.{ProcUtil, WasmRuntimeDownloader}
31+
import scala.cli.internal.ProcUtil
3232
import scala.cli.packaging.Library.fullClassPathMaybeAsJar
3333
import scala.cli.util.ArgHelpers.*
3434
import scala.cli.util.ConfigDbUtils
@@ -478,101 +478,66 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
478478

479479
// Check if WASM mode is requested
480480
if wasmOpts.enabled then {
481-
val runtime = wasmOpts.runtime
482-
483-
if runtime.isJsBased then {
484-
// JS-based WASM path - uses Scala.js WASM with JavaScript helpers (Node.js or Deno)
485-
val esModule = true // WASM backend uses ES modules
486-
scratchDirOpt.foreach(os.makeDir.all(_))
487-
val jsDest = os.temp(
488-
dir = scratchDirOpt.orNull,
489-
prefix = "main",
490-
suffix = ".mjs",
491-
deleteOnExit = scratchDirOpt.isEmpty
492-
)
493-
494-
// Resolve Deno binary: check PATH first, download if needed
495-
val denoPathOpt: Option[String] = runtime match {
496-
case WasmRuntime.Deno =>
497-
val denoCmd = value(WasmRuntimeDownloader.denoCommand(
498-
wasmOpts.finalDenoVersion,
499-
build.options.archiveCache,
500-
logger
501-
))
502-
Some(denoCmd.head)
503-
case _ => None
504-
}
481+
val runtime = wasmOpts.runtime
482+
val esModule = true // WASM backend uses ES modules
483+
scratchDirOpt.foreach(os.makeDir.all(_))
484+
val jsDest = os.temp(
485+
dir = scratchDirOpt.orNull,
486+
prefix = "main",
487+
suffix = ".mjs",
488+
deleteOnExit = scratchDirOpt.isEmpty
489+
)
505490

506-
val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger)
507-
.copy(emitWasm = true, moduleKind = ScalaJsLinkerConfig.ModuleKind.ESModule)
508-
509-
val res = Package.linkJs(
510-
builds = builds,
511-
dest = jsDest,
512-
mainClassOpt = Some(mainClass),
513-
addTestInitializer = false,
514-
config = linkerConfig,
515-
fullOpt = value(build.options.scalaJsOptions.fullOpt),
516-
noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false),
517-
logger = logger,
518-
scratchDirOpt = scratchDirOpt
519-
).map { outputPath =>
520-
if showCommand then
491+
val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger)
492+
.copy(emitWasm = true, moduleKind = ScalaJsLinkerConfig.ModuleKind.ESModule)
493+
494+
val res = Package.linkJs(
495+
builds = builds,
496+
dest = jsDest,
497+
mainClassOpt = Some(mainClass),
498+
addTestInitializer = false,
499+
config = linkerConfig,
500+
fullOpt = value(build.options.scalaJsOptions.fullOpt),
501+
noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false),
502+
logger = logger,
503+
scratchDirOpt = scratchDirOpt
504+
).map { outputPath =>
505+
if showCommand then
506+
runtime match {
507+
case WasmRuntime.Deno =>
508+
Left(Runner.denoCommand(outputPath.toIO, args))
509+
case WasmRuntime.Node =>
510+
Left(Runner.jsCommand(outputPath.toIO, args, jsDom = false, emitWasm = true))
511+
}
512+
else {
513+
val process = value {
521514
runtime match {
522515
case WasmRuntime.Deno =>
523-
Left(Runner.denoCommand(outputPath.toIO, args, denoPathOpt = denoPathOpt))
524-
case _ =>
525-
Left(Runner.jsCommand(outputPath.toIO, args, jsDom = false, emitWasm = true))
526-
}
527-
else {
528-
val process = value {
529-
runtime match {
530-
case WasmRuntime.Deno =>
531-
Runner.runDeno(
532-
outputPath.toIO,
533-
args,
534-
logger,
535-
allowExecve = effectiveAllowExecve,
536-
emitWasm = true,
537-
denoPathOpt = denoPathOpt
538-
)
539-
case _ =>
540-
Runner.runJs(
541-
outputPath.toIO,
542-
args,
543-
logger,
544-
allowExecve = effectiveAllowExecve,
545-
jsDom = false,
546-
sourceMap = build.options.scalaJsOptions.emitSourceMaps,
547-
esModule = esModule,
548-
emitWasm = true
549-
)
550-
}
516+
Runner.runDeno(
517+
outputPath.toIO,
518+
args,
519+
logger,
520+
allowExecve = effectiveAllowExecve,
521+
emitWasm = true
522+
)
523+
case WasmRuntime.Node =>
524+
Runner.runJs(
525+
outputPath.toIO,
526+
args,
527+
logger,
528+
allowExecve = effectiveAllowExecve,
529+
jsDom = false,
530+
sourceMap = build.options.scalaJsOptions.emitSourceMaps,
531+
esModule = esModule,
532+
emitWasm = true
533+
)
551534
}
552-
process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest))
553-
Right((process, None))
554535
}
536+
process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest))
537+
Right((process, None))
555538
}
556-
value(res)
557-
}
558-
else {
559-
// Standalone WASM runtimes - not yet supported.
560-
// Scala.js currently produces JS-dependent WASM output.
561-
// Standalone support requires upstream Scala.js changes (scala-js/scala-js#4991).
562-
val runtimeName = runtime.name
563-
val extraNote = runtime match {
564-
case WasmRuntime.Wasmer =>
565-
" Note: Wasmer does not yet support WasmGC, which is required for Scala WASM output."
566-
case _ => ""
567-
}
568-
value(Left(new UnsupportedWasmRuntimeError(
569-
s"Standalone WASM runtime '$runtimeName' is not yet supported." +
570-
s"$extraNote" +
571-
" Scala.js currently produces JavaScript-dependent WASM output." +
572-
" Standalone WASM support is tracked at: https://github.com/scala-js/scala-js/issues/4991" +
573-
" Use --wasm-runtime node (default) or --wasm-runtime deno for JS-based WASM execution."
574-
)))
575539
}
540+
value(res)
576541
}
577542
else
578543
build.options.platform.value match {

modules/cli/src/main/scala/scala/cli/commands/shared/HelpGroupOptions.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ case class HelpGroupOptions(
4949
@Name("fmtHelp")
5050
@Tag(tags.implementation)
5151
@Tag(tags.inShortHelp)
52-
helpScalafmt: Boolean = false
52+
helpScalafmt: Boolean = false,
53+
@Group(HelpGroup.Help.toString)
54+
@HelpMessage("Show options for WebAssembly")
55+
@Name("wasmHelp")
56+
@Tag(tags.implementation)
57+
@Tag(tags.inShortHelp)
58+
helpWasm: Boolean = false
5359
) {
5460

5561
private def printHelpWithGroup(help: Help[?], helpFormat: HelpFormat, group: String): Nothing = {
@@ -68,6 +74,7 @@ case class HelpGroupOptions(
6874
def maybePrintGroupHelp(help: Help[?], helpFormat: HelpFormat): Unit = {
6975
if (helpJs) printHelpWithGroup(help, helpFormat, HelpGroup.ScalaJs.toString)
7076
else if (helpNative) printHelpWithGroup(help, helpFormat, HelpGroup.ScalaNative.toString)
77+
else if (helpWasm) printHelpWithGroup(help, helpFormat, HelpGroup.Wasm.toString)
7178
}
7279
}
7380

modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,23 @@ final case class SharedOptions(
285285
)
286286
}
287287

288-
private def buildWasmOptions(opts: WasmOptions): options.WasmOptions = {
288+
private def buildWasmOptions(
289+
opts: WasmOptions
290+
): Either[BuildException, options.WasmOptions] = {
289291
import opts._
290-
options.WasmOptions(
291-
enabled = wasm,
292-
runtime =
293-
wasmRuntime.flatMap(options.WasmRuntime.parse).getOrElse(options.WasmRuntime.default),
294-
denoVersion = denoVersion
292+
val wasmEnabled = wasm || wasmRuntime.isDefined
293+
val parsedRuntime: Either[BuildException, options.WasmRuntime] =
294+
wasmRuntime.fold(Right(options.WasmRuntime.default)) { rt =>
295+
options.WasmRuntime.parse(rt).toRight {
296+
val validValues = options.WasmRuntime.all.map(_.name).mkString(", ")
297+
new scala.build.errors.UnrecognizedWasmRuntimeError(rt, validValues)
298+
}
299+
}
300+
parsedRuntime.map(runtime =>
301+
options.WasmOptions(
302+
enabled = wasmEnabled,
303+
runtime = runtime
304+
)
295305
)
296306
}
297307

@@ -320,7 +330,7 @@ final case class SharedOptions(
320330
}
321331
val parsedPlatform = platform.map(Platform.normalize).flatMap(Platform.parse)
322332
// WASM mode requires Scala.js platform for compilation
323-
val wasmEnabled = wasmOptions.wasm
333+
val wasmEnabled = wasmOptions.wasm || wasmOptions.wasmRuntime.isDefined
324334
val platformOpt = value {
325335
(parsedPlatform, js.js, native.native, wasmEnabled) match {
326336
case (Some(p: Platform.JS.type), _, false, _) => Right(Some(p))
@@ -426,7 +436,7 @@ final case class SharedOptions(
426436
),
427437
scalaJsOptions = scalaJsOptions(js),
428438
scalaNativeOptions = snOpts,
429-
wasmOptions = buildWasmOptions(wasmOptions),
439+
wasmOptions = value(buildWasmOptions(wasmOptions)),
430440
javaOptions = value(scala.cli.commands.util.JvmUtils.javaOptions(jvm)),
431441
jmhOptions = scala.build.options.JmhOptions(
432442
jmhVersion = benchmarking.jmhVersion,

modules/cli/src/main/scala/scala/cli/commands/shared/WasmOptions.scala

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,15 @@ import scala.cli.commands.tags
88

99
// format: off
1010
final case class WasmOptions(
11-
@Group(HelpGroup.Scala.toString)
11+
@Group(HelpGroup.Wasm.toString)
1212
@Tag(tags.experimental)
1313
@HelpMessage("Enable WebAssembly output (Scala.js WASM backend). Uses Node.js by default. To show more options for WASM pass `--help-wasm`")
1414
wasm: Boolean = false,
1515

1616
@Group(HelpGroup.Wasm.toString)
1717
@Tag(tags.experimental)
18-
@HelpMessage("WASM runtime to use: node (default), deno. Standalone runtimes (wasmtime, wasmedge) planned for future releases.")
19-
wasmRuntime: Option[String] = None,
20-
21-
@Group(HelpGroup.Wasm.toString)
22-
@Tag(tags.experimental)
23-
@HelpMessage("Version of Deno to use. If Deno is not found on PATH, it will be downloaded automatically.")
24-
denoVersion: Option[String] = None
18+
@HelpMessage("WASM runtime to use: node (default), deno")
19+
wasmRuntime: Option[String] = None
2520
)
2621
// format: on
2722

0 commit comments

Comments
 (0)