Build em somente um passo com Want

O assunto foge bastante do conteúdo aqui do blog, mas…

Want é uma ferramenta para automatizar a compilação de projetos delphi criada por Juancarlo Anez como podemos constatar no sourceforge. Sua criação foi inspirada na ferramenta Apache Ant e com isso acabou suportando a grande maioria de tarefas que esta tem a disposição. Não encontrei muita documentação a respeito do want mas a documentação do Apache Ant foi suficiente para minhas dúvidas e inclusive o próprio autor a indica.

De onde vem o nome? Conforme cita o autor a primeira idéia foi representar “Windows Ant” só que ele queria algo que também incluísse o Kylix/FreePascal. Porém como era fácil pensar: want compile, want install, ou seja, desejo compilar, desejo instalar, o nome acabou sendo mantido. 😀

2.0 Por que utilizar?
Diria que automatizar o processo de compilação, principalmente quando se tem vários programas ou mesmo vários módulos, algumas diretivas de compilação e outras coisinhas mais, te leva a deixar de se preocupar com vários detalhes que geralmente são esquecidos no momento da distribuição dos binários para o Cliente e como diz a experiência, uma falha neste processo sempre causa alguns problemas e alguma insatisfação.
Além disso, uma das etapas que o Joel defende em seu blog no artigo 12 passos para um código melhor é exatamente o build em somente um passo. Admirando suas idéias e pensamentos (o cara é realmente bom no que escreve e pensa) não poderia deixar de perceber que realmente é um passo de grande importancia! E acredito que temos um bom motivo pra fazê-lo ao somar todo o processo que vai do Ctrl+F9 até a disponibilização em um ftp ou website.

3.0 Copiando e instalando
Do sourceforge você irá baixar este arquivo compactado que contém o binário, o código fonte do want e do dunit (do mesmo autor) além de alguns exemplos de script em xml.
Pra facilitar, coloque o arquivo want.exe em um diretório que esteja no path de seu sistema operacional (ou direcione o path até ele).
Copie também este programa ftp, feito em Delphi com os componentes Indy que será utilizado como exemplo de compilação.

A versão do Indy necessária é a 9.00.10 que vem por padrão no Delphi 7 mas não no 6. Pensando na frustação de quem usa o Delphi 6 e quer seguir o artigo deixo a dica: Remova o Indy que vem em seu Delphi através de “Adicionar/Remover programas – Borland Delphi 6/Alterar” desmarcando apenas o Indy. Depois baixe este arquivo, extraia e compile a package. Se ainda restar dúvida, veja as notas específicas para a sua versão de delphi direto do site do Indy.

4.0 Criando um script de compilação (want.xml)
Acompanhe a criação das tags observando o arquivo want.xml da pasta do programa ftp.

4.1 O cabeçalho do arquivo
O script possui seção geral (como todo xml) onde se é definido o nome do projeto, o diretório base que será utilizado na definição dos caminhos e o alvo padrão que será chamado quando não for fornecido parâmetro algum ao want.exe. Facilita bastante utilizar . ou .. para indicar o diretório atual e o anterior, principalmente quando resolver mudar o caminho dos fontes.

<project name="ftpth" basedir="." default="all">
</project>

4.2 Definindo propriedades
Considere propriedade como sendo uma varíavel do tipo string. Ela tem um nome e um conteúdo. Para definí-la você utiliza a sintaxe:

<property name="nomevar" value="valor"/>


Para definir o valor, além do texto, pode-se utilizar alguma outra propriedade já definida, variáveis vindas da linha de comando e chaves de arquivos ini.

<property name="nomevar" value="${propertyJaDefinida}textoadicional"/>

4.3 Lendo chaves de um arquivo ini
Antes um detalhe: Os caminhos de arquivos/diretórios no want, mesmo em ambiente windows são definidos utilizando barra normal.

Se sua empresa/projeto possui vários programadores, suspeito que cada um deles prefira instalar aquela suite de componentes ou até mesmo o próprio Delphi em um caminho diferente. Diria que cada um bagunça como pode, então iremos isolar os caminhos destes componentes em um arquivo ini com seções e chaves pré-definidas pra facilitar uma posterior manutenção no script. Nesta mesma “bagunça” cada programador há de querer que os binários resultantes da compilação fiquem em um diretório específico, então, mais uma chave no arquivo ini. Outro para os arquivos zipados, outro para isto, aquilo, aquilo…

Teoria: Para ler uma chave de um arquivo ini utilizamos o operador ?{}. A linha a seguir lê a chave “key1” da seção “seção1” do arquivo “th.ini” atribuindo seu valor a propriedade “nomeprop”:

  <property name="nomeprop" value="?{th.ini:seção1:key1}"/>


Embora o programa a ser compilado (ftpth) não necessite de componentes que não tenham dcu’s fora da pasta lib, e o want também disponibilize uma chave para incluir o LibraryPath do delphi, vou ilustrar a inclusão. Ela está comentada utilizando <!– –> e dentro da seção project:

