Publication is configured eagerly before the whole build script is executed and tasks are registered

Imagine a Kotlin build script like this:

plugins {
    `java-base`
    `maven-publish`
}

publishing {
    println("Inside publishing {}")

    publications {
        println("Inside publications {}")

        register<MavenPublication>("maven") {
            println("Inside maven {}")

            val javadoc by tasks.getting

            artifact(tasks.get("javadoc"))

            pom {
                name = "test"
            }
        }
    }
}

tasks {
    println("Inside tasks {}")

    register<Jar>("javadoc") {
        println("Inside javadoc {}")

        archiveClassifier.set("javadoc")
    }
}

The intent here is simple: to create a “maven” publication and attach Javadocs to it. This build script, I believe, uses all the “configuration avoidance” feature, like register to lazily register Named Domain Objects: Tasks and Publications.

As far as I expect from this “laziness”, Gradle should first execute the build whole build script itself, but not the register blocks closures. So here publishing.publications and tasks should be executed first, and then the appropriate register<MavenPublication>("maven") or register<Jar>("javadoc") blocks, if they are needed.

Yet the output of the ./gradlew publishToMavenLocal with this script (note those printlns here and there) is:

Inside publishing {}
Inside publications {}
Inside maven {}

FAILURE: Build failed with an exception.

* Where:
Build file '/home/madhead/Projects/tmp/build.gradle.kts' line: 34

* What went wrong:
Could not create domain object 'maven' (MavenPublication)
> Task with name 'javadoc' not found in root project 'tmp'.

So, it never executes the tasks block, but instead jumps straight into the “maven” publication configuration. Obviously, it cannot find the “javadoc” task definition because the tasks block was not executed yet, so it fails.

The fix is simple: place the tasks block before the publishing block:

plugins {
    `java-base`
    `maven-publish`
}

tasks {
    println("Inside tasks {}")

    register<Jar>("javadoc") {
        println("Inside javadoc {}")
        
        archiveClassifier.set("javadoc")
    }
}

publishing {
    println("Inside publishing {}")

    publications {
        println("Inside publications {}")

        register<MavenPublication>("maven") {
            println("Inside maven {}")

            val javadoc by tasks.getting

            artifact(tasks.get("javadoc"))

            pom {
                name = "test"
            }
        }
    }
}

It works as expected: first it executes the tasks block but does not configure the “javadoc” task. Then it executes all the publications related code (including the “maven” publication configuration, which is required for the publishToMavenLocal task), and in the middle of that configuration it actually instantiates the “javadoc” task

Inside tasks {}
Inside publishing {}
Inside publications {}
Inside maven {}
Inside javadoc {}

BUILD SUCCESSFUL in 1s
3 actionable tasks: 2 executed, 1 up-to-date

What am I missing here? Why is the “maven” publication configuration is eager and not lazy here? How do I make it lazy?

Most containers are currently indeed not treated lazily.
Of the built-in containers I’m only aware of the tasks container being treated fully lazily currently.
For all other containers it is mainly consistent and future proof to use the lazy methods, but it doesn’t actually matter much.

The actual problems here are indeed more in your code.

  1. You break task-configuration avoidance yourself by using tasks.get(...). Better would be tasks.named(...) which does give you a task provider, not a task instance and so does not cause the task to be realized. It would not change much here, as this also would require the task to be registered already.
    Even better than looking up the task would be, to indeed register the task before and just save the instance you get back like val javadoc by tasks.registering(Jar::class) or val javadoc = tasks.register<Jar>("javadoc") and then use that instead of looking it up by name again.

  2. You should usually not add artifacts to publications manually, but instead should properly prepare or configure the software component you are publishing.
    In your case, you should just do at top-level java { withJavadocJar() } and remove practically everything you showed. This will automatically register the proper task, and also add its outcome as proper feature variant for the software component, so that also variant-aware resolution can be used to retrieve it.

1 Like

Thanks for the suggestions, @Vampire! Indeed I missed that getting thing.

As for the java { withJavadocJar() } and properly configuring the components. You’re right. In my real build I use Kotlin Multiplatform plugin which creates them automatically. Also with Kotlin Multiplatform one cannot have the Javadoc JAR as this is Java/JVM-specific thing. And there is no java. In my build I use Dokka to generate something that looks like a Javadoc (but it’s not) and then I add it to the publications so that the Maven Central is happy (having the Javadocs is obligatory):

tasks {
    val dokkaHtml by getting(DokkaTask::class)

    register<Jar>("javadoc") {
        dependsOn(dokkaHtml)
        archiveClassifier.set("javadoc")
        from(dokkaHtml.outputDirectory)
    }
}

publishing {
    publications {
        this.withType<MavenPublication> {
            val javadoc by tasks.getting

            artifact(javadoc)

            …
        }
    }
}

Again here I use getting, but I will try to replace it with proper named.

Thanks!

If you replace getting with existing, that would be the same as using named just using the by property delegate mechanism.

But the point stays, that you should usually never add an artifact to a publication directly, but either build up a proper component with variants, or add a variant to the component that you are publishing.

So for a Java project for example:

val javadocElements by configurations.consumable("javadocElements") {
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
        attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType.JAVADOC))
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
    }
    val javadoc by tasks.existing
    outgoing.artifact(javadoc)
}
val java by components.getting(AdhocComponentWithVariants::class)
java.addVariantsFromConfiguration(javadocElements) {}
publishing {
    publications {
        register<MavenPublication>("foo") {
            from(java)
        }
    }
}

The type of the component created by the Kotlin Multiplatform plugin is org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSoftwareComponentWithCoordinatesAndPublication. It does not extend the AdhocComponentWithVariants and so does not have the addVariantsFromConfiguration method. I cannot use that API.

How bad is simply adding the artifact with the artifact(jar)?

Well, it is just uploading that additional file and that’s it.
To make OSSRH happy, this is of course enough.
But one example.
If you manually create a source jar and use artifact(sourceJar), it is not discoverable as variant in the Gradle Module Metadata, which some tools, including the IntelliJ Kotlin Plugin use to download sources.
So it is usually always better to publish artifacts as proper variants.

How to do it with KMM I don’t know though.

1 Like

One more question then… Why are the components / model tasks are deprecated?

components - Displays the components produced by project ':pipeline'. [deprecated]
dependentComponents - Displays the dependent components of components in project ':pipeline'. [deprecated]
model - Displays the configuration model of project ':pipeline'. [deprecated]

What’s the replacement?

Oh, I see! There is a whole new doc: Understanding variant selection! Need to catch up with that.

The outgoingVariants is the answer!

It’s not a replacement, it is something completely different. :smiley:
Those tasks are part of the deprecated “software model” architecture which Gradle should transition to and was developed with the native tasks, but was then instead deprecated.

1 Like

How to do it with KMM I don’t know though.

Seems it is not possible yet: https://youtrack.jetbrains.com/issue/KT-58830/Expose-AdhocComponentWithVariants-API-on-KGP-generated-component

1 Like

Yep, exactly :slight_smile:

I didn’t mean the exact replacement, but just a way to view the components of a Gradle build.

1 Like