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

scala.build.options.BuildOptions.scala Maven / Gradle / Ivy

package scala.build.options

import com.github.plokhotnyuk.jsoniter_scala.core.*
import coursier.cache.{ArchiveCache, FileCache, UnArchiver}
import coursier.core.{Repository, Version}
import coursier.parse.RepositoryParser
import coursier.util.{Artifact, Task}
import dependency.*

import java.io.File
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.security.MessageDigest

import scala.build.EitherCps.{either, value}
import scala.build.actionable.{ActionableDiagnostic, ActionablePreprocessor}
import scala.build.errors.*
import scala.build.interactive.Interactive
import scala.build.interactive.Interactive.*
import scala.build.internal.Constants.*
import scala.build.internal.CsLoggerUtil.*
import scala.build.internal.Regexes.scala3NightlyNicknameRegex
import scala.build.internal.{Constants, OsLibc, StableScalaVersion, Util}
import scala.build.internals.EnvVar
import scala.build.options.BuildRequirements.ScopeRequirement
import scala.build.options.validation.BuildOptionsRule
import scala.build.{Artifacts, Logger, Os, Position, Positioned}
import scala.collection.immutable.Seq
import scala.concurrent.Await
import scala.concurrent.duration.*
import scala.util.Properties
import scala.util.control.NonFatal

