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

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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 package dorkbox.license
import 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 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 * Copyright
*/ */
@ -27,8 +45,9 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
/** /**
* If not specified, will use the current year * If not specified, will use the current year
*/ */
fun copyright(copyright: Int) { fun copyright(copyright: Int = LocalDate.now().year): CopyrightRange {
copyrights.add(copyright) copyrights.add(copyright)
return CopyrightRange(copyright, copyrights)
} }
/** /**
@ -67,6 +86,29 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
authors.add(author) 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 * 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 (name != other.name) return false
if (license != other.license) return false if (license != other.license) return false
if (description != other.description) return false
if (copyrights != other.copyrights) return false if (copyrights != other.copyrights) return false
if (urls != other.urls) return false if (urls != other.urls) return false
if (notes != other.notes) return false if (notes != other.notes) return false
if (authors != other.authors) return false if (authors != other.authors) return false
if (extras != other.extras) return false
return true return true
} }
@ -97,30 +141,125 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
override fun hashCode(): Int { override fun hashCode(): Int {
var result = name.hashCode() var result = name.hashCode()
result = 31 * result + license.hashCode() result = 31 * result + license.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + copyrights.hashCode() result = 31 * result + copyrights.hashCode()
result = 31 * result + urls.hashCode() result = 31 * result + urls.hashCode()
result = 31 * result + notes.hashCode() result = 31 * result + notes.hashCode()
result = 31 * result + authors.hashCode() result = 31 * result + authors.hashCode()
result = 31 * result + extras.hashCode()
return result 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 { companion object {
private val LINE_SEPARATOR = System.getProperty("line.separator") private const val serialVersionUID = 1L
private val newLineRegex = "\n".toRegex()
// 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. * 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) val b = StringBuilder(256)
sortAndClean(licenses) sortAndClean(licenses)
val NL = LINE_SEPARATOR
val HEADER = " - "
val SPACER = " "
val SPACR1 = " "
var first = true var first = true
licenses.forEach { license -> licenses.forEach { license ->
@ -132,59 +271,72 @@ class LicenseData(val name: String, val license: License) : Comparable<LicenseDa
b.append(NL).append(NL) b.append(NL).append(NL)
} }
b.append(HEADER).append(license.name).append(" - ").append(NL) buildLicenseString(b, license, prefixOffset)
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)
}
}
} }
return b.toString() return b.toString()
} }
/** // NOTE: we ALWAYS use unix line endings!
* fixes new lines that may appear in the text private fun buildLicenseString(b: StringBuilder, license: LicenseData, prefixOffset: Int) {
* @param text text to format line(prefixOffset, b, HEADER, license.name, " - ", license.description)
* @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 <= ' ' }
var space = "" license.urls.forEach {
for (i in 0 until spacer) { line(prefixOffset, b, SPACER3, it)
space += spacerSize
} }
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 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] val firstLicense = licenses[0]
// remove dupes // 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 package dorkbox.license
import org.gradle.api.DefaultTask 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 org.gradle.api.tasks.TaskAction
import java.io.File 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 // only want to build these files once
private var alreadyBuilt = false private var alreadyBuilt = false
lateinit var licenses: MutableList<LicenseData> companion object {
lateinit var outputDir: File const val LICENSE_FILE = "LICENSE"
lateinit var rootDir: File const val LICENSE_BLOB = "LICENSE.blob"
}
@Input lateinit var licenses: MutableList<LicenseData>
@OutputDirectory lateinit var outputDir: File
@InputDirectory lateinit var rootDir: File
init { init {
outputs.upToDateWhen { outputs.upToDateWhen {
@ -25,6 +39,13 @@ internal open class LicenseInjector : DefaultTask() {
} }
alreadyBuilt = true 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 // true if there was any work done
didWork = buildLicenseFiles(outputDir, licenses) && buildLicenseFiles(rootDir, licenses) didWork = buildLicenseFiles(outputDir, licenses) && buildLicenseFiles(rootDir, licenses)
} }
@ -37,23 +58,26 @@ internal open class LicenseInjector : DefaultTask() {
if (!outputDir.exists()) outputDir.mkdirs() if (!outputDir.exists()) outputDir.mkdirs()
val licenseText = LicenseData.buildString(licenses) val licenseText = LicenseData.buildString(licenses)
val licenseFile = File(outputDir, "LICENSE") val licenseFile = File(outputDir, LICENSE_FILE)
if (fileIsNotSame(licenseFile, licenseText)) { if (fileIsNotSame(licenseFile, licenseText)) {
// write out the LICENSE and various license files // write out the LICENSE and various license files
needsToDoWork = true needsToDoWork = true
} }
licenses.forEach { if (!needsToDoWork) {
val license = it.license licenses.forEach {
val file = File(outputDir, license.licenseFile) val license = it.license
val sourceText = license.licenseText val file = File(outputDir, license.licenseFile)
val sourceText = license.licenseText
if (fileIsNotSame(file, sourceText)) { if (fileIsNotSame(file, sourceText)) {
needsToDoWork = true needsToDoWork = true
}
} }
} }
return needsToDoWork return needsToDoWork
} }
@ -66,24 +90,41 @@ internal open class LicenseInjector : DefaultTask() {
if (!outputDir.exists()) outputDir.mkdirs() if (!outputDir.exists()) outputDir.mkdirs()
val licenseText = LicenseData.buildString(licenses) 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)) { if (fileIsNotSame(licenseFile, licenseText)) {
// write out the LICENSE files // write out the LICENSE files
licenseFile.writeText(licenseText) licenseFile.writeText(licenseText)
hasDoneWork = true
}
licenses.forEach { // save off the blob, so we can check when reading dependencies if we can
val license = it.license // import this license info as extra license info for the project
val file = File(outputDir, license.licenseFile) ObjectOutputStream(FileOutputStream(licenseBlob)).use { oos ->
val sourceText = license.licenseText 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 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 return hasDoneWork

View File

@ -19,12 +19,21 @@ import License
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project 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.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.bundling.AbstractArchiveTask import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.api.tasks.compile.AbstractCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.*
import java.io.File 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 * License definition and management plugin for the Gradle build system
@ -35,9 +44,9 @@ class LicensePlugin : Plugin<Project> {
val outputDir = File(project.buildDir, "licensing") val outputDir = File(project.buildDir, "licensing")
// Create the Plugin extension object (for users to configure our execution). // 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" group = "other"
} }
@ -45,21 +54,17 @@ class LicensePlugin : Plugin<Project> {
licenseInjector.rootDir = project.rootDir licenseInjector.rootDir = project.rootDir
licenseInjector.licenses = extension.licenses 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 -> 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 val licensing = extension.licenses
if (licensing.isNotEmpty()) { 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** // only include license "notes" if we are a custom license **which is the license itself**
if (license == License.CUSTOM) { if (license == License.CUSTOM) {
val notes = licenseData.notes.asSequence().joinToString("") val notes = licenseData.notes.joinToString("")
newLic.comments.set(notes) newLic.comments.set(notes)
} }
} }
@ -101,19 +106,6 @@ class LicensePlugin : Plugin<Project> {
// there aren't always maven publishing used // 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) // the task will only build files that it needs to (and will only run once)
project.tasks.forEach { project.tasks.forEach {
if (it is AbstractArchiveTask) { 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> { fun output() : List<File> {
val files = mutableSetOf<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 { licenses.forEach {
files.add(File(outputDir, it.license.licenseFile)) 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 * Adds a new license section using the project's name as the assigned name
*/ */
fun license(license: License, licenseData: Action<LicenseData>) { fun license(license: License, licenseAction: Action<LicenseData>) {
val licenseAction = LicenseData(projectName, license) val licenseData = LicenseData(projectName, license)
licenseData.execute(licenseAction) licenseAction.execute(licenseData)
licenses.add(licenseAction) licenses.add(licenseData)
} }
/** /**
* Adds a new license section using the specified name as the assigned name * Adds a new license section using the specified name as the assigned name
*/ */
fun license(name: String, license: License, licenseData: Action<LicenseData>) { fun license(name: String, license: License, licenseAction: Action<LicenseData>) {
val licenseAction = LicenseData(name, license) val licenseData = LicenseData(name, license)
licenseData.execute(licenseAction) licenseAction.execute(licenseData)
licenses.add(licenseAction) licenses.add(licenseData)
} }
} }