All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scala.build.internal.Runner.scala Maven / Gradle / Ivy

package scala.build.internal

import coursier.jvm.Execve
import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv
import org.scalajs.jsenv.nodejs.NodeJSEnv
import org.scalajs.jsenv.{Input, RunConfig}
import sbt.testing.{Framework, Status}

import java.io.File
import java.nio.file.{Files, Path, Paths}

import scala.build.EitherCps.{either, value}
import scala.build.Logger
import scala.build.errors._
import scala.build.testrunner.{AsmTestRunner, TestRunner}
import scala.util.{Failure, Properties, Success}

object Runner {

  def maybeExec(
    commandName: String,
    command: Seq[String],
    logger: Logger,
    cwd: Option[os.Path] = None,
    extraEnv: Map[String, String] = Map.empty
  ): Process =
    run0(
      commandName,
      command,
      logger,
      allowExecve = true,
      cwd,
      extraEnv
    )

  def run(
    command: Seq[String],
    logger: Logger,
    cwd: Option[os.Path] = None,
    extraEnv: Map[String, String] = Map.empty
  ): Process =
    run0(
      "unused",
      command,
      logger,
      allowExecve = false,
      cwd,
      extraEnv
    )

  def run0(
    commandName: String,
    command: Seq[String],
    logger: Logger,
    allowExecve: Boolean,
    cwd: Option[os.Path],
    extraEnv: Map[String, String]
  ): Process = {

    import logger.{log, debug}

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

    if (allowExecve && Execve.available()) {
      debug("execve available")

      for (dir <- cwd)
        Chdir.chdir(dir.toString)

      Execve.execve(
        findInPath(command.head).fold(command.head)(_.toString),
        commandName +: command.tail.toArray,
        (sys.env ++ extraEnv).toArray.sorted.map { case (k, v) => s"$k=$v" }
      )
      sys.error("should not happen")
    }
    else {
      val b = new ProcessBuilder(command: _*)
        .inheritIO()
      if (extraEnv.nonEmpty) {
        val env = b.environment()
        for ((k, v) <- extraEnv)
          env.put(k, v)
      }
      for (dir <- cwd)
        b.directory(dir.toIO)
      val process = b.start()
      process
    }
  }

  def envCommand(env: Map[String, String]): Seq[String] =
    env.toVector.sortBy(_._1).map {
      case (k, v) =>
        s"$k=$v"
    }

  def jvmCommand(
    javaCommand: String,
    javaArgs: Seq[String],
    classPath: Seq[os.Path],
    mainClass: String,
    args: Seq[String],
    extraEnv: Map[String, String] = Map.empty,
    useManifest: Option[Boolean] = None,
    scratchDirOpt: Option[os.Path] = None
  ): Seq[String] = {

    def command(cp: Seq[os.Path]) =
      envCommand(extraEnv) ++
        Seq(javaCommand) ++
        javaArgs ++
        Seq(
          "-cp",
          cp.iterator.map(_.toString).mkString(File.pathSeparator),
          mainClass
        ) ++
        args

    val initialCommand = command(classPath)

    val useManifest0 = useManifest.getOrElse {
      Properties.isWin && {
        val commandLen = initialCommand.map(_.length).sum + (initialCommand.length - 1)
        // On Windows, total command lengths have this limit. Note that the same kind
        // of limit applies the environment, so that we can't sneak in info via env vars to
        // overcome the command length limit.
        // See https://devblogs.microsoft.com/oldnewthing/20031210-00/?p=41553
        commandLen >= 32767
      }
    }

    if (useManifest0) {
      val manifestJar = ManifestJar.create(classPath, scratchDirOpt = scratchDirOpt)
      command(Seq(manifestJar))
    }
    else initialCommand
  }

  def runJvm(
    javaCommand: String,
    javaArgs: Seq[String],
    classPath: Seq[os.Path],
    mainClass: String,
    args: Seq[String],
    logger: Logger,
    allowExecve: Boolean = false,
    cwd: Option[os.Path] = None,
    extraEnv: Map[String, String] = Map.empty,
    useManifest: Option[Boolean] = None,
    scratchDirOpt: Option[os.Path] = None
  ): Process = {

    val command = jvmCommand(
      javaCommand,
      javaArgs,
      classPath,
      mainClass,
      args,
      Map.empty,
      useManifest,
      scratchDirOpt
    )

    if (allowExecve)
      maybeExec("java", command, logger, cwd = cwd, extraEnv = extraEnv)
    else
      run(command, logger, cwd = cwd, extraEnv = extraEnv)
  }

  private def endsWithCaseInsensitive(s: String, suffix: String): Boolean =
    s.length >= suffix.length &&
    s.regionMatches(true, s.length - suffix.length, suffix, 0, suffix.length)

