mirror of https://github.com/sbt/sbt.git
[2.x] feat: dependencyLicenseInfo (#8506)
* Add JSON output support to dependencyLicenseInfo - Add LicenseInfo rendering object with text and JSON output - Add dependencyLicenseInfo input task key - Implement dependencyLicenseInfo task with JSON format support - Supports --out option for file output - Auto-detects JSON format from .json file extension - Follows same pattern as dependencyTree task Resolves #7771
This commit is contained in:
parent
847703cd5e
commit
aeabb90d2d
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2023, Scala center
|
||||||
|
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt
|
||||||
|
package internal
|
||||||
|
package graph
|
||||||
|
package rendering
|
||||||
|
|
||||||
|
import sbt.internal.graph.*
|
||||||
|
|
||||||
|
object LicenseInfo {
|
||||||
|
def render(graph: ModuleGraph): String =
|
||||||
|
graph.nodes
|
||||||
|
.filter(_.isUsed)
|
||||||
|
.groupBy(_.license)
|
||||||
|
.toSeq
|
||||||
|
.sortBy(_._1)
|
||||||
|
.map { case (license, modules) =>
|
||||||
|
license.getOrElse("No license specified") + "\n" +
|
||||||
|
modules.map(m => s"\t ${m.id.idString}").mkString("\n")
|
||||||
|
}
|
||||||
|
.mkString("\n\n")
|
||||||
|
|
||||||
|
def renderJson(graph: ModuleGraph): String = {
|
||||||
|
// Create JSON array manually: [{license: "...", modules: [...]}, ...]
|
||||||
|
def escapeJson(str: String): String =
|
||||||
|
str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r")
|
||||||
|
|
||||||
|
def formatModuleList(modules: Vector[String]): String =
|
||||||
|
modules.map(m => s""""${escapeJson(m)}"""").mkString("[", ",", "]")
|
||||||
|
|
||||||
|
val groups = graph.nodes
|
||||||
|
.filter(_.isUsed)
|
||||||
|
.groupBy(_.license)
|
||||||
|
.toSeq
|
||||||
|
.sortBy(_._1)
|
||||||
|
.map { case (license, modules) =>
|
||||||
|
val licenseStr = license.getOrElse("No license specified")
|
||||||
|
val moduleList = formatModuleList(modules.map(_.id.idString).toVector.sorted)
|
||||||
|
s"""{"license":"${escapeJson(licenseStr)}","modules":$moduleList}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.mkString("[", ",", "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,8 @@ abstract class DependencyTreeKeys:
|
||||||
private[sbt] val dependencyTreeModuleGraphStore =
|
private[sbt] val dependencyTreeModuleGraphStore =
|
||||||
taskKey[ModuleGraph]("The stored module-graph from the last run")
|
taskKey[ModuleGraph]("The stored module-graph from the last run")
|
||||||
val whatDependsOn = inputKey[String]("Shows information about what depends on the given module")
|
val whatDependsOn = inputKey[String]("Shows information about what depends on the given module")
|
||||||
|
val dependencyLicenseInfo =
|
||||||
|
inputKey[String]("Displays license information for dependencies in text or JSON format")
|
||||||
private[sbt] val dependencyTreeCrossProjectId = settingKey[ModuleID]("")
|
private[sbt] val dependencyTreeCrossProjectId = settingKey[ModuleID]("")
|
||||||
|
|
||||||
// 0 was added to avoid conflict with sbt-dependency-tree
|
// 0 was added to avoid conflict with sbt-dependency-tree
|
||||||
|
|
|
||||||
|
|
@ -254,8 +254,52 @@ OPTIONS
|
||||||
}
|
}
|
||||||
output
|
output
|
||||||
},
|
},
|
||||||
|
dependencyLicenseInfo := (Def.inputTaskDyn {
|
||||||
|
val s = streams.value
|
||||||
|
val args = ArgsParser.parsed.toList
|
||||||
|
val isHelp = args.contains(Arg.Help)
|
||||||
|
val isQuiet = args.contains(Arg.Quiet)
|
||||||
|
if isHelp then Def.task { s.log.info(licenseInfoUsageText); "" }
|
||||||
|
else
|
||||||
|
val formatOpt = (args
|
||||||
|
.collect { case Arg.Format(fmt) => fmt })
|
||||||
|
.reverse
|
||||||
|
.headOption
|
||||||
|
val outFileNameOpt = (args
|
||||||
|
.collect { case Arg.Out(out) => out })
|
||||||
|
.reverse
|
||||||
|
.headOption
|
||||||
|
val outFileOpt = outFileNameOpt.map(new File(_))
|
||||||
|
val format = (formatOpt, outFileNameOpt) match
|
||||||
|
case (None, Some(out)) if out.endsWith(".json") => Fmt.Json
|
||||||
|
case (Some(fmt), _) => fmt
|
||||||
|
case _ => Fmt.Tree
|
||||||
|
Def.task {
|
||||||
|
val graph = dependencyTreeModuleGraph0.value
|
||||||
|
val output = format match
|
||||||
|
case Fmt.Json => rendering.LicenseInfo.renderJson(graph)
|
||||||
|
case _ => rendering.LicenseInfo.render(graph)
|
||||||
|
handleOutput(output, outFileOpt, isQuiet, s.log)
|
||||||
|
}
|
||||||
|
}).evaluated,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def licenseInfoUsageText: String =
|
||||||
|
s"""dependencyLicenseInfo task displays license information for dependencies.
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
dependencyLicenseInfo [subcommand] [options]
|
||||||
|
|
||||||
|
SUBCOMMAND
|
||||||
|
json Prints JSON (default is text)
|
||||||
|
help Prints this help
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
--quiet Returns the output as task value
|
||||||
|
--out <file> Writes the output to the specified file;
|
||||||
|
The file extension will influence the default subcommand
|
||||||
|
"""
|
||||||
|
|
||||||
private def handleOutput(
|
private def handleOutput(
|
||||||
content: String,
|
content: String,
|
||||||
outputFileOpt: Option[File],
|
outputFileOpt: Option[File],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
name := "license-info-test"
|
||||||
|
scalaVersion := "3.3.1"
|
||||||
|
|
||||||
|
libraryDependencies += "org.scala-lang" % "scala-library" % scalaVersion.value
|
||||||
|
|
||||||
|
TaskKey[Unit]("check") := {
|
||||||
|
import java.io.File
|
||||||
|
import sbt.io.IO
|
||||||
|
|
||||||
|
// Check text file
|
||||||
|
val textFile = new File("target/licenses.txt")
|
||||||
|
require(textFile.exists(), s"Text file ${textFile.getPath} does not exist")
|
||||||
|
val textContent = IO.read(textFile)
|
||||||
|
require(textContent.nonEmpty, "Text file is empty")
|
||||||
|
require(textContent.contains("scala-library") || textContent.contains("No license specified"),
|
||||||
|
"Text file should contain license information")
|
||||||
|
|
||||||
|
// Check JSON file
|
||||||
|
val jsonFile = new File("target/licenses.json")
|
||||||
|
require(jsonFile.exists(), s"JSON file ${jsonFile.getPath} does not exist")
|
||||||
|
val jsonContent = IO.read(jsonFile)
|
||||||
|
require(jsonContent.nonEmpty, "JSON file is empty")
|
||||||
|
require(jsonContent.trim.startsWith("[") && jsonContent.trim.endsWith("]"),
|
||||||
|
"JSON file should be a valid JSON array")
|
||||||
|
require(jsonContent.contains("\"license\"") && jsonContent.contains("\"modules\""),
|
||||||
|
"JSON file should contain license and modules fields")
|
||||||
|
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Test dependencyLicenseInfo text output
|
||||||
|
> dependencyLicenseInfo --out target/licenses.txt
|
||||||
|
$ exists target/licenses.txt
|
||||||
|
# Test dependencyLicenseInfo JSON output
|
||||||
|
> dependencyLicenseInfo json --out target/licenses.json
|
||||||
|
$ exists target/licenses.json
|
||||||
|
> check
|
||||||
|
|
||||||
Loading…
Reference in New Issue