Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use DependencyResolver instead of IvySbt#Module directly #86

Merged
merged 1 commit into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}