  private def findInPath(app: String): Option[Path] = {
    val asIs = Paths.get(app)
    if (Paths.get(app).getNameCount >= 2) Some(asIs)
    else {
      def pathEntries =
        Option(System.getenv("PATH"))
          .iterator
          .flatMap(_.split(File.pathSeparator).iterator)
      def pathSep =
        if (Properties.isWin)
          Option(System.getenv("PATHEXT"))
            .iterator
            .flatMap(_.split(File.pathSeparator).iterator)
        else Iterator("")
      def matches = for {
        dir <- pathEntries
        ext <- pathSep
        app0 = if (endsWithCaseInsensitive(app, ext)) app else app + ext
        path = Paths.get(dir).resolve(app0)
        if Files.isExecutable(path)
      } yield path
      matches.take(1).toList.headOption
    }
  }

  def jsCommand(
    entrypoint: File,
    args: Seq[String],
    jsDom: Boolean = false
  ): Seq[String] = {

    val nodePath = findInPath("node").fold("node")(_.toString)
    val command  = Seq(nodePath, entrypoint.getAbsolutePath) ++ args

    if (jsDom)
      // FIXME We'd need to replicate what JSDOMNodeJSEnv does under-the-hood to get the command in that case.
      // --command is mostly for debugging purposes, so I'm not sure it matters much here…
      sys.error("Cannot get command when JSDOM is enabled.")
    else
      "node" +: command.tail
  }

  def runJs(
    entrypoint: File,
    args: Seq[String],
    logger: Logger,
    allowExecve: Boolean = false,
    jsDom: Boolean = false,
    sourceMap: Boolean = false,
    esModule: Boolean = false
  ): Either[BuildException, Process] = either {

    import logger.{log, debug}

    val nodePath = value(findInPath("node").map(_.toString).toRight(NodeNotFoundError()))

    if (!jsDom && allowExecve && Execve.available()) {

      val command = Seq(nodePath, entrypoint.getAbsolutePath) ++ args

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

      debug("execve available")
      Execve.execve(
        command.head,
        "node" +: command.tail.toArray,
        sys.env.toArray.sorted.map { case (k, v) => s"$k=$v" }
      )
      sys.error("should not happen")
    }
    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) Nil
        else "-" :: args.toList
      val envJs =
        if (jsDom)
          new JSDOMNodeJSEnv(
            JSDOMNodeJSEnv.Config()
              .withExecutable(nodePath)
              .withArgs(nodeArgs)
              .withEnv(Map.empty)
          )
        else new NodeJSEnv(
          NodeJSEnv.Config()
            .withExecutable(nodePath)
            .withArgs(nodeArgs)
            .withEnv(Map.empty)
            .withSourceMap(sourceMap)
        )

      val inputs = Seq(
        if (esModule) Input.ESModule(entrypoint.toPath)
        else Input.Script(entrypoint.toPath)
      )

      val config    = RunConfig().withLogger(logger.scalaJsLogger)
      val processJs = envJs.start(inputs, config)

      processJs.future.value.foreach {
        case Failure(t) =>
          throw new Exception(t)
        case Success(_) =>
      }

      val processField =
        processJs.getClass.getDeclaredField("org$scalajs$jsenv$ExternalJSRun$$process")
      processField.setAccessible(true)
      val process = processField.get(processJs).asInstanceOf[Process]
      process
    }
  }

  def runNative(
    launcher: File,
    args: Seq[String],
    logger: Logger,
    allowExecve: Boolean = false,
    extraEnv: Map[String, String] = Map.empty
  ): Process = {

    import logger.{log, debug}

    val command = Seq(launcher.getAbsolutePath) ++ args

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

    if (allowExecve && Execve.available()) {
      debug("execve available")
      Execve.execve(
        command.head,
        launcher.getName +: command.tail.toArray,
        (sys.env ++ extraEnv).toArray.sorted.map { case (k, v) => s"$k=$v" }
      )
      sys.error("should not happen")
    }
    else {
      val builder = new ProcessBuilder(command: _*)
        .inheritIO()
      val env = builder.environment()
      for ((k, v) <- extraEnv)
        env.put(k, v)
      builder.start()
    }
  }

  private def runTests(
    classPath: Seq[Path],
    framework: Framework,
    requireTests: Boolean,
    args: Seq[String],
    parentInspector: AsmTestRunner.ParentInspector
  ): Either[NoTestsRun, Boolean] = {

    val taskDefs =
      AsmTestRunner.taskDefs(
        classPath,
        keepJars = false,
        framework.fingerprints().toIndexedSeq,
        parentInspector
      ).toArray

    val runner       = framework.runner(args.toArray, Array(), null)
    val initialTasks = runner.tasks(taskDefs)
    val events       = TestRunner.runTasks(initialTasks.toIndexedSeq, System.out)

    val doneMsg = runner.done()
    if (doneMsg.nonEmpty)
      System.out.println(doneMsg)

    if (requireTests && events.isEmpty)
      Left(new NoTestsRun)
    else
      Right {
        !events.exists { ev =>
          ev.status == Status.Error ||
          ev.status == Status.Failure ||
          ev.status == Status.Canceled
        }
      }
  }

  def frameworkName(
    classPath: Seq[Path],
    parentInspector: AsmTestRunner.ParentInspector
  ): Either[NoTestFrameworkFoundError, String] = {
    val fwOpt = AsmTestRunner.findFrameworkService(classPath)
      .orElse {
        AsmTestRunner.findFramework(
          classPath,
          TestRunner.commonTestFrameworks,
          parentInspector
        )
      }
    fwOpt match {
      case Some(fw) => Right(fw.replace('/', '.').replace('\\', '.'))
      case None     => Left(new NoTestFrameworkFoundError)
    }
  }

  def testJs(
    classPath: Seq[Path],
    entrypoint: File,
    requireTests: Boolean,
    args: Seq[String],
    testFrameworkOpt: Option[String],
    logger: Logger,
    jsDom: Boolean,
    esModule: Boolean
  ): Either[TestError, Int] = either {
    import org.scalajs.jsenv.Input
    import org.scalajs.jsenv.nodejs.NodeJSEnv
    import org.scalajs.testing.adapter.TestAdapter
    val nodePath = findInPath("node").fold("node")(_.toString)
    val jsEnv =
      if (jsDom)
        new JSDOMNodeJSEnv(
          JSDOMNodeJSEnv.Config()
            .withExecutable(nodePath)
            .withArgs(Nil)
            .withEnv(Map.empty)
        )
      else new NodeJSEnv(
        NodeJSEnv.Config()
          .withExecutable(nodePath)
          .withArgs(Nil)
          .withEnv(Map.empty)
          .withSourceMap(NodeJSEnv.SourceMap.Disable)
      )
    val adapterConfig = TestAdapter.Config().withLogger(logger.scalaJsLogger)
    val inputs = Seq(
      if (esModule) Input.ESModule(entrypoint.toPath)
      else Input.Script(entrypoint.toPath)
    )
    var adapter: TestAdapter = null

    logger.debug(s"JS tests class path: $classPath")

    val parentInspector = new AsmTestRunner.ParentInspector(classPath)
    val frameworkName0 = testFrameworkOpt match {
      case Some(fw) => fw
      case None     => value(frameworkName(classPath, parentInspector))
    }

    val res =
      try {
        adapter = new TestAdapter(jsEnv, inputs, adapterConfig)

        val frameworks = adapter.loadFrameworks(List(List(frameworkName0))).flatten

        if (frameworks.isEmpty)
          Left(new NoFrameworkFoundByBridgeError)
        else if (frameworks.length > 1)
          Left(new TooManyFrameworksFoundByBridgeError)
        else {
          val framework = frameworks.head
          runTests(classPath, framework, requireTests, args, parentInspector)
        }
      }
      finally if (adapter != null) adapter.close()

    if (value(res)) 0
    else 1
  }

  def testNative(
    classPath: Seq[Path],
    launcher: File,
    frameworkNameOpt: Option[String],
    requireTests: Boolean,
    args: Seq[String],
    logger: Logger
  ): Either[TestError, Int] = either {

    import scala.scalanative.testinterface.adapter.TestAdapter

    logger.debug(s"Native tests class path: $classPath")

    val parentInspector = new AsmTestRunner.ParentInspector(classPath)
    val frameworkName0 = frameworkNameOpt match {
      case Some(fw) => fw
      case None     => value(frameworkName(classPath, parentInspector))
    }

    val config = TestAdapter.Config()
      .withBinaryFile(launcher)
      .withEnvVars(sys.env.toMap)
      .withLogger(logger.scalaNativeTestLogger)

    var adapter: TestAdapter = null

    val res =
      try {
        adapter = new TestAdapter(config)

        val frameworks = adapter.loadFrameworks(List(List(frameworkName0))).flatten

        if (frameworks.isEmpty)
          Left(new NoFrameworkFoundByBridgeError)
        else if (frameworks.length > 1)
          Left(new TooManyFrameworksFoundByBridgeError)
        else {
          val framework = frameworks.head
          runTests(classPath, framework, requireTests, args, parentInspector)
        }
      }
      finally if (adapter != null) adapter.close()

    if (value(res)) 0
    else 1
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy