Skip to content

Instantly share code, notes, and snippets.

@xuwei-k
Created September 6, 2024 23:17
Show Gist options
  • Save xuwei-k/5ea6e1cb00c425d754b3be54d93e960c to your computer and use it in GitHub Desktop.
Save xuwei-k/5ea6e1cb00c425d754b3be54d93e960c to your computer and use it in GitHub Desktop.
addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.6.1")
scalacOptions += "-Xsource:3"
enablePlugins(SbtPlugin)
import sbt._
import sbt.Keys._
import sbt.internal.util.ManagedLogger
import sbt.librarymanagement.DependencyResolution
import sbtlicensereport.SbtLicenseReport.autoImportImpl.LicenseCategory
import scala.xml.XML
import sjsonnew.BasicJsonProtocol._
import sjsonnew.JsonFormat
import sjsonnew.JsonWriter
import sjsonnew.support.scalajson.unsafe.PrettyPrinter
object FastLicensePlugin extends AutoPlugin {
object autoImport {
case class LicenseValue(name: String, url: String, lib: String) {
lazy val normalizedLicenses: Seq[LicenseCategory] =
sbtlicensereport.license.LicenseCategory.all.filter(_.unapply(name))
lazy val normalizedLicenseNames: Seq[String] = {
val x = normalizedLicenses.map(_.name)
if (x.isEmpty) {
Seq(s"Unknown($name)")
} else {
x
}
}
override def toString: String = toJsonString(this)
}
object LicenseValue {
implicit val orderInstance: Ordering[LicenseValue] =
Ordering.by(x => (x.lib, x.name, x.url))
implicit val instance: JsonFormat[LicenseValue] =
caseClass3(apply, unapply)("name", "url", "pom")
}
val fastLicense = taskKey[Seq[LicenseValue]]("")
val fastAllLicense = taskKey[Seq[LicenseValue]]("")
val allUnmanagedJars = taskKey[Seq[File]]("")
}
import autoImport.*
override def trigger: PluginTrigger = allRequirements
private val configs = Seq(Compile, Test)
private def toJsonString[A: JsonWriter](a: A): String = {
val builder = new sjsonnew.Builder(sjsonnew.support.scalajson.unsafe.Converter.facade)
implicitly[JsonWriter[A]].write(a, builder)
PrettyPrinter.apply(
builder.result.getOrElse(sys.error("invalid json"))
)
}
private val subProjects: Def.Initialize[Task[List[ResolvedProject]]] = Def.task {
val extracted = Project.extract(state.value)
val currentBuildUri = extracted.currentRef.build
extracted.structure.units
.apply(currentBuildUri)
.defined
.values
.toList
}
override def buildSettings: Seq[Def.Setting[?]] = Def.settings(
allUnmanagedJars := {
Def
.taskDyn(
subProjects.value
.flatMap { p =>
configs.map { x =>
LocalProject(p.id) / x / unmanagedJars
}
}
.join
.map(_.flatten)
)
.value
.map(_.data)
.distinct
},
fastAllLicense := {
val values: Seq[LicenseValue] = Def
.taskDyn {
subProjects.value.map { p =>
LocalProject(p.id) / fastLicense
}.join
}
.value
.flatten
.distinct
.sorted
val markdown = values
.flatMap { x =>
x.normalizedLicenseNames.map(_ -> x.lib)
}
.groupBy(_._1)
.toList
.sortBy(_._1)
.map {
case (license, xs) =>
xs.distinct.sorted
.map(_._2)
.map(x => s"- [${x}](https://repo1.maven.org/maven2/${x})")
.mkString(s"## $license\n", "\n", "\n")
}
.mkString("\n")
sys.env.get("GITHUB_STEP_SUMMARY").map(file).filter(_.isFile).foreach { summary =>
val withDetails = s"\n<details><summary>licenses</summary>\n\n${markdown}\n\n</details>\n"
IO.append(summary, withDetails)
}
IO.write(
file("target") / "licenses.md",
markdown
)
val unknownLibs = values
.filter(
_.normalizedLicenses.isEmpty
)
.distinct
.sorted
val log = streams.value.log
def writeUnknown(s: String): Unit =
IO.write(file("target") / "unknown-license.json", s)
if (unknownLibs.nonEmpty) {
val errorMsg = Seq(
"found unknown licenses\n",
"```json",
toJsonString(unknownLibs),
"```",
""
).mkString("\n")
log.error(errorMsg)
writeUnknown(errorMsg)
} else {
log.info("not found unknown licenses")
writeUnknown("")
}
values
}
)
def jarPathToPomPath(p: File): String = {
val s = p.getCanonicalPath
val _ :+ artifactId :+ v :+ _ = s.split('/').toList
(s.split('/').init :+ s"${artifactId}-${v}.pom").mkString("/")
}
private def parentPomXml(x: scala.xml.Elem, csHome: File): Seq[scala.xml.Elem] = {
val parent = x \\ "parent"
val parentModule = sbt
.ModuleID(
organization = (parent \ "groupId").text.trim,
name = (parent \ "artifactId").text.trim,
revision = (parent \ "version").text.trim
)
if (parentModule.organization.nonEmpty && parentModule.name.nonEmpty && parentModule.revision.nonEmpty) {
// TODO don't use coursir cache dir directory
// TODO maven central only
val parentPom = Seq[String](
csHome.getCanonicalPath,
"https/repo1.maven.org/maven2",
parentModule.organization.split('.').mkString("/"),
parentModule.name,
parentModule.revision,
s"${parentModule.name}-${parentModule.revision}.pom"
).mkString("/")
Seq(XML.loadFile(parentPom))
} else {
Nil
}
}
def allParent(x: scala.xml.Elem, csHome: File): Seq[scala.xml.Elem] = {
parentPomXml(x, csHome).flatMap { a =>
a +: allParent(a, csHome)
}
}
private def findLicense(
jarPath: File,
dependencyResolutionValue: DependencyResolution,
log: ManagedLogger
): Def.Initialize[Seq[LicenseValue]] = Def.setting {
val csHome = csrCacheDirectory.value
val pomPath = jarPathToPomPath(jarPath)
if (file(pomPath).isFile) {
val simpleJarPath = pomPath.split("repo1.maven.org/maven2/").last
val pomFile = XML.loadFile(pomPath)
val parent = allParent(pomFile, csHome)
val licenses = (pomFile ++ parent).map(x => x \\ "license")
val values = licenses
.map(x =>
LicenseValue(
url = (x \ "url").text.trim,
name = (x \ "name").text.trim,
lib = simpleJarPath
)
)
.filter(_.name.nonEmpty)
.distinct
.sorted
if (values.isEmpty) {
log.warn(s"not found ${simpleJarPath}")
}
values
} else {
sys.error(s"not found ${pomPath}")
}
}
override def projectSettings: Seq[Def.Setting[?]] = Def.settings(
fastLicense := {
val result = Def.taskDyn {
Seq(
(Compile / externalDependencyClasspath).value,
(Test / externalDependencyClasspath).value
).flatten
.map(_.data)
.filterNot { x =>
allUnmanagedJars.value.contains(x)
}
.map(x => findLicense(x, dependencyResolution.value, streams.value.log))
.join
.map(_.flatten)
}.value
streams.value.log.info(s"[${name.value}] found ${result.size} values")
result
}
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment