How can I specify a dependency file extension and still have Gradle resolve transitive dependencies?

Hey folks,

I have a Gradle project which declares a dependency on various Jenkins related artefacts. These are a bit unusual as their pom.xml packaging element is set to hpi (which is some Jenkins specific packaging format that it uses for plugins).

Despite publishing hpi artefacts they also publish a jar artefact and it’s this that I want to get hold of, so I can add it to my compile classpath.

I thought this would be as simple as adding @jar to the dependency specification e.g.

testImplementation "org.jenkins-ci.plugins:job-dsl:${jobDslVersion}@jar"

When I add the file extension specifier, Gradle does download the jar artefact`. However… it also then stops downloading any of the dependency’s own transitive dependencies. This in turn causes compilation and runtime failures.

I’ve spent a good few hours trying to understand Gradle’s dependency resolution functionality via the docs and feel like I’m none the wiser. So I have created a repro repository which aims to demonstrate the problem.

Full details in the README and build.gradle files of the repository, very happy to answer any questions here if there’s anything that’s unclear.

Most grateful if someone could help me figure out how to get this working.

Cheers!

Edd

If you depend on pure coordinates, you depend on the declared dependencies and the artifact that is declared in the module descriptor as is, so in your case the hpi file.

If you specify with @jar that you want the .jar artifact, you only get that exact artifact without any dependencies, because the dependencies are declared for the main artifact, not for auxiliary artifacts.

The optimal solution for you as consumer would be, if those artifacts are published with Gradle Module Metadata with feature variants, so that you can depend on the jar variant including its dependencies.

As a work-around, you can use component metadata rules to “fix up” the metadata so that the jar file is the main artifact instead of the hpi file.

If you for example add this in your settings script, you should get the desired result:

dependencyResolutionManagement {
    components {
        withModule('org.jenkins-ci.plugins:job-dsl') {
            allVariants {
                withFiles {
                    addFile("${id.name}-${id.version}.jar")
                }
            }
        }
    }
}

Thank you for the prompt reply @Vampire, that has definitely helped me progress.

I now realise that some of the transitive dependencies themselves are also packaged as .hpi files. Is there a way I can use the Gradle DSL to configure something to the effect of “For all transitive dependencies of job-dsl add each transitive dependency’s .jar file”?

If they would publish feature variants that are designed for it, maybe.
If not, then I don’t think so.
Or at least I don’t have an idea right away, short of dirty things like letting Gradle resolve it, iterate over the resolution result, and then doing the according dependencies or component metadata rules if possible.
But that might not even work properly.

Inspired by the above I’ve come up with something which seems to work:

components {
    all { ComponentMetadataDetails cmd ->
        final Set<String> jenkinsPluginGroups = [
            "io.jenkins.plugins",
            "org.jenkins-ci.plugins",
            "org.jenkinsci.plugins",
            "com.cloudbees.jenkins.plugins"
        ]
        final Set<String> pluginsThatDoNotPublishAnHpiArtefact = [
            "job-dsl-core",
            "lib-durable-task"
        ]
        if(jenkinsPluginGroups.contains(cmd.id.id.group)) {
            allVariants {
                withFiles {
                    addFile("${id.name}-${id.version}.jar")
                    if(!pluginsThatDoNotPublishAnHpiArtefact.contains(id.name)) {
                        addFile("${id.name}-${id.version}.hpi")
                    }
                }
            }
        }
    }
}

There’s a catch (of course): It seems that not all Jenkins plugins publish an .hpi artefact, so I’ve had to implement a curated list of plugins that I use which I know do not publish an .hpi artefact. I have also had to provide a list of groupIds which identify Jenkins plugins. I think that the effort involved in curating these lists should be pretty minimal so I’m going to go with it. However if anyone knows of a way of checking to see if each dependency publishes an .hpi file in a more dynamic way then I’m all ears!

Sharing the current solution in case it helps anyone else in the future. Thanks again for the help @Vampire .

1 Like