Skip to content

Instantly share code, notes, and snippets.

@OndrejSpanel
Last active October 21, 2024 18:48
Show Gist options
  • Save OndrejSpanel/08ff4327c22f05dd0141063b5ef28df1 to your computer and use it in GitHub Desktop.
Save OndrejSpanel/08ff4327c22f05dd0141063b5ef28df1 to your computer and use it in GitHub Desktop.
SBT Zinc Analysis plugin
import sbt.Keys.*
import sbt.*
import scala.collection.compat.*
/**
* This plugin prints which source files are compiled and why
* */
object ZincAnalysis extends AutoPlugin {
private def format[T](a: Array[T]) = a.mkString("[", ",", "]")
private def arrayDeepEquals[T <: AnyRef](a: Array[T], b: Array[T]) = {
java.util.Arrays.deepEquals(a.asInstanceOf[Array[AnyRef]], b.asInstanceOf[Array[AnyRef]])
}
override lazy val projectSettings = Seq(
Compile / compileIncremental := {
import sbt.internal.inc.*
val log = streams.value.log
val result = (Compile / compileIncremental).value
val oldResults = (Compile / previousCompile).value
val analysis = result.analysis.asInstanceOf[Analysis]
val oldAnalysis = oldResults.analysis.asScala.map(_.asInstanceOf[Analysis])
val newCompilations = analysis.compilations.allCompilations.diff(oldAnalysis.map(_.compilations.allCompilations).getOrElse(Nil))
log.info(s"Compilation cycles: ${newCompilations.size}")
val prevCompilations = analysis.compilations.allCompilations.drop(1).zip(analysis.compilations.allCompilations).toMap
for (compilation <- newCompilations) {
val now = compilation.getStartTime
val currentStamps = analysis.stamps.products.filter { stamp =>
!stamp._2.getLastModified.filter(_ == now).isEmpty
}
val prevStamps = prevCompilations.get(compilation).flatMap(c => oldAnalysis.map(_.stamps))
val recompiledClasses = analysis.apis.internal.collect {
case (src, api) if api.compilationTimestamp() == now => src
}.toSet
val recompiledSources = recompiledClasses.flatMap { className =>
analysis.relations.definesClass(className)
}
val recompiledSourceMap = recompiledClasses.view.flatMap { className =>
analysis.relations.definesClass(className).map { file =>
className -> file
}
}.toMap
log.info(s"Recompiled sources: ${recompiledSources.size}")
recompiledSources.foreach { s =>
log.info(s" $s${if (analysis.stamps.sources.get(s) != prevStamps.flatMap(_.sources.get(s))) " modified" else ""}")
}
log.info(s"Recompiled classes: ${recompiledClasses.size}")
val classChanges = recompiledClasses.flatMap { className =>
val newClass = analysis.apis.internal.get(className)
val oldClass = oldAnalysis.flatMap(_.apis.internal.get(className))
val changes = Seq(
(oldClass, newClass) match {
case (Some(o), Some(n)) =>
val changes = Seq(
Option.when(o.apiHash != n.apiHash)("apiHash"),
Option.when(!o.nameHashes.sameElements(n.nameHashes))("nameHashes"),
Option.when(!xsbt.api.SameAPI(o.api.classApi, n.api.classApi))(s"class api"),
Option.when(!xsbt.api.SameAPI(o.api.objectApi, n.api.objectApi))(s"object api"),
Option.when(!xsbt.api.SameAPI(o.api, n.api))(s"api"),
).flatten
Option.when(changes.nonEmpty) {
changes.mkString(", ")
}
case (Some(o), None) => Some("api removed")
case (None, Some(n)) => Some("new api")
case _ => None
}
).flatten
Option.when(changes.nonEmpty) {
className -> changes.mkString(",")
}
}
val sourceGroups = classChanges.groupBy(kv => recompiledSourceMap(kv._1))
sourceGroups.foreach { case (source, classes) =>
log.info(s" $source changed:")
classes.foreach { case (cls, reason) =>
log.info(s" $cls - $reason")
}
}
}
result
}
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment