Licensing can now load license data from dependencies. There is master list for uncontrolled dependencies, and if a project ALSO uses this plugin, it's License info will be automatically added.

master
nathan 2020-08-06 01:59:55 +02:00
parent 5a16322ed5
commit 823d6b25e5
10 changed files with 868 additions and 145 deletions

View File

@ -1,5 +1,4 @@
- Licensing -
- Licensing - License definitions and legal management plugin for the Gradle build system
https://git.dorkbox.com/dorkbox/Licensing
Copyright 2019 - The Apache Software License, Version 2.0
Copyright 2020 - The Apache Software License, Version 2.0
Dorkbox LLC
License definitions and legal management plugin for the Gradle build system

View File

@ -16,25 +16,24 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.time.Instant
println("Gradle ${project.gradle.gradleVersion}")
plugins {
java
`java-gradle-plugin`
id("com.gradle.plugin-publish") version "0.10.1"
id("com.dorkbox.Licensing") version "1.4"
id("com.dorkbox.VersionUpdate") version "1.4.1"
id("com.dorkbox.GradleUtils") version "1.2"
id("com.gradle.plugin-publish") version "0.12.0"
kotlin("jvm") version "1.3.21"
id("com.dorkbox.VersionUpdate") version "1.7"
id("com.dorkbox.GradleUtils") version "1.8"
kotlin("jvm") version "1.3.72"
}
object Extras {
// set for the project
const val description = "License definitions and legal management plugin for the Gradle build system"
const val group = "com.dorkbox"
const val version = "1.4.1"
const val version = "2.0"
// set as project.ext
const val name = "Gradle Licensing Plugin"
@ -52,17 +51,7 @@ object Extras {
///// assign 'Extras'
///////////////////////////////
GradleUtils.load("$projectDir/../../gradle.properties", Extras)
description = Extras.description
group = Extras.group
version = Extras.version
licensing {
license(License.APACHE_2) {
author(Extras.vendor)
url(Extras.url)
note(Extras.description)
}
}
GradleUtils.fixIntellijPaths()
sourceSets {
main {
@ -83,14 +72,18 @@ sourceSets {
}
repositories {
mavenLocal() // this must be first!
jcenter()
maven {
url = uri("https://plugins.gradle.org/m2/")
}
}
dependencies {
// the kotlin version is taken from the plugin, so it is not necessary to set it here
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin")
implementation ("com.dorkbox:Version:1.0")
implementation("com.dorkbox:Version:1.2")
}
java {
@ -152,3 +145,4 @@ pluginBundle {
}
}
}

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

0
gradlew vendored Executable file → Normal file
View File

View File

@ -0,0 +1,301 @@
package dorkbox.license
import License
import com.dorkbox.version.Version
import org.gradle.api.IllegalDependencyNotation
/**
* Creates a license chain, based on the mavenId, so other license info can be looked up backed on THAT maven ID
*
* For example...
* :: "com.dorkbox:Version:1.0", with license data as APACHE_2
* :: "com.dorkbox:Version:2.0", with license data as GPL_3
* "com.dorkbox:Version:1.0" -> it will return APACHE_2
* "com.dorkbox:Version:2.0" -> it will return GPL_3
* "com.dorkbox:Version:3.0" -> it will return GPL_3
*
* :: "com.dorkbox:Version", with license data as APACHE_2
* "com.dorkbox:Version:1.0" -> it will return APACHE_2
* "com.dorkbox:Version:2.0" -> it will return APACHE_2
* "com.dorkbox:Version:3.0" -> it will return APACHE_2
*
* This will return the Version project license info, because ALL group license info will be collapsed to a single license!! BE CAREFUL!
* :: "com.dorkbox", with license data as APACHE_2
* "com.dorkbox:Version:1.0" -> it will return APACHE_2
* "com.dorkbox:Version:2.0" -> it will return APACHE_2
* "com.dorkbox:Console:2.0" -> it will return APACHE_2, AND it will return the Version project license info!! DO NOT DO THIS!
*
*/
data class LicenseChain(val mavenId: String, val licenseData: LicenseData)
object AppLicensing {
// have to add the version this license applies from
private val allLicenseData = mutableMapOf<String, MutableList<Pair<Version, LicenseData>>>()
// NOTE: the END copyright for these are determined by the DATE of the files!
//
// Super important! These are dependency projects -- The only requirement (in the most general, permissive way) is that we provide
// attribution to each. HOWEVER, if there is a library that DOES NOT provide proper/correct attributions to THEMSELVES (meaning, they are
// a modification to another library and do not credit that library) -- then we are not transversly in violation. THEY are in violation,
// and not us, since we are correctly attributing their work (they are incorrectly attributing whatever THEY should).
//
// We DO NOT have to maintain a FULL HISTORY CHAIN of contributions of all dependent libraries -- only the library + license that we use.
private val map = listOf(
LicenseChain("org.jetbrains.kotlin",
LicenseData("Kotlin", License.APACHE_2).apply {
copyright(2000)
author("JetBrains s.r.o. and Kotlin Programming Language contributors")
url("https://github.com/JetBrains/kotlin")
note("Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply")
note("See: https://github.com/JetBrains/kotlin/blob/master/license/README.md")
}
),
LicenseChain("org.jetbrains:annotations",
LicenseData("Java Annotations", License.APACHE_2).apply {
description("Annotations for JVM-based languages")
url("https://github.com/JetBrains/java-annotations")
copyright(2000)
author("JetBrains s.r.o.")
}
),
LicenseChain("org.jetbrains.kotlinx",
LicenseData("kotlinx.coroutines", License.APACHE_2).apply {
description("Library support for Kotlin coroutines with multiplatform support")
url("https://github.com/Kotlin/kotlinx.coroutines")
copyright(2000)
author("JetBrains s.r.o.")
}
),
LicenseChain("io.github.microutils:kotlin-logging",
LicenseData("kotlin-logging", License.APACHE_2).apply {
description("Lightweight logging framework for Kotlin")
url("https://github.com/MicroUtils/kotlin-logging")
copyright(2016)
author("Ohad Shai")
}
),
LicenseChain("org.slf4j:slf4j-api",
LicenseData("SLF4J", License.MIT).apply {
description("Simple facade or abstraction for various logging frameworks")
url("http://www.slf4j.org")
copyright(2004)
author("QOS.ch")
}
),
LicenseChain("net.java.dev.jna:jna:1.0",
LicenseData("JNA", License.LGPLv2_1).apply {
description("Simplified native library access for Java.")
url("https://github.com/twall/jna")
copyright(2004)
author("Timothy Wall")
}
),
LicenseChain("net.java.dev.jna:jna:4.0",
LicenseData("JNA", License.APACHE_2).apply {
description("Simplified native library access for Java.")
url("https://github.com/twall/jna")
copyright(2013)
author("Timothy Wall")
}
),
LicenseChain("net.java.dev.jna:jna-platform:1.0",
LicenseData("JNA-Platform", License.LGPLv2_1).apply {
description("Mappings for a number of commonly used platform functions")
url("https://github.com/twall/jna")
copyright(2013)
author("Timothy Wall")
}
),
LicenseChain("net.java.dev.jna:jna-platform:4.0",
LicenseData("JNA-Platform", License.APACHE_2).apply {
description("Mappings for a number of commonly used platform functions")
url("https://github.com/twall/jna")
copyright(2013)
author("Timothy Wall")
}
),
LicenseChain("com.hierynomus:sshj",
LicenseData("SSHJ", License.APACHE_2).apply {
description("SSHv2 library for Java")
url("https://github.com/hierynomus/sshj")
copyright(2009)
author("Jeroen van Erp")
author("SSHJ Contributors")
extra("Apache MINA", License.APACHE_2) {
it.url("https://mina.apache.org/sshd-project/")
it.copyright(2003).to(2017)
it.author("The Apache Software Foundation")
}
extra("Apache Commons-Net", License.APACHE_2) {
it.url("https://commons.apache.org/proper/commons-net/")
it.copyright(2001).to(2017)
it.author("The Apache Software Foundation")
}
extra("JZlib", License.APACHE_2) {
it.url("http://www.jcraft.com/jzlib")
it.copyright(2002).to(2008)
it.author("Atsuhiko Yamanaka")
it.author("JCraft, Inc.")
}
extra("Bouncy Castle Crypto", License.APACHE_2) {
it.url("http://www.bouncycastle.org")
it.copyright(2000).to(2006)
it.author("The Legion of the Bouncy Castle Inc")
}
extra("ed25519-java", License.CC0) {
it.url("https://github.com/str4d/ed25519-java")
it.author("https://github.com/str4d")
}
}
),
LicenseChain("org.bouncycastle",
LicenseData("Bouncy Castle Crypto", License.APACHE_2).apply {
description("Lightweight cryptography API and JCE Extension")
copyright(2000)
author("The Legion of the Bouncy Castle Inc")
url("http://www.bouncycastle.org")
}
),
LicenseChain("com.fasterxml.uuid:java-uuid-generator",
LicenseData("Java Uuid Generator", License.APACHE_2).apply {
description("A set of Java classes for working with UUIDs")
copyright(2002)
author("Tatu Saloranta (tatu.saloranta@iki.fi)")
author("Contributors. See source release-notes/CREDITS")
url("https://github.com/cowtowncoder/java-uuid-generator")
}
),
LicenseChain("org.tukaani:xz",
LicenseData("XZ for Java", License.CC0).apply {
description("Complete implementation of XZ data compression in pure Java")
author("Lasse Collin")
author("Igor Pavlov")
url("https://tukaani.org/xz/java.html")
}
),
LicenseChain("io.netty",
LicenseData("Netty", License.APACHE_2).apply {
description("An event-driven asynchronous network application framework")
copyright(2014)
author("The Netty Project")
author("Contributors. See source NOTICE")
url("https://netty.io")
}
),
LicenseChain("org.lwjgl:lwjgl-xxhash",
LicenseData("Lightweight Java Game Library", License.BSD_3).apply {
description("Java library that enables cross-platform access to popular native APIs")
copyright(2012)
author("Lightweight Java Game Library")
url("https://github.com/LWJGL/lwjgl3")
}
),
LicenseChain("net.jodah:typetools",
LicenseData("TypeTools", License.APACHE_2).apply {
description("A simple, zero-dependency library for working with types. Supports Java 1.6+ and Android.")
copyright(2010)
author("Jonathan Halterman and friends")
url("https://github.com/jhalterman/typetools")
}
)
)
// NOTE: the END copyright for these are determined by the DATE of the files!
init {
map.forEach {
license(it)
}
}
// NOTE: generated license information copyright date is based on the DATE of the manifest file in the jar!
fun getLicense(name: String): LicenseData? {
var (moduleId, version) = getFromModuleName(name)
var internalList = allLicenseData[moduleId]
var offset = 1
while (internalList == null && offset < 3) {
// try using simpler module info (since we can specify more generic license info)
val thing = getFromModuleName(name, offset++)
moduleId = thing.first
version = thing.second
internalList = allLicenseData[moduleId]
}
// println(" - found list entries")
internalList?.forEach {
// println(" - checking $version against ${it.first}")
// if MY version is >= to the version saved in our internal DB, then we use that license
if (version.greaterThanOrEqualTo(it.first)) {
return it.second
}
}
return null
}
private fun license(licenseChain: LicenseChain) {
val (moduleId, version) = getFromModuleName(licenseChain.mavenId)
val internalList = allLicenseData.getOrPut(moduleId) { mutableListOf() }
internalList.add(Pair(version, licenseChain.licenseData))
// largest version number is first, smallest version number is last.
// when checking WHAT license applies to WHICH version, we start at the largest (so we stop looking at the first match <= to us)
// this is because if X.Y=MIT, then X.Y+1=MIT and X+1.Y+1=MIT
// a real-world example is JNA. JNA v3 -> GPL, JNA v4 -> APACHE
// to demonstrate:
// v1 -> GPL
// v4 -> MIT
// v8 -> APACHE
// We are version 6, so we are MIT
// We are version 12, so we are APACHE
// We are version 2, so we are GPL
internalList.sortByDescending { it.first.toString() }
}
private fun getFromModuleName(fullName: String, override: Int = 0) : Pair<String, Version> {
val split = fullName.split(':')
val moduleId = when ((split.size - override).coerceAtLeast(0)) {
0 -> {
fullName
}
1 -> {
split[0]
}
else -> {
val group = split[0]
val name = split[1]
"$group:$name"
}
}
val version = when (split.size) {
3 -> Version.from(split[2])
else -> Version.from(0)
}
if (split.size > 4) {
throw IllegalDependencyNotation("Supplied String module notation '${moduleId}' is invalid. " +
"Example notations: 'com.dorkbox:Version:1.0', 'org.mockito:mockito-core:1.9.5:javadoc'")
}
// println("Got: $moduleId, $version from $fullName")
return Pair(moduleId, version)
}
}

View File

@ -0,0 +1,222 @@
package dorkbox.license
import org.gradle.api.Project
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedDependency
import java.io.*
import java.time.Instant
import java.time.ZoneId
import java.util.*
import java.util.concurrent.Executor
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
class DependencyScanner(private val project: Project, private val extension: Licensing) {
fun scanForLicenseData() {
// The default configuration extends from the runtime configuration, which means that it contains all the dependencies and artifacts of the runtime configuration, and potentially more.
// THIS MUST BE IN "afterEvaluate" or run from a specific task.
// Using the "runtime" classpath (weirdly) DOES NOT WORK. Only "default" works.
val projectDependencies = mutableListOf<Dependency>()
val existingNames = mutableSetOf<String>()
project.configurations.getByName("default").resolvedConfiguration.firstLevelModuleDependencies.forEach { dep ->
// we know the FIRST series will exist
val makeDepTree = makeDepTree(dep, existingNames)
if (makeDepTree != null) {
// it's only null if we've ALREADY scanned it
projectDependencies.add(makeDepTree)
}
}
val missingLicenseInfo = mutableListOf<Dependency>()
val actuallyMissingLicenseInfo = mutableListOf<Dependency>()
if (extension.licenses.isNotEmpty()) {
// when we scan, we ONLY want to scan a SINGLE LAYER (if we have license info for module ID, then we don't need license info for it's children)
println("\tLicense Detection")
print("\t\tScanning for preloaded license data...")
val primaryLicense = extension.licenses.first()
var found = false
projectDependencies.forEach { info: Dependency ->
val license: LicenseData? = AppLicensing.getLicense(info.mavenId())
if (license == null) {
missingLicenseInfo.add(info)
} else {
if (!primaryLicense.extras.contains(license)) {
// get the OLDEST date from the artifacts and use that as the copyright date
var oldestDate = 0L
info.artifacts.forEach { artifact ->
// get the date of the manifest file (which is the first entry)
ZipInputStream(FileInputStream(artifact.file)).use {
oldestDate = oldestDate.coerceAtLeast(it.nextEntry.lastModifiedTime.toMillis())
}
}
if (oldestDate == 0L) {
oldestDate = Instant.now().toEpochMilli()
}
val year = Date(oldestDate).toInstant().atZone(ZoneId.systemDefault()).toLocalDate().year
if (license.copyrights.size == 1) {
CopyrightRange(license.copyrights.first(), license.copyrights).to(year)
} else {
license.copyright(year)
}
primaryLicense.extras.add(license)
}
}
}
if (found) {
found = false
println(" found")
} else {
println()
}
print("\t\tScanning for embedded license data...")
if (missingLicenseInfo.isNotEmpty()) {
missingLicenseInfo.forEach { info ->
// see if we have it in the dependency jar
val output = ByteArrayOutputStream()
var missingFound = false
info.artifacts.forEach { artifact ->
ZipFile(artifact.file).use {
try {
val ze = it.getEntry(LicenseInjector.LICENSE_BLOB)
if (ze != null) {
it.getInputStream(ze).use { licenseStream ->
licenseStream.copyTo(output)
missingFound = true
found = true
return@forEach
}
}
}
catch (ignored: Exception) {
}
}
}
if (!missingFound) {
actuallyMissingLicenseInfo.add(info)
} else {
// println("Found license info in: $info")
ObjectInputStream(ByteArrayInputStream(output.toByteArray())).use { ois ->
val size = ois.readInt()
for (i in 0 until size) {
val license = LicenseData("", License.CUSTOM)
license.readObject(ois)
// println("Adding license: $license")
if (!primaryLicense.extras.contains(license)) {
primaryLicense.extras.add(license)
}
}
}
}
}
}
if (found) {
found = false
println(" found")
} else {
println()
}
if (actuallyMissingLicenseInfo.isNotEmpty()) {
println("\t\tLicense information is missing for the following. Please submit an issue with this information to include it in future license scans\n")
actuallyMissingLicenseInfo.forEach { missingDep ->
val flatDependencies = mutableSetOf<Dependency>()
missingDep.children.forEach {
flattenDep(it, flatDependencies)
}
println("\t\t ${missingDep.mavenId()} ${flatDependencies.map { it.mavenId() }}")
}
}
}
}
// how to resolve dependencies
// NOTE: it is possible, when we have a project DEPEND on an older version of that project (ie: bootstrapped from an older version)
// we can have infinite recursion.
// This is a problem, so we limit how much a dependency can show up the the tree
private fun makeDepTree(dep: ResolvedDependency, existingNames: MutableSet<String>): Dependency? {
val module = dep.module.id
val group = module.group
val name = module.name
val version = module.version
if (!existingNames.contains("$group:$name")) {
// println("Searching: $group:$name:$version")
val artifacts: List<DependencyInfo> = dep.moduleArtifacts.map { artifact: ResolvedArtifact ->
val artifactModule = artifact.moduleVersion.id
DependencyInfo(artifactModule.group, artifactModule.name, artifactModule.version, artifact.file.absoluteFile)
}
val children = mutableListOf<Dependency>()
dep.children.forEach {
existingNames.add("$group:$name")
val makeDep = makeDepTree(it, existingNames)
if (makeDep != null) {
children.add(makeDep)
}
}
return Dependency(group, name, version, artifacts, children.toList())
}
// we already have this dependency in our chain.
return null
}
/**
* Flatten the dependency children
*/
fun flattenDeps(dep: Dependency): List<Dependency> {
val flatDeps = mutableSetOf<Dependency>()
flattenDep(dep, flatDeps)
return flatDeps.toList()
}
private fun flattenDep(dep: Dependency, flatDeps: MutableSet<Dependency>) {
flatDeps.add(dep)
dep.children.forEach {
flattenDep(it, flatDeps)
}
}
data class Dependency(val group: String,
val name: String,
val version: String,
val artifacts: List<DependencyInfo>,
val children: List<Dependency>) {
fun mavenId(): String {
return "$group:$name:$version"
}
override fun toString(): String {
return mavenId()
}
}
data class DependencyInfo(val group: String, val name: String, val version: String, val file: File) {
val id: String
get() {
return "$group:$name:$version"
}
}
}

View File

@ -16,9 +16,27 @@
package dorkbox.license
import License
import org.gradle.api.Action
import org.gradle.api.GradleException
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.time.LocalDate
class LicenseData(val name: String, val license: License) : Comparable<LicenseData> {
class LicenseData(var name: String, var license: License) : java.io.Serializable, Comparable<LicenseData> {
/**
* Description/title
*/
var description = ""
/**
* If not specified, will be blank after the name
*/
fun description(description: String) {
this.description = description
}
/**
* Copyright
*/
@ -27,8 +45,9 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
/**
* If not specified, will use the current year
*/
fun copyright(copyright: Int) {
fun copyright(copyright: Int = LocalDate.now().year): CopyrightRange {
copyrights.add(copyright)
return CopyrightRange(copyright, copyrights)
}
/**
@ -67,6 +86,29 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
authors.add(author)
}
/**
* Extra License information
*/
val extras = mutableListOf<LicenseData>()
/**
* Specifies the extra license information for this project
*/
fun extra(name: String, license: License, licenseAction: Action<LicenseData>) {
val licenseData = LicenseData(name, license)
licenseAction.execute(licenseData)
extras.add(licenseData)
}
/**
* Specifies the extra license information for this project
*/
fun extra(name: String, license: License, licenseAction: (LicenseData) -> Unit) {
val licenseData = LicenseData(name, license)
licenseAction(licenseData)
extras.add(licenseData)
}
/**
* ignore case when sorting these
*/
@ -86,10 +128,12 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
if (name != other.name) return false
if (license != other.license) return false
if (description != other.description) return false
if (copyrights != other.copyrights) return false
if (urls != other.urls) return false
if (notes != other.notes) return false
if (authors != other.authors) return false
if (extras != other.extras) return false
return true
}
@ -97,30 +141,125 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + license.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + copyrights.hashCode()
result = 31 * result + urls.hashCode()
result = 31 * result + notes.hashCode()
result = 31 * result + authors.hashCode()
result = 31 * result + extras.hashCode()
return result
}
@Throws(IOException::class)
fun writeObject(s: ObjectOutputStream) {
s.writeUTF(name)
s.writeUTF(license.name)
s.writeUTF(description)
s.writeInt(copyrights.size)
copyrights.forEach {
s.writeInt(it)
}
s.writeInt(urls.size)
urls.forEach {
s.writeUTF(it)
}
s.writeInt(notes.size)
notes.forEach {
s.writeUTF(it)
}
s.writeInt(authors.size)
authors.forEach {
s.writeUTF(it)
}
s.writeInt(extras.size)
if (extras.size > 0) {
extras.forEach {
it.writeObject(s)
}
}
}
// Gradle only needs to serialize objects, so this isn't strictly needed
@Throws(IOException::class)
fun readObject(s: ObjectInputStream) {
name = s.readUTF()
license = License.valueOfLicenseName(s.readUTF())
description = s.readUTF()
val copyrightsSize = s.readInt()
for (i in 1..copyrightsSize) {
copyrights.add(s.readInt())
}
val urlsSize = s.readInt()
for (i in 1..urlsSize) {
urls.add(s.readUTF())
}
val notesSize = s.readInt()
for (i in 1..notesSize) {
notes.add(s.readUTF())
}
val authorsSize = s.readInt()
for (i in 1..authorsSize) {
authors.add(s.readUTF())
}
val extrasSize = s.readInt()
for (i in 1..extrasSize) {
val dep = LicenseData("", License.CUSTOM)
dep.readObject(s) // can recursively create objects
extras.add(dep)
}
}
companion object {
private val LINE_SEPARATOR = System.getProperty("line.separator")
private val newLineRegex = "\n".toRegex()
private const val serialVersionUID = 1L
// NOTE: we ALWAYS use unix line endings!
private const val NL = "\n"
private const val HEADER = " - "
private const val HEADR4 = " ---- "
private const val SPACER3 = " "
private const val SPACER4 = " "
private fun prefix(prefix: Int, builder: StringBuilder): StringBuilder {
if (prefix == 0) {
builder.append("")
} else {
for (i in 0 until prefix) {
builder.append(" ")
}
}
return builder
}
private fun line(prefix: Int, builder: StringBuilder, vararg strings: Any) {
prefix(prefix, builder)
strings.forEach {
builder.append(it.toString())
}
builder.append(NL)
}
/**
* Returns the LICENSE text file, as a combo of the listed licenses. Duplicates are removed.
*/
fun buildString(licenses: MutableList<LicenseData>): String {
fun buildString(licenses: MutableList<LicenseData>, prefixOffset: Int = 0): String {
val b = StringBuilder(256)
sortAndClean(licenses)
val NL = LINE_SEPARATOR
val HEADER = " - "
val SPACER = " "
val SPACR1 = " "
var first = true
licenses.forEach { license ->
@ -132,59 +271,72 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
b.append(NL).append(NL)
}
b.append(HEADER).append(license.name).append(" - ").append(NL)
license.urls.forEach {
b.append(SPACER).append(it).append(NL)
}
b.append(SPACER).append("Copyright")
if (license.copyrights.isEmpty()) {
// append the current year
b.append(" ").append(LocalDate.now().year)
}
else {
license.copyrights.forEach {
b.append(" ").append(it).append(",")
}
b.deleteCharAt(b.length-1)
}
b.append(" - ").append(license.license.preferedName).append(NL)
license.authors.forEach {
b.append(SPACR1).append(it).append(NL)
}
if (license.license === License.CUSTOM) {
license.notes.forEach {
b.append(fixSpace(it, SPACER, 1)).append(NL)
}
}
else {
license.notes.forEach {
b.append(SPACER).append(it).append(NL)
}
}
buildLicenseString(b, license, prefixOffset)
}
return b.toString()
}
/**
* fixes new lines that may appear in the text
* @param text text to format
* @param spacer how big will the space in front of each line be?
*/
private fun fixSpace(text: String, spacerSize: String, spacer: Int): String {
val trimmedText = text.trim { it <= ' ' }
// NOTE: we ALWAYS use unix line endings!
private fun buildLicenseString(b: StringBuilder, license: LicenseData, prefixOffset: Int) {
line(prefixOffset, b, HEADER, license.name, " - ", license.description)
var space = ""
for (i in 0 until spacer) {
space += spacerSize
license.urls.forEach {
line(prefixOffset, b, SPACER3, it)
}
return space + trimmedText.replace(newLineRegex, "\n" + space)
prefix(prefixOffset, b).append(SPACER3).append("Copyright")
if (license.copyrights.isEmpty()) {
// append the current year
b.append(" ").append(LocalDate.now().year)
}
else if (license.copyrights.size == 1) {
// this is 2001
b.append(" ").append(license.copyrights.first())
} else {
// is this 2001,2002,2004,2014 <OR> 2001-2014
val sumA = license.copyrights.sum()
val sumB = license.copyrights.first().rangeTo(license.copyrights.last()).sum()
if (sumA == sumB) {
// this is 2001-2004
b.append(" ").append(license.copyrights.first()).append("-").append(license.copyrights.last())
} else {
// this is 2001,2002,2004
license.copyrights.forEach {
b.append(" ").append(it).append(",")
}
b.deleteCharAt(b.length-1)
}
}
b.append(HEADER).append(license.license.preferedName).append(NL)
license.authors.forEach {
line(prefixOffset, b, SPACER4, it)
}
if (license.license === License.CUSTOM) {
line(prefixOffset, b, HEADR4)
}
license.notes.forEach {
line(prefixOffset, b, SPACER3, it)
}
// now add the DEPENDENCY license information. This info is nested (and CAN contain duplicates from elsewhere!)
if (license.extras.isNotEmpty()) {
var isFirstExtra = true
line(prefixOffset, b, SPACER3, NL, "Extra license information")
license.extras.forEach { extraLicense ->
if (isFirstExtra) {
isFirstExtra = false
} else {
b.append(NL)
}
buildLicenseString(b, extraLicense, prefixOffset + 4)
}
}
}
/**
@ -195,7 +347,7 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
return
}
// The FIRST one is always FIRST! (the rest are alphabetical)
// The FIRST one is always FIRST! (the rest are alphabetical by name)
val firstLicense = licenses[0]
// remove dupes
@ -218,3 +370,21 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
}
}
}
class CopyrightRange internal constructor(private val start: Int, private val copyrights: MutableList<Int>) {
fun to(copyRight: Int) {
if (start >= copyRight) {
throw GradleException("Cannot have a start copyright date that is equal or greater than the `to` copyright date")
}
val newStart = start+1
if (newStart < copyRight) {
// increment by 1, since the first part of the range is already added
copyrights.addAll((newStart).rangeTo(copyRight))
}
}
fun toNow() {
to(LocalDate.now().year)
}
}

View File

@ -1,16 +1,30 @@
package dorkbox.license
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.io.FileOutputStream
import java.io.ObjectOutputStream
import javax.inject.Inject
internal open class LicenseInjector : DefaultTask() {
internal open class LicenseInjector @Inject constructor(val extension: Licensing) : DefaultTask() {
// only want to build these files once
private var alreadyBuilt = false
lateinit var licenses: MutableList<LicenseData>
lateinit var outputDir: File
lateinit var rootDir: File
companion object {
const val LICENSE_FILE = "LICENSE"
const val LICENSE_BLOB = "LICENSE.blob"
}
@Input lateinit var licenses: MutableList<LicenseData>
@OutputDirectory lateinit var outputDir: File
@InputDirectory lateinit var rootDir: File
init {
outputs.upToDateWhen {
@ -25,6 +39,13 @@ internal open class LicenseInjector : DefaultTask() {
}
alreadyBuilt = true
// now we want to add license information that we know about from our dependencies to our list
// just to make it clear, license information CAN CHANGE BETWEEN VERSIONS! For example, JNA changed from GPL to Apache in version 4+
// we associate the artifact group + id + (start) version as a license.
// if a license for a dependency is UNKNOWN, then we emit a warning to the user to add it as a pull request
// if a license version is not specified, then we use the default
DependencyScanner(project, extension).scanForLicenseData()
// true if there was any work done
didWork = buildLicenseFiles(outputDir, licenses) && buildLicenseFiles(rootDir, licenses)
}
@ -37,23 +58,26 @@ internal open class LicenseInjector : DefaultTask() {
if (!outputDir.exists()) outputDir.mkdirs()
val licenseText = LicenseData.buildString(licenses)
val licenseFile = File(outputDir, "LICENSE")
val licenseFile = File(outputDir, LICENSE_FILE)
if (fileIsNotSame(licenseFile, licenseText)) {
// write out the LICENSE and various license files
needsToDoWork = true
}
licenses.forEach {
val license = it.license
val file = File(outputDir, license.licenseFile)
val sourceText = license.licenseText
if (!needsToDoWork) {
licenses.forEach {
val license = it.license
val file = File(outputDir, license.licenseFile)
val sourceText = license.licenseText
if (fileIsNotSame(file, sourceText)) {
needsToDoWork = true
if (fileIsNotSame(file, sourceText)) {
needsToDoWork = true
}
}
}
return needsToDoWork
}
@ -66,24 +90,41 @@ internal open class LicenseInjector : DefaultTask() {
if (!outputDir.exists()) outputDir.mkdirs()
val licenseText = LicenseData.buildString(licenses)
val licenseFile = File(outputDir, "LICENSE")
if (licenseText.isEmpty()) {
println("\tNo License information defined in the project. Unable to build license data")
} else {
val licenseFile = File(outputDir, LICENSE_FILE)
val licenseBlob = File(outputDir, LICENSE_BLOB)
if (fileIsNotSame(licenseFile, licenseText)) {
// write out the LICENSE files
licenseFile.writeText(licenseText)
hasDoneWork = true
}
if (fileIsNotSame(licenseFile, licenseText)) {
// write out the LICENSE files
licenseFile.writeText(licenseText)
licenses.forEach {
val license = it.license
val file = File(outputDir, license.licenseFile)
val sourceText = license.licenseText
// save off the blob, so we can check when reading dependencies if we can
// import this license info as extra license info for the project
ObjectOutputStream(FileOutputStream(licenseBlob)).use { oos ->
oos.writeInt(licenses.size)
licenses.forEach {
it.writeObject(oos)
}
}
if (fileIsNotSame(file, sourceText)) {
// write out the various license text files
file.writeText(sourceText)
hasDoneWork = true
}
licenses.forEach {
val license = it.license
val file = File(outputDir, license.licenseFile)
val sourceText = license.licenseText
if (fileIsNotSame(file, sourceText)) {
// write out the various license text files
file.writeText(sourceText)
hasDoneWork = true
}
}
}
return hasDoneWork

View File

@ -19,12 +19,21 @@ import License
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.compile.AbstractCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File
import java.io.*
import java.time.Instant
import java.time.ZoneId
import java.util.*
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
/**
* License definition and management plugin for the Gradle build system
@ -35,9 +44,9 @@ class LicensePlugin : Plugin<Project> {
val outputDir = File(project.buildDir, "licensing")
// Create the Plugin extension object (for users to configure our execution).
val extension = project.extensions.create(Licensing.NAME, Licensing::class.java, project, outputDir)
val extension: Licensing = project.extensions.create(Licensing.NAME, Licensing::class.java, project, outputDir)
val licenseInjector = project.tasks.create("generateLicenseFiles", LicenseInjector::class.java).apply {
val licenseInjector = project.tasks.create("generateLicenseFiles", LicenseInjector::class.java, extension).apply {
group = "other"
}
@ -45,21 +54,17 @@ class LicensePlugin : Plugin<Project> {
licenseInjector.rootDir = project.rootDir
licenseInjector.licenses = extension.licenses
// the task will only build files that it needs to (and will only run once)
project.tasks.forEach {
when (it) {
is KotlinCompile -> it.dependsOn += licenseInjector
is AbstractCompile -> it.dependsOn += licenseInjector
is AbstractArchiveTask -> it.dependsOn += licenseInjector
}
}
project.afterEvaluate { prj ->
// collect all of the dependencies
project.configurations.asIterable().forEach { extension.projectDependencies.addAll(it.dependencies) }
// the task will only build files that it needs to (and will only run once)
project.tasks.forEach {
when (it) {
is AbstractCompile -> it.dependsOn(licenseInjector)
is AbstractArchiveTask -> it.dependsOn(licenseInjector)
}
}
project.configurations.asIterable().forEach { extension.projectDependencies.addAll(it.dependencies) }
val licensing = extension.licenses
if (licensing.isNotEmpty()) {
@ -87,7 +92,7 @@ class LicensePlugin : Plugin<Project> {
// only include license "notes" if we are a custom license **which is the license itself**
if (license == License.CUSTOM) {
val notes = licenseData.notes.asSequence().joinToString("")
val notes = licenseData.notes.joinToString("")
newLic.comments.set(notes)
}
}
@ -101,19 +106,6 @@ class LicensePlugin : Plugin<Project> {
// there aren't always maven publishing used
}
// now we want to add license information that we know about from our dependencies to our list
// just to make it clear, license information CAN CHANGE BETWEEN VERSIONS! For example, JNA changed from GPL to Apache in version 4+
// we associate the artifact group + id + (start) version as a license.
// if a license for a dependency is UNKNOWN, then we emit a warning to the user to add it as a pull request
// if a license version is not specified, then we use the default
val projectLicenses = mutableSetOf<String>()
for (dependency in extension.projectDependencies) {
println("DEP: " + dependency.group + dependency.name)
}
// the task will only build files that it needs to (and will only run once)
project.tasks.forEach {
if (it is AbstractArchiveTask) {
@ -128,3 +120,5 @@ class LicensePlugin : Plugin<Project> {
}

View File

@ -34,7 +34,9 @@ open class Licensing(project: Project, private val outputDir: File) {
fun output() : List<File> {
val files = mutableSetOf<File>()
files.add(File(outputDir, "LICENSE"))
files.add(File(outputDir, LicenseInjector.LICENSE_FILE))
files.add(File(outputDir, LicenseInjector.LICENSE_BLOB))
licenses.forEach {
files.add(File(outputDir, it.license.licenseFile))
}
@ -56,18 +58,18 @@ open class Licensing(project: Project, private val outputDir: File) {
/**
* Adds a new license section using the project's name as the assigned name
*/
fun license(license: License, licenseData: Action<LicenseData>) {
val licenseAction = LicenseData(projectName, license)
licenseData.execute(licenseAction)
licenses.add(licenseAction)
fun license(license: License, licenseAction: Action<LicenseData>) {
val licenseData = LicenseData(projectName, license)
licenseAction.execute(licenseData)
licenses.add(licenseData)
}
/**
* Adds a new license section using the specified name as the assigned name
*/
fun license(name: String, license: License, licenseData: Action<LicenseData>) {
val licenseAction = LicenseData(name, license)
licenseData.execute(licenseAction)
licenses.add(licenseAction)
fun license(name: String, license: License, licenseAction: Action<LicenseData>) {
val licenseData = LicenseData(name, license)
licenseAction.execute(licenseData)
licenses.add(licenseData)
}
}