Skip to content

Commit

Permalink
Don't require cs to be installed any more
Browse files Browse the repository at this point in the history
  • Loading branch information
alexarchambault committed Oct 8, 2021
1 parent 119e589 commit 78bf50f
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 43 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ Scala CLI is an experimental tool to run/compile/test Scala that aims at being a

Building Scala CLI requires:
- a JVM (>= 8)
- [`cs`](https://get-coursier.io/docs/cli-installation#native-launcher) in the `PATH` (running just `cs` in the terminal needs to work)

The Scala CLI sources ship with Mill launchers, so that Mill itself doesn't need to be installed on your system.

Expand Down
1 change: 1 addition & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ trait CliIntegrationBase extends SbtModule with ScalaCliPublishModule with HasTe
| def munitVersion = "${TestDeps.munit.dep.version}"
| def dockerTestImage = "${Docker.testImage}"
| def dockerAlpineTestImage = "${Docker.alpineTestImage}"
| def cs = "${settings.cs().replace("\\", "\\\\")}"
|}
|""".stripMargin
if (!os.isFile(dest) || os.read(dest) != code)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ object TestUtil {
.map(_.getAbsolutePath)
}

lazy val cs = fromPath("cs").getOrElse {
System.err.println("Warning: cannot find cs in PATH")
"cs"
}
def cs = Constants.cs

def threadPool(prefix: String, size: Int): ExecutorService =
Executors.newFixedThreadPool(size, daemonThreadFactory(prefix))
Expand Down
2 changes: 2 additions & 0 deletions project/deps.sc
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def graalVmVersion = "21.2.0"

def csDockerVersion = "2.0.16"

def buildCsVersion = "2.0.16"

object Docker {
def muslBuilder =
"messense/rust-musl-cross@sha256:12d0dd535ef7364bf49cb2608ae7eaf60e40d07834eb4d9160c592422a08d3b3"
Expand Down
202 changes: 164 additions & 38 deletions project/settings.sc
Original file line number Diff line number Diff line change
@@ -1,43 +1,169 @@
import $ivy.`com.goyeau::mill-scalafix:0.2.5`
import $ivy.`io.github.alexarchambault.mill::mill-native-image_mill0.9:0.1.9`
import $file.deps, deps.{Deps, Docker}
import $file.deps, deps.{Deps, Docker, buildCsVersion}

import com.goyeau.mill.scalafix.ScalafixModule
import de.tobiasroeser.mill.vcs.version.VcsVersion
import io.github.alexarchambault.millnativeimage.NativeImage
import java.io.File
import java.io.{ByteArrayOutputStream, File, FileInputStream, InputStream}
import java.nio.charset.StandardCharsets
import java.util.Locale
import java.util.zip.{GZIPInputStream, ZipFile}
import mill._, scalalib._
import scala.collection.JavaConverters._
import scala.util.Properties

lazy val cs: String =
if (Properties.isWin) {
val pathExt = Option(System.getenv("PATHEXT"))
.toSeq
.flatMap(_.split(File.pathSeparator).toSeq)
val path = Option(System.getenv("PATH"))
.toSeq
.flatMap(_.split(File.pathSeparator))
.map(new File(_))

def candidates =
for {
dir <- path.iterator
ext <- pathExt.iterator
} yield new File(dir, s"cs$ext")

candidates
.filter(_.canExecute)
.toStream
.headOption
.map(_.getAbsolutePath)
.getOrElse {
System.err.println("Warning: could not find cs in PATH.")
"cs"
}
private def withGzipContent[T](gzFile: File)(f: InputStream => T): T = {
var fis: FileInputStream = null
var gzis: GZIPInputStream = null
try {
fis = new FileInputStream(gzFile)
gzis = new GZIPInputStream(fis)
f(gzis)
}
finally {
if (gzis != null) gzis.close()
if (fis != null) fis.close()
}
}

private def withFirstFileInZip[T](zip: File)(f: InputStream => T): T = {
var zf: ZipFile = null
var is: InputStream = null
try {
zf = new ZipFile(zip)
val ent = zf.entries().asScala.find(e => !e.isDirectory).getOrElse {
throw new NoSuchElementException(s"No file found in $zip")
}
is = zf.getInputStream(ent)
f(is)
}
finally {
if (zf != null)
zf.close()
if (is != null)
is.close()
}
}

private def readFully(is: InputStream): Array[Byte] = {
val buffer = new ByteArrayOutputStream
val data = Array.ofDim[Byte](16384)
var nRead = 0
while ({
nRead = is.read(data, 0, data.length)
nRead != -1
})
buffer.write(data, 0, nRead)
buffer.flush()
buffer.toByteArray
}

def cs: T[String] = T.persistent {

val ext = if (Properties.isWin) ".exe" else ""
val dest = T.dest / s"cs-$buildCsVersion$ext"

def downloadOpt(): Option[String] = {
val arch = sys.props.getOrElse("os.arch", "").toLowerCase(Locale.ROOT)
val urlOpt = arch match {
case "x86_64" | "amd64" =>
if (Properties.isWin) Some(
if (buildCsVersion == "2.0.16")
"https://github.com/coursier/coursier/releases/download/v2.0.13/cs-x86_64-pc-win32.exe"
else
s"https://github.com/coursier/coursier/releases/download/v$buildCsVersion/cs-x86_64-pc-win32.zip"
)
else if (Properties.isMac) Some(
if (buildCsVersion == "2.0.16")
"https://github.com/coursier/coursier/releases/download/v2.0.16/cs-x86_64-apple-darwin"
else
s"https://github.com/coursier/coursier/releases/download/v$buildCsVersion/cs-x86_64-apple-darwin.gz"
)
else if (Properties.isLinux) Some(
if (buildCsVersion == "2.0.16")
"https://github.com/coursier/coursier/releases/download/v2.0.16/cs-x86_64-pc-linux"
else
s"https://github.com/coursier/coursier/releases/download/v$buildCsVersion/cs-x86_64-pc-linux.gz"
)
else None
case "aarch64" =>
if (Properties.isLinux) Some(
if (buildCsVersion == "2.0.16")
"https://github.com/coursier/coursier/releases/download/v2.0.16/cs-aarch64-pc-linux"
else
s"https://github.com/coursier/coursier/releases/download/v$buildCsVersion/cs-aarch64-pc-linux.gz"
)
else None
case _ =>
None
}

urlOpt.map { url =>
val cache = coursier.cache.FileCache()
val task = cache.logger.using(cache.file(coursier.util.Artifact(url)).run)
val maybeFile =
try task.unsafeRun()(cache.ec)
catch {
case t: Throwable =>
throw new Exception(t)
}
val f = maybeFile.fold(ex => throw new Exception(ex), identity)
val exec =
if (f.getName.endsWith(".gz")) {
val b = withGzipContent(f)(readFully)
os.write(dest, b)
dest
}
else if (f.getName.endsWith(".zip")) {
val b = withFirstFileInZip(f)(readFully)
os.write(dest, b)
dest
}
else
os.Path(f, os.pwd)

if (!Properties.isWin)
exec.toIO.setExecutable(true)

exec.toString
}
}

def fromPath: String =
if (Properties.isWin) {
val pathExt = Option(System.getenv("PATHEXT"))
.toSeq
.flatMap(_.split(File.pathSeparator).toSeq)
val path = Option(System.getenv("PATH"))
.toSeq
.flatMap(_.split(File.pathSeparator))
.map(new File(_))

def candidates =
for {
dir <- path.iterator
ext <- pathExt.iterator
} yield new File(dir, s"cs$ext")

candidates
.filter(_.canExecute)
.toStream
.headOption
.map(_.getAbsolutePath)
.getOrElse {
System.err.println("Warning: could not find cs in PATH.")
"cs"
}
}
else
"cs"

if (os.isFile(dest))
dest.toString
else
"cs"
(downloadOpt().getOrElse(fromPath): String)
}

// should be the default index in the upcoming coursier release (> 2.0.16)
def jvmIndex = "https://github.com/coursier/jvm-index/raw/master/index.json"
Expand Down Expand Up @@ -74,7 +200,7 @@ def getGhToken(): String =
trait CliLaunchers extends SbtModule { self =>

trait CliNativeImage extends NativeImage {
def nativeImageCsCommand = Seq(cs)
def nativeImageCsCommand = Seq(cs())
def nativeImagePersist = System.getenv("CI") != null
def nativeImageGraalVmJvmId = s"graalvm-java11:${deps.graalVmVersion}"
def nativeImageOptions = T {
Expand All @@ -93,7 +219,7 @@ trait CliLaunchers extends SbtModule { self =>

private def staticLibDirName = "native-libs"

private def copyCsjniutilTo(destDir: os.Path): Unit = {
private def copyCsjniutilTo(cs: String, destDir: os.Path): Unit = {
val jniUtilsVersion = Deps.jniUtils.dep.version
val libRes = os.proc(
cs,
Expand All @@ -106,7 +232,7 @@ trait CliLaunchers extends SbtModule { self =>
val libPath = os.Path(libRes.out.text().trim, os.pwd)
os.copy.over(libPath, destDir / "csjniutils.lib")
}
private def copyIpcsocketDllTo(destDir: os.Path): Unit = {
private def copyIpcsocketDllTo(cs: String, destDir: os.Path): Unit = {
val ipcsocketVersion = Deps.ipcSocket.dep.version
val libRes = os.proc(
cs,
Expand All @@ -119,7 +245,7 @@ trait CliLaunchers extends SbtModule { self =>
val libPath = os.Path(libRes.out.text().trim, os.pwd)
os.copy.over(libPath, destDir / "ipcsocket.lib")
}
private def copyIpcsocketMacATo(destDir: os.Path): Unit = {
private def copyIpcsocketMacATo(cs: String, destDir: os.Path): Unit = {
val ipcsocketVersion = Deps.ipcSocket.dep.version
val libRes = os.proc(
cs,
Expand All @@ -132,7 +258,7 @@ trait CliLaunchers extends SbtModule { self =>
val libPath = os.Path(libRes.out.text().trim, os.pwd)
os.copy.over(libPath, destDir / "libipcsocket.a")
}
private def copyIpcsocketLinuxATo(destDir: os.Path): Unit = {
private def copyIpcsocketLinuxATo(cs: String, destDir: os.Path): Unit = {
val ipcsocketVersion = Deps.ipcSocket.dep.version
val libRes = os.proc(
cs,
Expand All @@ -150,15 +276,15 @@ trait CliLaunchers extends SbtModule { self =>
os.makeDir.all(dir)

if (Properties.isWin) {
copyCsjniutilTo(dir)
copyIpcsocketDllTo(dir)
copyCsjniutilTo(cs(), dir)
copyIpcsocketDllTo(cs(), dir)
}

if (Properties.isMac)
copyIpcsocketMacATo(dir)
copyIpcsocketMacATo(cs(), dir)

if (Properties.isLinux && arch == "x86_64")
copyIpcsocketLinuxATo(dir)
copyIpcsocketLinuxATo(cs(), dir)

PathRef(dir)
}
Expand Down Expand Up @@ -242,7 +368,7 @@ trait CliLaunchers extends SbtModule { self =>
import sys.process._
// format: off
Seq(
cs, "java-home",
cs(), "java-home",
"--jvm", s"graalvm-java11:$graalVmVersion",
"--jvm-index", jvmIndex
).!!.trim
Expand Down

0 comments on commit 78bf50f

Please sign in to comment.