Skip to content

Commit

Permalink
Merge pull request #86 from mdedetrich/use-dependency-resolver
Browse files Browse the repository at this point in the history
Use DependencyResolver instead of IvySbt#Module directly
  • Loading branch information
eed3si9n authored Sep 13, 2023
2 parents 7150102 + da82f08 commit 5bdab60
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 83 deletions.
11 changes: 4 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
lazy val lang3 = "org.apache.commons" % "commons-lang3" % "3.12.0"
lazy val repoSlug = "sbt/sbt-license-report"

crossScalaVersions := Seq("2.12.17", "2.10.7")
val scala212 = "2.12.18"

scalaVersion := scala212
crossScalaVersions := Seq(scala212)
organization := "com.github.sbt"
name := "sbt-license-report"
enablePlugins(SbtPlugin)
libraryDependencies += lang3
scriptedLaunchOpts += s"-Dplugin.version=${version.value}"
pluginCrossBuild / sbtVersion := {
scalaBinaryVersion.value match {
case "2.10" => "0.13.18"
case "2.12" => "1.2.8" // set minimum sbt version
}
}

// publishing info
licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.html"))
Expand Down
3 changes: 0 additions & 3 deletions src/main/scala-2.10/sbtlicensereport/SbtCompat.scala

This file was deleted.

8 changes: 0 additions & 8 deletions src/main/scala-2.12/sbtlicensereport/SbtCompat.scala

This file was deleted.

2 changes: 2 additions & 0 deletions src/main/scala/sbtlicensereport/SbtLicenseReport.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sbtlicensereport

import sbt._
import sbt.librarymanagement.ivy.IvyDependencyResolution
import Keys._
import license._

