274 lines
12 KiB
Kotlin
274 lines
12 KiB
Kotlin
package dorkbox.license
|
|
|
|
import 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.zip.ZipFile
|
|
import java.util.zip.ZipInputStream
|
|
|
|
object LicenseDependencyScanner {
|
|
// scans and loads license data into the extension
|
|
// - from jars on runtime/compile classpath
|
|
|
|
data class ScanDep(val project: Project, val preloadedText: MutableList<String>, val embeddedText: MutableList<String>, val missingText: MutableList<String>)
|
|
|
|
// THIS MUST BE IN "afterEvaluate" or run from a specific task.
|
|
fun scanForLicenseData(project: Project, allProjects: Boolean, licenses: MutableList<LicenseData>): ScanDep {
|
|
|
|
val preloadedText = mutableListOf<String>();
|
|
val embeddedText = mutableListOf<String>();
|
|
val missingText = mutableListOf<String>();
|
|
|
|
|
|
// NOTE: there will be some duplicates, so we want to remove them
|
|
val dependencies = mutableSetOf<ProjAndDependency>()
|
|
|
|
if (allProjects) {
|
|
project.allprojects.forEach { proj ->
|
|
// root + children
|
|
dependencies.addAll(scan(proj, "compileClasspath"))
|
|
dependencies.addAll(scan(proj, "runtimeClasspath"))
|
|
}
|
|
} else {
|
|
// only the root project
|
|
dependencies.addAll(scan(project, "compileClasspath"))
|
|
dependencies.addAll(scan(project, "runtimeClasspath"))
|
|
}
|
|
|
|
// this will contain duplicates if sub-projects ALSO have the same deps
|
|
val projectDependencies = dependencies.toList()
|
|
|
|
val missingLicenseInfo = mutableSetOf<ProjAndDependency>()
|
|
val actuallyMissingLicenseInfo = mutableSetOf<ProjAndDependency>()
|
|
|
|
val alreadyScanDeps = mutableSetOf<Dependency>()
|
|
|
|
if (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)
|
|
val primaryLicense = licenses.first()
|
|
|
|
// scan to see if we have in our predefined section
|
|
projectDependencies.forEach { projAndDep: ProjAndDependency ->
|
|
val dep = projAndDep.dep
|
|
if (alreadyScanDeps.contains(dep)) {
|
|
return@forEach
|
|
}
|
|
|
|
val data: LicenseData? = try {
|
|
AppLicensing.getLicense(dep.mavenId())
|
|
} catch (e: Exception) {
|
|
println("\tError getting license information for ${dep.mavenId()}")
|
|
null
|
|
}
|
|
|
|
if (data == null) {
|
|
missingLicenseInfo.add(projAndDep)
|
|
} else {
|
|
if (!primaryLicense.extras.contains(data)) {
|
|
alreadyScanDeps.add(dep)
|
|
|
|
preloadedText.add("\t\t${dep.mavenId()} [${data.license}]")
|
|
|
|
// NOTE: the END copyright for these are determined by the DATE of the files!
|
|
// Some dates are WRONG (because the jar build is mucked with), so we manually fix it
|
|
if (data.copyright == 0) {
|
|
// get the OLDEST date from the artifacts and use that as the copyright date
|
|
var oldestDate = 0L
|
|
dep.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()
|
|
}
|
|
|
|
// as per US Code Title 17, Chapter 4; for "visually perceptive copies" (which includes software).
|
|
// http://www.copyright.gov/title17/92chap4.html
|
|
// it is ONLY... © year name
|
|
val year = Date(oldestDate).toInstant().atZone(ZoneId.systemDefault()).toLocalDate().year
|
|
data.copyright = year
|
|
}
|
|
|
|
// otherwise the copyright was specified
|
|
|
|
primaryLicense.extras.add(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// now scan to see if the jar has a license blob in it
|
|
if (missingLicenseInfo.isNotEmpty()) {
|
|
missingLicenseInfo.forEach { projAndDep: ProjAndDependency ->
|
|
val dep = projAndDep.dep
|
|
if (alreadyScanDeps.contains(dep)) {
|
|
return@forEach
|
|
}
|
|
|
|
// see if we have it in the dependency jar
|
|
var licenseData: License? = null
|
|
dep.artifacts.forEach search@{ artifact ->
|
|
val file = artifact.file
|
|
try {
|
|
if (file.canRead()) {
|
|
ZipFile(file).use {
|
|
// read the license blob information
|
|
val ze = it.getEntry(LicenseInjector.LICENSE_BLOB)
|
|
if (ze != null) {
|
|
it.getInputStream(ze).use { licenseStream ->
|
|
try {
|
|
ObjectInputStream(licenseStream).use { ois ->
|
|
val data = LicenseData("", License.CUSTOM)
|
|
ois.readInt() // weird stuff from serialization. No idea what this value is for, but it is REQUIRED
|
|
data.readObject(ois)
|
|
licenseData = data.license
|
|
|
|
// as per US Code Title 17, Chapter 4; for "visually perceptive copies" (which includes software).
|
|
// http://www.copyright.gov/title17/92chap4.html
|
|
// it is ONLY... © year name
|
|
//
|
|
// this is correctly saved in the license blob
|
|
if (!primaryLicense.extras.contains(data)) {
|
|
primaryLicense.extras.add(data)
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
println("\t$dep [ERROR $file], ${e.message ?: e.javaClass}")
|
|
}
|
|
|
|
return@search
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
println("\t$dep [ERROR $file], ${e.message ?: e.javaClass}")
|
|
}
|
|
}
|
|
|
|
|
|
if (licenseData != null) {
|
|
alreadyScanDeps.add(dep)
|
|
embeddedText.add("\t\t$dep [$licenseData]")
|
|
} else {
|
|
actuallyMissingLicenseInfo.add(projAndDep)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (actuallyMissingLicenseInfo.isNotEmpty()) {
|
|
// we have to prune sub-project data first...
|
|
val projectMavenIds = mutableSetOf<String>()
|
|
project.allprojects.forEach {
|
|
projectMavenIds.add("${it.group}:${it.name}:${it.version}")
|
|
}
|
|
|
|
actuallyMissingLicenseInfo.forEach { missingDepAndProj ->
|
|
// we DO NOT want to show missing deps for project sub-projects.
|
|
val proj = missingDepAndProj.project
|
|
val dep = missingDepAndProj.dep
|
|
|
|
if (alreadyScanDeps.contains(dep)) {
|
|
return@forEach
|
|
}
|
|
alreadyScanDeps.add(dep)
|
|
|
|
val mavenId = dep.mavenId()
|
|
|
|
// if the missing dep IS ALSO the same as a subproject .... we don't want to report it
|
|
if (!projectMavenIds.contains(mavenId)) {
|
|
missingText.add("\t ${dep.mavenId()}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ScanDep(project, preloadedText, embeddedText, missingText)
|
|
}
|
|
|
|
|
|
/**
|
|
* THIS MUST BE IN "afterEvaluate" or run from a specific task.
|
|
*
|
|
* 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 quite deep recursion. A project can never depend on itself, but we check if a project has already been added, and
|
|
* don't parse it more than once
|
|
*
|
|
* This is an actual problem...
|
|
*/
|
|
private fun scan(project: Project, configurationName: String): List<ProjAndDependency> {
|
|
|
|
val projectDependencies = mutableListOf<ProjAndDependency>()
|
|
val config = project.configurations.getByName(configurationName)
|
|
if (!config.isCanBeResolved) {
|
|
return projectDependencies
|
|
}
|
|
|
|
try {
|
|
config.resolve()
|
|
} catch (e: Throwable) {
|
|
println("Unable to resolve the $configurationName configuration for the project ${project.name}")
|
|
}
|
|
|
|
val list = LinkedList<ResolvedDependency>()
|
|
|
|
config.resolvedConfiguration.lenientConfiguration.getFirstLevelModuleDependencies(org.gradle.api.specs.Specs.SATISFIES_ALL).forEach { dep ->
|
|
list.add(dep)
|
|
}
|
|
|
|
var next: ResolvedDependency
|
|
while (list.isNotEmpty()) {
|
|
next = list.poll()
|
|
|
|
val module = next.module.id
|
|
val group = module.group
|
|
val name = module.name
|
|
val version = module.version
|
|
|
|
val artifacts = try {
|
|
next.moduleArtifacts.map { artifact: ResolvedArtifact ->
|
|
val artifactModule = artifact.moduleVersion.id
|
|
Artifact(artifactModule.group, artifactModule.name, artifactModule.version, artifact.file.absoluteFile)
|
|
}
|
|
} catch (e: Exception) {
|
|
listOf()
|
|
}
|
|
projectDependencies.add(ProjAndDependency(project, Dependency(group, name, version, artifacts)))
|
|
}
|
|
|
|
return projectDependencies
|
|
}
|
|
|
|
internal data class ProjAndDependency(val project: Project, val dep: Dependency)
|
|
|
|
internal data class Dependency(
|
|
val group: String,
|
|
val name: String,
|
|
val version: String,
|
|
val artifacts: List<Artifact>) {
|
|
|
|
fun mavenId(): String {
|
|
return "$group:$name:$version"
|
|
}
|
|
|
|
override fun toString(): String {
|
|
return mavenId()
|
|
}
|
|
}
|
|
|
|
internal data class Artifact(val group: String, val name: String, val version: String, val file: File) {
|
|
val id: String
|
|
get() {
|
|
return "$group:$name:$version"
|
|
}
|
|
}
|
|
}
|