Bula de Remédio

janeiro 3, 2016

Build an Unusual Project with Gradle

Filed under: Build, Gerencia de Configuração e Mudanças — jadsonjs @ 2:31 am

Gradle is the newest build systems that is became very popular in the Java world by its power and flexibility.

Gradle rather than having a configuration based on xml like Ant and Maven is written in Groovy a scripting language on the JVM. This gives us a pure programming language to develop our build on. Using Groovy Gradle can use any existing ant targets, Maven plug-ins or any Java classes making it a very powerful tool. It also uses the standard maven conventions so as long as everything is laid out like a maven project it can find everything without issue.

Ant

For any non-trivial projects it becomes mind-bending, and takes great care to ensure that complex builds are truly portable. Its imperative nature can lead to replication of configuration between builds

 

Maven

Maven takes the opposite approach and expects you to completely integrate with the Maven lifecycle. Experienced Ant users find this particularly jarring as Maven removes many of the freedoms you have in Ant. Maven configuration is very verbose, and if you want to do anything that is “not the Maven way” you have to write a plugin or use the hacky Ant integration.

 

Gradle

Gradle combines good parts of both tools and builds on top of them with DSL and other improvements. It has Ant’s power and flexibility with Maven’s life-cycle and ease of use.

gradle_2

The end result is a tool that was released in 2012 and gained a lot of attention in a short period of time. For example, Google adopted Gradle as the default build tool for the Android OS.

Gradle DSL is designed to solve a specific problem: move software through its life cycle, from compilation through static analysis and testing until packaging and deployment.

 

Don’t know Gradle? Gradle Initial Tutorial

My presentation about Gradle

 

To show the Gradle power and flexibility, I needed to make a build for a legacy project that generated a EAR files with some JAR and WAR modules inside it. Very different from the Maven/Gradle default structure and some specific tasks that exist just in this project.

In this post, I put the Gradle script that I used for make the Continuos Integration of this project with Gradle.

 

The unusual project structure is show bellow:

 


Project Structure


 

Project

—  src1

—  src2

—  src3

— app

|– project.ear

|– project.jar (compiled classes from src1, src2. src3)

|–project.war

|– WEB-INF

|– web.xml

|–secondWar.war

|– WEB-INF

|– web.xml

|–ejb-facede.jar

|–META-INF

|–ejb-jar.xml

|–META-INF

|–application.xml


 

And here is the Gradle script the make the build of this project. We are using the Gradle version 2.4.

The first step is include the required Gradle plugins.  Gradle has a War and EAR plugins that create War and Ear zip files. But with this plugins I could not create the EAR structure show above, so I used only the Java plugin and created the packing task manually.

 

Starting the build script include the required plugins:


/* A java plugin, with basic tasks to manager Java project */
apply plugin: 'java'

/* Plugin manager Eclipse IDE classpath */
apply plugin: 'eclipse'

/* Plugin to deploy the system using SSH */
apply plugin: "org.hidetake.ssh"

/* Plugin to version the SQL script using flyway tool */
apply plugin: 'org.flywaydb.flyway'

 

Now we need define some information about the application like the name and version. How we are using the Jenkins Continuos Integration Server to make the build of the system, we are passing some values from Jenkins Jobs parameter and reading this parameter with System.getProperty(“propertie_name”)

 

  
  /** Default task */
   defaultTasks 'build'

   /** Parametros para a build (serão definidos pelo jenkins ) */
   def application_name = 'myApp'; /*  Default name of the application*/
   /* Version of the application, pass by jenkins */
   def application_version = System.getProperty("application_version");

 

 

The next one is define some information about our source code properties:


/* Source and Target Java Version */
sourceCompatibility = 1.7
targetCompatibility = 1.7

/* Source code encoding */
compileJava.options.encoding = 'ISO-8859-1'
compileTestJava.options.encoding = 'ISO-8859-1'
compileJava.options.debugOptions.debugLevel = "source,lines,vars"
compileTestJava.options.debugOptions.debugLevel = "source,lines,vars"

/* Group of the output file */
group 'br.my_company'

/* System Version pass by parameter by jenkins */
version = application_version

 

Next step is define the structure of the project source folders:

 


sourceSets {
   /* Source Folder structure */
   main {
     java {
        srcDir 'src1'
        srcDir 'src2'
        srcDir 'src3'
     }
     resources {
       srcDir 'src1'
       srcDir 'src2'
       srcDir 'src3'
     }
   }
}

 

Now I define the repositories where the Gradle will download the dependencies of the project. In other words, the JAR files used by our application:

 


/* declare the maven central repository */
repositories {
   mavenCentral()
}

/* declare our own repository */
repositories {

 /* Declare an own internal Maven repository */
 maven {
   url "http://someURL.br/artifactory/architecture_dependencies"
    credentials {
      username '${artifactory_user}'
      password '${artifactory_password}'
    }
  }
}

 

In my case, I create 4 configuration that represent 4 types of dependence that we have is our project. For this configuration and for the default configuration of Gradle, I set all as a no transitive. To just download the Jars declared in this script and do not resolve transitive dependence (dependence of dependence) automatically.


configurations {
 compile {
 transitive = false
 }
 
 runtime{
 transitive = false
 }
 
 testCompile{
 transitive = false
 }
 
 testRuntime{
 transitive = false
 }

 /* Internal dependence */
 internalLib {
 transitive = false
 }
 
 /* external dependence */
 externalLib {
 transitive = false
 }
 
 /* Jars files to copy to JBOSS deploy lib */
 deployLib {
   transitive = false
 }
 
 /* Jar files that can not be copy to JBOSS deploy directory */
 noDeployLib {
   transitive = false
 }
 
 // some configuration extends others
 deployLib.extendsFrom internalLib
 compile.extendsFrom deployLib, noDeployLib
}

// don't cache dynamic versions at all //
configurations.all {
 resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
}
// don't cache changing modules at all
configurations.all {
 resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}

 

Now between the dependencies closure we define the dependencies of our application:

 


dependencies {

  internalLib(group: 'br.my_company', name: 'comunsClasses', version: '1.4.11')

  externalLib(group: 'br.my_company', name: 'sharedResources', version: '1.6.8')

  deployLib(group: 'activation', name: 'activation', version: '1.1')
  deployLib(group: 'hibernate3', name: 'hibernate3', version: '3.2')

  noDeployLib(group: 'el-api', name: 'el-api', version: '6.0.20')
  noDeployLib(group: 'junit', name:'junit', version:'4.8')
  noDeployLib(group: 'mockito-all', name: 'mockito-all', version: '1.9.5')
  noDeployLib(group: 'org.powermock', name: 'powermock-core', version: '1.6.1')
   
}

 

 

Now we start to define the task used to create a EAR of the system.

 

At first, we define some variables with information about the project structure.

 


  def projectDirectory = "my_application_name"
  def secondWarDirectory = 'seconde_war_name'
  def earOutPut = "$buildDir/tmp/earOutput"
  def facedeName = "ejb_facede"
  def facedeDir = "app/"+projectDirectory+".ear/"+facedeName+".jar"

 

 

And then, I stated to create the tasks the will generated the parts of EAR final file. For each JAR or WAR file inside the EAR, I created a Gradle task.

The most of these tasks are tasks the create a zip file from a specific directory of the system project.

 


/* Create a JAR to the EJB module of the system, zipping the facade directory */
task createEJBFacedeJar(description : 'Create the JAR with de system EJB', type: Jar) << {
   archiveName = facedeName+".jar";
   from ("$facedeDir/")
}

/*Create the application_name.war file from the war directory */
task createWar(description : 'Created the Web aplication', type: War) << {
    // where the directory is located //
    from 'app/'+projectDirectory+'.ear/'+projectDirectory+'.war'
    baseName = application_name
}

/*Create a second war file, zip the directory if JSPs, CSS and JavaScript files */
task createSecondWar(description : 'Create the web aplication ', type: War) << {

  // where the directory is located //
  from 'app/'+projectDirectory+'.ear/'+secondWarDirectory+'.war'

  baseName = 'second_war_name'
}

/* Copy to temp directory of EAR the *.jars files and the META-INF directory */
task copyEarResouces(description : 'Copy EAR resources like META-INF') << {
 copy {
    delete "$earOutPut"
    from "app/"+projectDirectory+".ear/lib"
    into "$earOutPut/lib"
 }
 
 copy {
    from "app/"+projectDirectory+".ear/META-INF"
    into "$earOutPut/META-INF"
 }
}

/* Copy to temp directory the war file */
task copyWarFile (description : 'Copy WAR') << {
 copy {
    from "$buildDir/libs"
    into "$earOutPut"
    include "**/"+projectDirectory+"*.war"
    rename(/(.*)/, projectDirectory+'.war')
 }
}

