diff --git a/.github/scripts/gpg-setup.sh b/.github/scripts/gpg-setup.sh new file mode 100755 index 0000000..5146775 --- /dev/null +++ b/.github/scripts/gpg-setup.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -eu + +# from https://github.com/coursier/apps/blob/f1d2bf568bf466a98569a85c3f23c5f3a8eb5360/.github/scripts/gpg-setup.sh + +echo "$PGP_SECRET" | base64 --decode | gpg --import --no-tty --batch --yes + +echo "allow-loopback-pinentry" >>~/.gnupg/gpg-agent.conf +echo "pinentry-mode loopback" >>~/.gnupg/gpg.conf + +gpg-connect-agent reloadagent /bye diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f409a05..96e04ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,8 @@ on: push: branches: - master + tags: + - "v*" pull_request: jobs: @@ -12,34 +14,91 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - jdk: [graalvm-ce-java11@20.3.0] - name: Test ${{ matrix.os }} -- ${{ matrix.jdk }} + name: Test ${{ matrix.os }} steps: - uses: actions/checkout@v2 with: submodules: true - - uses: coursier/cache-action@v3 - - uses: olafurpg/setup-scala@v10 + fetch-depth: 0 + - uses: coursier/cache-action@v6.3 + - uses: coursier/setup-action@v1.2.0-M3 with: - java-version: ${{ matrix.jdk }} - - uses: actions/setup-node@v1 + jvm: 8 + - name: Compile and test + run: ./mill __.test + - name: Compile to native + run: ./mill show nativeImage + - name: Copy JVM launcher + run: ./mill -i ci.copyJvmLauncher artifacts/ + if: runner.os == 'Linux' + - name: Copy launcher + run: ./mill -i ci.copyLauncher artifacts/ + - uses: actions/upload-artifact@v3 + with: + name: launchers + path: artifacts/ + if-no-files-found: error + retention-days: 2 + + format: + runs-on: ubuntu-latest + name: Format + steps: + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + - uses: coursier/cache-action@v6.3 + - uses: coursier/setup-action@v1.2.0-M3 with: - node-version: "10.x" - - name: Set up environment - run: | - curl -Lo coursier https://git.io/coursier-cli && chmod +x coursier && ./coursier --help - yarn --help - java -version - shell: bash + jvm: 8 + apps: scalafmt - name: Check formatting - if: matrix.os != 'windows-latest' - run: | - ./bin/scalafmt --test - - name: Compile and test jsonrpc4s - run: | - sbt "+test" - - name: Compile to native - if: matrix.os != 'windows-latest' - run: | - gu install native-image - sbt "snailgun-cli/graalvm-native-image:packageBin" + run: scalafmt --test + + publish: + needs: [format, test] + runs-on: ubuntu-latest + name: Publish + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + - uses: coursier/cache-action@v6.3 + - uses: coursier/setup-action@v1.2.0-M3 + with: + jvm: 8 + - name: GPG setup + run: .github/scripts/gpg-setup.sh + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + - name: Publish + run: ./mill ci.publishSonatype __.publishArtifacts + env: + PGP_PASSWORD: ${{ secrets.PGP_PASSPHRASE }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + + launchers: + needs: [format, test] + name: Upload launchers + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 + - uses: coursier/cache-action@v6.3 + - uses: coursier/setup-action@v1.2.0-M3 + with: + jvm: 8 + - uses: actions/download-artifact@v3 + with: + name: launchers + path: artifacts/ + - run: ./mill -i ci.uploadLaunchers artifacts/ + env: + UPLOAD_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.mill-version b/.mill-version new file mode 100644 index 0000000..42624f3 --- /dev/null +++ b/.mill-version @@ -0,0 +1 @@ +0.10.2 \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf index 04db1c2..b7e2581 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,8 +1,9 @@ maxColumn = 100 -docstrings = JavaDoc +docstrings.style = "Asterisk" assumeStandardLibraryStripMargin = true align.tokens=[] align.openParenCallSite = false align.openParenDefnSite = false binPack.literalArgumentLists = true -version = "2.7.3" +version = "3.5.2" +runner.dialect = scala213 diff --git a/build.sbt b/build.sbt deleted file mode 100644 index 486a6a2..0000000 --- a/build.sbt +++ /dev/null @@ -1,44 +0,0 @@ -import build.BuildKeys._ -import build.Dependencies - -lazy val `snailgun-core` = project - .in(file("core")) - .settings(testSuiteSettings) - .settings( - fork in run in Compile := true, - fork in test in Test := true, - libraryDependencies ++= Seq( - Dependencies.jna, - Dependencies.jnaPlatform - ) - ) - -lazy val `snailgun-cli` = project - .in(file("cli")) - .dependsOn(`snailgun-core`) - .enablePlugins(GraalVMNativeImagePlugin) - .settings(testSuiteSettings) - .settings( - fork in run in Compile := true, - fork in test in Test := true, - libraryDependencies ++= List(Dependencies.scopt), - graalVMNativeImageOptions ++= List( - "--no-server", - "--no-fallback", - // Required by GraalVM Native Image, otherwise error - "--allow-incomplete-classpath", - "--enable-all-security-services", - "-H:+PrintClassInitialization", - "-H:+ReportExceptionStackTraces", - "--report-unsupported-elements-at-runtime", - "--initialize-at-build-time=scala.Symbol,scala.Function1,scala.Function2,scala.runtime.StructuralCallSite,scala.runtime.EmptyMethodCache" - ) - ) - -lazy val snailgun = project - .in(file(".")) - .aggregate(`snailgun-core`, `snailgun-cli`) - .settings( - releaseEarly := { () }, - skip in publish := true - ) diff --git a/build.sc b/build.sc new file mode 100644 index 0000000..9ba2d13 --- /dev/null +++ b/build.sc @@ -0,0 +1,305 @@ +import mill._ +import mill.scalalib._ +import mill.scalalib.publish._ + +import $ivy.`io.get-coursier::coursier-launcher:2.1.0-M2` + +import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.1.4` +import de.tobiasroeser.mill.vcs.version._ + +import $ivy.`io.github.alexarchambault.mill::mill-native-image::0.1.19` +import io.github.alexarchambault.millnativeimage.NativeImage + +import $ivy.`io.github.alexarchambault.mill::mill-native-image-upload:0.1.19` +import io.github.alexarchambault.millnativeimage.upload.Upload + +import scala.concurrent.duration._ +import scala.util.Properties + +def scalaVersions = Seq("2.12.15", "2.13.8") +def mainScalaVersion = scalaVersions.last + +object core extends Cross[Core](scalaVersions: _*) +object cli extends Cross[Cli](scalaVersions: _*) + +class Core(val crossScalaVersion: String) extends CrossSbtModule with SnailgunPublishModule { + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"net.java.dev.jna:jna-platform:5.6.0" + ) + object test extends Tests { + def testFramework = "utest.runner.Framework" + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"com.lihaoyi::utest:0.7.2", + ivy"com.lihaoyi::pprint:0.6.0", + ivy"com.googlecode.java-diff-utils:diffutils:1.3.0", + ivy"io.monix::monix:3.3.0", + ivy"ch.epfl.scala:nailgun-server:ee3c4343" + ) + } +} + +class Cli(val crossScalaVersion: String) + extends CrossSbtModule + with NativeImage + with SnailgunPublishModule { + def moduleDeps = Seq( + core() + ) + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"com.github.scopt::scopt:4.0.0-RC2" + ) + def mainClass = Some("snailgun.Cli") + + def nativeImageClassPath = runClasspath() + def nativeImageMainClass = mainClass().getOrElse { + sys.error("No main class found") + } + def nativeImageGraalVmJvmId = "graalvm-java17:22.1.0" + + def transitiveJars: T[Agg[PathRef]] = { + + def allModuleDeps(todo: List[JavaModule]): List[JavaModule] = + todo match { + case Nil => Nil + case h :: t => + h :: allModuleDeps(h.moduleDeps.toList ::: t) + } + + T { + mill.define.Target.traverse(allModuleDeps(this :: Nil).distinct)(m => T.task(m.jar()))() + } + } + + def jarClassPath = T { + val cp = runClasspath() ++ transitiveJars() + cp.filter(ref => os.exists(ref.path) && !os.isDir(ref.path)) + } + + def standaloneLauncher = T { + + val cachePath = os.Path(coursier.cache.FileCache().location, os.pwd) + def urlOf(path: os.Path): Option[String] = + if (path.startsWith(cachePath)) { + val segments = path.relativeTo(cachePath).segments + val url = segments.head + "://" + segments.tail.mkString("/") + Some(url) + } else None + + import coursier.launcher.{ + AssemblyGenerator, + BootstrapGenerator, + ClassPathEntry, + Parameters, + Preamble + } + import scala.util.Properties.isWin + val cp = jarClassPath().map(_.path) + val mainClass0 = mainClass().getOrElse(sys.error("No main class")) + + val dest = T.ctx().dest / (if (isWin) "launcher.bat" else "launcher") + + val preamble = Preamble() + .withOsKind(isWin) + .callsItself(isWin) + val entries = cp.map { path => + urlOf(path) match { + case None => + val content = os.read.bytes(path) + val name = path.last + ClassPathEntry.Resource(name, os.mtime(path), content) + case Some(url) => ClassPathEntry.Url(url) + } + } + val loaderContent = coursier.launcher.ClassLoaderContent(entries) + val params = Parameters + .Bootstrap(Seq(loaderContent), mainClass0) + .withDeterministic(true) + .withPreamble(preamble) + + BootstrapGenerator.generate(params, dest.toNIO) + + PathRef(dest) + } +} + +def ghOrg = "jvican" +def ghName = "snailgun" +trait SnailgunPublishModule extends PublishModule { + import mill.scalalib.publish._ + def artifactName = "snailgun-" + super.artifactName() + def pomSettings = PomSettings( + description = artifactName(), + organization = "me.vican.jorge", + url = s"https://github.com/$ghOrg/$ghName", + licenses = Seq(License.`Apache-2.0`), + versionControl = VersionControl.github(ghOrg, ghName), + developers = Seq( + Developer( + "jvican", + "Jorge Vicente Cantero", + "https://github.com/jvican", + Some("jorge@vican.me") + ) + ) + ) + def publishVersion = + finalPublishVersion() +} + +private def computePublishVersion(state: VcsState, simple: Boolean): String = + if (state.commitsSinceLastTag > 0) + if (simple) { + val versionOrEmpty = state.lastTag + .filter(_ != "latest") + .filter(_ != "nightly") + .map(_.stripPrefix("v")) + .flatMap { tag => + if (simple) { + val idx = tag.lastIndexOf(".") + if (idx >= 0) + Some(tag.take(idx + 1) + (tag.drop(idx + 1).toInt + 1).toString + "-SNAPSHOT") + else + None + } else { + val idx = tag.indexOf("-") + if (idx >= 0) Some(tag.take(idx) + "+" + tag.drop(idx + 1) + "-SNAPSHOT") + else None + } + } + .getOrElse("0.0.1-SNAPSHOT") + Some(versionOrEmpty) + .filter(_.nonEmpty) + .getOrElse(state.format()) + } else { + val rawVersion = os + .proc("git", "describe", "--tags") + .call() + .out + .text() + .trim + .stripPrefix("v") + .replace("latest", "0.0.0") + .replace("nightly", "0.0.0") + val idx = rawVersion.indexOf("-") + if (idx >= 0) rawVersion.take(idx) + "-" + rawVersion.drop(idx + 1) + "-SNAPSHOT" + else rawVersion + } + else + state.lastTag + .getOrElse(state.format()) + .stripPrefix("v") + +private def finalPublishVersion = { + val isCI = System.getenv("CI") != null + if (isCI) + T.persistent { + val state = VcsVersion.vcsState() + computePublishVersion(state, simple = false) + } + else + T { + val state = VcsVersion.vcsState() + computePublishVersion(state, simple = true) + } +} + +def nativeImage = T { + cli(mainScalaVersion).nativeImage() +} + +object ci extends Module { + def publishSonatype(tasks: mill.main.Tasks[PublishModule.PublishData]) = T.command { + publishSonatype0( + data = define.Target.sequence(tasks.value)(), + log = T.ctx().log + ) + } + + private def publishSonatype0( + data: Seq[PublishModule.PublishData], + log: mill.api.Logger + ): Unit = { + + val credentials = sys.env("SONATYPE_USERNAME") + ":" + sys.env("SONATYPE_PASSWORD") + val pgpPassword = sys.env("PGP_PASSWORD") + val timeout = 10.minutes + + val artifacts = data.map { case PublishModule.PublishData(a, s) => + (s.map { case (p, f) => (p.path, f) }, a) + } + + val isRelease = { + val versions = artifacts.map(_._2.version).toSet + val set = versions.map(!_.endsWith("-SNAPSHOT")) + assert( + set.size == 1, + s"Found both snapshot and non-snapshot versions: ${versions.toVector.sorted.mkString(", ")}" + ) + set.head + } + val publisher = new scalalib.publish.SonatypePublisher( + uri = "https://s01.oss.sonatype.org/service/local", + snapshotUri = "https://s01.oss.sonatype.org/content/repositories/snapshots", + credentials = credentials, + signed = true, + // format: off + gpgArgs = Seq( + "--detach-sign", + "--batch=true", + "--yes", + "--pinentry-mode", "loopback", + "--passphrase", pgpPassword, + "--armor", + "--use-agent" + ), + // format: on + readTimeout = timeout.toMillis.toInt, + connectTimeout = timeout.toMillis.toInt, + log = log, + awaitTimeout = timeout.toMillis.toInt, + stagingRelease = isRelease + ) + + publisher.publishAll(isRelease, artifacts: _*) + } + + def copyLauncher(directory: String = "artifacts") = T.command { + val nativeLauncher = cli(mainScalaVersion).nativeImage().path + Upload.copyLauncher( + nativeLauncher, + directory, + "snailgun", + compress = true + ) + } + + def copyJvmLauncher(directory: String = "artifacts") = T.command { + val platformExecutableJarExtension = if (Properties.isWin) ".bat" else "" + val launcher = cli(mainScalaVersion).standaloneLauncher().path + os.copy( + launcher, + os.Path(directory, os.pwd) / s"snailgun$platformExecutableJarExtension", + createFolders = true, + replaceExisting = true + ) + } + + private def ghToken(): String = Option(System.getenv("UPLOAD_GH_TOKEN")).getOrElse { + sys.error("UPLOAD_GH_TOKEN not set") + } + def uploadLaunchers(directory: String = "artifacts") = T.command { + val version = cli(mainScalaVersion).publishVersion() + + val path = os.Path(directory, os.pwd) + val launchers = os.list(path).filter(os.isFile(_)).map { path => + path.toNIO -> path.last + } + val (tag, overwriteAssets) = + if (version.endsWith("-SNAPSHOT")) ("nightly", true) + else ("v" + version, false) + System.err.println(s"Uploading to tag $tag (overwrite assets: $overwriteAssets)") + Upload.upload(ghOrg, ghName, ghToken(), tag, dryRun = false, overwrite = overwriteAssets)( + launchers: _* + ) + } +} diff --git a/cli/src/main/resources/META-INF/native-image/snailgun/native-image.properties b/cli/src/main/resources/META-INF/native-image/snailgun/native-image.properties new file mode 100644 index 0000000..b2924b8 --- /dev/null +++ b/cli/src/main/resources/META-INF/native-image/snailgun/native-image.properties @@ -0,0 +1,3 @@ +Args = --no-fallback \ + --enable-url-protocols=https \ + --report-unsupported-elements-at-runtime \ No newline at end of file diff --git a/cli/src/main/scala/snailgun/Cli.scala b/cli/src/main/scala/snailgun/Cli.scala index 1697fb6..59134a1 100644 --- a/cli/src/main/scala/snailgun/Cli.scala +++ b/cli/src/main/scala/snailgun/Cli.scala @@ -14,11 +14,10 @@ import java.net.ConnectException /** * An implementation of a CLI via case-app. * - * Unfortunately, GraalVM Native Image doesn't correctly generate a native - * image because of parsing errors and warnings generated by the use of macros - * in case-app via Shapeless. For that reason, this class is left here but it's - * not used by default, preferring a CLI implementation that requires no macros - * and works with GraalVM Native Image. + * Unfortunately, GraalVM Native Image doesn't correctly generate a native image because of parsing + * errors and warnings generated by the use of macros in case-app via Shapeless. For that reason, + * this class is left here but it's not used by default, preferring a CLI implementation that + * requires no macros and works with GraalVM Native Image. */ abstract class Cli(in: InputStream, out: PrintStream, err: PrintStream) { def exit(code: Int): Unit diff --git a/core/src/main/scala/snailgun/protocol/Protocol.scala b/core/src/main/scala/snailgun/protocol/Protocol.scala index 11dc94e..91737bc 100644 --- a/core/src/main/scala/snailgun/protocol/Protocol.scala +++ b/core/src/main/scala/snailgun/protocol/Protocol.scala @@ -1,6 +1,5 @@ package snailgun.protocol -import snailgun.Terminal import snailgun.logging.Logger import java.net.Socket @@ -32,13 +31,12 @@ import scala.util.control.NonFatal /** * An implementation of the nailgun protocol in Scala. * - * It follows http://www.martiansoftware.com/nailgun/protocol.html and has - * been slightly inspired in the C and Python clients. The implementation has - * been simplified more than these two and optimized for readability. + * It follows http://www.martiansoftware.com/nailgun/protocol.html and has been slightly inspired in + * the C and Python clients. The implementation has been simplified more than these two and + * optimized for readability. * - * The protocol is designed to be used by different instances of - * [[snailgun.Client]] implementing different communication mechanisms (e.g. - * TCP / Unix Domain sockets / Windows Named Pipes). + * The protocol is designed to be used by different instances of [[snailgun.Client]] implementing + * different communication mechanisms (e.g. TCP / Unix Domain sockets / Windows Named Pipes). */ class Protocol( streams: Streams, @@ -57,19 +55,14 @@ class Protocol( val NailgunFileSeparator = java.io.File.separator val NailgunPathSeparator = java.io.File.pathSeparator - def allEnvironment: Map[String, String] = { - def interactive(fd: Int): String = - Integer.toString(Terminal.hasTerminalAttached(fd)) - def skipIfNative(f: => String) = - if (!interactiveSession || System.getProperty("java.vm.name") == "Substrate VM") "0" else f - environment ++ Map( + def allEnvironment: Map[String, String] = + environment ++ Seq( "NAILGUN_FILESEPARATOR" -> NailgunFileSeparator, "NAILGUN_PATHSEPARATOR" -> NailgunPathSeparator, - "NAILGUN_TTY_0" -> skipIfNative(interactive(0)), - "NAILGUN_TTY_1" -> skipIfNative(interactive(1)), - "NAILGUN_TTY_2" -> skipIfNative(interactive(2)) + "NAILGUN_TTY_0" -> streams.inIsATty.toString, + "NAILGUN_TTY_1" -> streams.outIsATty.toString, + "NAILGUN_TTY_2" -> streams.errIsATty.toString ) - } def sendCommand( cmd: String, @@ -269,11 +262,11 @@ class Protocol( } /** - * Swallows any exception thrown by the closure [[f]] if client exits before - * the timeout of [[Protocol.Time.SendThreadWaitTerminationMillis]]. + * Swallows any exception thrown by the closure [[f]] if client exits before the timeout of + * [[Protocol.Time.SendThreadWaitTerminationMillis]]. * - * Ignoring exceptions in this scenario makes sense (exception could have - * been caught by server finishing connection with client concurrently). + * Ignoring exceptions in this scenario makes sense (exception could have been caught by server + * finishing connection with client concurrently). */ private def swallowExceptionsIfServerFinished(f: => Unit): Unit = { try f diff --git a/core/src/main/scala/snailgun/protocol/Streams.scala b/core/src/main/scala/snailgun/protocol/Streams.scala index 095fef8..d04ee73 100644 --- a/core/src/main/scala/snailgun/protocol/Streams.scala +++ b/core/src/main/scala/snailgun/protocol/Streams.scala @@ -4,11 +4,18 @@ import java.io.InputStream import java.io.OutputStream /** - * An instance of user-defined streams where the protocol will forward any - * stdout, stdin or stderr coming from the client. + * An instance of user-defined streams where the protocol will forward any stdout, stdin or stderr + * coming from the client. * - * Note that this is decoupled from the logger API, which is mostly used for - * tracing the protocol behaviour and reporting errors. The logger can be - * backed by some of these user-defined streams but it isn't a requirement. + * Note that this is decoupled from the logger API, which is mostly used for tracing the protocol + * behaviour and reporting errors. The logger can be backed by some of these user-defined streams + * but it isn't a requirement. */ -case class Streams(in: InputStream, out: OutputStream, err: OutputStream) +final case class Streams( + in: InputStream, + out: OutputStream, + err: OutputStream, + inIsATty: Int = 0, + outIsATty: Int = 0, + errIsATty: Int = 0 +) diff --git a/core/src/test/scala/snailgun/SnailgunBaseSuite.scala b/core/src/test/scala/snailgun/SnailgunBaseSuite.scala index 19be311..2282748 100644 --- a/core/src/test/scala/snailgun/SnailgunBaseSuite.scala +++ b/core/src/test/scala/snailgun/SnailgunBaseSuite.scala @@ -204,13 +204,17 @@ class SnailgunBaseSuite extends BaseSuite { } /** - * Starts a Nailgun server, creates a snailgun client and executes operations - * with that client. The server is killed when the client exits. + * Starts a Nailgun server, creates a snailgun client and executes operations with that client. + * The server is killed when the client exits. * - * @param streams The user-defined streams. - * @param log The logger instance for the test run. - * @param op A function that will receive the instantiated Client. - * @return The result of executing `op` on the client. + * @param streams + * The user-defined streams. + * @param log + * The logger instance for the test run. + * @param op + * A function that will receive the instantiated Client. + * @return + * The result of executing `op` on the client. */ def withRunningServer[T]( streams: Streams, diff --git a/core/src/test/scala/snailgun/logging/RecordingLogger.scala b/core/src/test/scala/snailgun/logging/RecordingLogger.scala index 7cd996a..c203148 100644 --- a/core/src/test/scala/snailgun/logging/RecordingLogger.scala +++ b/core/src/test/scala/snailgun/logging/RecordingLogger.scala @@ -65,8 +65,8 @@ class RecordingLogger( out.println { s"""Logger contains the following messages: |${getMessages - .map(s => s"[${s._1}] ${s._2}") - .mkString("\n ", "\n ", "\n")} + .map(s => s"[${s._1}] ${s._2}") + .mkString("\n ", "\n ", "\n")} """.stripMargin } } diff --git a/core/src/test/scala/snailgun/logging/Slf4jAdapter.scala b/core/src/test/scala/snailgun/logging/Slf4jAdapter.scala index e0726ce..04d7bfe 100644 --- a/core/src/test/scala/snailgun/logging/Slf4jAdapter.scala +++ b/core/src/test/scala/snailgun/logging/Slf4jAdapter.scala @@ -5,11 +5,11 @@ import org.slf4j.{Marker, Logger => Slf4jLogger} /** * Defines a slf4j-compliant logger wrapping Bloop logging utils. * - * This slf4j interface is necessary to be compatible with third-party libraries - * like lsp4s. It only intends to cover the basic functionality and it does not - * support slf4j markers. + * This slf4j interface is necessary to be compatible with third-party libraries like lsp4s. It only + * intends to cover the basic functionality and it does not support slf4j markers. * - * @param logger A logger interface. + * @param logger + * A logger interface. */ final class Slf4jAdapter[L <: Logger](logger: L) extends Slf4jLogger { def underlying: L = logger diff --git a/core/src/test/scala/snailgun/utils/DiffAssertions.scala b/core/src/test/scala/snailgun/utils/DiffAssertions.scala index da710ff..40502af 100644 --- a/core/src/test/scala/snailgun/utils/DiffAssertions.scala +++ b/core/src/test/scala/snailgun/utils/DiffAssertions.scala @@ -97,8 +97,8 @@ object DiffAssertions { sb.append( s"""#${header("Diff")} #${stripTrailingWhitespace( - Diff.unifiedDiff(obtained, expected, obtainedTitle, expectedTitle) - )}""" + Diff.unifiedDiff(obtained, expected, obtainedTitle, expectedTitle) + )}""" .stripMargin('#') ) sb.toString() diff --git a/mill b/mill new file mode 100755 index 0000000..62e5e18 --- /dev/null +++ b/mill @@ -0,0 +1,171 @@ +#!/usr/bin/env sh + +# This is a wrapper script, that automatically download mill from GitHub release pages +# You can give the required mill version with --mill-version parameter +# If no version is given, it falls back to the value of DEFAULT_MILL_VERSION +# +# Project page: https://github.com/lefou/millw +# Script Version: 0.4.2 +# +# If you want to improve this script, please also contribute your changes back! +# +# Licensed under the Apache License, Version 2.0 + + +DEFAULT_MILL_VERSION=0.10.0 + +set -e + +MILL_REPO_URL="https://github.com/com-lihaoyi/mill" + +if [ -z "${CURL_CMD}" ] ; then + CURL_CMD=curl +fi + +# Explicit commandline argument takes precedence over all other methods +if [ "$1" = "--mill-version" ] ; then + shift + if [ "x$1" != "x" ] ; then + MILL_VERSION="$1" + shift + else + echo "You specified --mill-version without a version." 1>&2 + echo "Please provide a version that matches one provided on" 1>&2 + echo "${MILL_REPO_URL}/releases" 1>&2 + false + fi +fi + +# Please note, that if a MILL_VERSION is already set in the environment, +# We reuse it's value and skip searching for a value. + +# If not already set, read .mill-version file +if [ -z "${MILL_VERSION}" ] ; then + if [ -f ".mill-version" ] ; then + MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)" + fi +fi + +if [ -n "${XDG_CACHE_HOME}" ] ; then + MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download" +else + MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download" +fi + +# If not already set, try to fetch newest from Github +if [ -z "${MILL_VERSION}" ] ; then + # TODO: try to load latest version from release page + echo "No mill version specified." 1>&2 + echo "You should provide a version via '.mill-version' file or --mill-version option." 1>&2 + + mkdir -p "${MILL_DOWNLOAD_PATH}" + LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || ( + # we might be on OSX or BSD which don't have -d option for touch + # but probably a -A [-][[hh]mm]SS + touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest" + ) || ( + # in case we still failed, we retry the first touch command with the intention + # to show the (previously suppressed) error message + LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" + ) + + # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993 + # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then + if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then + # we know a current latest version + MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) + fi + + if [ -z "${MILL_VERSION}" ] ; then + # we don't know a current latest version + echo "Retrieving latest mill version ..." 1>&2 + LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest" + MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) + fi + + if [ -z "${MILL_VERSION}" ] ; then + # Last resort + MILL_VERSION="${DEFAULT_MILL_VERSION}" + echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2 + else + echo "Using mill version ${MILL_VERSION}" 1>&2 + fi +fi + +MILL="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}" + +try_to_use_system_mill() { + MILL_IN_PATH="$(command -v mill || true)" + + if [ -z "${MILL_IN_PATH}" ]; then + return + fi + + UNIVERSAL_SCRIPT_MAGIC="@ 2>/dev/null # 2>nul & echo off & goto BOF" + + if ! head -c 128 "${MILL_IN_PATH}" | grep -qF "${UNIVERSAL_SCRIPT_MAGIC}"; then + if [ -n "${MILLW_VERBOSE}" ]; then + echo "Could not determine mill version of ${MILL_IN_PATH}, as it does not start with the universal script magic2" 1>&2 + fi + return + fi + + # Roughly the size of the universal script. + MILL_VERSION_SEARCH_RANGE="2403" + MILL_IN_PATH_VERSION=$(head -c "${MILL_VERSION_SEARCH_RANGE}" "${MILL_IN_PATH}" |\ + sed -n 's/^.*-DMILL_VERSION=\([^\s]*\) .*$/\1/p' |\ + head -n 1) + + if [ -z "${MILL_IN_PATH_VERSION}" ]; then + echo "Could not determine mill version, even though ${MILL_IN_PATH} has the universal script magic" 1>&2 + return + fi + + if [ "${MILL_IN_PATH_VERSION}" = "${MILL_VERSION}" ]; then + MILL="${MILL_IN_PATH}" + fi +} +try_to_use_system_mill + +# If not already downloaded, download it +if [ ! -s "${MILL}" ] ; then + + # support old non-XDG download dir + MILL_OLD_DOWNLOAD_PATH="${HOME}/.mill/download" + OLD_MILL="${MILL_OLD_DOWNLOAD_PATH}/${MILL_VERSION}" + if [ -x "${OLD_MILL}" ] ; then + MILL="${OLD_MILL}" + else + VERSION_PREFIX="$(echo $MILL_VERSION | cut -b -4)" + case $VERSION_PREFIX in + 0.0. | 0.1. | 0.2. | 0.3. | 0.4. ) + DOWNLOAD_SUFFIX="" + ;; + *) + DOWNLOAD_SUFFIX="-assembly" + ;; + esac + unset VERSION_PREFIX + + DOWNLOAD_FILE=$(mktemp mill.XXXXXX) + # TODO: handle command not found + echo "Downloading mill ${MILL_VERSION} from ${MILL_REPO_URL}/releases ..." 1>&2 + MILL_VERSION_TAG=$(echo $MILL_VERSION | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') + ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}" + chmod +x "${DOWNLOAD_FILE}" + mkdir -p "${MILL_DOWNLOAD_PATH}" + mv "${DOWNLOAD_FILE}" "${MILL}" + + unset DOWNLOAD_FILE + unset DOWNLOAD_SUFFIX + fi +fi + +unset MILL_DOWNLOAD_PATH +unset MILL_OLD_DOWNLOAD_PATH +unset OLD_MILL +unset MILL_VERSION +unset MILL_VERSION_TAG +unset MILL_REPO_URL + +exec "${MILL}" "$@" diff --git a/mill.bat b/mill.bat new file mode 100644 index 0000000..2ea5c03 --- /dev/null +++ b/mill.bat @@ -0,0 +1,115 @@ +@echo off + +rem This is a wrapper script, that automatically download mill from GitHub release pages +rem You can give the required mill version with --mill-version parameter +rem If no version is given, it falls back to the value of DEFAULT_MILL_VERSION +rem +rem Project page: https://github.com/lefou/millw +rem Script Version: 0.4.2 +rem +rem If you want to improve this script, please also contribute your changes back! +rem +rem Licensed under the Apache License, Version 2.0 + +rem setlocal seems to be unavailable on Windows 95/98/ME +rem but I don't think we need to support them in 2019 +setlocal enabledelayedexpansion + +set "DEFAULT_MILL_VERSION=0.10.0" + +set "MILL_REPO_URL=https://github.com/com-lihaoyi/mill" + +rem %~1% removes surrounding quotes +if [%~1%]==[--mill-version] ( + rem shift command doesn't work within parentheses + if not [%~2%]==[] ( + set MILL_VERSION=%~2% + set "STRIP_VERSION_PARAMS=true" + ) else ( + echo You specified --mill-version without a version. 1>&2 + echo Please provide a version that matches one provided on 1>&2 + echo %MILL_REPO_URL%/releases 1>&2 + exit /b 1 + ) +) + +if [!MILL_VERSION!]==[] ( + if exist .mill-version ( + set /p MILL_VERSION=<.mill-version + ) +) + +if [!MILL_VERSION!]==[] ( + set MILL_VERSION=%DEFAULT_MILL_VERSION% +) + +set MILL_DOWNLOAD_PATH=%USERPROFILE%\.mill\download + +rem without bat file extension, cmd doesn't seem to be able to run it +set MILL=%MILL_DOWNLOAD_PATH%\!MILL_VERSION!.bat + +if not exist "%MILL%" ( + set VERSION_PREFIX=%MILL_VERSION:~0,4% + set DOWNLOAD_SUFFIX=-assembly + if [!VERSION_PREFIX!]==[0.0.] set DOWNLOAD_SUFFIX= + if [!VERSION_PREFIX!]==[0.1.] set DOWNLOAD_SUFFIX= + if [!VERSION_PREFIX!]==[0.2.] set DOWNLOAD_SUFFIX= + if [!VERSION_PREFIX!]==[0.3.] set DOWNLOAD_SUFFIX= + if [!VERSION_PREFIX!]==[0.4.] set DOWNLOAD_SUFFIX= + set VERSION_PREFIX= + + for /F "delims=- tokens=1" %%A in ("!MILL_VERSION!") do set MILL_VERSION_BASE=%%A + for /F "delims=- tokens=2" %%A in ("!MILL_VERSION!") do set MILL_VERSION_MILESTONE=%%A + set VERSION_MILESTONE_START=!MILL_VERSION_MILESTONE:~0,1! + if [!VERSION_MILESTONE_START!]==[M] ( + set MILL_VERSION_TAG="!MILL_VERSION_BASE!-!MILL_VERSION_MILESTONE!" + ) else ( + set MILL_VERSION_TAG=!MILL_VERSION_BASE! + ) + + rem there seems to be no way to generate a unique temporary file path (on native Windows) + set DOWNLOAD_FILE=%MILL%.tmp + + set DOWNLOAD_URL=%MILL_REPO_URL%/releases/download/!MILL_VERSION_TAG!/!MILL_VERSION!!DOWNLOAD_SUFFIX! + + echo Downloading mill %MILL_VERSION% from %MILL_REPO_URL%/releases ... 1>&2 + + if not exist "%MILL_DOWNLOAD_PATH%" mkdir "%MILL_DOWNLOAD_PATH%" + rem curl is bundled with recent Windows 10 + rem but I don't think we can expect all the users to have it in 2019 + where /Q curl + if %ERRORLEVEL% EQU 0 ( + curl -f -L "!DOWNLOAD_URL!" -o "!DOWNLOAD_FILE!" + ) else ( + rem bitsadmin seems to be available on Windows 7 + rem without /dynamic, github returns 403 + rem bitsadmin is sometimes needlessly slow but it looks better with /priority foreground + bitsadmin /transfer millDownloadJob /dynamic /priority foreground "!DOWNLOAD_URL!" "!DOWNLOAD_FILE!" + ) + if not exist "!DOWNLOAD_FILE!" ( + echo Could not download mill %MILL_VERSION% 1>&2 + exit /b 1 + ) + + move /y "!DOWNLOAD_FILE!" "%MILL%" + + set DOWNLOAD_FILE= + set DOWNLOAD_SUFFIX= +) + +set MILL_DOWNLOAD_PATH= +set MILL_VERSION= +set MILL_REPO_URL= + +set MILL_PARAMS=%* + +if defined STRIP_VERSION_PARAMS ( + for /f "tokens=1-2*" %%a in ("%*") do ( + rem strip %%a - It's the "--mill-version" option. + rem strip %%b - it's the version number that comes after the option. + rem keep %%c - It's the remaining options. + set MILL_PARAMS=%%c + ) +) + +"%MILL%" %MILL_PARAMS% diff --git a/project/BuildPlugin.scala b/project/BuildPlugin.scala deleted file mode 100644 index dd8d462..0000000 --- a/project/BuildPlugin.scala +++ /dev/null @@ -1,206 +0,0 @@ -package build - -import java.io.File - -import bintray.BintrayKeys -import ch.epfl.scala.sbt.release.Feedback -import com.typesafe.sbt.SbtPgp.{autoImport => Pgp} -import sbt.{AutoPlugin, Def, Keys, PluginTrigger, Plugins, State, Task, ThisBuild} -import sbt.io.IO -import sbt.io.syntax.fileToRichFile -import sbt.librarymanagement.syntax.stringToOrganization -import sbtdynver.GitDescribeOutput -import ch.epfl.scala.sbt.release.ReleaseEarlyPlugin.{autoImport => ReleaseEarlyKeys} - -object BuildPlugin extends AutoPlugin { - import sbt.plugins.JvmPlugin - import sbt.plugins.IvyPlugin - import com.typesafe.sbt.SbtPgp - import ch.epfl.scala.sbt.release.ReleaseEarlyPlugin - - override def trigger: PluginTrigger = allRequirements - override def requires: Plugins = JvmPlugin && ReleaseEarlyPlugin && SbtPgp && IvyPlugin - val autoImport = BuildKeys - - override def globalSettings: Seq[Def.Setting[_]] = - BuildImplementation.globalSettings - override def buildSettings: Seq[Def.Setting[_]] = - BuildImplementation.buildSettings - override def projectSettings: Seq[Def.Setting[_]] = - BuildImplementation.projectSettings -} - -object BuildKeys { - import sbt.{Reference, RootProject, ProjectRef, BuildRef, file} - - def inProject(ref: Reference)(ss: Seq[Def.Setting[_]]): Seq[Def.Setting[_]] = - sbt.inScope(sbt.ThisScope.in(project = ref))(ss) - - def inProjectRefs( - refs: Seq[Reference] - )(ss: Def.Setting[_]*): Seq[Def.Setting[_]] = - refs.flatMap(inProject(_)(ss)) - - def inCompileAndTest(ss: Def.Setting[_]*): Seq[Def.Setting[_]] = - Seq(sbt.Compile, sbt.Test).flatMap(sbt.inConfig(_)(ss)) - - import sbt.Test - val testSuiteSettings: Seq[Def.Setting[_]] = List( - Keys.testFrameworks += new sbt.TestFramework("utest.runner.Framework"), - Keys.libraryDependencies ++= List( - Dependencies.monix % Test, - Dependencies.utest % Test, - Dependencies.pprint % Test, - Dependencies.nailgun % Test, - Dependencies.difflib % Test, - Dependencies.slf4jApi % Test - ) - ) - -} - -object BuildImplementation { - import sbt.{url, file} - import sbt.{Developer, Resolver, Watched, Compile, Test} - import sbtdynver.DynVerPlugin.{autoImport => DynVerKeys} - - def GitHub(org: String, project: String): java.net.URL = - url(s"https://github.com/$org/$project") - def GitHubDev(handle: String, fullName: String, email: String) = - Developer(handle, fullName, email, url(s"https://github.com/$handle")) - - final val globalSettings: Seq[Def.Setting[_]] = Seq( - Keys.cancelable := true, - Keys.testOptions in Test += sbt.Tests.Argument("-oD"), - Keys.publishArtifact in Test := false, - Pgp.pgpPublicRing := { - if (Keys.insideCI.value) file("/drone/.gnupg/pubring.asc") - else Pgp.pgpPublicRing.value - }, - Pgp.pgpSecretRing := { - if (Keys.insideCI.value) file("/drone/.gnupg/secring.asc") - else Pgp.pgpSecretRing.value - } - ) - - private final val ThisRepo = GitHub("jvican", "snailgun") - final val buildSettings: Seq[Def.Setting[_]] = Seq( - Keys.organization := "me.vican.jorge", - Keys.updateOptions := Keys.updateOptions.value.withCachedResolution(true), - Keys.scalaVersion := Dependencies.Scala212Version, - Keys.crossScalaVersions := Seq(Dependencies.Scala212Version, Dependencies.Scala213Version), - Keys.triggeredMessage := Watched.clearWhenTriggered, - Keys.resolvers := { - val oldResolvers = Keys.resolvers.value - val jvicanResolver = Resolver.bintrayRepo("jvican", "releases") - val scalacenterResolver = Resolver.bintrayRepo("scalacenter", "releases") - (oldResolvers :+ scalacenterResolver :+ jvicanResolver).distinct - }, - ReleaseEarlyKeys.releaseEarlyWith := { - // Only tag releases go directly to Maven Central, the rest go to bintray! - val isOnlyTag = DynVerKeys.dynverGitDescribeOutput.value - .map(v => v.commitSuffix.isEmpty && v.dirtySuffix.value.isEmpty) - if (isOnlyTag.getOrElse(false)) ReleaseEarlyKeys.SonatypePublisher - else ReleaseEarlyKeys.BintrayPublisher - }, - Keys.scmInfo := - Some( - sbt.ScmInfo(url("https://github.com/jvican/snailgun"), "git@github.com:jvican/snailgun.git") - ), - BintrayKeys.bintrayOrganization := Some("jvican"), - Keys.startYear := Some(2019), - Keys.autoAPIMappings := true, - Keys.publishMavenStyle := true, - Keys.homepage := Some(ThisRepo), - Keys.licenses := Seq( - "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") - ), - Keys.developers := List( - GitHubDev("jvican", "Jorge Vicente Cantero", "jorge@vican.me") - ) - ) - - import sbt.{CrossVersion, compilerPlugin} - final val projectSettings: Seq[Def.Setting[_]] = Seq( - BintrayKeys.bintrayPackage := "snailgun", - BintrayKeys.bintrayRepository := "releases", - // Add some metadata that is useful to see in every on-merge bintray release - BintrayKeys.bintrayPackageLabels := List( - "client", - "nailgun", - "server", - "scala", - "tooling" - ), - ReleaseEarlyKeys.releaseEarlyPublish := BuildDefaults.releaseEarlyPublish.value, - Keys.scalacOptions := reasonableCompileOptions(Keys.scalaVersion.value), - // Legal requirement: license and notice files must be in the published jar - Keys.resources in Compile ++= BuildDefaults.getLicense.value, - Keys.publishArtifact in Test := false, - Keys.publishArtifact in (Compile, Keys.packageDoc) := { - val output = DynVerKeys.dynverGitDescribeOutput.value - val version = Keys.version.value - BuildDefaults.publishDocAndSourceArtifact(output, version) - }, - Keys.publishArtifact in (Compile, Keys.packageSrc) := { - val output = DynVerKeys.dynverGitDescribeOutput.value - val version = Keys.version.value - BuildDefaults.publishDocAndSourceArtifact(output, version) - }, - Keys.publishLocalConfiguration in Compile := - Keys.publishLocalConfiguration.value.withOverwrite(true) - ) - - final def reasonableCompileOptions(version: String) = { - val base = "-deprecation" :: "-encoding" :: "UTF-8" :: "-feature" :: "-language:existentials" :: - "-language:higherKinds" :: "-language:implicitConversions" :: "-unchecked" :: - "-Ywarn-numeric-widen" :: "-Ywarn-value-discard" :: Nil - - if (!version.startsWith("2.13")) "-Yno-adapted-args" :: "-Xfuture" :: base else base - } - - object BuildDefaults { - val releaseEarlyPublish: Def.Initialize[Task[Unit]] = Def.task { - val logger = Keys.streams.value.log - val name = Keys.name.value - // We force publishSigned for all of the modules, yes or yes. - if (ReleaseEarlyKeys.releaseEarlyWith.value == ReleaseEarlyKeys.SonatypePublisher) { - logger.info(Feedback.logReleaseSonatype(name)) - } else { - logger.info(Feedback.logReleaseBintray(name)) - } - - Pgp.PgpKeys.publishSigned.value - } - - // From sbt-sensible https://gitlab.com/fommil/sbt-sensible/issues/5, legal requirement - val getLicense: Def.Initialize[Task[Seq[File]]] = Def.task { - val orig = (Keys.resources in Compile).value - val base = Keys.baseDirectory.value - val root = (Keys.baseDirectory in ThisBuild).value - - def fileWithFallback(name: String): File = - if ((base / name).exists) base / name - else if ((root / name).exists) root / name - else throw new IllegalArgumentException(s"legal file $name must exist") - - Seq(fileWithFallback("LICENSE.md"), fileWithFallback("NOTICE.md")) - } - - /** - * This setting figures out whether the version is a snapshot or not and configures - * the source and doc artifacts that are published by the build. - * - * Snapshot is a term with no clear definition. In this code, a snapshot is a revision - * that is dirty, e.g. has time metadata in its representation. In those cases, the - * build will not publish doc and source artifacts by any of the publishing actions. - */ - def publishDocAndSourceArtifact( - info: Option[GitDescribeOutput], - version: String - ): Boolean = { - val isStable = info.map(_.dirtySuffix.value.isEmpty) - !isStable.exists(stable => !stable || version.endsWith("-SNAPSHOT")) - } - } -} diff --git a/project/Dependencies.scala b/project/Dependencies.scala deleted file mode 100644 index 09ffe99..0000000 --- a/project/Dependencies.scala +++ /dev/null @@ -1,24 +0,0 @@ -package build - -object Dependencies { - import sbt.librarymanagement.syntax.stringToOrganization - val Scala212Version = "2.12.11" - val Scala213Version = "2.13.4" - - val jnaVersion = "5.6.0" - val nailgunVersion = "ee3c4343" - val difflibVersion = "1.3.0" - val caseAppVersion = "1.2.0-faster-compile-time" - val shapelessVersion = "2.3.3-lower-priority-coproduct" - - val monix = "io.monix" %% "monix" % "3.3.0" - val utest = "com.lihaoyi" %% "utest" % "0.7.2" - val pprint = "com.lihaoyi" %% "pprint" % "0.6.0" - val jna = "net.java.dev.jna" % "jna" % jnaVersion - val slf4jApi = "org.slf4j" % "slf4j-api" % "1.7.26" - val jnaPlatform = "net.java.dev.jna" % "jna-platform" % jnaVersion - val nailgun = "ch.epfl.scala" % "nailgun-server" % nailgunVersion - val difflib = "com.googlecode.java-diff-utils" % "diffutils" % difflibVersion - - val scopt = "com.github.scopt" %% "scopt" % "4.0.0-RC2" -} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index 7de0a93..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.4.4 diff --git a/project/build.sbt b/project/build.sbt deleted file mode 100644 index 3bc9f07..0000000 --- a/project/build.sbt +++ /dev/null @@ -1,9 +0,0 @@ -val `snailgun-build` = project - .in(file(".")) - .settings( - scalaVersion := "2.12.11", - addSbtPlugin("com.dwijnand" % "sbt-dynver" % "3.1.0"), - addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8.1"), - addSbtPlugin("ch.epfl.scala" % "sbt-release-early" % "2.1.1+4-9d76569a"), - addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.22") - ) diff --git a/project/metals.sbt b/project/metals.sbt deleted file mode 100644 index 343a26a..0000000 --- a/project/metals.sbt +++ /dev/null @@ -1,4 +0,0 @@ -// DO NOT EDIT! This file is auto-generated. -// This file enables sbt-bloop to create bloop config files. - -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.3")