diff --git a/.gitmodules b/.gitmodules index 30d8a879b6..76930b73ab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/coursier/test-metadata.git [submodule "modules/directories"] path = modules/directories - url = https://github.com/dirs-dev/directories-jvm.git + url = https://github.com/coursier/directories-jvm.git [submodule "modules/tests/handmade-metadata"] path = modules/tests/handmade-metadata url = https://github.com/coursier/handmade-metadata.git diff --git a/build.sbt b/build.sbt index 6484347585..013839d91f 100644 --- a/build.sbt +++ b/build.sbt @@ -130,7 +130,10 @@ lazy val paths = project("paths") .settings( pureJava, dontPublish, - addDirectoriesSources + addDirectoriesSources, + libraryDependencies ++= Seq( + Deps.jniUtils + ) ) lazy val cache = crossProject("cache")(JSPlatform, JVMPlatform) @@ -143,6 +146,7 @@ lazy val cache = crossProject("cache")(JSPlatform, JVMPlatform) addPathsSources, Mima.previousArtifacts, libraryDependencies ++= Seq( + Deps.jniUtils, Deps.svm % Provided, Deps.windowsAnsi ), @@ -230,7 +234,8 @@ lazy val `bootstrap-launcher` = project("bootstrap-launcher") utest, libraryDependencies ++= Seq( Deps.collectionCompat % Test, - Deps.java8Compat % Test + Deps.java8Compat % Test, + Deps.jniUtils ), addPathsSources, addWindowsAnsiPsSources, @@ -244,6 +249,9 @@ lazy val `resources-bootstrap-launcher` = project("resources-bootstrap-launcher" .settings( pureJava, dontPublish, + libraryDependencies ++= Seq( + Deps.jniUtils + ), unmanagedSourceDirectories.in(Compile) ++= unmanagedSourceDirectories.in(`bootstrap-launcher`, Compile).value, mainClass.in(Compile) := Some("coursier.bootstrap.launcher.ResourcesLauncher"), proguardedBootstrap("coursier.bootstrap.launcher.ResourcesLauncher", resourceBased = true) @@ -324,7 +332,8 @@ lazy val env = project("env") libs ++= Seq( Deps.collectionCompat, Deps.dataClass % Provided, - Deps.jimfs % Test + Deps.jimfs % Test, + Deps.jniUtils ), utest ) diff --git a/modules/bootstrap-launcher/src/main/java/coursier/bootstrap/launcher/Bootstrap.java b/modules/bootstrap-launcher/src/main/java/coursier/bootstrap/launcher/Bootstrap.java index e46f4fa7b0..784b371ec6 100644 --- a/modules/bootstrap-launcher/src/main/java/coursier/bootstrap/launcher/Bootstrap.java +++ b/modules/bootstrap-launcher/src/main/java/coursier/bootstrap/launcher/Bootstrap.java @@ -16,17 +16,29 @@ private static void exit(String message) { } private static void maybeInitWindowsAnsi() throws InterruptedException, IOException { - if (!System.getProperty("coursier.bootstrap.windows-ansi", "").equalsIgnoreCase("false")) { - try { - // noop on Linux / macOS + + boolean isWindows = System.getProperty("os.name") + .toLowerCase(java.util.Locale.ROOT) + .contains("windows"); + + if (!isWindows) + return; + + if (System.getProperty("coursier.bootstrap.windows-ansi", "").equalsIgnoreCase("false")) + return; + + boolean useJni = coursier.paths.Util.useJni(); + try { + if (useJni) + coursier.jniutils.WindowsAnsiTerminal.enableAnsiOutput(); + else io.github.alexarchambault.windowsansi.WindowsAnsiPs.setup(); - } catch (InterruptedException | IOException e) { - boolean doThrow = Boolean.getBoolean("coursier.bootstrap.windows-ansi.throw-exception"); - if (doThrow || Boolean.getBoolean("coursier.bootstrap.windows-ansi.verbose")) - System.err.println("Error setting up Windows terminal for ANSI escape codes: " + e); - if (doThrow) - throw e; - } + } catch (InterruptedException | IOException e) { + boolean doThrow = Boolean.getBoolean("coursier.bootstrap.windows-ansi.throw-exception"); + if (doThrow || Boolean.getBoolean("coursier.bootstrap.windows-ansi.verbose")) + System.err.println("Error setting up Windows terminal for ANSI escape codes: " + e); + if (doThrow) + throw e; } } diff --git a/modules/cache/jvm/src/main/scala/coursier/cache/internal/Terminal.scala b/modules/cache/jvm/src/main/scala/coursier/cache/internal/Terminal.scala index 8d312f7a29..83f1816f10 100644 --- a/modules/cache/jvm/src/main/scala/coursier/cache/internal/Terminal.scala +++ b/modules/cache/jvm/src/main/scala/coursier/cache/internal/Terminal.scala @@ -65,10 +65,17 @@ object Terminal { private lazy val isWindows = System.getProperty("os.name").toLowerCase(java.util.Locale.ROOT).contains("windows") private def fromJLine(): Option[(Int, Int)] = - if (isWindows) { - val size = io.github.alexarchambault.windowsansi.WindowsAnsi.terminalSize() - Some((size.getWidth, size.getHeight)) - } else + if (isWindows) + Some { + if (coursier.paths.Util.useJni()) { + val size = coursier.jniutils.WindowsAnsiTerminal.terminalSize() + (size.getWidth, size.getHeight) + } else { + val size = io.github.alexarchambault.windowsansi.WindowsAnsi.terminalSize() + (size.getWidth, size.getHeight) + } + } + else None def consoleDims(): (Int, Int) = diff --git a/modules/cli/src/main/scala/coursier/cli/Coursier.scala b/modules/cli/src/main/scala/coursier/cli/Coursier.scala index aed78a308a..a131007439 100644 --- a/modules/cli/src/main/scala/coursier/cli/Coursier.scala +++ b/modules/cli/src/main/scala/coursier/cli/Coursier.scala @@ -23,7 +23,6 @@ import coursier.cli.search.Search import coursier.core.Version import coursier.install.InstallDir import coursier.launcher.internal.{FileUtil, Windows} -import io.github.alexarchambault.windowsansi.WindowsAnsi import shapeless._ import scala.util.control.NonFatal @@ -32,9 +31,14 @@ object Coursier extends CommandAppPreA(Parser[LauncherOptions], Help[LauncherOpt val isGraalvmNativeImage = sys.props.contains("org.graalvm.nativeimage.imagecode") - if (System.console() != null && Windows.isWindows) - try WindowsAnsi.setup() - catch { + if (System.console() != null && Windows.isWindows) { + val useJni = coursier.paths.Util.useJni() + try { + if (useJni) + coursier.jniutils.WindowsAnsiTerminal.enableAnsiOutput() + else + io.github.alexarchambault.windowsansi.WindowsAnsi.setup() + } catch { case NonFatal(e) => val doThrow = java.lang.Boolean.getBoolean("coursier.windows-ansi.throw-exception") if (doThrow || java.lang.Boolean.getBoolean("coursier.windows-ansi.verbose")) @@ -42,6 +46,7 @@ object Coursier extends CommandAppPreA(Parser[LauncherOptions], Help[LauncherOpt if (doThrow) throw e } + } CacheUrl.setupProxyAuth() diff --git a/modules/cli/src/main/scala/coursier/cli/params/EnvParams.scala b/modules/cli/src/main/scala/coursier/cli/params/EnvParams.scala index 8893349c7a..d36721728c 100644 --- a/modules/cli/src/main/scala/coursier/cli/params/EnvParams.scala +++ b/modules/cli/src/main/scala/coursier/cli/params/EnvParams.scala @@ -18,7 +18,7 @@ final case class EnvParams( // TODO Allow to customize some parameters of WindowsEnvVarUpdater / ProfileUpdater? def envVarUpdater: Either[WindowsEnvVarUpdater, ProfileUpdater] = if (Windows.isWindows) - Left(WindowsEnvVarUpdater()) + Left(WindowsEnvVarUpdater().withUseJni(Some(coursier.paths.Util.useJni()))) else Right( ProfileUpdater() diff --git a/modules/directories b/modules/directories index 006ca7ff80..7acadf2ab9 160000 --- a/modules/directories +++ b/modules/directories @@ -1 +1 @@ -Subproject commit 006ca7ff804ca48f692d59a7fce8599f8a1eadfc +Subproject commit 7acadf2ab9a4ce306d840d652cdb77fade11b94b diff --git a/modules/env/src/main/scala/coursier/env/WindowsEnvVarUpdater.scala b/modules/env/src/main/scala/coursier/env/WindowsEnvVarUpdater.scala index 66fc6d97a3..6bafe4ee96 100644 --- a/modules/env/src/main/scala/coursier/env/WindowsEnvVarUpdater.scala +++ b/modules/env/src/main/scala/coursier/env/WindowsEnvVarUpdater.scala @@ -4,26 +4,41 @@ import dataclass.data @data class WindowsEnvVarUpdater( powershellRunner: PowershellRunner = PowershellRunner(), - target: String = "User" + target: String = "User", + useJni: Option[Boolean] = None ) extends EnvVarUpdater { + private lazy val useJni0 = useJni.getOrElse { + // FIXME Should be coursier.paths.Util.useJni(), but it's not available from here. + !System.getProperty("coursier.jni", "").equalsIgnoreCase("false") + } + // https://stackoverflow.com/questions/9546324/adding-directory-to-path-environment-variable-in-windows/29109007#29109007 // https://docs.microsoft.com/fr-fr/dotnet/api/system.environment.getenvironmentvariable?view=netframework-4.8#System_Environment_GetEnvironmentVariable_System_String_System_EnvironmentVariableTarget_ // https://docs.microsoft.com/fr-fr/dotnet/api/system.environment.setenvironmentvariable?view=netframework-4.8#System_Environment_SetEnvironmentVariable_System_String_System_String_System_EnvironmentVariableTarget_ - private def getEnvironmentVariable(name: String): Option[String] = { - val output = powershellRunner.runScript(WindowsEnvVarUpdater.getEnvVarScript(name)).stripSuffix(System.lineSeparator()) - if (output == "null") // if ever the actual value is "null", we'll miss it - None - else - Some(output) - } + private def getEnvironmentVariable(name: String): Option[String] = + if (useJni0) + Option(coursier.jniutils.WindowsEnvironmentVariables.get(name)) + else { + val output = powershellRunner.runScript(WindowsEnvVarUpdater.getEnvVarScript(name)).stripSuffix(System.lineSeparator()) + if (output == "null") // if ever the actual value is "null", we'll miss it + None + else + Some(output) + } private def setEnvironmentVariable(name: String, value: String): Unit = - powershellRunner.runScript(WindowsEnvVarUpdater.setEnvVarScript(name, value)) + if (useJni0) + coursier.jniutils.WindowsEnvironmentVariables.set(name, value) + else + powershellRunner.runScript(WindowsEnvVarUpdater.setEnvVarScript(name, value)) private def clearEnvironmentVariable(name: String): Unit = - powershellRunner.runScript(WindowsEnvVarUpdater.clearEnvVarScript(name)) + if (useJni0) + coursier.jniutils.WindowsEnvironmentVariables.delete(name) + else + powershellRunner.runScript(WindowsEnvVarUpdater.clearEnvVarScript(name)) def applyUpdate(update: EnvironmentUpdate): Boolean = { diff --git a/modules/paths/src/main/java/coursier/paths/CoursierPaths.java b/modules/paths/src/main/java/coursier/paths/CoursierPaths.java index ec1d95b456..b85d04da39 100644 --- a/modules/paths/src/main/java/coursier/paths/CoursierPaths.java +++ b/modules/paths/src/main/java/coursier/paths/CoursierPaths.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.file.Files; +import dev.dirs.GetWinDirs; import dev.dirs.ProjectDirectories; /** @@ -87,7 +88,18 @@ private static ProjectDirectories coursierDirectories() throws IOException { if (coursierDirectories0 == null) synchronized (coursierDirectoriesLock) { if (coursierDirectories0 == null) { - coursierDirectories0 = ProjectDirectories.from(null, null, "Coursier"); + GetWinDirs getWinDirs; + if (coursier.paths.Util.useJni()) + getWinDirs = guids -> { + String[] dirs = new String[guids.length]; + for (int i = 0; i < guids.length; i++) { + dirs[i] = coursier.jniutils.WindowsKnownFolders.knownFolderPath("{" + guids[i] + "}"); + } + return dirs; + }; + else + getWinDirs = GetWinDirs.powerShellBased; + coursierDirectories0 = ProjectDirectories.from(null, null, "Coursier", getWinDirs); } } diff --git a/modules/paths/src/main/java/coursier/paths/Util.java b/modules/paths/src/main/java/coursier/paths/Util.java index 157d835891..9b80cd977d 100644 --- a/modules/paths/src/main/java/coursier/paths/Util.java +++ b/modules/paths/src/main/java/coursier/paths/Util.java @@ -5,6 +5,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.regex.Matcher; @@ -150,4 +151,47 @@ public static boolean useAnsiOutput() { } return useAnsiOutput0; } + + private static Boolean useJni0 = null; + public static boolean useJni() { + if (useJni0 != null) + return useJni0; + + boolean isWindows = System.getProperty("os.name") + .toLowerCase(Locale.ROOT) + .contains("windows"); + if (!isWindows) { + useJni0 = false; + return useJni0; + } + + String prop = System.getenv("COURSIER_JNI"); + if (prop == null || prop.isEmpty()) + prop = System.getProperty("coursier.jni", ""); + + boolean force = prop.equalsIgnoreCase("force"); + if (force) { + useJni0 = true; + return useJni0; + } + + boolean disabled = prop.equalsIgnoreCase("false"); + if (disabled) { + useJni0 = false; + return useJni0; + } + + // Try to get a dummy user env var from registry. If it fails, assume the JNI stuff is broken, + // and fallback on PowerShell scripts. + try { + coursier.jniutils.WindowsEnvironmentVariables.get("PATH"); + useJni0 = true; + } catch (Throwable t) { + if (System.getProperty("coursier.jni.check.throw", "").equalsIgnoreCase("true")) + throw new RuntimeException(t); + useJni0 = false; + } + + return useJni0; + } } diff --git a/project/Deps.scala b/project/Deps.scala index ca7a6ac9d4..221a9c8e9c 100644 --- a/project/Deps.scala +++ b/project/Deps.scala @@ -28,6 +28,7 @@ object Deps { def http4sDsl = "org.http4s" %% "http4s-dsl" % versions.http4s def java8Compat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.9.1" def jimfs = "com.google.jimfs" % "jimfs" % "1.1" + def jniUtils = "io.get-coursier.jniutils" % "windows-jni-utils" % "0.2.0" def jol = "org.openjdk.jol" % "jol-core" % "0.14" def jsoniterCore = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % versions.jsoniterScala def jsoniterMacros = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % versions.jsoniterScala diff --git a/project/Settings.scala b/project/Settings.scala index 770a976d09..94b1659727 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -1,5 +1,5 @@ -import java.io.ByteArrayOutputStream +import java.io.{ByteArrayOutputStream, File} import java.nio.charset.StandardCharsets import java.nio.file.Files import java.util.{Arrays, Locale} @@ -547,6 +547,12 @@ object Settings { proguardBinaryDeps.in(Proguard) ++= rtJarOpt.toSeq, // seems needed with sbt 1.4.0 proguardedJar := proguardedJarTask.value, proguardVersion.in(Proguard) := Deps.proguardVersion, + proguardOptions.in(Proguard) := { + val current = proguardOptions.in(Proguard).value + val idx = current.indexWhere(_.contains(File.separator + "windows-jni-utils")) + assert(idx >= 0, s"options: $current") + current.take(idx) ++ Seq(current(idx).replace("-libraryjars", "-injars")) ++ current.drop(idx + 1) + }, proguardOptions.in(Proguard) ++= Seq( "-dontnote", "-dontwarn",