final case class BuildOptions(
  suppressWarningOptions: SuppressWarningOptions = SuppressWarningOptions(),
  scalaOptions: ScalaOptions = ScalaOptions(),
  scalaJsOptions: ScalaJsOptions = ScalaJsOptions(),
  scalaNativeOptions: ScalaNativeOptions = ScalaNativeOptions(),
  internalDependencies: InternalDependenciesOptions = InternalDependenciesOptions(),
  javaOptions: JavaOptions = JavaOptions(),
  jmhOptions: JmhOptions = JmhOptions(),
  classPathOptions: ClassPathOptions = ClassPathOptions(),
  scriptOptions: ScriptOptions = ScriptOptions(),
  internal: InternalOptions = InternalOptions(),
  mainClass: Option[String] = None,
  testOptions: TestOptions = TestOptions(),
  notForBloopOptions: PostBuildOptions = PostBuildOptions(),
  sourceGeneratorOptions: SourceGeneratorOptions = SourceGeneratorOptions(),
  useBuildServer: Option[Boolean] = None
) {

  import BuildOptions.JavaHomeInfo

  lazy val platform: Positioned[Platform] =
    scalaOptions.platform.getOrElse(Positioned(List(Position.Custom("DEFAULT")), Platform.JVM))

  lazy val projectParams: Either[BuildException, Seq[String]] = either {
    value(scalaParams) match {
      case Some(scalaParams0) =>
        val platform0 = platform.value match {
          case Platform.JVM =>
            val jvmIdSuffix =
              javaOptions.jvmIdOpt.map(_.value)
                .orElse(Some(javaHome().value.version.toString))
                .map(" (" + _ + ")").getOrElse("")
            s"JVM$jvmIdSuffix"
          case Platform.JS =>
            val scalaJsVersion = scalaJsOptions.version.getOrElse(Constants.scalaJsVersion)
            s"Scala.js $scalaJsVersion"
          case Platform.Native =>
            s"Scala Native ${scalaNativeOptions.finalVersion}"
        }
        Seq(s"Scala ${scalaParams0.scalaVersion}", platform0)
      case None =>
        Seq("Java")
    }
  }

  lazy val scalaVersionIsExotic: Boolean = scalaParams.toOption.flatten.exists { scalaParameters =>
    scalaParameters.scalaVersion.startsWith("2") && scalaParameters.scalaVersion.exists(_.isLetter)
  }

  def addRunnerDependency: Option[Boolean] =
    notForBloopOptions.addRunnerDependencyOpt
      .orElse {
        if (platform.value == Platform.JVM && !scalaVersionIsExotic) None
        else Some(false)
      }

  private def scalaLibraryDependencies: Either[BuildException, Seq[AnyDependency]] = either {
    value(scalaParams).toSeq.flatMap { scalaParams0 =>
      if (platform.value != Platform.Native && scalaOptions.addScalaLibrary.getOrElse(true))
        Seq(
          if (scalaParams0.scalaVersion.startsWith("3."))
            dep"org.scala-lang::scala3-library::${scalaParams0.scalaVersion}"
          else
            dep"org.scala-lang:scala-library:${scalaParams0.scalaVersion}"
        )
      else Nil
    }
  }

  private def scalaCompilerDependencies: Either[BuildException, Seq[AnyDependency]] = either {
    value(scalaParams)
      .map(_ -> scalaOptions.addScalaCompiler.getOrElse(false))
      .toSeq
      .flatMap {
        case (sp, true) if sp.scalaVersion.startsWith("3") =>
          Seq(
            dep"org.scala-lang::scala3-compiler::${sp.scalaVersion}",
            dep"org.scala-lang::scala3-staging::${sp.scalaVersion}",
            dep"org.scala-lang::scala3-tasty-inspector::${sp.scalaVersion}"
          )
        case (sp, true) => Seq(dep"org.scala-lang:scala-compiler:${sp.scalaVersion}")
        case _          => Nil
      }
  }

  private def maybeJsDependencies: Either[BuildException, Seq[AnyDependency]] = either {
    if (platform.value == Platform.JS)
      value(scalaParams).toSeq.flatMap { scalaParams0 =>
        scalaJsOptions.jsDependencies(scalaParams0.scalaVersion)
      }
    else Nil
  }
  private def maybeNativeDependencies: Either[BuildException, Seq[AnyDependency]] = either {
    if (platform.value == Platform.Native)
      value(scalaParams).toSeq.flatMap { scalaParams0 =>
        scalaNativeOptions.nativeDependencies(scalaParams0.scalaVersion)
      }
    else Nil
  }
  private def defaultDependencies: Either[BuildException, Seq[Positioned[AnyDependency]]] = either {
    value(maybeJsDependencies).map(Positioned.none(_)) ++
      value(maybeNativeDependencies).map(Positioned.none(_)) ++
      value(scalaLibraryDependencies).map(Positioned.none(_)) ++
      value(scalaCompilerDependencies).map(Positioned.none(_))
  }

  private def semanticDbPlugins(logger: Logger): Either[BuildException, Seq[AnyDependency]] =
    either {
      val scalaVersion: Option[String] = value(scalaParams).map(_.scalaVersion)
      val generateSemDbs = scalaOptions.semanticDbOptions.generateSemanticDbs.getOrElse(false)
      scalaVersion match {
        case Some(sv) if sv.startsWith("2.") && generateSemDbs =>
          val semanticDbVersion = findSemanticDbVersion(sv, logger)
          Seq(
            dep"$semanticDbPluginOrganization:::$semanticDbPluginModuleName:$semanticDbVersion"
          )
        case _ => Nil
      }
    }

  /** Find the latest supported semanticdb version for @scalaVersion
    */
  def findSemanticDbVersion(scalaVersion: String, logger: Logger): String = {
    val versionsFuture =
      finalCache.logger.use {
        coursier.complete.Complete(finalCache)
          .withScalaVersion(scalaVersion)
          .withScalaBinaryVersion(scalaVersion.split('.').take(2).mkString("."))
          .withInput(s"org.scalameta:semanticdb-scalac_$scalaVersion:")
          .complete()
          .future()(finalCache.ec)
      }

    val versions =
      try
        Await.result(versionsFuture, FiniteDuration(10, "s"))._2
      catch {
        case NonFatal(e) =>
          logger.debug(s"Error while looking up semanticdb versions for scala $scalaVersion")
          Util.printException(e, logger.debug(_: String))
          Nil
      }

    versions.lastOption.getOrElse(semanticDbPluginVersion)
  }

  private def maybeJsCompilerPlugins: Either[BuildException, Seq[AnyDependency]] = either {
    if (platform.value == Platform.JS)
      value(scalaParams).toSeq.flatMap { scalaParams0 =>
        scalaJsOptions.compilerPlugins(scalaParams0.scalaVersion)
      }
    else Nil
  }
  private def maybeNativeCompilerPlugins: Seq[AnyDependency] =
    if (platform.value == Platform.Native) scalaNativeOptions.compilerPlugins
    else Nil
  def compilerPlugins(logger: Logger): Either[BuildException, Seq[Positioned[AnyDependency]]] =
    either {
      value(maybeJsCompilerPlugins).map(Positioned.none) ++
        maybeNativeCompilerPlugins.map(Positioned.none) ++
        value(semanticDbPlugins(logger)).map(Positioned.none) ++
        scalaOptions.compilerPlugins
    }

  private def semanticDbJavacPlugins: Either[BuildException, Seq[AnyDependency]] = either {
    val generateSemDbs = scalaOptions.semanticDbOptions.generateSemanticDbs.getOrElse(false)
    if (generateSemDbs)
      Seq(
        dep"$semanticDbJavacPluginOrganization:$semanticDbJavacPluginModuleName:$semanticDbJavacPluginVersion"
      )
    else
      Nil
  }

  def javacPluginDependencies: Either[BuildException, Seq[Positioned[AnyDependency]]] = either {
    value(semanticDbJavacPlugins).map(Positioned.none(_)) ++
      javaOptions.javacPluginDependencies
  }

  def allExtraJars: Seq[os.Path] =
    classPathOptions.extraClassPath
  def allExtraCompileOnlyJars: Seq[os.Path] =
    classPathOptions.extraCompileOnlyJars
  def allExtraSourceJars: Seq[os.Path] =
    classPathOptions.extraSourceJars

  private def addJvmTestRunner: Boolean =
    platform.value == Platform.JVM &&
    internalDependencies.addTestRunnerDependency
  private def addJsTestBridge: Option[String] =
    if (platform.value == Platform.JS && internalDependencies.addTestRunnerDependency)
      Some(scalaJsOptions.finalVersion)
    else None
  private def addNativeTestInterface: Option[String] = {
    val doAdd =
      platform.value == Platform.Native &&
      internalDependencies.addTestRunnerDependency &&
      Version("0.4.3").compareTo(Version(scalaNativeOptions.finalVersion)) <= 0
    if (doAdd) Some(scalaNativeOptions.finalVersion)
    else None
  }

  lazy val finalCache: FileCache[Task] = internal.cache.getOrElse(FileCache())
  // This might download a JVM if --jvm … is passed or no system JVM is installed

  lazy val archiveCache: ArchiveCache[Task] = ArchiveCache().withCache(finalCache)

  private lazy val javaCommand0: Positioned[JavaHomeInfo] =
    javaHomeLocation().map(JavaHomeInfo(_))

  def javaHomeLocationOpt(): Option[Positioned[os.Path]] =
    javaOptions.javaHomeLocationOpt(archiveCache, finalCache, internal.verbosityOrDefault)

  def javaHomeLocation(): Positioned[os.Path] =
    javaOptions.javaHomeLocation(archiveCache, finalCache, internal.verbosityOrDefault)

  def javaHome(): Positioned[JavaHomeInfo] = javaCommand0

  lazy val javaHomeManager =
    javaOptions.javaHomeManager(archiveCache, finalCache, internal.verbosityOrDefault)

  private val scala2NightlyRepo = Seq(coursier.Repositories.scalaIntegration.root)

  def finalRepositories: Either[BuildException, Seq[Repository]] = either {
    val nightlyRepos =
      if (scalaOptions.scalaVersion.exists(sv => ScalaVersionUtil.isScala2Nightly(sv.asString)))
        scala2NightlyRepo
      else
        Nil
    val snapshotRepositories =
      if classPathOptions.extraRepositories.contains("snapshots")
      then
        Seq(
          coursier.Repositories.sonatype("snapshots"),
          coursier.Repositories.sonatypeS01("snapshots")
        )
      else Nil
    val extraRepositories = classPathOptions.extraRepositories.filterNot(_ == "snapshots")

    val repositories = nightlyRepos ++
      extraRepositories ++
      internal.localRepository.toSeq

    val parseRepositories = value {
      RepositoryParser.repositories(repositories)
        .either
        .left.map(errors => new RepositoryFormatError(errors))
    }

    parseRepositories ++ snapshotRepositories
  }

  lazy val scalaParams: Either[BuildException, Option[ScalaParameters]] = either {
    val params =
      if EnvVar.Internal.ci.valueOpt.isEmpty then
        computeScalaParams(Constants.version, finalCache, value(finalRepositories)).orElse(
          // when the passed scala version is missed in the cache, we always force a cache refresh
          // https://github.com/VirtusLab/scala-cli/issues/1090
          computeScalaParams(
            Constants.version,
            finalCache.withTtl(0.seconds),
            value(finalRepositories)
          )
        )
      else
        computeScalaParams(
          Constants.version,
          finalCache.withTtl(0.seconds),
          value(finalRepositories)
        )
    value(params)
  }

  private[build] def computeScalaParams(
    scalaCliVersion: String,
    cache: FileCache[Task] = finalCache,
    repositories: Seq[Repository] = Nil
  ): Either[BuildException, Option[ScalaParameters]] = either {

    val defaultVersions = Set(
      Constants.defaultScalaVersion,
      Constants.defaultScala212Version,
      Constants.defaultScala213Version,
      scalaOptions.defaultScalaVersion.getOrElse(Constants.defaultScalaVersion)
    )

    val svOpt: Option[String] = scalaOptions.scalaVersion match {
      case Some(MaybeScalaVersion(None)) =>
        None
      // Do not validate Scala version in offline mode
      case Some(MaybeScalaVersion(Some(svInput))) if internal.offline.getOrElse(false) =>
        Some(svInput)
      // Do not validate Scala version if it is a default one
      case Some(MaybeScalaVersion(Some(svInput))) if defaultVersions.contains(svInput) =>
        Some(svInput)
      case Some(MaybeScalaVersion(Some(svInput))) =>
        val sv = value {
          svInput match {
            case sv if ScalaVersionUtil.scala3Lts.contains(sv) =>
              ScalaVersionUtil.validateStable(
                Constants.scala3LtsPrefix,
                cache,
                repositories
              )
            case sv if ScalaVersionUtil.scala2Lts.contains(sv) =>
              Left(new ScalaVersionError(
                s"Invalid Scala version: $sv. There is no official LTS version for Scala 2."
              ))
            case sv if sv == ScalaVersionUtil.scala3Nightly =>
              ScalaVersionUtil.GetNightly.scala3(cache)
            case scala3NightlyNicknameRegex(threeSubBinaryNum) =>
              ScalaVersionUtil.GetNightly.scala3X(
                threeSubBinaryNum,
                cache
              )
            case vs if ScalaVersionUtil.scala213Nightly.contains(vs) =>
              ScalaVersionUtil.GetNightly.scala2("2.13", cache)
            case sv if sv == ScalaVersionUtil.scala212Nightly =>
              ScalaVersionUtil.GetNightly.scala2("2.12", cache)
            case versionString if ScalaVersionUtil.isScala3Nightly(versionString) =>
              ScalaVersionUtil.CheckNightly.scala3(
                versionString,
                cache
              )
                .map(_ => versionString)
            case versionString if ScalaVersionUtil.isScala2Nightly(versionString) =>
              ScalaVersionUtil.CheckNightly.scala2(
                versionString,
                cache
              )
                .map(_ => versionString)
            case versionString if versionString.exists(_.isLetter) =>
              ScalaVersionUtil.validateNonStable(
                versionString,
                cache,
                repositories
              )
            case versionString =>
              ScalaVersionUtil.validateStable(
                versionString,
                cache,
                repositories
              )
          }
        }
        Some(sv)

      case None => Some(scalaOptions.defaultScalaVersion.getOrElse(Constants.defaultScalaVersion))
    }

    svOpt match {
      case Some(scalaVersion) =>
        val scalaBinaryVersion = scalaOptions.scalaBinaryVersion.getOrElse {
          ScalaVersion.binary(scalaVersion)
        }

        val maybePlatformSuffix = platform.value match {
          case Platform.JVM    => None
          case Platform.JS     => Some(scalaJsOptions.platformSuffix)
          case Platform.Native => Some(scalaNativeOptions.platformSuffix)
        }

        Some(ScalaParameters(scalaVersion, scalaBinaryVersion, maybePlatformSuffix))
      case None =>
        None
    }
  }

  def artifacts(
    logger: Logger,
    scope: Scope,
    maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e)
  ): Either[BuildException, Artifacts] = either {
    val isTests = scope == Scope.Test
    val scalaArtifactsParamsOpt = value(scalaParams) match {
      case Some(scalaParams0) =>
        val params = Artifacts.ScalaArtifactsParams(
          params = scalaParams0,
          compilerPlugins = value(compilerPlugins(logger)),
          addJsTestBridge = addJsTestBridge.filter(_ => isTests),
          addNativeTestInterface = addNativeTestInterface.filter(_ => isTests),
          scalaJsVersion =
            if (platform.value == Platform.JS) Some(scalaJsOptions.finalVersion) else None,
          scalaJsCliVersion =
            if (platform.value == Platform.JS)
              Some(notForBloopOptions.scalaJsLinkerOptions.finalScalaJsCliVersion)
            else None,
          scalaNativeCliVersion =
            if (platform.value == Platform.Native) {
              val scalaNativeFinalVersion = scalaNativeOptions.finalVersion
              if scalaNativeOptions.version.isEmpty && scalaNativeFinalVersion != Constants.scalaNativeVersion
              then
                scalaNativeOptions.maxDefaultNativeVersions.map(_._2).distinct
                  .map(reason => s"[${Console.YELLOW}warn${Console.RESET}] $reason")
                  .foreach(reason => logger.message(reason))
                logger.message(
                  s"[${Console.YELLOW}warn${Console.RESET}] Scala Native default version ${Constants.scalaNativeVersion} is not supported in this build. Using $scalaNativeFinalVersion instead."
                )
              Some(scalaNativeFinalVersion)
            }
            else None,
          addScalapy =
            if (notForBloopOptions.doSetupPython.getOrElse(false))
              Some(notForBloopOptions.scalaPyVersion.getOrElse(Constants.scalaPyVersion))
            else
              None
        )
        Some(params)
      case None =>
        None
    }
    val addRunnerDependency0 = addRunnerDependency.orElse {
      if (scalaArtifactsParamsOpt.isDefined) None
      else Some(false) // no runner in pure Java mode
    }
    val maybeArtifacts = Artifacts(
      scalaArtifactsParamsOpt,
      javacPluginDependencies = value(javacPluginDependencies),
      extraJavacPlugins = javaOptions.javacPlugins.map(_.value),
      defaultDependencies = value(defaultDependencies),
      extraDependencies = classPathOptions.extraDependencies.toSeq,
      compileOnlyDependencies = classPathOptions.extraCompileOnlyDependencies.toSeq,
      extraClassPath = allExtraJars,
      extraCompileOnlyJars = allExtraCompileOnlyJars,
      extraSourceJars = allExtraSourceJars,
      fetchSources = classPathOptions.fetchSources.getOrElse(false),
      addJvmRunner = addRunnerDependency0,
      addJvmTestRunner = isTests && addJvmTestRunner,
      addJmhDependencies = jmhOptions.finalJmhVersion,
      extraRepositories = value(finalRepositories),
      keepResolution = internal.keepResolution,
      includeBuildServerDeps = useBuildServer.getOrElse(true),
      cache = finalCache,
      logger = logger,
      maybeRecoverOnError = maybeRecoverOnError
    )
    value(maybeArtifacts)
  }

  private def allCrossScalaVersionOptions: Seq[BuildOptions] = {
    val scalaOptions0 = scalaOptions.normalize
    val sortedExtraScalaVersions = scalaOptions0
      .extraScalaVersions
      .toVector
      .map(coursier.core.Version(_))
      .sorted
      .map(_.repr)
      .reverse
    this +: sortedExtraScalaVersions.map { sv =>
      copy(
        scalaOptions = scalaOptions0.copy(
          scalaVersion = Some(MaybeScalaVersion(sv)),
          extraScalaVersions = Set.empty
        )
      )
    }
  }

  private def allCrossScalaPlatformOptions: Seq[BuildOptions] = {
    val scalaOptions0 = scalaOptions.normalize
    val sortedExtraPlatforms = scalaOptions0
      .extraPlatforms
      .toVector
    this +: sortedExtraPlatforms.map { case (pf, pos) =>
      copy(
        scalaOptions = scalaOptions0.copy(
          platform = Some(Positioned(pos.positions, pf)),
          extraPlatforms = Map.empty
        )
      )
    }
  }

  def crossOptions: Seq[BuildOptions] = {
    val allOptions = for {
      svOpt   <- allCrossScalaVersionOptions
      svPfOpt <- svOpt.allCrossScalaPlatformOptions
    } yield svPfOpt
    allOptions.drop(1) // First one if basically 'this', dropping it
  }

  private def clearJsOptions: BuildOptions =
    copy(scalaJsOptions = ScalaJsOptions())
  private def clearNativeOptions: BuildOptions =
    copy(scalaNativeOptions = ScalaNativeOptions())
  private def normalize: BuildOptions = {
    var opt = this

    if (platform.value != Platform.JS)
      opt = opt.clearJsOptions
    if (platform.value != Platform.Native)
      opt = opt.clearNativeOptions

    opt.copy(
      scalaOptions = opt.scalaOptions.normalize
    )
  }

  lazy val hash: Option[String] = {
    val md = MessageDigest.getInstance("SHA-1")

    var hasAnyOverride = false

    BuildOptions.hasHashData.add(
      "",
      normalize,
      s => {
        val bytes = s.getBytes(StandardCharsets.UTF_8)
        if (bytes.nonEmpty) {
          hasAnyOverride = true
          md.update(bytes)
        }
      }
    )

    if (hasAnyOverride) {
      val digest        = md.digest()
      val calculatedSum = new BigInteger(1, digest)
      val hash          = String.format(s"%040x", calculatedSum).take(10)
      Some(hash)
    }
    else None
  }

  def orElse(other: BuildOptions): BuildOptions =
    BuildOptions.monoid.orElse(this, other)

  def validate: Seq[Diagnostic] = BuildOptionsRule.validateAll(this)

  def logActionableDiagnostics(logger: Logger): Unit = {
    val actionableDiagnostics = ActionablePreprocessor.generateActionableDiagnostics(this)
    actionableDiagnostics match {
      case Left(e) =>
        logger.debug(e)
      case Right(diagnostics) =>
        logger.log(diagnostics)
    }
  }

  lazy val interactive: Either[BuildException, Interactive] =
    internal.interactive.map(_()).getOrElse(Right(InteractiveNop))
}