/* Copy to temp directory the second war file */
task copySecondWarFile (description : 'Copy WAR ') << {
 description = "Copy the War File to tmp output directory"
 copy {
   from "$buildDir/libs"
   into "$earOutPut"
   include "**/"+secondWarName+"*.war"
   rename(/(.*)/, secondWarName+'.war')
 }
}

/* Copy to the temp directory the EJB jar file */
task copyEJBFacedeFile(description : 'Copy the JAR with the system EJB') << { 
   copy { 
     from "$buildDir/libs" 
     into "$earOutPut" 
     include "**/"+facedeName+".jar" 
   } 
}

/* Finally, zip the temp directory, creating the EAR zip file */
task earSystem(description : 'Create the EAR of system', type: Jar) << {
 archiveName = application_name+".ear";
 from ("$earOutPut/")
}


 

Here is an example of the power of the Gradle. I needed a specific task that probably few people need to do and is difficult to someone imagine to put this task as a default task in any build tool like Ant, Maven ou Gradle.

I needed to create a properties file inside the EAR file where this properties file should contain the version of some dependencies of my system and the release date. Some of this information are contained inside the own Gradle build script.

How with Gradle we can use programming languages structures like for and if and Groovy/Java API like FileWriter and Properties classes, this task became little bit easy.

 

 

task changeVersion (description : 'Change the information') << { 
   def props = new Properties() 
   def writer = new FileWriter(file('src/system_version.properties')) 
   try { 
     /* write all internal dependencies version */
     configurations.internalLib.dependencies.each { 
         dep -> props.setProperty(dep.name, dep.version); 
     } 
     /* write all external dependencies version*/
     configurations.externalLib.dependencies.each { 
         dep -> props.setProperty(dep.name, dep.version); 
     } 
     
     /* write the system version and release date*/
     props.setProperty("system", version); 
     props.setProperty("releaseDate", new Date().format('dd/MM/yyyy')); 
     props.store(writer, "System Version File"); 
     writer.flush() 
   } finally { 
     writer.close() 
   } 
} 

 

Now I just define the order of these tasks will run, to create the EAR file correctly:


assemble.dependsOn earSystem
earSystem.dependsOn copyWarFile, copySecondWarFile, copyEJBFacedeFile, copyJARFile
copyWarFile.dependsOn createWar
copySecondWarFile.dependsOn createSecondWar
copyEJBJARFile.dependsOn createEJBFacedeJar
copyJARFile.dependsOn jar
jar.dependsOn changeVersion

 

The build process is finish!!!

To run the tests, I create two tasks, one for each type of test. These tests are not call in the build, just when we call the specific task.

/* Task to run the unit tests for each build */
task unitTest(type: Test, dependsOn: testClasses) {

   include '**/AllUnitTests.*'
   exclude '**/AllIntegrationTests.*'
}

/* Task to run the integration tests just for especific builds */
task integrationTest(type: Test, dependsOn: testClasses) {

   include '**/AllIntegrationTests.*'
   exclude '**/AllUnitTests.*'

}

/* Configuration for all tests */
tasks.withType(Test) {

    /* Memory the will be used in the test */
    jvmArgs "-XX:MaxPermSize=1024m"
}
/* Disable test in the build, run the test only when call the specific task */
test {
   exclude '**/*'
}

 

 

But we also need that the project be compiled in our IDE. For this task, we can use the eclipse plugin:

 


eclipse {

 // Configure the file .classpath of eclipse
 pathVariables 'GRADLE_USER_HOME': file('${gradle.gradleUserHomeDir}')

 // where the eclipse will put the .class files
 classpath {
    defaultOutputDir = file('app/'+projectDirectory+'.ear/'+projectDirectory+'.jar')
 }

}

/* Executa a task after configure the eclipse class path */
eclipseClasspath {
 copyToLibDir.execute()
}

 

Using this plugin, all dependencies declared in the Gradle script will be automatically put in the .classpath eclipse file to make the build of the system inside Eclipse IDE. We don’t need configure the eclipse class path manually, nevermore.

 

In the dependencies closure we declared the dependencies of our system. But the dependence that the  Gradle need to run the build (in other words: where the Gradle will find the plugins), we need to declare inside the buidscript closure.


