Dependency configuration with multiple roles?

I am using the following idiom to include some asciidoctor-generated docs in a distribution. In the asciidoctor project I have:

val docs by configurations.registering {
    isCanBeConsumed = true
    isCanBeResolved = false
    isCanBeDeclared = false
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named<DocsType>(DocsType.USER_MANUAL))
   }
}

artifacts {
    add(docs.name, asciidoctor.get().outputDir) {
        builtBy(asciidoctor)
    }
}

and in the consuming project I have:

val docFiles by configurations.creating {
    isCanBeConsumed = false
    isCanBeResolved = true
    isCanBeDeclared = true
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named<DocsType>(DocsType.USER_MANUAL))
    }
}

dependencies {
    docFiles ( project(path = ":myDocProject") )
    ....
}

distributions {
    main {
        contents {
            ....
            into ("docs") {
                from(docFiles)
            }
        }
    }
}

In the declaring configurations documentation it says that only one of isCanBeConsumed, isCanBeResolved and isCanBeDeclared should be set to true, but in my hands I need to set two of them in the consuming project for this to work. Am I using an incorrect approach, or is the Gradle documentation not quite correct here?

I am testing with Gradle 8.14.1 and 9.5.1 and asciidoctor version 4.0.4, so far without enabling the configuration cache (which causes other issues with asciidoctor). I am aware of ResolvableConfiguration and ConsumableConfiguration, but those are still incubating so I have avoided them in production so far.

The documentation is right.
You should have one where canBeDeclared is true where you declare the dependency (dependencyScope with the incubating helpers) and one where canBeResolved is true (resolvable with the incubating helpers) that extends from the dependency scope one.

OK - many thanks for the hint. I now have:

val docFilesDecl by configurations.creating {
    isCanBeConsumed = false
    isCanBeResolved = false
    isCanBeDeclared = true
}

val docFiles by configurations.creating {
    extendsFrom(docFilesDecl)
    isCanBeConsumed = false
    isCanBeResolved = true
    isCanBeDeclared = false
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named<DocsType>(DocsType.USER_MANUAL))
    }
}

dependencies {
    docFilesDecl ( project(path = ":myDocProject") )
    ....
}

with the distributions block unchanged. With the incubating helpers, I can set up the configurations like this:

val docFilesDecl = configurations.dependencyScope("docFilesDecl")

val docFiles = configurations.resolvable("docFiles") {
    extendsFrom(docFilesDecl)
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named<DocsType>(DocsType.USER_MANUAL))
    }
}

Exactly.
Maybe one more detail, don’t use creating but registering.
It is always better to register instead of create, and for configurations just like tasks, Gradle treats them lazy already.

Yes, I wrote that build script when I knew a lot less about Gradle than I do now :wink:

For the sake of anyone else coming across this thread, there is one wrinkle with registering and older versions of Gradle: registering returns a provider of a configuration, so the extension needs to be done like this before Gradle version 9.4.0:

val docFiles by configurations.registering {
    extendsFrom(docFilesDecl.get())
    ....

which introduces an element of eagerness. The extendsFrom method that takes one or more providers was introduced in 9.4.0, and is currently still incubating.