Expand Down Expand Up @@ -87,6 +88,7 @@ object SbtLicenseReport extends AutoPlugin {
val originatingModule = DepModuleInfo(organization.value, name.value, version.value)
license.LicenseReport.makeReport(
ivyModule.value,
IvyDependencyResolution(ivyConfiguration.value),
licenseConfigurations.value,
licenseSelection.value,
overrides,
Expand Down
145 changes: 80 additions & 65 deletions src/main/scala/sbtlicensereport/license/LicenseReport.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package sbtlicensereport
package license

import org.apache.ivy.core.report.ResolveReport
import org.apache.ivy.core.resolve.IvyNode
import sbt._
import scala.util.control.Exception._
import sbtlicensereport.SbtCompat._
import sbt.io.Using
import sbt.internal.librarymanagement.IvySbt
import sbt.librarymanagement.{
DependencyResolution,
UnresolvedWarning,
UnresolvedWarningConfiguration,
UpdateConfiguration
}

case class DepModuleInfo(organization: String, name: String, version: String) {
override def toString = s"${organization} # ${name} # ${version}"
Expand Down Expand Up @@ -33,7 +37,7 @@ object DepLicense {
}
}

case class LicenseReport(licenses: Seq[DepLicense], orig: ResolveReport) {
case class LicenseReport(licenses: Seq[DepLicense], orig: UpdateReport) {
override def toString = s"""|## License Report ##
|${licenses.mkString("\t", "\n\t", "\n")}
|""".stripMargin
Expand Down Expand Up @@ -107,23 +111,30 @@ object LicenseReport {
}
}

private def getModuleInfo(dep: IvyNode): DepModuleInfo = {
private def getModuleInfo(dep: ModuleReport): DepModuleInfo = {
// TODO - null handling...
DepModuleInfo(dep.getModuleId.getOrganisation, dep.getModuleId.getName, dep.getModuleRevision.getId.getRevision)
DepModuleInfo(dep.module.organization, dep.module.name, dep.module.revision)
}

def makeReport(
module: IvySbt#Module,
depRes: DependencyResolution,
configs: Set[String],
licenseSelection: Seq[LicenseCategory],
overrides: DepModuleInfo => Option[LicenseInfo],
exclusions: DepModuleInfo => Option[Boolean],
originatingModule: DepModuleInfo,
log: Logger
): LicenseReport = {
val (report, err) = resolve(module, log)
err foreach (x => throw x) // Bail on error
makeReportImpl(report, configs, licenseSelection, overrides, exclusions, originatingModule, log)
// Ideally we should be using just standard sbt update task however due to
// https://github.com/coursier/coursier/issues/1790 coursier cannot correctly
// resolve license information from Ivy modules, so instead we just use
// IvyDependencyResolution directly
val updateReport = resolve(depRes, module, log) match {
case Left(exception) => throw exception.resolveException
case Right(updateReport) => updateReport
}
makeReportImpl(updateReport, configs, licenseSelection, overrides, exclusions, originatingModule, log)
}

/**
Expand All @@ -132,71 +143,83 @@ object LicenseReport {
*/
private def pickLicense(
categories: Seq[LicenseCategory]
)(licenses: Array[org.apache.ivy.core.module.descriptor.License]): LicenseInfo = {
if (licenses.isEmpty) {
)(licenses: Vector[(String, Option[String])]): LicenseInfo = {
// Even though the url is optional this field seems to always exist
val licensesWithUrls = licenses.collect { case (name, Some(url)) => (name, url) }
if (licensesWithUrls.isEmpty) {
return LicenseInfo(LicenseCategory.NoneSpecified, "", "")
}
// We look for a license matching the category in the order they are defined.
// i.e. the user selects the licenses they prefer to use, in order, if an artifact is dual-licensed (or more)
for (category <- categories) {
for (license <- licenses) {
if (category.unapply(license.getName)) {
return LicenseInfo(category, license.getName, license.getUrl)
for (license <- licensesWithUrls) {
val (name, url) = license
if (category.unapply(name)) {
return LicenseInfo(category, name, url)
}
}
}
val license = licenses(0)
LicenseInfo(LicenseCategory.Unrecognized, license.getName, license.getUrl)
val license = licensesWithUrls(0)
LicenseInfo(LicenseCategory.Unrecognized, license._1, license._2)
}

/** Picks a single license (or none) for this dependency. */
private def pickLicenseForDep(
dep: IvyNode,
dep: ModuleReport,
configs: Set[String],
categories: Seq[LicenseCategory],
originatingModule: DepModuleInfo
): Option[DepLicense] =
for {
d <- Option(dep)
cs = dep.getRootModuleConfigurations.toSet
filteredConfigs = if (cs.isEmpty) cs else cs.filter(configs)
if !filteredConfigs.isEmpty
if !filteredConfigs.forall(d.isEvicted)
desc <- Option(dep.getDescriptor)
licenses = Option(desc.getLicenses)
.filterNot(_.isEmpty)
.getOrElse(Array(new org.apache.ivy.core.module.descriptor.License("none specified", "none specified")))
homepage = Option
.apply(desc.getHomePage)
.flatMap(loc =>
nonFatalCatch[Option[URL]]
.withApply((_: Throwable) => Option.empty[URL])
.apply(Some(url(loc)))
): Option[DepLicense] = {
val cs = dep.configurations
val filteredConfigs = if (cs.isEmpty) cs else cs.filter(configs.map(ConfigRef.apply))

if (dep.evicted || filteredConfigs.isEmpty)
None
else {
val licenses = dep.licenses
val homepage = dep.homepage.map(string => new URL(string))
Some(
DepLicense(
getModuleInfo(dep),
pickLicense(categories)(licenses),
homepage,
filteredConfigs.map(_.name).toSet,
originatingModule
)
// TODO - grab configurations.
} yield DepLicense(
getModuleInfo(dep),
pickLicense(categories)(licenses),
homepage,
filteredConfigs,
originatingModule
)
)
}
}

// TODO: Use https://github.com/sbt/librarymanagement/pull/428 instead when merged and released
private def moduleKey(m: ModuleID) = (m.organization, m.name, m.revision)

private def allModuleReports(configurations: Vector[ConfigurationReport]): Vector[ModuleReport] =
configurations.flatMap(_.modules).groupBy(mR => moduleKey(mR.module)).toVector map { case (_, v) =>
v reduceLeft { (agg, x) =>
agg.withConfigurations(
(agg.configurations, x.configurations) match {
case (v, _) if v.isEmpty => x.configurations
case (ac, v) if v.isEmpty => ac
case (ac, xc) => ac ++ xc
}
)
}
}

private def getLicenses(
report: ResolveReport,
report: UpdateReport,
configs: Set[String] = Set.empty,
categories: Seq[LicenseCategory] = LicenseCategory.all,
originatingModule: DepModuleInfo
): Seq[DepLicense] = {
import collection.JavaConverters._
for {
dep <- report.getDependencies.asInstanceOf[java.util.List[IvyNode]].asScala
dep <- allModuleReports(report.configurations)
report <- pickLicenseForDep(dep, configs, categories, originatingModule)
} yield report
}

private def makeReportImpl(
report: ResolveReport,
report: UpdateReport,
configs: Set[String],
categories: Seq[LicenseCategory],
overrides: DepModuleInfo => Option[LicenseInfo],
Expand All @@ -216,22 +239,14 @@ object LicenseReport {
LicenseReport(licenses, report)
}

// Hacky way to go re-lookup the report
private def resolve(module: IvySbt#Module, log: Logger): (ResolveReport, Option[ResolveException]) =
module.withModule(log) { (ivy, desc, default) =>
import org.apache.ivy.core.resolve.ResolveOptions
val resolveOptions = new ResolveOptions
val resolveId = ResolveOptions.getDefaultResolveId(desc)
resolveOptions.setResolveId(resolveId)
import org.apache.ivy.core.LogOptions.LOG_QUIET
resolveOptions.setLog(LOG_QUIET)
val resolveReport = ivy.resolve(desc, resolveOptions)
val err =
if (resolveReport.hasError) {
val messages = resolveReport.getAllProblemMessages.toArray.map(_.toString).distinct
val failed = resolveReport.getUnresolvedDependencies.map(node => IvyRetrieve.toModuleID(node.getId))
Some(new ResolveException(messages, failed))
} else None
(resolveReport, err)
}
private def resolve(
depRes: DependencyResolution,
module: IvySbt#Module,
log: Logger
): Either[UnresolvedWarning, UpdateReport] = {
val uc = UpdateConfiguration().withLogging(UpdateLogging.DownloadOnly)
val uwc = UnresolvedWarningConfiguration()

depRes.update(module, uc, uwc, log)
}
}

0 comments on commit 5bdab60

Please sign in to comment.