Adding a component variant w/o introducing ambigui...
# plugin-development
a
Adding a component variant w/o introducing ambiguity I am trying to add a component variant which uses
runtime
or
runtimeElements
as a base variant but that introduces an ambiguity, which i'm struggling to find a way to avoid. Is there an example how it's supposed to be done? Here is my playground plugin https://github.com/aloubyansky/playground/blob/gradle-comp-variants/plugin/src/main/java/org/example/PlaygroundPlugin.java And here is the outcome of running it https://github.com/aloubyansky/playground/tree/gradle-comp-variants. Thanks!
t
a
Hi @tony, yeah, perhaps i'm overlooking something. My issue is that I add a new variant with
runtimeElements
as the base one and add a new attribute to it. But when I resolve a configuration, I don't ask for that attribute. What I get in a disambiguation rule is a value for the attribute defined in my custom variant with consumer value
null
, since i didn't ask for it. And I can't seem to remove this variant as a choice. If that makes sense.
the only way around it that appears to work so far is if i add two variants: one is a complete copy of the base variant with a new attribute with value
off
, for example. The other variant would add a dependency that i need to the base variant and the new attribute with value
on
, for example. Then in this case, i could add a disambiguation rule that would select the variant with value
off
if the consumer value for the new attribute is
null
that appears to work, although perhaps there is a better way to do that
t
have you also read https://docs.gradle.org/current/userguide/how_to_share_outputs_between_projects.html#variant-aware-sharing ? I'm struggling a bit with you saying the consumer isn't requesting the attribute. Per the doc above, this kind of inter-project artifact-sharing is typically done with custom configurations that have some additional attribute(s) declared to help distinguish from "standard" configurations
v
Your
greetingClasspath
(which you did not configure in a non-legacy way with the "canbe" properties) misses attributes. As you do not request any attribute at all, Gradle does not really know what you are after. If you for example add
Copy code
config.attributes(attrs -> {
   attrs.attribute(Category.CATEGORY_ATTRIBUTE, getObjectFactory().named(Category.class, Category.LIBRARY));
   attrs.attribute(Usage.USAGE_ATTRIBUTE, getObjectFactory().named(Usage.class, Usage.JAVA_RUNTIME));
   attrs.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, getObjectFactory().named(LibraryElements.class, LibraryElements.JAR));
   attrs.attribute(Bundling.BUNDLING_ATTRIBUTE, getObjectFactory().named(Bundling.class, Bundling.EXTERNAL));
   attrs.attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, getObjectFactory().named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM));
   attrs.attribute(Attribute.of("other-attr", String.class), "on");
});
your new variant will be matched. Also, if you invent a new attribute, should usually always add it to the attribute schema too, otherwise some things will not work properly.
a
the thing is i don't always want to select the attribute i added
other-attr
the article Tony referenced suggests a different approach - re-use
LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE
with a custom value, i'm wondering whether that could possibly work for my use-case
by adding a new variant with a new attribute to component (external) the way i'm doing it, i'm introducing ambiguity, i realize that
i suppose Gradle would fail to resolve pre-defined/standard configurations after that?
i also create various custom configuration, in some of them i want to select the variant i added, in others i don't want it
v
by adding a new variant with a new attribute to component (external) the way i'm doing it, i'm introducing ambiguity, i realize that
Not really, if you do not request the attribute, the original variant wins
I would not semantically abuse an attribute with totally different meaning. In the linked docs the variant is for a different artifact, it is for an instrumented jar, so the library elements is suitable for that.
At most maybe the bundling could be slightly releated, with a value of "augmented" and adding a compatibility rule that external is fine if augmented is requested or something like that. 🤷‍♂️
a
@Vampire if i remove
attrs.attribute(Attribute.of("other-attr", String.class), "on");
from your suggestion, it will fail, right?
v
That's the opposite of what I just told you 😉
a
but it does, unless i'm missing something
v
no, it does not, I verified that before I wrote it
Also see step 2 in the algorithm link I just sent
a
ok, i admit i have diverged from the original by adding another attribute in the mix
v
[...] IF multiple candidates remain, check if one: [...] ◦ Has no missing attributes compared to another candidate.
a
@Vampire if i just add this
Copy code
diff --git a/plugin/src/main/java/org/example/PlaygroundPlugin.java b/plugin/src/main/java/org/example/PlaygroundPlugin.java
index da768d7c..7ac16a1c 100644
--- a/plugin/src/main/java/org/example/PlaygroundPlugin.java
+++ b/plugin/src/main/java/org/example/PlaygroundPlugin.java
@@ -7,6 +7,11 @@ import org.gradle.api.Project;
 import org.gradle.api.Plugin;
 import org.gradle.api.artifacts.ComponentMetadataDetails;
 import org.gradle.api.attributes.Attribute;
+import org.gradle.api.attributes.Bundling;
+import org.gradle.api.attributes.Category;
+import org.gradle.api.attributes.LibraryElements;
+import org.gradle.api.attributes.Usage;
+import org.gradle.api.attributes.java.TargetJvmEnvironment;
 import org.gradle.api.plugins.JavaPlugin;
 
 
@@ -26,6 +31,13 @@ public class PlaygroundPlugin implements Plugin<Project> {
 
         project.getConfigurations().register("greetingClasspath", config -> {
             config.extendsFrom(project.getConfigurations().getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME));
+            config.attributes(attrs -> {
+                attrs.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, Category.LIBRARY));
+                attrs.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME));
+                attrs.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.getObjects().named(LibraryElements.class, LibraryElements.JAR));
+                attrs.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.EXTERNAL));
+                attrs.attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, project.getObjects().named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM));
+            });
         });
i get
Copy code
$ ./gradlew :app:greeting
> Task :app:greeting FAILED

[Incubating] Problems report is available at: file:///home/aloubyansky/git/playground/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:greeting'.
> Could not resolve all files for configuration ':app:greetingClasspath'.
   > Could not resolve xom:xom:1.3.9.
     Required by:
         project :app
      > Cannot choose between the available variants of xom:xom:1.3.9:
          - otherVariant
          - runtime
        All of them match the consumer attributes:
          - Variant 'otherVariant' capability 'xom:xom:1.3.9':
              - Unmatched attributes:
                  - Provides org.gradle.category 'library' but the consumer didn't ask for it
                  - Provides org.gradle.libraryelements 'jar' but the consumer didn't ask for it
                  - Provides org.gradle.status 'release' but the consumer didn't ask for it
                  - Provides org.gradle.usage 'java-runtime' but the consumer didn't ask for it
                  - Provides other-attr 'on' but the consumer didn't ask for it
          - Variant 'runtime' capability 'xom:xom:1.3.9':
              - Unmatched attributes:
                  - Provides org.gradle.category 'library' but the consumer didn't ask for it
                  - Provides org.gradle.libraryelements 'jar' but the consumer didn't ask for it
                  - Provides org.gradle.status 'release' but the consumer didn't ask for it
                  - Provides org.gradle.usage 'java-runtime' but the consumer didn't ask for it

* Try:
> Ambiguity errors are explained in more detail at <https://docs.gradle.org/8.13/userguide/variant_model.html#sub:variant-ambiguity>.
> Review the variant matching algorithm at <https://docs.gradle.org/8.13/userguide/variant_attributes.html#sec:abm_algorithm>.
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at <https://help.gradle.org>.

BUILD FAILED in 473ms
5 actionable tasks: 2 executed, 1 from cache, 2 up-to-date
it does work with
attrs.attribute(Attribute.of("other-attr", String.class), "on");
but with that it also works w/o the rest of the attributes added
v
It still says "but the consumer didn't ask for it" in your error message, so something is not right.
a
right, i didn't ask for it
v
You did now with that patch
Or should have
a
no, i don't ask for the
other-attr
i just created a new variant with that attribute but i don't ask for it
v
That would be fine, read the algorithm I linked you to
But you have "didn't ask for it" for all attributes still in that error message
If I apply that patch you showed and execute the greeting task, there is no error but it works just fine
a
ah, for you mean for other attributes, right
oh, sorry, it works after
clean
v
Also dependency insight shows proper result
Copy code
./gradlew app:dependencyInsight --configuration greetingClasspath --dependency xom:xom
Copy code
> Task :app:dependencyInsight
xom:xom:1.3.9
  Variant runtime:
    | Attribute Name                 | Provided     | Requested    |
    |--------------------------------|--------------|--------------|
    | org.gradle.status              | release      |              |
    | org.gradle.category            | library      | library      |
    | org.gradle.libraryelements     | jar          | jar          |
    | org.gradle.usage               | java-runtime | java-runtime |
    | org.gradle.dependency.bundling |              | external     |
    | org.gradle.jvm.environment     |              | standard-jvm |

xom:xom:1.3.9
\--- greetingClasspath

A web-based, searchable dependency report is available by adding the --scan option.
a
interesting, i somehow assumed that i would not need to request the rest of the attributes explicitly
v
If you request your custom attribute it would probably work as that variant is the only one providing it
👍 1
But if you request no attributes at all, Gradle does not know what to do
a
that means every configuration, such as
runtimeClasspath
, always requests these attributes?
i thought that if i extend a configuration, such as
implementation
, i would somehow inherit the attributes relevant for that one and i would need to add mine "on top" of that
since i don't necessarily know which attributes are relevant besides mine
v
You have to know which attributes are relevant, because when you define a resolvable configuration you-and-only-you-alone can know what the purpose of that configuration is and thus which attributes need to be requested. For example is the configuration used as a compile classpath, then it needs the
java-api
variants. But if the configuration is used as a runtime classpath, then it needs the
java-runtime
variants. And so on.
👍 1
On
implementation
there are no attributes defined, that would be useless, as it is only a dependency bucket where you declare dependencies.
👍 1
You could extend
compileClasspath
or
runtimeClasspath
but that wouldn't help either. You would still only inherit the dependencies (additionally the
compileOnly
, resp.
runtimeOnly
), but not the attributes.
You can actually declare directly on a dependency some attributes, those are inherited as they are bound to the dependency.
👍 1
But be sure that there is way more to is that you didn't grasp yet, I needed quite some time to wrap my head around all this and am still not sure I got it completely. 😄 There are also attributes that are on configurations, and attributes that are on artifacts, but when declaring what you want, you declare them at the same place unfortunately. And there are artifact views with or without variant reselection with which you can then choose a different variant (like
sourcesElements
) for the resolved dependencies or can trigger artifact transforms that transform the resolved artifacts. .......
The whole attribute / variant aware resolution stuff is very mighty, but quite complex. But the hardest part most often is when defining new attributes, what they should be, and what their values should be, especially as it is more or less cemented in stone for published components that declare those attributes. :-D
a
i've seen some bits of that, yeah, i'm asking for trouble following that path, i suppose
so, i guess, i'll be back with questions 😄
👌 1
v
Maybe, or you just have to overcome the quite steep learning curve 🙂
👍 1
a
i appreciate your support @Vampire and @tony
👌 1