buildscript {
 
  repositories {
    jcenter()
  }
 
 repositories {
    maven {
       url "https://plugins.gradle.org/m2/"
    }
  }
 
 dependencies {
 classpath "org.jfrog.buildinfo:build-info-extractor-gradle:3.1.1"
 classpath "org.hidetake:gradle-ssh-plugin:1.1.3"
 classpath "org.flywaydb:flyway-gradle-plugin:3.2.1" /* Dependency of flyway */
 classpath "postgresql:postgresql:9.1-901.jdbc4" /* Postgres driver to flyway */
 }
}

 

To automate the system deploy, you can use a org.hidetake.ssh plugin to copy by SSH the Jar, War or Ear of your system. This is a example of a ssh copy task:


task deploySystem (description: 'Deploy the system by SSH') <<{
 
 // properties read pass by jenkins
 def deploy_system_url = System.getProperty("servidorDeploy");
 def deploy_system_directory = System.getProperty("diretorioDeploy");
 def deploy_system_port = System.getProperty("portaDeploy");
 def deploy_system_user = System.getProperty("usuarioDeploy");
 def deploy_system_password = System.getProperty("passwordDeploy");

   /* Define the remote servers, you can use a forEach to run several */
   ssh.remotes {
      server1 {
          host = deploy_system_url
          port = new Integer(deploy_system_port)
          user = deploy_system_user
          password = deploy_system_password
          knownHosts = allowAnyHosts
      }
   }

  ssh.run {
    session(ssh.remotes.server1) {
       put from: "$buildDir/libs/*.ear", into: deploy_system_directory
       execute 'sudo service tomcat restart'
    }
  }
}

Database version is a important part of build when you want to eliminate manual deploys and use Continuos Delivery

Gradle has integration with the Flyway, a tool to  database migrations. The follow task make this integration:


/* apply the sqls and version the database
 *
 * flywayMigrate Migrates the database
 * flywayClean Drops all objects in the configured schemas
 * flywayInfo Prints the details and status information about all the migrations
 * flywayValidate Validates the applied migrations against the ones available on the classpath
 * flywayBaseline Baselines an existing database, excluding all migrations up to and including baselineVersion
 * flywayRepair Repairs the metadata table
 */
task applyScripts (description: 'Apply SQL scritps in the database') << {
 
 // properties get from jenkins
 def migrate_system_database_url =  System.getProperty("servidorMigracao"); 
 def migrate_system_database_name = System.getProperty("bancoDadosMigracao"); 
 def migrate_system_user =  System.getProperty("usuarioMigracao");
 def migrate_system_password = System.getProperty("passwordMigracao");
 

 flyway {
    driver='org.postgresql.Driver'
    url='jdbc:postgresql://'+migrate_system_database_url+'/'+migrate_system_database_name+''
    user = migrate_system_user
    password = migrate_system_password
 
   // List are defined as comma-separated values
   flyway.schemas=['agenda','file','evaluation','vacation']
 
   // location of the SQLs scripts
   locations = ["filesystem:/$rootDir/../PROJECT_WITH_SCRIPTS/db/migration"]
 
   encoding = 'ISO-8859-1'
 
   println 'migrating: '+migrate_system_database_url+'/'+migrate_system_database_name
  }
}

 

The Gradle Wrapper allow people to run Gradle without need to install it.

To use wrapper into your project you need to create the follow task in your build script:


/** Allow to run gradle without install */ 
task wrapper(type: Wrapper) { 
  gradleVersion = '2.4'
} 

And, after that, run the wrapper task:

gradle -q wrapper 

After this the Gradle will generated “shell scripts” and Jar files in your project. You need to version this scripts in your project

Everyone can now download the scripts and Jars files and execute the build using these scripts:

./gradlew build

 

This script (gradlew.sh or gradlew.bat) will make the same thing of install Gradle and run grade -q build command.

 


 

This was a complete example of a Gradle build script used to make a Continuos Integration process in a complex real legacy project that use an unusual structure.

This exemple showed the power and flexibility of Gradle that let us decide step by step how the build will be made. Sometimes you can stop a company to change the entire system structure to fit in the maven way to do things. Gradle helped us a lot.

But if you have new projects you can also use Gradle, without use so much configuration to build fast forward your project.

Because of this flexibility and live philosophy, people are switching Maven by Gradle as build system.

 


 

Deixe um comentário »

Nenhum comentário ainda.

RSS feed for comments on this post. TrackBack URI

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

Blog no WordPress.com.

%d blogueiros gostam disto: