Groovifique seu build!

Este blog deixou de ser mantido, mas o autor continua escrevendo aqui. Não deixe de assinar o novo feed!

groovy_build.pngEu sei que fiquei um bom tempo sem escrever nada, mas tentarei resolver tal situação com este post, que será longo e interessante! Por isso, economizarei nos “lero-leros” e compensarei nos códigos.

Descreverei a seguir o funcionamento do processo de build de um projeto que venho desenvolvendo. Mesmo este projeto não sendo grande, o processo de empacotamento dos arquivos é meio complicado. Apenas para te dar uma idéia, neste projeto estou usando Maven para controle de dependências e build básico do projeto; também são usados scripts Groovy para preparação do sistema de Ajuda e para geração dos artefatos finais (distribuição dos binários, códigos-fonte e Java Web Start) que são disponibilizados aos usuários.

Pretendo mostrar como podemos criar facilmente um build script de razoável complexidade usando Groovy.

Problema 1: executando script Groovy através do Maven

Neste post eu mostrei o código de um script Groovy que criei para fazer a indexação das páginas de Ajuda da aplicação. Tudo funciona muito bem, mas eu ainda tinha que executar o script manualmente.

Já que estou usando o Maven, seria muito bom se eu conseguisse configurá-lo de tal forma que este executasse o script Groovy de forma automática. E, de fato, é algo bem simples de ser feito:

  1.  
  2. <?xml version="1.0" encoding="UTF-8"?>
  3. <project>
  4.     <!– informações do projeto –>
  5.     <build>
  6.         <plugins>
  7.             <plugin>
  8.                 <groupId>org.codehaus.mojo.groovy</groupId>
  9.                 <artifactId>groovy-maven-plugin</artifactId>
  10.                 <executions>
  11.                     <execution>
  12.                         <phase>generate-resources</phase>
  13.                         <goals>
  14.                             <goal>execute</goal>
  15.                         </goals>
  16.                         <configuration>
  17.                             <source>${pom.basedir}/src/main/groovy/IndexHelp.groovy</source>
  18.                         </configuration>
  19.                     </execution>
  20.                 </executions>
  21.             </plugin>
  22.         </plugins>
  23.     </build>
  24. </project>
  25.  

Com isso, sempre que o Maven executar a fase generate-resources (que é relacionada à geração de arquivos a serem incluídos nos pacotes), o script IndexHelp.groovy é executado.

Embora exista a possibilidade de se incluir os scripts Groovy diretamente no XML, eu não recomendaria por questões de organização.

Problema 2: criar uma nova release é chato!

Atualmente, cada nova release deste projeto é composta de três pacotes:

  • Pacote binário (que contém apenas os arquivos JAR e outros arquivos com informações sobre o projeto (README, CHANGELOG etc). É compactado no formato ZIP;
  • Pacote completo contendo os fontes da aplicação. É basicamente uma alternativa ao checkout do VCS, também disponibilizado em um arquivo ZIP;
  • Distribuição Java Web Start, onde o usuário baixa o descritor JNLP e o Java Web Start se encarrega de baixar ou atualizar os arquivos necessários do servidor web.

Abaixo segue um esquema sobre os procedimentos necessários para se criar cada um dos pacotes: (se você zela pela sua saúde, não leia)

  • Pacote binário
    1. Criar um diretório em um local qualquer;
    2. Copiar JARs gerados pelo Maven no diretório criado;
    3. Copiar os arquivos de informações (README, CHANGELOG etc) no diretório criado;
    4. Empacotar o diretório criado em um arquivo ZIP;
    5. Remover o diretório criado;
    6. Gerar um checksum MD5 para conferir a integridade do arquivo após o upload do mesmo no servidor.
  • Pacote com os fontes
    1. Criar um diretório em um local qualquer;
    2. Copiar toda a estrutura de arquivos do projeto no diretório criado;
    3. Remover arquivos de log, arquivos ocultos e arquivos criados pelo VCS;
    4. Remover os arquivos binários (JAR, .class);
    5. Empacotar o diretório criado em um arquivo ZIP;
    6. Remover o diretório criado;
    7. Gerar um checksum MD5 para conferir a integridade do arquivo após o upload do mesmo no servidor.
  • Java Web Start
    1. Criar uma Keystore e uma chave para assinar digitalmente os arquivos JAR (caso ainda não tenha sido criado);
    2. Siga os passos 1 e 2 descritos no primeiro item;
    3. Copiar os descritores JNLP no diretório criado;
    4. Assinar os arquivos JARs com a chave gerada no primeiro passo;
    5. Empacotar o diretório criado em um arquivo ZIP (para diminuir o tamanho do arquivo e aumentar o desempenho do upload);
    6. Remover o diretório criado;
    7. Gerar um checksum MD5 para conferir a integridade do arquivo após o upload do mesmo no servidor.

Se você ainda não meteu uma bala na própria cabeça, me diga uma coisa: isso é chato ou não é? (se você acha que não é, procure um médico… agora).

Nem é preciso dizer que fazer manualmente todo esse processo é bastante difícil e chato. Mas então, como fazemos para resolver esta situação? Creio que o nome que veio na sua cabeça é aquele: Ant.

Não irei entrar no mérito da questão se o Ant é bom ou não, mas eu particularmente não sou muito chegado ao Ant. Sei lá, acho que a sintaxe XML poderia ser mais enxuta. Mas nem tudo está perdido, pois existe uma solução para este problema.

Gant - escrevendo targets Ant com Groovy

Confesso que, num primeiro momento, achei este projeto um tanto quanto estranho, mas vi que trata-se de uma ferramenta que combina com perfeição o poder do Ant com a simplicidade do Groovy.

Scripts Gant são simples scripts Groovy, mas mais poderosos pois são capazes de declarar targets no estilo Ant com 0% de gordura trans XML. :)

Para saber como instalar o Gant em seu sistema, visite o site do projeto.

Problema 3: gerando o pacote com os binários

Finalmente um pouco de ação!

Bom, a primeira coisa que fiz foi criar um arquivo de build vazio, chamado build.gant (que é o nome padrão), no diretório raíz do projeto (no mesmo local onde fica o arquivo pom.xml).

Os trechos de código mostrados adiante foram adaptados a partir do build script original do projeto, que é mais polido. Este se encontra disponível caso você queira ver como funciona.

Primeiramente, eu precisei coletar algumas informações do usuário:

  1.  
  2. app = ‘xxx’ // nome da aplicação
  3. out = ‘xxx’ // diretório onde os arquivos resultantes serão colocados
  4. version = ‘xxx’ // versão a ser gerada
  5.  
  6. target(prepareFolder : ‘Prepare the release output folder’) {
  7.   app = cString(‘Application name’, app)
  8.   out = cString(‘Folder where the generated files will be placed’, out)
  9.   Ant.mkdir(dir:out)
  10.   version = cString(‘Release version’, version)
  11. }
  12.  
  13. def cString(msg, defaultValue = ) {
  14.   print "$msg [$defaultValue]: "
  15.   def value = System.in.readLine()
  16.   if (value.trim() == ) return defaultValue
  17.   value
  18. }
  19.  

Neste trecho de script definimos um target para criar o diretório que receberá os arquivos gerados. O método Ant.mkdir é uma task do Gant que permite a criação de diretórios, e seu funcionamento é idêntico ao funcionamento da task Ant homônima.

O target que gera o pacote binário pode ser visto a seguir:

  1.  
  2. target(bin : ‘Generate the binary package’) {
  3.   depends(prepareFolder)
  4.   def folder = "$out/$app-$version-bin"
  5.   def zip = "${folder}.zip"
  6.  
  7.   Ant.echo ‘Generating the binary package’
  8.   Ant.delete(dir:folder)
  9.   Ant.delete(file:zip)
  10.   Ant.mkdir(dir:folder)
  11.   Ant.copy(toDir:folder) {
  12.     fileset(dir:‘obexftp-frontend-core/target/executable-netbeans.dir’)
  13.     fileset(dir:‘info’, excludes:‘**/*SOURCES*.txt’)
  14.   }
  15.   Ant.zip(destFile:"${folder}.zip") {
  16.     zipfileset(dir:folder)
  17.   }
  18.   md5("${folder}.zip")
  19.   Ant.delete(dir:folder)
  20.   Ant.echo ‘Binary package generated successfully’
  21. }
  22.  
  23. void md5(file) {
  24.   Ant.echo "Generating the Checksum of the ‘$file’ file…"
  25.   Ant.exec(executable:‘md5sum’, output:"${file}.md5sum.txt") {
  26.     arg(value:‘-b’)
  27.     arg(value:file)
  28.   }
  29. }
  30.  

Muito simples! :) Temos um target chamado bin que depende de um outro chamado prepareFolder. Esse target ainda faz a cópia dos arquivos desejados para uma pasta temporária, os empacota em um arquivo ZIP, gera o checksum MD5 desse arquivo e remove o diretório temporário.

Já podemos perceber que é possível de se criar qualquer target ou código que use as tasks do Ant sem que seja necessário manjar tudo do Gant; basta abrir a documentação das tasks do Ant no seu browser e as utilizar seguindo essas regrinhas de sintaxe. É fácil!

Problema 4: gerando o pacote com os fontes

Não há muita novidade aqui, como você pode ver no exemplo de script abaixo:

  1.  
  2. target(source : ‘Generate the source package’) {
  3.   depends(prepareFolder)
  4.   def folder = "$out/$app-$version-src"
  5.   def zip = "${folder}.zip"
  6.  
  7.   Ant.echo ‘Generating the source package’
  8.   Ant.delete(dir:folder)
  9.   Ant.delete(file:zip)
  10.   Ant.mkdir(dir:folder)
  11.   Ant.copy(toDir:folder, includeEmptyDirs:false) {
  12.     fileset(dir:‘.’, excludes:‘**/target/** **/.** **/log/**’)
  13.   }
  14.   Ant.zip(destfile:"${folder}.zip") {
  15.     zipfileset(dir:folder)
  16.   }
  17.   md5("${folder}.zip")
  18.   Ant.delete(dir:folder)
  19.   Ant.echo ‘Source package generated successfully’
  20. }
  21.  

A diferença aqui é que usamos vários patterns no parâmetro excludes, em fileset.

E não pense que esses códigos não funcionam, pois eles funcionam sim! O fato de eu estar apenas colocando alguns trechos não muda nada!

Problema 5: gerando a distribuição Java Web Start

Aqui a coisa fica um pouco mais cabeluda, mas nada que complique a vida de um usuário Gant. :)

Se uma aplicação precisa rodar com um nível restritivo mais baixo, todos os arquivos JAR desta aplicação devem ser assinados digitalmente para que o Java Web Start possa saber que tais arquivos JAR não foram alterados por terceiros.

Para fazer isso, o primeiro passo é gerar uma keystore e uma chave para que possamos assinar os JARs do projeto:

  1.  
  2. alias = ‘myself’
  3. keystore = ‘myKeystore’
  4. storepass = ‘***’
  5. keypass = ‘***’
  6.  
  7. target (jws : ‘Generate the Java Web Start package’) {
  8.   depends(prepareFolder)
  9.   def folder = "$out/$app-$version-jws"
  10.   def zip = "${folder}.zip"
  11.  
  12.   readJwsInfo()
  13.   Ant.echo ‘Generating the Java Web Start package’
  14.   Ant.delete(dir:folder)
  15.   Ant.delete(file:zip)
  16.   Ant.mkdir(dir:folder)
  17.   Ant.copy(toDir:folder) {
  18.     fileset(dir:‘obexftp-frontend-core/target/executable-netbeans.dir’)
  19.     fileset(dir:’src/main/jws’)
  20.     fileset(dir:’src/main/resources’, excludes:‘**/*.xcf’)
  21.   }
  22.   signJars(folder)
  23.   Ant.zip(destFile:"${folder}.zip") {
  24.     zipfileset(dir:folder)
  25.   }
  26.   md5("${folder}.zip")
  27.   Ant.delete(dir:folder)
  28.   Ant.echo ‘Java Web Start package generated successfully’
  29. }
  30.  
  31. void readJwsInfo() {
  32.   alias = cString(‘Key alias’, alias)
  33.   keystore = cString(‘Keystore file’, keystore)
  34.   storepass = cString(‘Keystore password’, storepass)
  35.   keypass = cString(‘Key password’, keypass)
  36. }
  37.  
  38. void signJars(folder) {
  39.   def params = [lazy:true, ‘keystore’:keystore, ‘alias’:alias, ’storepass’:storepass]
  40.   if (keypass != ) params.‘keypass’ = keypass
  41.   Ant.signjar(params) {
  42.     fileset(dir:folder, includes:‘**/*.jar’)
  43.   }
  44. }
  45.  

Perceba que a utilização das tasks seguem um padrão de sintaxe bastante peculiar. O que pode complicar um pouco é saber quais tasks aceitam quais parâmetros, mas tudo isso pode ser consultado diretamente na documentação do Ant.

Legal! Com algumas poucas tasks fizemos tudo o que precisávamos. Ou melhor, quase tudo…

Problema 6: testando a distribuição Java Web Start

Da forma que está, ainda seria necessário fazer a implantação da distribuição Java Web Start (no servidor de produção) para testá-la! Mas lembre-se que estamos criando o build script em Groovy, e, por isso, tal questão pode ser facilmente resolvida. :)

Um jeito simples que encontrei foi definir o codebase (local onde a distribuição Java Web Start é implantada) em tempo de build.

Primeiramente, abri os arquivos JNLP e substituí o valor do parâmetro codebase por um token %CODEBASE%:

  1.  
  2. <?xml version="1.0" encoding="UTF-8"?>
  3. <jnlp spec="1.0+" codebase="%CODEBASE%">
  4.     <!– conteúdo do arquivo –>
  5. </jnlp>
  6.  

Então, durante o build, o script pede para o usuário preencher o codebase:

  1.  
  2. jwsCodebase = ‘http://projeto.com/javawebstart’
  3. target (jws : ‘Generate the Java Web Start package’) {
  4.   // …
  5.   Ant.copy(toDir:folder) {
  6.     fileset(dir:‘obexftp-frontend-core/target/executable-netbeans.dir’)
  7.     fileset(dir:’src/main/jws’)
  8.     fileset(dir:’src/main/resources’, excludes:‘**/*.xcf’)
  9.   }
  10.   replaceCodebase(folder)
  11.   signJars(folder)
  12.   // …
  13. }
  14.  
  15. void readJwsInfo() {
  16.   jwsCodebase = cString(‘Java Web Start codebase’, jwsCodebase)
  17.   alias = cString(‘Key alias’, alias)
  18.   // …
  19. }
  20.  
  21. void replaceCodebase(folder) {
  22.   Ant.replace(dir:folder, token:‘%CODEBASE%’, value:jwsCodebase)
  23. }
  24.  

Fazendo isso, podemos criar uma distribuição Java Web Start cujo codebase aponte para uma URL qualquer. Então, basta mandar os arquivos para o endereço correspondente e fazer o teste da aplicação. Se o download e verificação ocorrerem normalmente, basta gerar a versão oficial da distribuição Java Web Start e proceder com a implantação.

Mas, como rodar um target?

Crie vergonha nessa cara e use o help! :D

$ cd myProject/
$ ls build.gant
build.gant

$ gant -h

Conclusão

Não há nada que não possa ser feito com Groovy e um pouco de imaginação. E, o que é melhor, com pouco esforço.

Imagem modificada por: Daniel F. Martins

Tags: , , , , , , , , ,