diff --git a/LICENSE b/LICENSE index 40ab3f3..b3ecd61 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index 9625d63..77d8bfb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { } } } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ee69dd6..ac33e99 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/src/dorkbox/license/AppLicensing.kt b/src/dorkbox/license/AppLicensing.kt new file mode 100644 index 0000000..06c4aad --- /dev/null +++ b/src/dorkbox/license/AppLicensing.kt @@ -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>>() + + // 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 { + 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) + } +} diff --git a/src/dorkbox/license/DependencyScanner.kt b/src/dorkbox/license/DependencyScanner.kt new file mode 100644 index 0000000..b2aa534 --- /dev/null +++ b/src/dorkbox/license/DependencyScanner.kt @@ -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() + val existingNames = mutableSetOf() + 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() + val actuallyMissingLicenseInfo = mutableListOf() + + 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() + 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): 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 = dep.moduleArtifacts.map { artifact: ResolvedArtifact -> + val artifactModule = artifact.moduleVersion.id + DependencyInfo(artifactModule.group, artifactModule.name, artifactModule.version, artifact.file.absoluteFile) + } + + val children = mutableListOf() + 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 { + val flatDeps = mutableSetOf() + flattenDep(dep, flatDeps) + return flatDeps.toList() + } + + private fun flattenDep(dep: Dependency, flatDeps: MutableSet) { + flatDeps.add(dep) + dep.children.forEach { + flattenDep(it, flatDeps) + } + } + + data class Dependency(val group: String, + val name: String, + val version: String, + val artifacts: List, + val children: List) { + + 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" + } + } +} diff --git a/src/dorkbox/license/LicenseData.kt b/src/dorkbox/license/LicenseData.kt index fd322ae..93af8ba 100644 --- a/src/dorkbox/license/LicenseData.kt +++ b/src/dorkbox/license/LicenseData.kt @@ -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 { + +class LicenseData(var name: String, var license: License) : java.io.Serializable, Comparable { + /** + * 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() + + /** + * Specifies the extra license information for this project + */ + fun extra(name: String, license: License, licenseAction: Action) { + 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 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): String { + fun buildString(licenses: MutableList, 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 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) { + 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) + } +} diff --git a/src/dorkbox/license/LicenseInjector.kt b/src/dorkbox/license/LicenseInjector.kt index 57cd7ce..aaedf36 100644 --- a/src/dorkbox/license/LicenseInjector.kt +++ b/src/dorkbox/license/LicenseInjector.kt @@ -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 - 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 + @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 diff --git a/src/dorkbox/license/LicensePlugin.kt b/src/dorkbox/license/LicensePlugin.kt index fb50bd0..88db881 100644 --- a/src/dorkbox/license/LicensePlugin.kt +++ b/src/dorkbox/license/LicensePlugin.kt @@ -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 { 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 { 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 { // 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 { // 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() - 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 { } + + diff --git a/src/dorkbox/license/Licensing.kt b/src/dorkbox/license/Licensing.kt index 5112d45..1b9140a 100644 --- a/src/dorkbox/license/Licensing.kt +++ b/src/dorkbox/license/Licensing.kt @@ -34,7 +34,9 @@ open class Licensing(project: Project, private val outputDir: File) { fun output() : List { val files = mutableSetOf() - 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) { - val licenseAction = LicenseData(projectName, license) - licenseData.execute(licenseAction) - licenses.add(licenseAction) + fun license(license: License, licenseAction: Action) { + 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) { - val licenseAction = LicenseData(name, license) - licenseData.execute(licenseAction) - licenses.add(licenseAction) + fun license(name: String, license: License, licenseAction: Action) { + val licenseData = LicenseData(name, license) + licenseAction.execute(licenseData) + licenses.add(licenseData) } }