Spring-boot: bootRepackage depends on EAR plugin task causing circular dependencies between tasks

Created on 9 Sep 2015  路  17Comments  路  Source: spring-projects/spring-boot

Build configured to run tasks in the following order war->bootRepackage->ear.

Relevant gradle build configuration:

war {
............
}

bootRepackage {
    withJarTask war
}

dependencies {
    deploy files(bootRepackage)
}

ear {
    deploymentDescriptor {
        displayName = project.name
        webModule(war.archiveName, '/service')
    }

}

The issue happens in such configuration due to circular dependencies between bootRepackage and EAR tasks.

The next code snippet registers archive task deps as default dependencies:

https://github.com/spring-projects/spring-boot/blob/master/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java

TaskDependency runtimeProjectDependencyJarTasks = runtimeConfiguration
                .getTaskDependencyFromProjectDependency(true, JavaPlugin.JAR_TASK_NAME);
        task.dependsOn(
                project.getConfigurations().getByName(Dependency.ARCHIVES_CONFIGURATION)
                        .getAllArtifacts().getBuildDependencies(),
                runtimeProjectDependencyJarTasks);

The above code causes bootRepackage to depend on the next tasks:

bootRepackage task deps [task 'distZip', task ':distTar', task ':ear', task ':war']
bug

All 17 comments

Any ideas on how to fix this apart of using separate builds\change task dependency graph?

Which version of Boot are you using? I think this problem may be caused by the application plugin which Boot's plugin applies for you in 1.2.x. 1.3.0 no longer applies the application plugin so if you're using 1.2.x, please try with 1.3.0.M5.

classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE")

So, yes it's 1.2.5.RELESE.
Will check in few mins 1.3.0.M5 (how stable is it btw?)

Same issue with 1.3.0.M5

here is snippet from ready tasks graph:

bootRepackage task deps [task ':ear', task ':findMainClass', task ':war']

still ear included by default.

Ear dependency comes from:

project.getConfigurations().getByName(Dependency.ARCHIVES_CONFIGURATION)
                    .getAllArtifacts().getBuildDependencies();

Can you share a complete build.grade that reproduces the problem please?

Sure. It's multi project build.

-project
    - projecta
    - projectb

project build.gradle (parent):

buildscript {
    repositories {
        mavenCentral()
        maven { url 'http://repo.spring.io/plugins-release' }
        maven { url artifactoryVirtualRemoteRepos }

    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.0.M5")
        classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7'
        classpath "io.spring.gradle:dependency-management-plugin:0.5.2.RELEASE"
        classpath 'com.sourcemuse.gradle.plugin:gradle-mongo-plugin:0.8.0'
        classpath "org.jfrog.buildinfo:build-info-extractor-gradle:3.1.1"
    }
}

subprojects {

    apply from: '../common.gradle'

    apply plugin: 'idea'
    apply plugin: 'eclipse'
    apply plugin: 'mongo'
    apply plugin: 'propdeps-idea'
    apply plugin: 'propdeps-eclipse'
    apply plugin: 'maven-publish'
    apply plugin: "io.spring.dependency-management"
    apply plugin: 'com.jfrog.artifactory'

    ext {
        versionNumberString = project.version.toString()
    }

    idea {
        module {
            downloadJavadoc = true
        }
    }

    test {
        include "**/unit/**"
    }

    task integrationTest(type: Test) {
        environment "MONGO_DB_HOST", "127.0.0.1"
        include "**/integration/**"
    }

    mongo {
        port 27017
        logging 'console'
        storageLocation "build/mongodb"
        logFilePath "build/embedded-mongo.log"
    }

    repositories {
        mavenCentral()
    }

    sourceCompatibility = globalSourceCompatibility
    targetCompatibility = globalTargetCompatibility

    dependencyManagement {
        dependencies {
            /** common **/
            dependency 'org.apache.commons:commons-lang3:3.4'
            dependency 'commons-io:commons-io:2.4'
            dependency 'org.apache.commons:commons-collections4:4.0'
            dependency 'com.google.guava:guava:18.0'
            dependency 'junit:junit:4.12'
            dependency 'org.springframework.ldap:spring-ldap-core:2.0.4.RELEASE'
            dependency 'org.springframework.retry:spring-retry:1.1.2.RELEASE'

            /** client deps **/
            dependency 'org.springframework:spring-webmvc:4.1.7.RELEASE'
            dependency 'org.springframework:spring-web:4.1.7.RELEASE'
            dependency 'org.springframework.retry:spring-retry:1.1.2.RELEASE'
            dependency 'org.apache.httpcomponents:httpclient:4.5'
            dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6'
            dependency 'com.fasterxml.jackson.core:jackson-core:2.4.6'
            dependency 'com.fasterxml.jackson.core:jackson-annotations:2.4.6'
            dependency 'org.aspectj:aspectjweaver:1.8.6'
        }
    }

    dependencies {
        compile 'org.apache.commons:commons-lang3'
        compile 'commons-io:commons-io'
        compile 'com.google.guava:guava'
        compile 'org.apache.commons:commons-collections4'
        compile 'org.springframework.retry:spring-retry'

        testCompile 'junit:junit'
    }

    task wrapper(type: Wrapper) {
        gradleVersion = globalGradleWrapperVersion
    }

    artifactory {
        clientConfig.setIncludeEnvVars(true)
        contextUrl = artifactoryContextUrl
        publish {
            repository {
                repoKey = 'libs-release-local'
                username = "${artifactoryDeployerUser}"
                password = "${artifactoryDeployerPassword}"
            }
        }
        resolve {
            repoKey = artifactoryVirtualReleaseRepos
        }
    }

}

projecta build gradle (this one fails to run sequence war->bootRepackage->ear)

plugins {
    id "nebula.os-package" version "2.2.6"
}

apply plugin: 'war'
apply plugin: 'spring-boot'
apply plugin: 'ear'
apply plugin: 'rpm'

war {
    dependsOn createBuildInfoFile
    baseName = globalProjectBaseName
    version = versionNumberString

    from(buildDir) {
        include "build-info.properties"
        into("WEB-INF/classes")
    }
}

bootRepackage {
    withJarTask war
}

dependencies {
    deploy files(bootRepackage)
}

ear {
    baseName = globalProjectBaseName
    version = versionNumberString

    deploymentDescriptor {
        displayName = project.name
        webModule(war.archiveName, '/service')
    }

    configurations.archives {
        exclude 'application.yml'
    }
};

tasks.withType(Test) {
    reports.html.destination = file("${reporting.baseDir}/${name}")
}

task sourceJar(type: Jar) {
    from sourceSets.main.allJava
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-data-rest'
    compile 'org.springframework.boot:spring-boot-starter-data-mongodb'
    compile 'org.springframework.boot:spring-boot-starter-logging'
    compile 'com.fasterxml.jackson.datatype:jackson-datatype-joda'
    compile 'org.springframework.ldap:spring-ldap-core'

    /** must be providedRuntime for production builds! **/
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")

    testCompile('org.springframework.boot:spring-boot-starter-test')
}

publishing {
    publications {
        mavenJar(MavenPublication) {
            from components.java
        }
        mavenWar(MavenPublication) {
            from components.web
        }
        mavenEar(MavenPublication) {
            artifacts = [ear.archivePath, ear]
        }
    }
}

artifactoryPublish {
    publications('mavenJar', 'mavenWar', 'mavenEar')
    publishArtifacts = true
    properties = ['groupId': globalProjectBaseGroup, 'artifactId': globalProjectBaseName, 'version': versionNumberString]
    publishPom = true
    publishIvy = true
}

projectb build gradle (jar library)

apply plugin: 'java'

jar {
    baseName = globalProjectBaseName
    version = versionNumberString
}

dependencies {
    compile 'org.springframework:spring-webmvc'
    compile 'org.springframework:spring-web'
    compile 'org.springframework.retry:spring-retry'
    compile 'org.apache.httpcomponents:httpclient'
    compile 'com.fasterxml.jackson.core:jackson-databind'
    compile 'com.fasterxml.jackson.core:jackson-core'
    compile 'com.fasterxml.jackson.core:jackson-annotations'
    compile 'org.aspectj:aspectjweaver'
}

jar {
    baseName = project.name
    version = versionNumberString

    manifest {
        attributes 'Implementation-Title': project.name.tokenize('-').collect { it.capitalize() }.join(' '),
                'Implementation-Version': version,
                'Build-time': buildTimestamp
    }
}


publishing {
    publications {
        mavenJar(MavenPublication) {
            from components.java
        }
    }
}

artifactoryPublish {
    skip = false
    publications('mavenJar')
    publishArtifacts = true
    properties = ['groupId': globalProjectBaseGroup, 'artifactId': globalProjectBaseName, 'version': versionNumberString]
    publishPom = true
}

I can send those project zipped if you want to check it out in action (should be easier to determine where issue is).

gradle clean buld

image

Something that I can just unzip/clone and run would certainly help. I can't see where common.gradle is coming from at the moment, for example.

I'm also a bit confused by a few things:

  • Why do you have a single project that produces a war and an ear? I'd typically expect the ear to be created by a separate project that consumes one or more wars from other projects.
  • Why are you using bootRepackage, which creates an executable war, if you're then trying to wrap everything in an ear which needs to be deployed to an app server?

here it is https://github.com/clajder/bootRepackage-ear.git

>gradle clean build
  • there is need only for 1 war per ear. Ear is used to set proper webmodule context. There are no ejbs or other war modules
  • it's executable war with provided tomcat libs so later I'll be able to run same war during testing using embedded tomcat + package same war into ear to run on any env (uat, pre-prd) with internal app server which is not tomcat at all

I know that issue can be fixed easily moving ear packaging into another prj that depends on war. If there are no other workarounds I will go with this approach.

thanks

Thanks. That's helped a lot. The problem is that the bootRepackage task is configured to depend on all of the tasks that contribute artifacts to the project's archives configuration:

project.getConfigurations().getByName(Dependency.ARCHIVES_CONFIGURATION)
                        .getAllArtifacts().getBuildDependencies()

That creates a cycle as the one artifact is the ear file. The creation of the ear file depends on bootRepackage as you've configured it to contain the repackaged war file:

dependencies {
    deploy files(bootRepackage)
}

There's no need for bootRepackage to depend on the ear task so that's something that we can improve. In the meantime, you could package the original war file in the ear and keep the repackaged version separate to use for testing. To do that, you just need to change the dependencies of the deploy configuration:

dependencies {
    deploy files(war)
}

This will work of course - as it packs original war into ear removing circular dependency.

Btw I tried to move ear packaging into a separate project that depends on :war project - it doesn't work as well as there is no registered outgoing bootRepackage artifact. Are there any ways to make ear packaging project to depend on bootRepackage output?

the next sctipt snippet picks up war archive configuration and doesn't see bootrepackage one:

dependencies {
    deploy project(path:':projecta', configuration: 'archives')
}

ear {
    baseName = globalProjectBaseName
    version = versionNumberString

    deploymentDescriptor {
        displayName = project.name
        webModule(project(':projecta').war.archiveName, '/service')
    }

    configurations.archives {
        exclude 'application.yml'
        exclude '**/*.tar'
        exclude '**/*.zip'
    }
};

how to reference for packaging bootRepackage task output?

Perhaps you could add the output of the bootRepackage task to a new repackagedArchives configuration and then depend on that?

There is no way to define new configuration + define outgoing artifacts for the task which depends on bootRepackage.

It's weird...but the same cyclic dependency appears again

Ah, yes. That makes sense actually. It doesn't break the cycle, just adds an extra step to it. Ok, sounds like we need to improve the plugin to reduce the tasks that it depends upon. As I said above, there's no need for bootRepackage to depend on the ear task so we should fix that.

Potentially makes sense to give freedom to select on what tasks bootRepackage task depends on.

For all fighting this, snippet from my workaround:

      //if you need to do sth like this:
         project.artifacts {
            myConf myTaskDependantOnBootRepackage
        }
     //you can remove then bootRepackage dependencies to Archives like this:
        project.tasks.matching {it.name == "bootRepackage"}.each {
            Set<Object> deps =  it.taskDependencies.values
            def toBeeRemoved = deps.find {
                it.class.name.startsWith("org.gradle.api.internal.artifacts.DefaultPublishArtifactSet")
            }
            deps.removeAll(toBeeRemoved)
        }
Was this page helpful?
0 / 5 - 0 ratings