Created
September 6, 2024 23:17
-
-
Save xuwei-k/5ea6e1cb00c425d754b3be54d93e960c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.6.1") | |
scalacOptions += "-Xsource:3" | |
enablePlugins(SbtPlugin) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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