gradle / gradle

Adaptable, fast automation for all
https://gradle.org
Apache License 2.0
16.92k stars 4.74k forks source link

Perform strict resolution of Configuration, so that attribute matching can be resolved exactly #30661

Open aSemy opened 1 month ago

aSemy commented 1 month ago

Expected Behavior

When resolving a Configuration it is possible to strictly resolve based on the provided attributes.

When attribute values are mismatched, Gradle will not silently resolve random files.

If such issues occur when multiple Configurations extend each other, it's exceptionally easy to track down and diagnose the issue.

Current Behavior (optional)

When resolving a Configuration it is not possible to strictly resolve based on the provided attributes.

When attribute values are mismatched, Gradle will silently resolve random files.

If such issues occur when multiple Configurations extend each other, it's exceptionally difficult to track down and diagnose the issue.

As investigated in https://github.com/gradle/gradle/issues/27594 it is not possible to manually filter using an ArtifactView in a configuration-cache compatible way. Even if it was, the workaround is very verbose and repetitive, inviting further bugs.

Investigating mismatches is exceptionally complicated, as tools like dependencyInsight basically require you know what the issue is before you know it, and have UX issues, or are incompatible with composite builds.

Context

Example: it is impossible to protect against mistakes

When an attribute value has a mistake, it's not easy to see what the problem is. Gradle will run without error and resolve unrequested files.

In the following example the attributes of demoConfWindowsDebugResolver and demoConfWindowsDebugProvider appear to match, so it's not clear why Gradle wouldn't resolve the correct file.

  1. Create a Gradle project with the following build.gradle.kts

    // build.gradle.kts
    
    @file:Suppress("UnstableApiUsage")
    
    plugins {
      kotlin("multiplatform") version "2.0.0"
    }
    
    repositories {
      mavenCentral()
    }
    
    kotlin {
      jvm()
      iosX64()
      iosArm64()
    }
    
    object DemoAttributes {
      val demo =
        Attribute("demo")
      val target =
        Attribute("demo.target")
      val buildType =
        Attribute("demo.buildType")
    
      private fun Attribute(name: String): Attribute<String> =
        Attribute.of(name, String::class.java)
    }
    
    project.dependencies.attributesSchema {
      attribute(DemoAttributes.demo)
      attribute(DemoAttributes.target)
      attribute(DemoAttributes.buildType)
    }
    
    val compilationTaskWindowsDebug by project.tasks.registering(Sync::class) {
      description = "Dummy task that represents a Windows Debug compilation task"
      temporaryDir.resolve("input").resolve("windows-debug.txt").apply {
        parentFile.mkdirs()
        writeText("Windows Debug file")
      }
      from(temporaryDir.resolve("input"))
      into(temporaryDir.resolve("output"))
    }
    
    val demoConf: Configuration by configurations.creating {
      declarable()
    }
    
    val demoConfWindowsDebugResolver: Configuration by configurations.creating {
      description = "Resolve Windows Debug compiled files"
      resolvable()
      extendsFrom(demoConf)
      attributes {
        attribute(DemoAttributes.demo, "true")
        attribute(DemoAttributes.target, "windows")
        attribute(DemoAttributes.buildType, "debug")
      }
    }
    
    val demoConfWindowsDebugProvider: Configuration by configurations.creating {
      description = "Provide Windows Debug compiled files"
      consumable()
      attributes {
        attribute(DemoAttributes.demo, "true")
        attribute(DemoAttributes.target, "Windows")
        attribute(DemoAttributes.buildType, "debug")
      }
      outgoing {
        artifact(compilationTaskWindowsDebug)
      }
    }
    
    val incomingFilesResolverTask by tasks.registering {
      val incomingFiles = demoConfWindowsDebugResolver.incoming.files
      inputs.files(incomingFiles)
      doLast {
        val fileNames = incomingFiles.asFileTree.map { it.name }
        println("artifactViewFiles: ${incomingFiles.asFileTree.map { it.name }}")
        if (fileNames.singleOrNull() != "windows-debug.txt") {
          error("Invalid resolution. Expected [windows-debug.txt] by was $fileNames")
        }
      }
    }
    
    val artifactViewFilesResolverTask by tasks.registering {
      val artifactViewFiles = demoConfWindowsDebugResolver.incoming.artifactView { lenient(false) }.files
      inputs.files(artifactViewFiles)
      doLast {
        val fileNames = artifactViewFiles.asFileTree.map { it.name }
        println("artifactViewFiles: ${artifactViewFiles.asFileTree.map { it.name }}")
        if (fileNames.singleOrNull() != "windows-debug.txt") {
          error("Invalid resolution. Expected [windows-debug.txt] by was $fileNames")
        }
      }
    }
    
    dependencies {
      demoConf(project)
    }
    
    //region utils
    fun Configuration.consumable(
      visible: Boolean = false
    ) {
      isVisible = visible
      isCanBeResolved = false
      isCanBeConsumed = true
      isCanBeDeclared = false
    }
    
    fun Configuration.resolvable(
      visible: Boolean = false
    ) {
      isVisible = visible
      isCanBeResolved = true
      isCanBeConsumed = false
      isCanBeDeclared = false
    }
    
    fun Configuration.declarable(
      visible: Boolean = false
    ) {
      isVisible = visible
      isCanBeResolved = false
      isCanBeConsumed = false
      isCanBeDeclared = true
    }
    //endregion
  2. Run ./gradlew artifactViewFilesResolverTask or ./gradlew demoConfWindowsDebugResolver.

  3. Observe that the resolved files do not match the expected value.

    > Invalid resolution. Expected [windows-debug.txt] by was [test-jvm.jar, kotlin-stdlib-2.0.0.jar, annotations-13.0.jar]

Ideally it would be possible to tell Gradle to match (all? some?) attributes exactly. This would trigger a resolution error, because "Windows" != "windows".

The mismatch can be even more obscured when the attribute values are derived from non-String values. E.g. if an attribute value is supposed to be an enum, a plugin might accidentally do FooEnum.Windows.lowercase() in one location, but not in another.

ov7a commented 1 month ago

This issue needs a decision from the team responsible for that area. They have been informed. Response time may vary.

big-guy commented 4 weeks ago

I think there might be a few issues tied up in this:

We want to make this better, but I'm not sure if a different resolution mode is what we want to do. We've added the appropriate labels for this, but we're not going to directly work on this right now.