From 5d7a3ea8cdd91f1aee5cbbbbd081295bc780ee80 Mon Sep 17 00:00:00 2001
From: Lorenzo Gabriele <lorenzolespaul@gmail.com>
Date: Sat, 2 Mar 2024 11:57:19 +0100
Subject: [PATCH] Support Scala Native 0.5.0-RC1 (#3054)

Scala Native `0.5.0-RC1` was just released:
https://github.com/scala-native/scala-native/releases/tag/v0.5.0-RC1

Pull Request: https://github.com/com-lihaoyi/mill/pull/3054
---
 build.sc                                      |  23 ++-
 .../src/mill/contrib/bloop/BloopTests.scala   |   2 +-
 .../scalanativelib/ScalaNativeModule.scala    |  12 +-
 .../src/utest/tests/ArgsParserTests.scala     |   4 +-
 .../test/src/utest/tests/MainTests.scala      |   6 +-
 .../HelloNativeWorldTests.scala               |  33 +++--
 .../scalanativelib/ScalaTestsErrorTests.scala |   2 +-
 .../scalanative/build/MillUtils.scala}        |   0
 .../worker/ScalaNativeWorkerImpl.scala        | 135 ++++++++++++++++++
 9 files changed, 192 insertions(+), 25 deletions(-)
 rename scalanativelib/worker/0.4/src/{mill/scalanativelib/worker/scala/scalanative/Utils.scala => scala/scalanative/build/MillUtils.scala} (100%)
 create mode 100644 scalanativelib/worker/0.5/src/mill/scalanativelib/worker/ScalaNativeWorkerImpl.scala

diff --git a/build.sc b/build.sc
index 3b65aedf441..2e0c46af17b 100644
--- a/build.sc
+++ b/build.sc
@@ -86,6 +86,14 @@ object Deps {
     val scalanativeTestRunner = ivy"org.scala-native::test-runner:${scalanativeVersion}"
   }
 
