Skip to content

Instantly share code, notes, and snippets.

@carlosedp
Last active May 3, 2023 19:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carlosedp/c3ea2814ac5392051a562e95c1298239 to your computer and use it in GitHub Desktop.
Save carlosedp/c3ea2814ac5392051a562e95c1298239 to your computer and use it in GitHub Desktop.
Find plugin updates for Scala cli, mill build tool and ammonite scripts

This is a Scala script to check for Mill build plugin and scala-cli lib updates. The plugin requires scala-cli.

Run directly from the gist with:

scala-cli https://gist.github.com/carlosedp/c3ea2814ac5392051a562e95c1298239/raw/checkdeps.sc

Or download the script, make it executable and run as:

checkdeps.sc

# or

checkdeps.sc /user/repos/myproject

It prints something like:

Checking files in directory /Users/user/repos/mill
Mill has updates. Currently on 0.10.5, latest version: 0.10.7. Bump your .mill-version and/or mill launcher script.
Checking plugins for file build.sc
  - Plugin de.tototec.de.tobiasroeser.mill.vcs.version has updates. Using 0.1.4. Latest version is 1.3.0.
  - Plugin com.github.lolgab.mill-mima has updates. Using 0.0.11. Latest version is 1.1.0.
  - Could not find plugin net.sourceforge.htmlcleaner.htmlcleaner in Scaladex (https://index.scala-lang.org).

image

The script supports the following styles plugin declarations:

// Separate lines:
import $ivy.`io.github.davidgregory084::mill-tpolecat::0.3.1`
import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.1.3`
// Ivy import and plugin import in the same line
import $ivy.`com.goyeau::mill-scalafix::0.2.9`, com.goyeau.mill.scalafix.ScalafixModule
// Multiline import
import $ivy.{
  `com.lihaoyi::requests:0.2.0 compat`,
  `com.lihaoyi::ujson:0.7.5 compat`
}
// Multiple imports in the same line:
import $ivy.{`com.lihaoyi::requests:0.2.0 compat`, `com.lihaoyi::ujson:0.7.5 compat`}
// scala-cli style imports with "using":
//> using lib "io.kevinlee::just-semver:0.5.0"
#!/usr/bin/env -S scala-cli shebang
// checkdeps.sc
// Install [scala-cli](https://scala-cli.virtuslab.org/) to use
//> using scala "3.3.0-RC5"
//> using lib "com.lihaoyi::os-lib:0.9.1"
//> using lib "com.lihaoyi::requests:0.8.0"
//> using lib "com.lihaoyi::ujson:3.1.0"
//> using lib "com.lihaoyi::fansi::0.4.0"
//> using lib "io.kevinlee::just-semver:0.6.0"
import just.semver.SemVer
import fansi.Color.*
val DEBUG = true
val SILENT = true
/** Handles mill and scala scripts plugin updates. */
object Checkdeps:
// ------------------- Handle Mill Versions ------------------------ //
/**
* Checks for mill updates.
* @param path
* Is the path where `.sc` and/or `.mill-version` files are located
*/
def checkMill(path: os.Path = os.pwd): Unit =
println(s"Checking for updates in Ammonite/Mill plugins/libs in ${Yellow(path.toString)}")
if os.exists(path / ".mill-version") then
getMillVersion(os.read(path / ".mill-version").trim) match
case Some(msg) => println(msg)
case None =>
/**
* Compares the current version with latest release
*
* @param currentVer
* Is the current version defined in `.mill-version`
* @return
* an `Option[String]` with the update message or `None` if no updates are
* available
*/
def getMillVersion(currentVer: String): Option[String] =
val latest =
ujson.read(requests.get("https://api.github.com/repos/com-lihaoyi/mill/releases/latest"))("tag_name").str.trim
if SemVer.parse(currentVer).toOption.get < SemVer.parse(latest).toOption.get then
Some(
s"${fansi.Bold.On("Mill has updates.")} Currently on $currentVer, latest version: ${Red(latest)}. Bump your ${Yellow(".mill-version")} and/or ${Yellow("mill")} launcher script."
)
else None
// ------------------- Handle Plugin Versions ------------------------ //
/**
* Checks for updates on scala plugin files (`.sc`). The script supports both
* `$ivy` and scala-cli `//> using` styles. For more supported styles, check
* the tests
*
* @param file
* is the file to be checked
*/
def checkPlugins(file: os.Path): Unit =
if os.exists(file) then
val data = loadFile(file)
val p = getPlugins(data)
p.foreach: plugin =>
if DEBUG then println(s" → ${plugin("artifact")}")
val isMill = if file.baseName == "build" then true else false
getPluginUpdates(plugin, isMill = isMill) match
case Some(msg) => println(msg)
case None =>
/**
* Reads a .sc file and returns the file lines
* @param file
* Is the file contents to be checked
* @return
* an `IndexedSeq[String] containing the file lines
*/
def loadFile(file: os.Path): IndexedSeq[String] =
// Lets read the .sc file
println(s"Checking plugins and libs for: " + Yellow(s"${file.baseName}.${file.ext}"))
os.read.lines(file)
/**
* Gets the plugins matching pattern
* @param pluginList
* Is the file contents to be checked
* @return
* an array of maps in the format `Map("org" -> "com.lihaoyi", "artifact" ->
* "requests", "version" -> "0.2.0")` containing each identified plugin.
*/
def getPlugins(pluginList: IndexedSeq[String]): Array[Map[String, String]] =
// Filter plugins between ``
pluginList
.flatMap("""(?:\`|.*using lib \")([^\s]+).*?(?:\`|\".*)""".r.findAllMatchIn(_).toList.map(_.group(1)))
.filter(!_.contains("MILL_VERSION"))
.filter(""".+:+.+:.+""".r.findFirstIn(_).isDefined)
.map(_.split(":+"))
.map(a => Map("org" -> a(0), "artifact" -> a(1), "version" -> a(2)))
.toArray
/**
* Checks for plugin update
* @param plugin
* Is map containing plugin org, artifact and current version
* @param millVer
* Is the mill version used to check for the artifact
* @return
* an `Option[String]` with a message if the plugin has updates or if there
* was an error checking. Could also be `None` if no update is found.
*/
def getPluginUpdates(
plugin: Map[String, String],
millVer: String = "0.10",
scalaVer: String = "2.13",
isMill: Boolean = false
): Option[String] =
val scaladexURL = "https://index.scala-lang.org"
// Search Scaladex for plugin
val url = if isMill then s"$scaladexURL/api/artifacts/${plugin("org")}/${plugin("artifact")}_mill${millVer}_${scalaVer}" else
s"$scaladexURL/api/artifacts/${plugin("org")}/${plugin("artifact")}_${scalaVer}"
val doc = ujson.read(requests.get(url))
val pluginName = s"${plugin("org")}:${plugin("artifact")}"
if doc("items").arr.isEmpty then
return Some(
s" ✗ ${Red("Could not find the plugin or versions")} for plugin ${Green(pluginName)} in Scaladex ($scaladexURL)."
)
// Check if current version is lower than the available one
val currentVersion = SemVer.parse(plugin("version")) match
case Left(value) => return Some(s" ✗ ${Red("Could not parse current version for ")} ${Green(pluginName)}")
case Right(value) => value
val latestVer =
doc("items").arr.map(_("version")).toArray.map(_.str).flatMap(SemVer.parse(_).toOption).sortWith(_ < _).last
val ret = if currentVersion < latestVer then
Some(s" ↳ ${Green(pluginName)} has updates. Using ${currentVersion.render}. Latest version is ${Red(latestVer.render)}.")
else if !SILENT then Some(s" ✓ ${Green(pluginName)} is already up-to-date.") else None
return ret
// Run the script
val path = if args.mkString != "" then args.mkString else os.pwd.toString
Checkdeps.checkMill(os.Path(path))
os.list(os.Path(path)).filter(f => f.ext == "sc" || f.ext == "scala").foreach(Checkdeps.checkPlugins(_))
// checkdeps.test.scala
// Install [scala-cli](https://scala-cli.virtuslab.org/)
// Test with `scala-cli test checkdeps.test.scala`
//> using scala "3.3.0-RC5"
//> using lib "org.scalameta::munit:1.0.0-M7"
//> using lib "io.kevinlee::just-semver:0.6.0"
//> using file "checkdeps.sc"
// import utest._
import munit.FunSuite
import just.semver.SemVer
import checkdeps.Checkdeps._
class MillDepsSpec extends FunSuite:
test("check Mill version with update"):
val data = getMillVersion("0.10.1")
assert(!data.get.isEmpty())
assert(data.get.contains("Mill has updates"))
test("check Mill version without update"):
val current =
ujson.read(requests.get("https://api.github.com/repos/com-lihaoyi/mill/releases/latest"))("tag_name").str.trim
val d = getMillVersion(current)
assert(d.isEmpty)
class PluginDepsSpec extends munit.FunSuite:
test("check plugin parsing for single line import"):
val testData = """
// Some comment
import $ivy.`io.github.davidgregory084::mill-tpolecat::0.3.1`
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assert(
p.contains(Map("org" -> "io.github.davidgregory084", "artifact" -> "mill-tpolecat", "version" -> "0.3.1")),
)
test("check plugin parsing for scala-cli file"):
val testData = """
//> using lib "dev.zio::zio:2.0.13"
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assert(
p.contains(Map("org" -> "dev.zio", "artifact" -> "zio", "version" -> "2.0.13")),
)
test("check plugin parsing for single line import with two plugins"):
val testData = """
// Some comment
import $ivy.{`com.lihaoyi::requests:0.2.0 compat`, `com.lihaoyi::ujson:0.7.5 compat`}
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assert(
p.contains(Map("org" -> "com.lihaoyi", "artifact" -> "requests", "version" -> "0.2.0")),
p.contains(Map("org" -> "com.lihaoyi", "artifact" -> "ujson", "version" -> "0.7.5")),
)
test("check plugin parsing for single import with two plugins"):
val testData = """
// Some comment
import $ivy.{
`com.lihaoyi::requests:0.2.0 compat`,
`com.lihaoyi::ujson:0.7.5 compat`
}
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assert(
p.contains(Map("org" -> "com.lihaoyi", "artifact" -> "requests", "version" -> "0.2.0")),
p.contains(Map("org" -> "com.lihaoyi", "artifact" -> "ujson", "version" -> "0.7.5")),
)
test("check plugin parsing ivy and plugin import the same line"):
val testData = """
// Some comment
import $ivy.`com.goyeau::mill-scalafix::0.2.9`, com.goyeau.mill.scalafix.ScalafixModule
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assert(
p.contains(Map("org" -> "com.goyeau", "artifact" -> "mill-scalafix", "version" -> "0.2.9")),
)
test("check plugin parsing for scala-cli using style"):
val testData = """
// Some comment
//> using lib "io.kevinlee::just-semver:0.5.0"
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assert(
p.contains(Map("org" -> "io.kevinlee", "artifact" -> "just-semver", "version" -> "0.5.0")),
)
test("check plugin with no update"):
// Lets get the plugin versions
val pluginversions = ujson.read(
requests.get(
s"https://index.scala-lang.org/api/project?organization=joan38&repository=mill-scalafix",
),
)
// now get the latest version
val latestVer =
pluginversions("versions").arr.map(_.str).flatMap(SemVer.parse(_).toOption).sortWith(_ < _).last
// and check against it
val testData = raw"""
import $$ivy.`com.goyeau::mill-scalafix::${latestVer.render}`
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assert(
p.contains(Map("org" -> "com.goyeau", "artifact" -> "mill-scalafix", "version" -> latestVer.render)),
)
// to make sure we don't show updates
assertEquals(getPluginUpdates(p(0)).get.contains("up-to-date"), true)
test("check plugin with update"):
val testData = """
// Some comment
import $ivy.`io.kevinlee::just-semver:0.5.0`
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assertEquals(getPluginUpdates(p(0)).get.contains("has updates"), true)
test("check plugin with error in version parsing"):
val testData = """
// Some comment
import $ivy.`io.kevinlee::just-semver:x.y.z`
""".stripMargin.split("\n").toIndexedSeq
val p = getPlugins(testData)
assertEquals(getPluginUpdates(p(0)).get.contains("Could not parse"), true)
@lefou
Copy link

lefou commented Aug 29, 2022

All shown available version updates for plugins are incorrect.

E.g. for mill-vcs-version plugin there is no 1.3.0 version available. https://github.com/lefou/mill-vcs-version/releases

$ cs complete de.tototec:de.tobiasroeser.mill.vcs.version_mill0.10_2.13:
0.1.4
0.1.4-1-070499
0.1.4-2-1d5e89
0.1.4-3-8d2857
0.1.4-4-408a15
0.1.4-5-2f474f
0.1.4-8-9b9584
0.1.4-9-3e6b30
0.1.4-10-5eae6a
0.1.4-11-6a09d2
0.2.0
0.2.0-1-7a1aa2
0.2.0-2-c2dd7c
0.2.0-3-3c8f11
0.2.0-4-ff5103

@carlosedp
Copy link
Author

carlosedp commented Aug 29, 2022

I think it's fixed now @lefou :

❯ checkdeps.sc ~/repos/scala-playground/zio-scalajs-stack
Compiling /Users/cdepaula/.dotfiles/bin/checkdeps.sc
Checking for Ammonite/Mill plugin updates in /Users/cdepaula/repos/scala-playground/zio-scalajs-stack
Checking plugins for: build.sc
  - Plugin mill-scalafix has updates. Using 0.2.9. Latest version is 0.2.10.
  - Plugin mill-tpolecat has updates. Using 0.3.0. Latest version is 0.3.1.
  - Plugin de.tobiasroeser.mill.vcs.version has updates. Using 0.1.3. Latest version is 0.2.0.
Checking plugins for: checkdeps.sc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment