Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,8 @@ trait Core extends ScalaCliCrossSbtModule
| def toolkitVersionForNative04 = "${Deps.toolkitVersionForNative04}"
| def toolkitVersionForNative05 = "${Deps.toolkitVersionForNative05}"
|
| def defaultDenoVersion = "2.1.4"
|
| def typelevelOrganization = "${Deps.typelevelToolkit.dep.module.organization.value}"
| def typelevelToolkitDefaultVersion = "${Deps.typelevelToolkitVersion}"
| def typelevelToolkitMaxScalaNative = "${Deps.Versions.maxScalaNativeForTypelevelToolkit}"
Expand Down
146 changes: 140 additions & 6 deletions modules/build/src/main/scala/scala/build/internal/Runner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,60 @@ object Runner {
run(command, logger, cwd = cwd, extraEnv = extraEnv)
}

// Detects the major version of Node.js on PATH; cached for the JVM lifetime (lazy val).
// Returns None if node is not found or version cannot be parsed.
private lazy val nodeMajorVersion: Option[Int] =
try {
val process = new ProcessBuilder("node", "--version")
.redirectErrorStream(true)
.start()
val output = new String(process.getInputStream.readAllBytes()).trim
process.waitFor()
// Node version format: "v22.5.0" -> extract 22
if (output.startsWith("v"))
output.drop(1).takeWhile(_.isDigit) match {
case s if s.nonEmpty => Some(s.toInt)
case _ => None
}
else None
}
catch {
case _: Exception => None
}

// Node 24+ (V8 13+) has wasm-exnref enabled by default; older versions need --experimental-wasm-exnref.
private def nodeNeedsWasmFlag: Boolean =
nodeMajorVersion.forall(_ < 24) // true if unknown or < 24

// Detects the major version of Deno on PATH; cached for the JVM lifetime (lazy val).
// Returns None if deno is not found or version cannot be parsed.
private lazy val denoMajorVersion: Option[Int] =
try {
val process = new ProcessBuilder("deno", "--version")
.redirectErrorStream(true)
.start()
val output = new String(process.getInputStream.readAllBytes()).trim
process.waitFor()
// Deno version format: "deno 2.1.0 (release, aarch64-apple-darwin)\nv8 13.x\ntypescript 5.x"
// Extract major from first line
val firstLine = output.linesIterator.nextOption().getOrElse("")
val versionStr = firstLine.stripPrefix("deno ").takeWhile(c => c.isDigit || c == '.')
versionStr.takeWhile(_.isDigit) match {
case s if s.nonEmpty => Some(s.toInt)
case _ => None
}
}
catch {
case _: Exception => None
}

// Deno 2.x+ bundles V8 13+ which has wasm-exnref enabled by default; no flag needed.
private def denoNeedsWasmFlag: Boolean =
denoMajorVersion.flatMap { major =>
if (major >= 2) Some(false) // Deno 2.x+ has V8 13+ with wasm-exnref by default
else Some(true)
}.getOrElse(true) // true if unknown

