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

scala.scalanative.build.Discover.scala Maven / Gradle / Ivy

There is a newer version: 0.5.5
Show newest version
package scala.scalanative
package build

import java.io.File
import java.nio.file.{Path, Paths}
import scala.util.Try
import scala.sys.process._
import scalanative.build.IO.RichPath

/** Utilities for discovery of command-line tools and settings required to build
 *  Scala Native applications.
 */
object Discover {

  /** Compilation mode name from SCALANATIVE_MODE env var or default. */
  def mode(): Mode =
    getenv("SCALANATIVE_MODE").map(build.Mode(_)).getOrElse(build.Mode.default)

  def optimize(): Boolean =
    getenv("SCALANATIVE_OPTIMIZE").forall(_.toBoolean)

  /** LTO variant used for release mode from SCALANATIVE_LTO env var or default.
   */
  def LTO(): LTO =
    getenv("SCALANATIVE_LTO").map(build.LTO(_)).getOrElse(build.LTO.None)

  /** GC variant used from SCALANATIVE_GC env var or default. */
  def GC(): GC =
    getenv("SCALANATIVE_GC").map(build.GC(_)).getOrElse(build.GC.default)

  /** Use the clang binary on the path or via LLVM_BIN env var. */
  def clang(): Path = {
    val path = discover("clang", "LLVM_BIN")
    checkClangVersion(path)
    path
  }

  /** Use the clang++ binary on the path or via LLVM_BIN env var. */
  def clangpp(): Path = {
    val path = discover("clang++", "LLVM_BIN")
    checkClangVersion(path)
    path
  }

  private def filterExisting(paths: Seq[String]): Seq[String] =
    paths.filter(new File(_).exists())

  /** Find default clang compilation options. */
  def compileOptions(): Seq[String] = {
    val includes = {
      val llvmIncludeDir =
        Try(Process("llvm-config --includedir").lineStream_!.toSeq)
          .getOrElse(Seq.empty)
      // dirs: standard, macports, brew M1 arm
      val includeDirs =
        getenv("SCALANATIVE_INCLUDE_DIRS")
          .map(_.split(File.pathSeparatorChar).toSeq)
          .getOrElse(
            filterExisting(
              Seq(
                "/usr/local/include",
                "/opt/local/include",
                "/opt/homebrew/include"
              )
            )
          )

      (includeDirs ++ llvmIncludeDir).map(s => s"-I$s")
    }
    includes :+ "-Qunused-arguments"
  }

  /** Find default options passed to the system's native linker. */
  def linkingOptions(): Seq[String] = {
    val libs = {
      val llvmLibDir =
        Try(Process("llvm-config --libdir").lineStream_!.toSeq)
          .getOrElse(Seq.empty)

      val libDirs =
        getenv("SCALANATIVE_LIB_DIRS")
          .map(_.split(File.pathSeparatorChar).toSeq)
          .getOrElse(
            filterExisting(
              Seq("/usr/local/lib", "/opt/local/lib", "/opt/homebrew/lib")
            )
          )

      (libDirs ++ llvmLibDir).map(s => s"-L$s")
    }
    libs
  }

  /** Tests whether the clang compiler is greater or equal to the minumum
   *  version required.
   */
  private[scalanative] def checkClangVersion(pathToClangBinary: Path): Unit = {
    def versionMajorFull(clang: String): (Int, String) = {
      val versionCommand = Seq(clang, "--version")
      val versionString = Process(versionCommand)
        .lineStream_!(silentLogger())
        .headOption
        .getOrElse {
          throw new BuildException(s"""Problem running '${versionCommand
            .mkString(" ")}'. Please check clang setup.
               |Refer to ($docSetup)""".stripMargin)
        }
      // Apple macOS clang is different vs brew installed or Linux
      // Apple LLVM version 10.0.1 (clang-1001.0.46.4)
      // clang version 11.0.0
      try {
        val versionArray = versionString.split(" ")
        val versionIndex = versionArray.indexWhere(_.equals("version"))
        val version = versionArray(versionIndex + 1)
        val majorVersion = version.split("\\.").head
        (majorVersion.toInt, version)
      } catch {
        case t: Throwable =>
          throw new BuildException(s"""Output from '$versionCommand' unexpected.
                 |Was expecting '... version n.n.n ...'.
                 |Got '$versionString'.
                 |Cause: ${t}""".stripMargin)
      }
    }

    val (majorVersion, version) = versionMajorFull(pathToClangBinary.abs)

    if (majorVersion < clangMinVersion) {
      throw new BuildException(
        s"""Minimum version of clang is '$clangMinVersion'.
             |Discovered version '$version'.
             |Please refer to ($docSetup)""".stripMargin
      )
    }
  }

  /** Minimum version of clang */
  private[scalanative] final val clangMinVersion = 6

  /** Link to setup documentation */
  private[scalanative] val docSetup =
    "http://www.scala-native.org/en/latest/user/setup.html"

  /** Discover the binary path using environment variables or the command from
   *  the path.
   */
  private[scalanative] def discover(
      binaryName: String,
      envPath: String
  ): Path = {
    val binPath = sys.env.get(envPath)

    val command: Seq[String] = {
      if (Platform.isWindows) {
        val binName = s"${binaryName}.exe"
        val arg = binPath.fold(binName)(p => s"$p:$binName")
        Seq("where", arg)
      } else {
        val arg = binPath.fold(binaryName) { p =>
          Paths.get(p, binaryName).toString()
        }
        Seq("which", arg)
      }
    }

    val path = Process(command)
      .lineStream_!(silentLogger())
      .map { p => Paths.get(p) }
      .headOption
      .getOrElse {
        throw new BuildException(
          s"""'$binaryName' not found in PATH or via '$envPath' environment variable.
            |Please refer to ($docSetup)""".stripMargin
        )
      }
    path
  }

  private def silentLogger(): ProcessLogger =
    ProcessLogger(_ => (), _ => ())

  private def getenv(key: String): Option[String] =
    Option(System.getenv.get(key))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy