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" } } }