private def endsWithCaseInsensitive(s: String, suffix: String): Boolean =
s.length >= suffix.length &&
s.regionMatches(true, s.length - suffix.length, suffix, 0, suffix.length)
Expand Down Expand Up @@ -218,11 +272,13 @@ object Runner {
def jsCommand(
entrypoint: File,
args: Seq[String],
jsDom: Boolean = false
jsDom: Boolean = false,
emitWasm: Boolean = false
): Seq[String] = {

val nodePath = findInPath("node").fold("node")(_.toString)
val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args
val nodePath = findInPath("node").fold("node")(_.toString)
val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List("--experimental-wasm-exnref") else Nil
val command = Seq(nodePath) ++ nodeFlags ++ Seq(entrypoint.getAbsolutePath) ++ args

if (jsDom)
// FIXME We'd need to replicate what JSDOMNodeJSEnv does under-the-hood to get the command in that case.
Expand All @@ -239,14 +295,16 @@ object Runner {
allowExecve: Boolean = false,
jsDom: Boolean = false,
sourceMap: Boolean = false,
esModule: Boolean = false
esModule: Boolean = false,
emitWasm: Boolean = false
): Either[BuildException, Process] = either {
val nodePath: String =
value(findInPath("node")
.map(_.toString)
.toRight(NodeNotFoundError()))
val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List("--experimental-wasm-exnref") else Nil
if !jsDom && allowExecve && Execve.available() then {
val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args
val command = Seq(nodePath) ++ nodeFlags ++ Seq(entrypoint.getAbsolutePath) ++ args

logger.log(
s"Running ${command.mkString(" ")}",
Expand All @@ -262,12 +320,25 @@ object Runner {
)
sys.error("should not happen")
}
else if (emitWasm) {
// For WASM mode with ES modules, run node directly instead of NodeJSEnv.
// NodeJSEnv's stdin piping with "-" doesn't work with Input.ESModule.
val command = Seq(nodePath) ++ nodeFlags ++ Seq(entrypoint.getAbsolutePath) ++ args

logger.log(
s"Running ${command.mkString(" ")}",
" Running" + System.lineSeparator() +
command.iterator.map(_ + System.lineSeparator()).mkString
)

new ProcessBuilder(command: _*).inheritIO().start()
}
else {
val nodeArgs =
// Scala.js runs apps by piping JS to node.
// If we need to pass arguments, we must first make the piped input explicit
// with "-", and we pass the user's arguments after that.
if args.isEmpty then Nil else "-" :: args.toList
nodeFlags ++ (if args.isEmpty then Nil else "-" :: args.toList)
val envJs =
if jsDom then
new JSDOMNodeJSEnv(
Expand Down Expand Up @@ -304,6 +375,69 @@ object Runner {
}
}

def denoCommand(
entrypoint: File,
args: Seq[String],
denoPathOpt: Option[String] = None
): Seq[String] = {
val denoPath = denoPathOpt.getOrElse(findInPath("deno").fold("deno")(_.toString))
val denoFlags = Seq("run", "--allow-read")
Seq(denoPath) ++ denoFlags ++ Seq(entrypoint.getAbsolutePath) ++ args
}

def runDeno(
entrypoint: File,
args: Seq[String],
logger: Logger,
allowExecve: Boolean = false,
emitWasm: Boolean = false,
denoPathOpt: Option[String] = None
): Either[BuildException, Process] = either {
val denoPath: String = denoPathOpt.getOrElse {
value(findInPath("deno")
.map(_.toString)
.toRight(DenoNotFoundError()))
}
val denoFlags = Seq("run", "--allow-read")
val extraEnv =
if (emitWasm && denoNeedsWasmFlag) Map("DENO_V8_FLAGS" -> "--experimental-wasm-exnref")
else Map.empty

if (allowExecve && Execve.available()) {
val command = Seq(denoPath) ++ denoFlags ++ Seq(entrypoint.getAbsolutePath) ++ args

logger.log(
s"Running ${command.mkString(" ")}",
" Running" + System.lineSeparator() +
command.iterator.map(_ + System.lineSeparator()).mkString
)

logger.debug("execve available")
Execve.execve(
command.head,
"deno" +: command.tail.toArray,
(sys.env ++ extraEnv).toArray.sorted.map { case (k, v) => s"$k=$v" }
)
sys.error("should not happen")
}
else {
val command = Seq(denoPath) ++ denoFlags ++ Seq(entrypoint.getAbsolutePath) ++ args

logger.log(
s"Running ${command.mkString(" ")}",
" Running" + System.lineSeparator() +
command.iterator.map(_ + System.lineSeparator()).mkString
)

val builder = new ProcessBuilder(command*)
.inheritIO()
val env = builder.environment()
for ((k, v) <- extraEnv)
env.put(k, v)
builder.start()
}
}

def runNative(
launcher: File,
args: Seq[String],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ object DirectivesPreprocessingUtils {
directives.ScalaNative.handler,
directives.ScalaVersion.handler,
directives.Sources.handler,
directives.Tests.handler
directives.Tests.handler,
directives.Wasm.handler
).map(_.mapE(_.buildOptions))

val usingDirectiveWithReqsHandlers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading