diff --git a/build.gradle.kts b/build.gradle.kts index 585c46a..b68ed27 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -320,6 +320,7 @@ dependencies { implementation(localGroovy()) compileOnly("org.apache.ant:ant:1.10.12") + implementation("com.intershop.version:semantic-version:1.0.0") testImplementation("com.intershop.gradle.test:test-gradle-plugin:4.1.2") testImplementation(gradleTestKit()) diff --git a/src/main/kotlin/com/intershop/gradle/icm/tasks/CreateLibList.kt b/src/main/kotlin/com/intershop/gradle/icm/tasks/CreateLibList.kt index 9f51306..082414c 100644 --- a/src/main/kotlin/com/intershop/gradle/icm/tasks/CreateLibList.kt +++ b/src/main/kotlin/com/intershop/gradle/icm/tasks/CreateLibList.kt @@ -18,9 +18,9 @@ package com.intershop.gradle.icm.tasks import com.intershop.gradle.icm.utils.DependencyListUtil +import com.intershop.version.semantic.SemanticVersion import org.gradle.api.DefaultTask import org.gradle.api.GradleException -import org.gradle.api.file.Directory import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory @@ -31,17 +31,15 @@ import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction import java.io.FileInputStream -import java.util.* +import java.util.Properties import javax.inject.Inject /** * Collects all libraries (recursively through all (sub-)projects) and write IDs to a file */ open class CreateLibList @Inject constructor( - objectFactory: ObjectFactory ) : DefaultTask() { - - private val copiedLibrariesDirectoryProperty: Property = - project.objects.directoryProperty().convention(project.layout.buildDirectory.dir(BUILD_FOLDER)) + objectFactory: ObjectFactory, +) : DefaultTask() { companion object { const val DEFAULT_NAME = "CreateLibraries" @@ -50,6 +48,7 @@ open class CreateLibList @Inject constructor( fun getName(type: String): String { return "${type.lowercase()}${DEFAULT_NAME}" } + fun getOutputPath(type: String): String { return "librarylist/${type.lowercase()}/file.list" } @@ -77,28 +76,28 @@ open class CreateLibList @Inject constructor( */ @TaskAction fun execute() { - val dependencies = mutableSetOf() + // map group+name to another map mapping -> version to cartridgeNames (space separated list) + // e.g. "aopalliance:aopalliance" -> { + // "1.0" -> "ac_order_export_xml_b2b ac_cxml_order_injection", + // "1.0.1" -> "ac_order_export_xml_b2b ac_cxml_order_injection" + // } val dependenciesVersions = mutableMapOf>() cartridgeDescriptors.get().forEach { val props = getCartridgeProperties(it) - dependencies.addAll(getLibraryIDs(props).apply { - this.forEach { - val groupAndName = it.substringBeforeLast(':') - val versionInProjects = dependenciesVersions.computeIfAbsent(groupAndName, { mutableMapOf() }) - versionInProjects.compute( - it - ) { _, p -> if (null == p) getCartridgeName(props) else p + " " + getCartridgeName(props) } + getLibraryIDs(props).forEach { libraryID -> + val groupAndName = libraryID.substringBeforeLast(':') + val version = libraryID.substringAfterLast(':') + val versionToCartridges = dependenciesVersions.computeIfAbsent(groupAndName) { mutableMapOf() } + versionToCartridges.compute(version) { _, cartNames -> + if (null == cartNames) getCartridgeName(props) else cartNames + " " + getCartridgeName(props) } - }) + } } - val conflicts = dependenciesVersions.filter { e -> 1 < e.value.size }.map { it.value }.toList() - if (conflicts.isNotEmpty()) { - throw GradleException( - "Unable to process libraries. Dependencies ${conflicts}" + - " are required by cartridge-projects in non-unique versions." - ) - } + // convert dependenciesVersions into a list of dependencyIds resolving version conflicts if required + val dependencies = dependenciesVersions.toSortedMap().map { (groupAndName, versionToCartridges) -> + toDependencyId(groupAndName, resolveVersionConflict(groupAndName, versionToCartridges)) + }.toMutableList() exludeLibraryLists.get().forEach { val excludeList = DependencyListUtil.getIDList(environmentType.get(), it) @@ -108,7 +107,7 @@ open class CreateLibList @Inject constructor( val sortedDeps = dependencies.toList().sorted() val listFile = libraryListFile.asFile.get() - if(listFile.exists()) { + if (listFile.exists()) { listFile.delete() } @@ -119,6 +118,77 @@ open class CreateLibList @Inject constructor( } } + /** + * Resolves a version conflict if required (more than 1 version referenced). The conflict is resolved by choosing + * the highest version except the version gap is a major update - then a GradleException is thrown. If the gap is + * a minor update a warning is logged, on a patch update an info message is logged. + * ATTENTION: this method requires the versions to be semantic version parseable by + * com.vdurmont.semver4j.Semver.Semver(java.lang.String) + */ + private fun resolveVersionConflict(groupAndName: String, versionToCartridges: Map): String { + // not a conflict at all ? + if (versionToCartridges.size == 1) { + return versionToCartridges.keys.first() + } + + project.logger.debug("Trying to resolve a version conflict for dependency '{}' requiring the versions {}", + groupAndName, versionToCartridges.keys) + + // parse to org.gradle.util.internal.VersionNumber + val versions = versionToCartridges.keys.map { versionStr -> + try { + SemanticVersion.valueOf(versionStr) + } catch (e: Exception) { + throw GradleException( + "The version string '$versionStr' can not be parsed therefore the conflict resolution must " + + "be done manually: The dependency '$groupAndName' is required in ${versionToCartridges.size} " + + "different versions by the following cartridges: " + + "$versionToCartridges", e) + } + }.sortedDescending() + + // check for major/minor/patch version jump + var prev: SemanticVersion? = null + for (curr in versions) { + if (prev == null) { + prev = curr + continue // skip first iteration (nothing to compare) + } + if (prev.major != curr.major) { + throw GradleException( + "There's a major version conflict for dependency '$groupAndName': ${prev.version} <-> " + + "${curr.version}. Please resolve this conflict analyzing the dependencies of the following " + + "cartridges: $versionToCartridges") + } + val chosen = maxOf(prev, curr) + if (prev.minor != curr.minor) { + project.logger.warn( + "There's a minor version conflict for dependency '{}': {} <-> {}. Version {} is chosen. If " + + "this is not the correct version please resolve this conflict by analyzing the dependencies " + + "of the following cartridges: {}", + groupAndName, prev.version, curr.version, chosen.version, versionToCartridges) + } + if (prev.patch != curr.patch) { + project.logger.info( + "There's a patch version conflict for dependency '{}': {} <-> {}. Version {} is chosen. If " + + "this is not the correct version please resolve this conflict by analyzing the dependencies " + + "of the following cartridges: {}", + groupAndName, prev.version, curr.version, chosen.version, versionToCartridges) + } + prev = curr + } + + // finally take the first (highest) version + val chosen = versions.first().version + project.logger.debug("Resolved the version conflict for dependency '{}' choosing version {}", + groupAndName, chosen) + return chosen + } + + private fun toDependencyId(groupAndName: String, version: String): String { + return "$groupAndName:$version" + } + private fun getCartridgeProperties(propsFile: RegularFile): Properties { val props = Properties() FileInputStream(propsFile.asFile).use { @@ -126,13 +196,13 @@ open class CreateLibList @Inject constructor( } return props } - private fun getCartridgeName(props:Properties): String { + + private fun getCartridgeName(props: Properties): String { return props["cartridge.name"].toString() } - private fun getLibraryIDs(props:Properties): Set { + private fun getLibraryIDs(props: Properties): Set { val dependsOnLibs = props["cartridge.dependsOnLibs"].toString() return if (dependsOnLibs.isEmpty()) setOf() else dependsOnLibs.split(";").toSet() } } -