+  object Scalanative_0_5 {
+    val scalanativeVersion = "0.5.0-RC1"
+    val scalanativeTools = ivy"org.scala-native::tools:${scalanativeVersion}"
+    val scalanativeUtil = ivy"org.scala-native::util:${scalanativeVersion}"
+    val scalanativeNir = ivy"org.scala-native::nir:${scalanativeVersion}"
+    val scalanativeTestRunner = ivy"org.scala-native::test-runner:${scalanativeVersion}"
+  }
+
   trait Play {
     def playVersion: String
     def playBinVersion: String = playVersion.split("[.]").take(2).mkString(".")
@@ -395,7 +403,8 @@ trait MillBaseTestsModule extends MillJavaModule with TestModule {
       s"-DTEST_SCALA_3_2_VERSION=${Deps.testScala32Version}",
       s"-DTEST_SCALA_3_3_VERSION=${Deps.testScala33Version}",
       s"-DTEST_SCALAJS_VERSION=${Deps.Scalajs_1.scalaJsVersion}",
-      s"-DTEST_SCALANATIVE_VERSION=${Deps.Scalanative_0_4.scalanativeVersion}",
+      s"-DTEST_SCALANATIVE_0_4_VERSION=${Deps.Scalanative_0_4.scalanativeVersion}",
+      s"-DTEST_SCALANATIVE_0_5_VERSION=${Deps.Scalanative_0_5.scalanativeVersion}",
       s"-DTEST_UTEST_VERSION=${Deps.utest.dep.version}",
       s"-DTEST_SCALATEST_VERSION=${Deps.TestDeps.scalaTest.dep.version}",
       s"-DTEST_TEST_INTERFACE_VERSION=${Deps.sbtTestInterface.dep.version}",
@@ -948,13 +957,13 @@ object contrib extends Module {
 
 object scalanativelib extends MillStableScalaModule {
   def moduleDeps = Seq(scalalib, scalanativelib.`worker-api`)
-  def testTransitiveDeps = super.testTransitiveDeps() ++ Seq(worker("0.4").testDep())
+  def testTransitiveDeps = super.testTransitiveDeps() ++ Seq(worker("0.4").testDep(), worker("0.5").testDep())
 
   object `worker-api` extends MillPublishScalaModule {
     def ivyDeps = Agg(Deps.sbtTestInterface)
   }
 
-  object worker extends Cross[WorkerModule]("0.4")
+  object worker extends Cross[WorkerModule]("0.4", "0.5")
 
   trait WorkerModule extends MillPublishScalaModule with Cross.Module[String] {
     def scalaNativeWorkerVersion = crossValue
@@ -962,6 +971,14 @@ object scalanativelib extends MillStableScalaModule {
     def testDepPaths = T { Seq(compile().classes) }
     def moduleDeps = Seq(scalanativelib.`worker-api`)
     def ivyDeps = scalaNativeWorkerVersion match {
+      case "0.5" =>
+        Agg(
+          Deps.osLib,
+          Deps.Scalanative_0_5.scalanativeTools,
+          Deps.Scalanative_0_5.scalanativeUtil,
+          Deps.Scalanative_0_5.scalanativeNir,
+          Deps.Scalanative_0_5.scalanativeTestRunner
+        )
       case "0.4" =>
         Agg(
           Deps.osLib,
diff --git a/contrib/bloop/test/src/mill/contrib/bloop/BloopTests.scala b/contrib/bloop/test/src/mill/contrib/bloop/BloopTests.scala
index 6a00aa86bcf..e5eb1428cc0 100644
--- a/contrib/bloop/test/src/mill/contrib/bloop/BloopTests.scala
+++ b/contrib/bloop/test/src/mill/contrib/bloop/BloopTests.scala
@@ -63,7 +63,7 @@ object BloopTests extends TestSuite {
       val sv = sys.props.getOrElse("TEST_SCALA_2_13_VERSION", ???)
       override def skipBloop: Boolean = isWin
       override def scalaVersion = sv
-      override def scalaNativeVersion = sys.props.getOrElse("TEST_SCALANATIVE_VERSION", ???)
+      override def scalaNativeVersion = sys.props.getOrElse("TEST_SCALANATIVE_0_4_VERSION", ???)
       override def releaseMode = T(ReleaseMode.Debug)
     }
 
diff --git a/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala b/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala
index faaa6a2fd67..02fa7c6da26 100644
--- a/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala
+++ b/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala
@@ -65,11 +65,15 @@ trait ScalaNativeModule extends ScalaModule { outer =>
   }
 
   def nativeIvyDeps: T[Agg[Dep]] = T {
-    val scalaVersionSpecific =
+    val scalaVersionSpecific = {
+      val version =
+        if (scalaNativeVersion().startsWith("0.4")) scalaNativeVersion()
+        else s"${scalaVersion()}+${scalaNativeVersion()}"
+
       if (ZincWorkerUtil.isScala3(scalaVersion()))
-        Agg(ivy"org.scala-native::scala3lib::${scalaNativeVersion()}")
-      else
-        Agg(ivy"org.scala-native::scalalib::${scalaNativeVersion()}")
+        Agg(ivy"org.scala-native::scala3lib::$version")
+      else Agg(ivy"org.scala-native::scalalib::$version")
+    }
 
     Agg(
       ivy"org.scala-native::nativelib::${scalaNativeVersion()}",
diff --git a/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/ArgsParserTests.scala b/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/ArgsParserTests.scala
index 7929f94783f..f1f43a188bf 100644
--- a/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/ArgsParserTests.scala
+++ b/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/ArgsParserTests.scala
@@ -6,14 +6,14 @@ import utest._
 object ArgsParserTests extends TestSuite {
 
   def tests: Tests = Tests {
-    'one - {
+    "one" - {
       val result = ArgsParser.parse("hello:world")
       assert(
         result.length == 2,
         result == Seq("hello", "world")
       )
     }
-    'two - { // we fail this test to check testing in scala.js
+    "two" - { // we fail this test to check testing in Scala Native
       val result = ArgsParser.parse("hello:world")
       assert(
         result.length == 80
diff --git a/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/MainTests.scala b/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/MainTests.scala
index d0498f314e0..eb1b1b19415 100644
--- a/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/MainTests.scala
+++ b/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/MainTests.scala
@@ -6,13 +6,13 @@ import utest._
 object MainTests extends TestSuite {
 
   val tests: Tests = Tests {
-    'vmName - {
-      'containNative - {
+    "vmName" - {
+      "containNative" - {
         assert(
           Main.vmName.contains("Native")
         )
       }
-      'containScala - {
+      "containScala" - {
         assert(
           Main.vmName.contains("Scala")
         )
diff --git a/scalanativelib/test/src/mill/scalanativelib/HelloNativeWorldTests.scala b/scalanativelib/test/src/mill/scalanativelib/HelloNativeWorldTests.scala
index 618320d60af..74ce743a57e 100644
--- a/scalanativelib/test/src/mill/scalanativelib/HelloNativeWorldTests.scala
+++ b/scalanativelib/test/src/mill/scalanativelib/HelloNativeWorldTests.scala
@@ -30,18 +30,25 @@ object HelloNativeWorldTests extends TestSuite {
     override def mainClass = Some("hello.Main")
   }
 
-  val scala213 = "2.13.6"
-  val scalaNative04 = "0.4.2"
+  val scala213 = sys.props.getOrElse("TEST_SCALA_2_13_VERSION", ???)
+  val scala31 = sys.props.getOrElse("TEST_SCALA_3_1_VERSION", ???)
+  val scala33 = sys.props.getOrElse("TEST_SCALA_3_3_VERSION", ???)
+  val scalaNative04Old = "0.4.2"
+  val scalaNative04 = sys.props.getOrElse("TEST_SCALANATIVE_0_4_VERSION", ???)
+  val scalaNative05 = sys.props.getOrElse("TEST_SCALANATIVE_0_5_VERSION", ???)
+  val utestVersion = sys.props.getOrElse("TEST_UTEST_VERSION", ???)
 
   object HelloNativeWorld extends TestUtil.BaseModule {
     implicit object ReleaseModeToSegments
         extends Cross.ToSegments[ReleaseMode](v => List(v.toString))
 
     val matrix = for {
-      scala <- Seq("3.2.1", "3.1.3", scala213, "2.12.13", "2.11.12")
-      scalaNative <- Seq(scalaNative04, "0.4.9")
+      scala <- Seq(scala33, scala31, scala213, "2.12.13", "2.11.12")
+      scalaNative <- Seq(scalaNative04Old, scalaNative04, scalaNative05)
       mode <- List(ReleaseMode.Debug, ReleaseMode.ReleaseFast)
-      if !(ZincWorkerUtil.isScala3(scala) && scalaNative == scalaNative04)
+      if !(ZincWorkerUtil.isScala3(scala) && scalaNative == scalaNative04Old)
+      if !(scala.startsWith("2.11") && scalaNative != scalaNative04Old)
+      if !(scala.startsWith("2.12") && scalaNative == scalaNative05)
     } yield (scala, scalaNative, mode)
 
     object helloNativeWorld extends Cross[RootModule](matrix)
@@ -64,7 +71,7 @@ object HelloNativeWorldTests extends TestSuite {
       object test extends ScalaNativeTests with TestModule.Utest {
         override def sources = T.sources { millSourcePath / "src" / "utest" }
         override def ivyDeps = super.ivyDeps() ++ Agg(
-          ivy"com.lihaoyi::utest::0.7.6"
+          ivy"com.lihaoyi::utest::$utestVersion"
         )
       }
     }
@@ -129,7 +136,7 @@ object HelloNativeWorldTests extends TestSuite {
         val Right((result, evalCount)) =
           helloWorldEvaluator(HelloNativeWorld.helloNativeWorld(
             scala213,
-            scalaNative04,
+            scalaNative04Old,
             ReleaseMode.Debug
           ).jar)
         val jar = result.path
@@ -155,7 +162,7 @@ object HelloNativeWorldTests extends TestSuite {
       }
       "artifactId_040" - testArtifactId(
         scala213,
-        scalaNative04,
+        scalaNative04Old,
         ReleaseMode.Debug,
         "hello-native-world_native0.4_2.13"
       )
@@ -202,14 +209,18 @@ object HelloNativeWorldTests extends TestSuite {
 
       testAllMatrix(
         (scala, scalaNative, releaseMode) => checkUtest(scala, scalaNative, releaseMode, cached),
-        skipScala = ZincWorkerUtil.isScala3 // Remove this once utest is released for Scala 3
+        skipScalaNative = v =>
+          v == scalaNative04Old ||
+            v.startsWith("0.5.") // Remove this once utest is released for Scala Native 0.5
       )
     }
     "testCached" - {
       val cached = true
       testAllMatrix(
         (scala, scalaNative, releaseMode) => checkUtest(scala, scalaNative, releaseMode, cached),
-        skipScala = ZincWorkerUtil.isScala3 // Remove this once utest is released for Scala 3
+        skipScalaNative = v =>
+          v == scalaNative04Old ||
+            v.startsWith("0.5.") // Remove this once utest is released for Scala Native 0.5
       )
     }
 
@@ -262,7 +273,7 @@ object HelloNativeWorldTests extends TestSuite {
       )
 
     val scalaNativeVersionSpecific =
-      if (scalaNativeVersion == scalaNative04) Set.empty
+      if (scalaNativeVersion == scalaNative04Old) Set.empty
       else Set("Main.nir", "ArgsParser.nir")
 
     common ++ scalaVersionSpecific ++ scalaNativeVersionSpecific
diff --git a/scalanativelib/test/src/mill/scalanativelib/ScalaTestsErrorTests.scala b/scalanativelib/test/src/mill/scalanativelib/ScalaTestsErrorTests.scala
index fcc13540fcd..37336c2d7da 100644
--- a/scalanativelib/test/src/mill/scalanativelib/ScalaTestsErrorTests.scala
+++ b/scalanativelib/test/src/mill/scalanativelib/ScalaTestsErrorTests.scala
@@ -10,7 +10,7 @@ object ScalaTestsErrorTests extends TestSuite {
   object ScalaTestsError extends TestUtil.BaseModule {
     object scalaTestsError extends ScalaNativeModule {
       def scalaVersion = sys.props.getOrElse("TEST_SCALA_3_3_VERSION", ???)
-      def scalaNativeVersion = sys.props.getOrElse("TEST_SCALANATIVE_VERSION", ???)
+      def scalaNativeVersion = sys.props.getOrElse("TEST_SCALANATIVE_0_4_VERSION", ???)
       object test extends ScalaTests with TestModule.Utest
       object testDisabledError extends ScalaTests with TestModule.Utest {
         override def hierarchyChecks(): Unit = {}
diff --git a/scalanativelib/worker/0.4/src/mill/scalanativelib/worker/scala/scalanative/Utils.scala b/scalanativelib/worker/0.4/src/scala/scalanative/build/MillUtils.scala
similarity index 100%
rename from scalanativelib/worker/0.4/src/mill/scalanativelib/worker/scala/scalanative/Utils.scala
rename to scalanativelib/worker/0.4/src/scala/scalanative/build/MillUtils.scala
diff --git a/scalanativelib/worker/0.5/src/mill/scalanativelib/worker/ScalaNativeWorkerImpl.scala b/scalanativelib/worker/0.5/src/mill/scalanativelib/worker/ScalaNativeWorkerImpl.scala
new file mode 100644
index 00000000000..891c90bde69
--- /dev/null
+++ b/scalanativelib/worker/0.5/src/mill/scalanativelib/worker/ScalaNativeWorkerImpl.scala
@@ -0,0 +1,135 @@
+package mill.scalanativelib.worker
+
+import java.io.File
+import java.lang.System.{err, out}
+
+import mill.scalanativelib.worker.api._
+import scala.scalanative.util.Scope
+import scala.scalanative.build.{
+  Build,
+  BuildTarget => ScalaNativeBuildTarget,
+  Config,
+  Discover,
+  GC,
+  Logger,
+  LTO,
+  Mode,
+  NativeConfig => ScalaNativeNativeConfig
+}
+import scala.scalanative.testinterface.adapter.TestAdapter
+
+import scala.concurrent.Await
+import scala.concurrent.duration.Duration
+import scala.concurrent.ExecutionContext.Implicits.global
+import java.nio.file.Files
+
+class ScalaNativeWorkerImpl extends mill.scalanativelib.worker.api.ScalaNativeWorkerApi {
+  implicit val scope: Scope = Scope.forever
+
+  def logger(level: NativeLogLevel): Logger =
+    Logger(
+      traceFn = msg => if (level.value >= NativeLogLevel.Trace.value) err.println(s"[trace] $msg"),
+      debugFn = msg => if (level.value >= NativeLogLevel.Debug.value) out.println(s"[debug] $msg"),
+      infoFn = msg => if (level.value >= NativeLogLevel.Info.value) out.println(s"[info] $msg"),
+      warnFn = msg => if (level.value >= NativeLogLevel.Warn.value) out.println(s"[warn] $msg"),
+      errorFn = msg => if (level.value >= NativeLogLevel.Error.value) err.println(s"[error] $msg")
+    )
+
+  def discoverClang(): File = Discover.clang().toFile
+  def discoverClangPP(): File = Discover.clangpp().toFile
+  def discoverCompileOptions(): Seq[String] = Discover.compileOptions()
+  def discoverLinkingOptions(): Seq[String] = Discover.linkingOptions()
+  def defaultGarbageCollector(): String = GC.default.name
+
+  def config(
+      mainClass: Either[String, String],
+      classpath: Seq[File],
+      nativeWorkdir: File,
+      nativeClang: File,
+      nativeClangPP: File,
+      nativeTarget: Option[String],
+      nativeCompileOptions: Seq[String],
+      nativeLinkingOptions: Seq[String],
+      nativeGC: String,
+      nativeLinkStubs: Boolean,
+      nativeLTO: String,
+      releaseMode: String,
+      nativeOptimize: Boolean,
+      nativeEmbedResources: Boolean,
+      nativeIncrementalCompilation: Boolean,
+      nativeDump: Boolean,
+      logLevel: NativeLogLevel,
+      buildTarget: BuildTarget
+  ): Either[String, Config] = {
+    val nativeConfig =
+      ScalaNativeNativeConfig.empty
+        .withClang(nativeClang.toPath)
+        .withClangPP(nativeClangPP.toPath)
+        .withTargetTriple(nativeTarget)
+        .withCompileOptions(nativeCompileOptions)
+        .withLinkingOptions(nativeLinkingOptions)
+        .withGC(GC(nativeGC))
+        .withLinkStubs(nativeLinkStubs)
+        .withMode(Mode(releaseMode))
+        .withOptimize(nativeOptimize)
+        .withLTO(LTO(nativeLTO))
+        .withDump(nativeDump)
+        .withBuildTarget(buildTarget match {
+          case BuildTarget.Application => ScalaNativeBuildTarget.application
+          case BuildTarget.LibraryDynamic => ScalaNativeBuildTarget.libraryDynamic
+          case BuildTarget.LibraryStatic => ScalaNativeBuildTarget.libraryStatic
+        })
+        .withEmbedResources(nativeEmbedResources)
+        .withIncrementalCompilation(nativeIncrementalCompilation)
+        .withBaseName("out")
+
+    val config = Config.empty
+      .withClassPath(classpath.map(_.toPath))
+      .withBaseDir(nativeWorkdir.toPath)
+      .withCompilerConfig(nativeConfig)
+      .withLogger(logger(logLevel))
+
+    if (buildTarget == BuildTarget.Application) {
+      mainClass match {
+        case Left(error) =>
+          Left(error)
+        case Right(mainClass) =>
+          Right(config.withMainClass(Some(mainClass)))
+      }
+    } else Right(config)
+  }
+
+  def nativeLink(nativeConfig: Object, outDirectory: File): File = {
+    val config = nativeConfig.asInstanceOf[Config]
+
+    val result = Await.result(Build.buildCached(config), Duration.Inf)
+
+    val resultInOutDirectory =
+      Files.move(result, outDirectory.toPath().resolve(result.getFileName()))
+
+    resultInOutDirectory.toFile()
+  }
+
+  def getFramework(
+      testBinary: File,
+      envVars: Map[String, String],
+      logLevel: NativeLogLevel,
+      frameworkName: String
+  ): (() => Unit, sbt.testing.Framework) = {
+    val config = TestAdapter.Config()
+      .withBinaryFile(testBinary)
+      .withEnvVars(envVars)
+      .withLogger(logger(logLevel))
+
+    val adapter = new TestAdapter(config)
+
+    (
+      () => adapter.close(),
+      adapter
+        .loadFrameworks(List(List(frameworkName)))
+        .flatten
+        .headOption
+        .getOrElse(throw new RuntimeException("Failed to get framework"))
+    )
+  }
+}