<!--
  <property name="delphibase" value="?{want.ini:paths:delphibase}"/>
  <property name="tponguard" value="${delphibase}/?{want.ini:path:tponguard}"/>
  <property name="jvcl" value="${delphibase}/?{want.ini:path:jvcl}"/>
  <property name="jcl" value="${delphibase}/?{want.ini:path:jcl}"/>
-->
  <property name="bin" value="?{want.ini:paths:bin}"/>
  <property name="src" value="?{want.ini:paths:src}"/>
  <property name="doc" value="?{want.ini:paths:doc}"/>

4.4 Variáveis vindas da linha de comando

Para as vezes em que for necessário definir algo somente no momento da chamada, existe a possibilidade de passar variáveis para o want através da flag -D.
Exemplo: Definindo que a propriedade nomeprop receberá o valor da variável nomevar
Na chamada do want: want acao -Dnomevar=conteudo
A variável no script:

  <property name="nomeprop" value="%{var_linha_cmd}"/>

4.5 Conjunto de propriedades
Para facilitar a posterior inclusão dos caminhos todos de uma só vez na chamada do compilador, um agrupamento dos caminhos acima é o ideal. Até porque existem sistemas que utilizam vários componentes de terceiros e ninguém costuma colocar tudo numa mesma pasta :D. Para isso, utiliza-se a tag patternset:

  <patternset id="sources">
<!--
    <include name="${tponguard}"/>
    <include name="${jvcl}"/>
    <include name="${jcl}"/>
-->
  </patternset>

4.6 Versionamento
Utilizando um arquivo ini e o auxílio de expressões regulares vamos definir algumas propriedades com as versões que serão utilizadas nos alvos daqui alguns minutos ;D. Ficarão posicionadas na seção project visando a visibilidade para todos alvos.

  <!-- buscando a versão do último build em um arquivo ini -->
  <property name="old.version" value="?{want.ini:ftpth:versao}" />
  <!-- build anterior - note a utilização de expressões regulares -->
  <regexp property="old.build" text="${old.version}"
          pattern="^.*.([0-9]+)$" subst="1" />
  <!-- incrementa o build number e prepara string com as versões -->
  <property name="build" value="={1 + ${old.build}}" />
  <regexp property="version" text="${old.version}" 
          pattern=".[0-9]*$" subst=".${build}" />
  <regexp property="comma.version" pattern="." subst="," 
          text="${version}"/>
  <property name="zipfile" value="${bin}/ftpth-${version}.zip"/>

4.7 Definindo alvos pré compilação

Como em makefile’s, muito utilizados em c, c++, e outras, é possível definir alvos que realizam alguma tarefa especifica e também alvos que possuem alguma dependência. Um alvo (target) sempre será especificado na linha de comando indicando para o executável o que fazer. O alvo pode estar definido em “default” na tag “project” e ser omitido na chamada.

Fazendo do alvo “clean” o responsável em limpar o diretório definido no arquivo ini por sua vez mapeado na propriedade bin:

  <target name="clean">
    <delete dir="${bin}" basedir=".">
      <include name="*.exe"/>
      <include name="*.map"/>
    </delete>
    <delete dir="${src}" basedir=".">
      <include name="*.dcu"/>
      <include name="*.drc"/>
    </delete>
    <delete dir="${doc}" basedir=".">
      <include name="*.*"/>
    </delete>
  </target>

Este alvo prepare cria o diretório onde ficarão os binários, pre-processa o arquivo de resources (com o ícone e informações da versão do arquivo) e gera o arquivo .res. Ele utiliza as propriedades de incremento de versão, este alvo será dependencia de outro, que chamaremos de “build”.

<target name="prepare">
<mkdir dir="${bin}"/>
<!-- exibindo mensagem com a versão -->
<echo message="version=${version}" />
<echo message="build=${build}" />
<!-- note que as marcações do arquivo ftpth.template.rc são substituídas automaticamente -->
<echo input="${src}/ftpth.template.rc" file="${src}/ftpth.rc" />
<!-- gerando o arquivo de resources -->
<brcc file="${src}/ftpth.rc" output="${src}/ftpth.res" />
</target>

4.8 Defindo os alvos de compilação
Nos alvos abaixo não é utilizado nem a metade das opções possíveis para a tag dcc. Porém acredito que a grande maioria (senão todos) os parâmetros do dcc32.exe estão disponíveis. Fica a dica de uma exploração no código fonte do want, mais precisamente no arquivo DelphiTasks.pas da linha 254 em diante.

  <target name="compile">
    <!-- Invoca o compilador dcc para o arquivo ftpth.dpr no diretório src -->
    <dcc basedir="${src}" source="ftpth.dpr">
      <!-- Os executáveis vão para o diretório bin -->
      <exeoutput path="../${bin}"/>
      <console value="true"/>
      <!-- Definindo uma diretiva de compilação -->
      <define name="DEBUG"/>
      <uselibrarypath value="true"/>
      <!-- Onde encontrar os sources (comentado pela utilização do library path)
      <unitpath  refid="sources"/>
      <includepath refid="sources"/>
      -->
      <resourcepath>
        <include name="${src}"/>
      </resourcepath>
    </dcc>
  </target>

Sendo o alvo anterior a compilação com informações de debug, vamos criar um novo a partir dele só que retirando as informações de debug e inserindo otimização no compilador. Se chamará “build” e depende dos alvos clean e prepare, ou seja, a sequencia de execução ao chamar “want build” será: clean, prepare, build.

  <target name="build" depends="clean,prepare">
    <dcc basedir="${src}" source="ftpth.dpr">
      <exeoutput path="../${bin}"/>
      <build     value="true"/>
      <optimize  value="true"/>
      <console value="true"/>
      <uselibrarypath value="true"/>
      <resourcepath>
        <include name="${src}"/>
      </resourcepath>
    </dcc>
  </target>

4.9 Gerando a documentação do fonte
Um exemplo de alvo que gera a documentação utilizando a ferramenta PasDoc. Pode notar que é invocado o executável propriamente dito com alguns parâmetros e em seu lugar poderia estar qualquer outra ferramenta que você conhece/utiliza que aceite parâmetros na chamada.
Uma coisinha nova que aparece é o operador @{} que faz com que o caminho relativo passado seja expandido e adaptado ao formato do sistema operacional.

  <target name="doc">
    <mkdir dir="${doc}"/>
    <exec executable="pasdoc">
      <arg value="--write-uses-list"/>
      <arg value="-T FtpTh"/>
      <arg value="-L br"/>
      <arg value="-v 2"/>
      <arg value="-M private,protected,published,public"/>
      <arg value="-E"/>
      <arg value="@{${doc}}"/>
      <arg value="@{${src}/*.pas}"/>
    </exec>
  </target>

4.10 ZIP
Não será necessário compactador externo já que a want tem uma tag nativa para tal. Não a conhecia, mas é a PasZLib.
No arquivo zip será incluído o binário da compilação, os fontes e sua documentação.

  <target name="zip">
    <zip zipfile="${zipfile}">
      <include name="${bin}/*.exe"/>
      <include name="${src}/*.*"/>
      <exclude name="${src}/*.dcu"/>
      <exclude name="${src}/*.exe"/>
      <include name="${doc}/*.*"/>
      <include name="want.xml"/>
      <include name="want.ini"/>
      <include name="want.css"/>
      <include name="want.html"/>
      <include name="ftpsend.template"/>
    </zip>
  </target>

4.11 FTP
Está se perguntando pra que este programa de ftp já que o windows tem um desde o início da leitura, certo?
Bom, o ftp padrão do windows funciona extremamente bem até você estar atrás de um proxy. Fizeram o favor de não implementar o modo passivo (command pasv) e isso me impossibilita de utilizá-lo.

Assim como no arquivo dos resources, teremos um arquivo de template com os comandos para envio pelo ftp com algumas variáveis. A idéia é pre-processar este arquivo e enviar o resultado como parâmetro para o programa que vai despachar o build.

Arquivo template: ftpsend.template

Arquivo resultante: ftpsend.ftp

  <target name="ftpsend" depends="zip">
    <property name="ftphost" value="?{want.ini:ftp:host}"/>
    <property name="ftpuser" value="?{want.ini:ftp:user}"/>
    <property name="ftpkey"  value="?{want.ini:ftp:key}"/>
    <property name="binfile" value="@{${zipfile}}"/>
    <property name="dirdoc"  value="@{${doc}/*.*}"/>
    <echo input="ftpsend.template" file="ftpsend.ftp" />
    <exec executable="binary/ftpth">
      <arg value="ftpsend.ftp"/>
    </exec>
    <delete dir="." file="ftpsend.ftp"/>
  </target>

4.12 CVS
Só fazendo uma “xerox” do alvo tag do script de build do próprio want:

  <target name="tag">
    <regexp property="cvs.tag" text="build_${version}" pattern="."   subst="_" />
    <cvs command="rtag -R -F ${cvs.tag} ." />
  </target>

4.13 Alvo default – O faz tudo!

Lembra-se que na seção project ficou definido que o alvo “all” deve ser o default?
Ele também fica responsável em gravar a versão no arquivo ini já que enquanto não chegar tudo no ftp podem acontecer erros e a versão não deveria ser incrementada por isso.

  <target name="all" depends="build,doc,tag,ftpsend">
    <!-- Gravando a versao no arquivo ini (só é executado se todo o processo tiver sucesso)-->
    <ini file="want.ini">
      <write section="ftpth" key="versao" value="${version}" />
    </ini>
  </target>

Conclusão
Foi ótimo digitar want e puf! está lá no site tudo correto, o número de versão, ícone, cvs etc 😀
Se chegou até aqui acredito que vai achar ruim que não foi colocado pelo menos três ótimos alvos:
1) compilação de sistemas modularizados em packages

2) DUnit (já existe a tag)
3) InnoSetup (também já tem tag)

Thiago Borges de Oliveira
thborges at gmail dot com

Anúncios

Um comentário sobre “Build em somente um passo com Want

Os comentários estão desativados.