object BuildOptions {
  def empty: BuildOptions = BuildOptions()

  final case class CrossKey(
    scalaVersion: String,
    platform: Platform
  )

  final case class JavaHomeInfo(
    javaHome: os.Path,
    javaCommand: String,
    version: Int
  ) {
    def envUpdates(currentEnv: Map[String, String]): Map[String, String] = {
      // On Windows, AFAIK, env vars are "case-insensitive but case-preserving".
      // If PATH was defined as "Path", we need to update "Path", not "PATH".
      // Same for JAVA_HOME
      def keyFor(name: String) =
        if (Properties.isWin)
          currentEnv.keys.find(_.equalsIgnoreCase(name)).getOrElse(name)
        else
          name
      val javaHomeKey = keyFor(EnvVar.Java.javaHome.name)
      val pathKey     = keyFor(EnvVar.Misc.path.name)
      val updatedPath = {
        val valueOpt = currentEnv.get(pathKey)
        val entry    = (javaHome / "bin").toString
        valueOpt.fold(entry)(entry + File.pathSeparator + _)
      }
      Map(
        javaHomeKey -> javaHome.toString,
        pathKey     -> updatedPath
      )
    }
  }

  object JavaHomeInfo {
    def apply(javaHome: os.Path): JavaHomeInfo = {
      val ext         = if (Properties.isWin) ".exe" else ""
      val javaCmd     = (javaHome / "bin" / s"java$ext").toString
      val javaVersion = OsLibc.javaVersion(javaCmd)
      JavaHomeInfo(javaHome, javaCmd, javaVersion)
    }
  }

  implicit val hasHashData: HasHashData[BuildOptions] = HasHashData.derive
  implicit val monoid: ConfigMonoid[BuildOptions]     = ConfigMonoid.derive
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy