package mill
package scalanativelib
import java.net.URLClassLoader
import coursier.Cache
import coursier.maven.MavenRepository
import mill.define.{Target, Task}
import mill.api.Result
import mill.modules.Jvm
import mill.scalalib.{Dep, DepSyntax, Lib, SbtModule, ScalaModule, TestModule, TestRunner}
import mill.api.Loose.Agg
import sbt.testing.{AnnotatedFingerprint, SubclassFingerprint}
import sbt.testing.Fingerprint
import upickle.default.{ReadWriter => RW, macroRW}
import mill.scalanativelib.api._
trait ScalaNativeModule extends ScalaModule { outer =>
def scalaNativeVersion: T[String]
override def platformSuffix = s"_native${scalaNativeBinaryVersion()}"
override def artifactSuffix: T[String] = s"${platformSuffix()}_${artifactScalaVersion()}"
trait Tests extends TestScalaNativeModule {
override def zincWorker = outer.zincWorker
override def scalaOrganization = outer.scalaOrganization()
override def scalaVersion = outer.scalaVersion()
override def scalaNativeVersion = outer.scalaNativeVersion()
override def releaseMode = outer.releaseMode()
override def logLevel = outer.logLevel()
override def moduleDeps = Seq(outer)
}
def scalaNativeBinaryVersion = T{ scalaNativeVersion().split('.').take(2).mkString(".") }
// This allows compilation and testing versus SNAPSHOT versions of scala-native
def scalaNativeToolsVersion = T{
if (scalaNativeVersion().endsWith("-SNAPSHOT"))
scalaNativeVersion()
else
scalaNativeBinaryVersion()
}
def scalaNativeWorker = T.task{ ScalaNativeWorkerApi.scalaNativeWorker().impl(bridgeFullClassPath()) }
def scalaNativeWorkerClasspath = T {
val workerKey = "MILL_SCALANATIVE_WORKER_" + scalaNativeBinaryVersion().replace('.', '_').replace('-', '_')
val workerPath = sys.props(workerKey)
if (workerPath != null)
Result.Success(Agg(workerPath.split(',').map(p => PathRef(os.Path(p), quick = true)): _*))
else
Lib.resolveDependencies(
Seq(Cache.ivy2Local, MavenRepository("https://repo1.maven.org/maven2")),
Lib.depToDependency(_, "2.12.4", ""),
Seq(ivy"com.lihaoyi::mill-scalanativelib-worker-${scalaNativeBinaryVersion()}:${sys.props("MILL_VERSION")}"),
ctx = Some(implicitly[mill.util.Ctx.Log])
)
}
def toolsIvyDeps = T{
Seq(
ivy"org.scala-native:tools_2.12:${scalaNativeVersion()}",
ivy"org.scala-native:util_2.12:${scalaNativeVersion()}",
ivy"org.scala-native:nir_2.12:${scalaNativeVersion()}"
)
}
override def transitiveIvyDeps: T[Agg[Dep]] = T{
ivyDeps() ++ nativeIvyDeps() ++ Task.traverse(moduleDeps)(_.transitiveIvyDeps)().flatten
}
def nativeLibIvy = T{ ivy"org.scala-native::nativelib_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}" }
def nativeIvyDeps = T{
Seq(nativeLibIvy()) ++
Seq(
ivy"org.scala-native::javalib_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}",
ivy"org.scala-native::auxlib_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}",
ivy"org.scala-native::scalalib_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}"
)
}
def bridgeFullClassPath = T {
Lib.resolveDependencies(
Seq(Cache.ivy2Local, MavenRepository("https://repo1.maven.org/maven2")),
Lib.depToDependency(_, scalaVersion(), platformSuffix()),
toolsIvyDeps(),
ctx = Some(implicitly[mill.util.Ctx.Log])
).map(t => (scalaNativeWorkerClasspath().toSeq ++ t.toSeq).map(_.path))
}
override def scalacPluginIvyDeps = super.scalacPluginIvyDeps() ++
Agg(ivy"org.scala-native:nscplugin_${scalaVersion()}:${scalaNativeVersion()}")
def logLevel: Target[NativeLogLevel] = T{ NativeLogLevel.Info }
def releaseMode: Target[ReleaseMode] = T { ReleaseMode.Debug }
def nativeWorkdir = T{ T.ctx().dest }
// Location of the clang compiler
def nativeClang = T{ scalaNativeWorker().discoverClang }
// Location of the clang++ compiler
def nativeClangPP = T{ scalaNativeWorker().discoverClangPP }
// GC choice, either "none", "boehm" or "immix"
def nativeGC = T{
Option(System.getenv.get("SCALANATIVE_GC"))
.getOrElse(scalaNativeWorker().defaultGarbageCollector)
}
def nativeTarget = T{ scalaNativeWorker().discoverTarget(nativeClang(), nativeWorkdir()) }
// Options that are passed to clang during compilation
def nativeCompileOptions = T{ scalaNativeWorker().discoverCompileOptions }
// Options that are passed to clang during linking
def nativeLinkingOptions = T{ scalaNativeWorker().discoverLinkingOptions }
// Whether to link `@stub` methods, or ignore them
def nativeLinkStubs = T { false }
def nativeLibJar = T{
resolveDeps(T.task{Agg(nativeLibIvy())})()
.filter{p => p.toString.contains("scala-native") && p.toString.contains("nativelib")}
.toList
.head
}
def nativeConfig = T.task {
val classpath = runClasspath().map(_.path).filter(_.toIO.exists).toList
scalaNativeWorker().config(
nativeLibJar().path,
finalMainClass(),
classpath,
nativeWorkdir(),
nativeClang(),
nativeClangPP(),
nativeTarget(),
nativeCompileOptions(),
nativeLinkingOptions(),
nativeGC(),
nativeLinkStubs(),
releaseMode(),
logLevel())
}
// Generates native binary
def nativeLink = T{ scalaNativeWorker().nativeLink(nativeConfig(), (T.ctx().dest / 'out)) }
// Runs the native binary
override def run(args: String*) = T.command{
Jvm.baseInteractiveSubprocess(
Vector(nativeLink().toString) ++ args,
forkEnv(),
workingDir = ammonite.ops.pwd)
}
}
trait TestScalaNativeModule extends ScalaNativeModule with TestModule { testOuter =>
case class TestDefinition(framework: String, clazz: Class[_], fingerprint: Fingerprint) {
def name = clazz.getName.reverse.dropWhile(_ == '$').reverse
}
override def testLocal(args: String*) = T.command { test(args:_*) }
override def test(args: String*) = T.command{
val outputPath = T.ctx().dest / "out.json"
// The test frameworks run under the JVM and communicate with the native binary over a socket
// therefore the test framework is loaded from a JVM classloader
val testClassloader =
new URLClassLoader(testClasspathJvm().map(_.path.toIO.toURI.toURL).toArray,
this.getClass.getClassLoader)
val frameworkInstances = TestRunner.frameworks(testFrameworks())(testClassloader)
val testBinary = testRunnerNative.nativeLink().toIO
val envVars = forkEnv()
val nativeFrameworks = (cl: ClassLoader) =>
frameworkInstances.zipWithIndex.map { case (f, id) =>
scalaNativeWorker().newScalaNativeFrameWork(f, id, testBinary, logLevel(), envVars)
}
val (doneMsg, results) = TestRunner.runTests(
nativeFrameworks,
testClasspathJvm().map(_.path),
Agg(compile().classes.path),
args
)
TestModule.handleResults(doneMsg, results)
}
private val supportedTestFrameworks = Set("utest", "scalatest")
// get the JVM classpath entries for supported test frameworks
def testFrameworksJvmClasspath = T{
Lib.resolveDependencies(
repositories,
Lib.depToDependency(_, scalaVersion(), ""),
transitiveIvyDeps().filter(d => d.cross.isBinary && supportedTestFrameworks(d.dep.module.name)),
ctx = Some(implicitly[mill.util.Ctx.Log])
)
}
def testClasspathJvm = T{
localClasspath() ++
transitiveLocalClasspath() ++
unmanagedClasspath() ++
testFrameworksJvmClasspath()
}
// creates a specific binary used for running tests - has a different (generated) main class
// which knows the names of all the tests and references to invoke them
object testRunnerNative extends ScalaNativeModule {
override def zincWorker = testOuter.zincWorker
override def scalaOrganization = testOuter.scalaOrganization()
override def scalaVersion = testOuter.scalaVersion()
override def scalaNativeVersion = testOuter.scalaNativeVersion()
override def moduleDeps = Seq(testOuter)
override def releaseMode = testOuter.releaseMode()
override def logLevel = testOuter.logLevel()
override def nativeLinkStubs = true
override def ivyDeps = testOuter.ivyDeps() ++ Agg(
ivy"org.scala-native::test-interface_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}"
)
override def mainClass = Some("scala.scalanative.testinterface.TestMain")
override def generatedSources = T {
val outDir = T.ctx().dest
ammonite.ops.write.over(outDir / "TestMain.scala", makeTestMain())
Seq(PathRef(outDir))
}
}
// generate a main class for the tests
def makeTestMain = T{
val frameworkInstances = TestRunner.frameworks(testFrameworks()) _
val testClasses =
Jvm.inprocess(testClasspathJvm().map(_.path), classLoaderOverrideSbtTesting = true, isolated = true, closeContextClassLoaderWhenDone = true,
cl => {
frameworkInstances(cl).flatMap { framework =>
val df = Lib.discoverTests(cl, framework, Agg(compile().classes.path))
df.map(d => TestDefinition(framework.getClass.getName, d._1, d._2))
}
}
)
val frameworks = testClasses.map(_.framework).distinct
val frameworksList =
if (frameworks.nonEmpty) frameworks.mkString("List(new _root_.", ", new _root_.", ")")
else {
throw new Exception(
"Cannot find any tests; make sure you defined the test framework correctly, " +
"and extend whatever trait or annotation necessary to mark your test suites"
)
}
val testsMap = makeTestsMap(testClasses)
s"""package scala.scalanative.testinterface
|object TestMain extends TestMainBase {
| override val frameworks = $frameworksList
| override val tests = Map[String, AnyRef]($testsMap)
| def main(args: Array[String]): Unit =
| testMain(args)
|}""".stripMargin
}
private def makeTestsMap(tests: Seq[TestDefinition]): String = {
tests
.map { t =>
val isModule = t.fingerprint match {
case af: AnnotatedFingerprint => af.isModule
case sf: SubclassFingerprint => sf.isModule
}
val inst =
if (isModule) s"_root_.${t.name}" else s"new _root_.${t.name}"
s""""${t.name}" -> $inst"""
}
.mkString(", ")
}
}
trait SbtNativeModule extends ScalaNativeModule with SbtModule