Skip to content

Allow duplicate project names if they have distinct groups #281

Description

@aSemy

Expected and Results

nmcp throws an error if projects have the same name, for example if the root project is called :foo and there's a subproject :bar:foo.

A problem occurred configuring root project 'foo'.
> Nmcp: some projects have the same name: '${it.name}'. This creates issues when resolving the aggregation.
  You can usually resolve this error by renaming your projects (including possibly your root project).
  Or you can disable this check by calling `allowDuplicateProjectNames.set(true)`.
  See https://github.com/gradle/gradle/issues/36167 for more details.

allowDuplicateProjectNames.set(true) disables the check - so I'm not blocked.

However, in my case the error is not valid. The two subprojects have distinct project.group values. A distinct group allows Gradle to differentiate the projects correctly.

// build.gradle.kts

group = "root"

// foo/build.gradle.kts

group = "foo"

Can the warning be updated to check if the name and group of projects clash?

Related environment and versions

No response

Reproduction steps

No response

Anything else?

'${'$'}{it.name}' -- oops!

Nmcp: some projects have the same name: '${'$'}{it.name}'. This creates issues when resolving the aggregation.

Activity

  1. aSemy commented on Sep 21, 2026

    @aSemy
    ContributorAuthor

    I think it's possible for nmcp to create a more refined check (check the resolved components from ResolvedComponentResult - if they are project dependencies then check to see if the expected project matches the actual).

    But really it should be fixed in Gradle, so I made a PR: gradle/gradle#39241

    I can also make a PR for the improved check in nmcp?

  2. martinbonnin commented on Sep 21, 2026

    @martinbonnin
    Member

    Thanks for raising this and offering to help. This is very kind but given that the workaround is pretty simple, I'd rather not add too much code for this.

    Overall, I feel like the false positives raise awareness about the issue without being blocking so I'm enclined to keep them.

    I've tweaked the error message a little bit there. Let me know if that works for you.

  3. aSemy commented on Sep 21, 2026

    @aSemy
    ContributorAuthor

    Thanks for raising this and offering to help. This is very kind but given that the workaround is pretty simple, I'd rather not add too much code for this.

    Agree! The updated check is mostly finished, but I realised it was overkill for such a small check in nmcp.

    For future reference, here's the check. Maybe it's useful elsewhere.

    import org.gradle.api.artifacts.component.ProjectComponentSelector
    import org.gradle.api.artifacts.result.ResolvedComponentResult
    import org.gradle.api.artifacts.result.ResolvedDependencyResult
    import org.gradle.api.artifacts.result.UnresolvedDependencyResult
    import org.gradle.api.attributes.Usage
    
    /**
     * Fails if one of the aggregated projects was swallowed by dependency resolution.
     *
     * Gradle identifies the projects taking part in dependency resolution by their `group:name` coordinates.
     * If two projects share the same coordinates, Gradle cannot tell them apart:
     * - if both of them expose the `nmcp` variant, one is silently replaced by the other and its artifacts never make
     * it to the aggregation.
     * - else the dependency is unresolved and Gradle reports a confusing "no matching variant" error about a project
     * that was never requested. This happens when the aggregating project itself takes part in the conflict.
     *
     * [root] is the component of the aggregating project, i.e. the project applying `com.gradleup.nmcp.aggregation`. It
     * is not necessarily the root project of the build.
     *
     * This is a best effort check: it may miss conflicts. Set `allowDuplicateProjectNames` to `true` to disable it.
     *
     * See https://github.com/gradle/gradle/issues/36167 for more details.
     */
    internal fun checkProjectCoordConflict(
        root: ResolvedComponentResult,
        allowDuplicateProjectNames: Boolean,
        lenient: Boolean,
    ) {
        if (allowDuplicateProjectNames) {
            return
        }
    
        root.dependencies.forEach { dependencyResult ->
            val requested = dependencyResult.requested as? ProjectComponentSelector ?: return@forEach
    
            when (dependencyResult) {
                is ResolvedDependencyResult   -> {
                    val selected = dependencyResult.selected
                    if (!requested.matchesStrictly(selected.id)) {
                        duplicateProjectsError(
                            "project '${requested.projectPath}' was replaced by ${selected.id.displayName} during dependency resolution because they have the same group and name ('${selected.moduleVersion?.module}'). The artifacts of '${requested.projectPath}' are missing from the aggregation.",
                        )
                    }
                }
    
                is UnresolvedDependencyResult -> {
                    if (lenient) {
                        // If lenient, unresolved projects are expected. They are projects without `com.gradleup.nmcp`.
                        return@forEach
                    }
                    // Gradle reports the missing `nmcp` variant on the component that actually took part in the
                    // resolution.
                    // - If it is [root], the requested project was collapsed into [root] by conflict
                    //   resolution and never resolved at all.
                    // - If it is the requested project itself, that project simply does not apply `com.gradleup.nmcp`,
                    //   so let Gradle report it.
                    if (!dependencyResult.missesTheNmcpVariantOn(root.id.displayName)) {
                        return@forEach
                    }
                    val rootModule = root.moduleVersion ?: return@forEach
    
                    duplicateProjectsError(
                        "project '${requested.projectPath}' could not be resolved because it has the same group and name as ${root.id.displayName} ('${rootModule.module}').",
                    )
                }
            }
        }
    }
    
    /**
     * Whether variant selection failed on [componentDisplayName] for lack of an `nmcp` variant.
     *
     * The failure itself is always about the requested project ("Could not resolve project ':foo'.") and its cause names
     * the component that variant selection actually ran on ("No matching variant of project ':foo' was found.").
     * Those two differ when the requested project was collapsed into another component by conflict resolution.
     *
     * This matches Gradle error messages and may stop matching in a future Gradle version. If it does, the aggregation
     * fails with the Gradle error instead of ours.
     */
    private fun UnresolvedDependencyResult.missesTheNmcpVariantOn(componentDisplayName: String): Boolean {
        val cause = failure.cause?.message.orEmpty()
    
        return cause.contains("No matching variant of $componentDisplayName was found") &&
            cause.contains("attribute '${Usage.USAGE_ATTRIBUTE.name}' with value '$usageValue'")
    }
    
    private fun duplicateProjectsError(reason: String): Nothing {
        error(
            """
            |Nmcp: $reason
            |You can usually resolve this error by renaming one of the projects (including possibly your root project) or by giving them distinct `group` values.
            |Or you can disable this check by calling `allowDuplicateProjectNames.set(true)`.
            |See https://github.com/gradle/gradle/issues/36167 for more details.
            """.trimMargin(),
        )
    }
  4. martinbonnin commented on Sep 21, 2026

    @martinbonnin
    Member

    Thanks again for the help! I'm going to close this one for now. Hopefully this can be handled on the Gradle side in the future. If this becomes too much of an issue, we can always revisit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions