Configurar ANT con librerías externas y generadores JFlex y CUP

por Mar 29, 2020Desarrollo de Software0 Comentarios

ANT básicamente es una herramienta de compilación para proyectos Java al estilo del make para C. En esta ocasión voy a explicar como configurar ANT para el uso de librerías externas y usando los generadores JFLEX y CUP.

Me propusieron en la universidad que realizara una presentación sobre ello, me venía perfecto después de haber estado haciendo varios ejemplos. Me dispuse a ordenar las investigaciones y a crear un proyecto de ejemplo sencillo con lo más importante para realizar la presentación. Con todo ello hecho me animo a presentarlo aquí también.

¿Qué es ANT?

Como ya he comentado, ANT es una herramienta para compilación de proyectos JAVA, y como tal escrita en el mismo lenguaje. Se podría decir que es una herramienta como make para programas en C solo que especialmente diseñada pensando en JAVA. Nos proporciona los medios para la realización de las tareas que componen el proceso de compilación, documentación, distribución, etc de un proyecto JAVA.

Es muy útil para los casos en los que se trabaja con distintos entornos de desarrollo para disponer de una manera estándar y fácil de compilar el proyecto. Sobretodo en proyectos complejos con muchas librerías o en proyectos web en los que las tareas de compilación comprenden más pasos. Además la mayoría de los entornos de desarrollo disponen de opciones para la ejecución de scripts de ANT.

Comentar que actualmente no es ANT la herramienta más usada para esto. Cada vez se está usando más una herramienta similar llamada Maven, la cual tiene algunas ventajas sobre el que ahora nos ocupa.

Instalación de ANT

La instalación de ANT es bien sencilla, básicamente consiste en situar los archivos de ant en las variables de entorno del sistema. Podemos conseguir los archivos de instalación en la web del proyecto. Pese a ser una obviedad, comentaré que se necesita tener instalado el JDK (Java Developer Kit) puesto que ant está escrito en Java.

Mac OS

Yo no he tenido que hacer nada para instalarlo, o viene por defecto o se instaló con el Xcode de Mac OS.

Linux

Si suponemos que hemos situado los archivos ANT en /usr/local/ant

export ANT_HOME=/usr/local/ant
export JAVA_HOME=/usr/local/jdk-1.5.0.05
export PATH=${PATH}:${ANT_HOME}/bin

Windows

Si suponemos que hemos situado los archivos de ANT en C:\ant\

set ANT_HOME=c:\ant
set JAVA_HOME=c:\jdk-1.5.0.05
set PATH=%PATH%;%ANT_HOME%\bin

Si hemos instalado ant correctamente podremos teclear el comando para ver su versión en la consola y nos deberá reconocer el comando.

Para ejecutar ant sobre un fichero build.xml nos situaremos en el directorio donde este el fichero y teclearemos ant para ejecutar el objetivo por defecto, o ant y un objetivo en concreto para ejecutar ese objetivo concreto (se ejecutarán también sus dependencias obviamente).

#Obtener la versión instalada de ant
$ant -version

#Ejecutar un script de ant con el objetivo por defecto
$ant

#Ejecutar un script de ant con el objetivo que elijamos
$ant objetivo

Algunas definiciones

Fichero de configuración de ANT build.xml

En el famoso make de C teníamos un fichero llamado makefile, en este caso tendremos un fichero xml llamado build.xml con los pasos que se han de seguir para la compilación de nuestro proyecto. Pasemos ahora a definir los elementos de dicho fichero (o etiquetas puesto que es un fichero XML).

  • project: Es el elemento raíz del fichero XML y como tal solo puede haber uno en el fichero. Es el propio proyecto con el que estamos trabajando
  • target: Elemento que agrupa un conjunto de tareas que se desean realizar en un momento determinado. Como veremos puede haber unos objetivos dependientes de otros
  • task: Es el código ejecutable que será aplicado a la aplicación en algún momento, puede contener distintas propiedades.
  • property: Parámetros en forma de par clave-valor (los nombres serán case-sensitive) para personalizar el proceso de construcción o simplemente como accesos directos a un dato que se use de manera repetitiva en el fichero xml.

Estructura de un proyecto Java

Continuemos ahora viendo una estructura de directorios para un proyecto Java. En primer lugar tenemos el directorio classes en donde se almacenarán las clases Java compiladas.

Estructura proyecto Java. Configurar ANT con librerías externas
  • Dist: Aplicación empaquetada en forma de jar autoejecutable. También se podría incluir aquí la documentación para su distribución aunque he optado por ponerla en la raíz del proyecto.
  • Doc: Documentación del proyecto en formato javadoc.
  • Lib: Bibliotecas externas que hayamos usado en nuestro proyecto.
  • Src: Código fuente de la aplicación organizado en los paquetes que hayamos decidido nosotros mismos.

Nuestro primer build.xml

Vamos a ver aquí nuestro primer ejemplo y sobre él explicaremos las partes básicas a parte de las etiquetas ya explicadas.

<project name="ProbandoAnt" default="compilar" basedir=".">

    <!-- propiedades globales del proyecto -->
    <property name="fuente" value="scr" />
    <property name="destino" value="classes" />

    <!-- Objetivo -->
    <target name="compilar">
        <!-- Tarea -->
        <javac srcdir="${fuente}" destdir="${destino}" />
    </target>

</project>

En la raíz del fichero encontramos dos atributos. Los explico a continuación.

  • Default: este especifica cual es el objetivo que se ejecutará por defecto al ejecutar este script de ant salvo que se especifique uno concreto.
  • Basedir: indica donde están situados los directorios del proyecto. En este caso como tendremos el fichero buid.xml al mismo nivel que los directorios del proyecto hemos puesto un punto.

Lo siguiente que nos encontraremos serán las propiedades que como dije son pares clave valor. Se pueden usar para tener un acceso rápido a los distintos directorios y evitar así problemas si alguno de ellos cambia. Accederemos a ellas posteriormente así ${propiedad}.

Finalmente nos encontramos con un objetivo sencillo que realiza la compilación con la tarea javac. Tiene como atributos srcdir que indica donde está el código fuente a compilar, y destdir indica en que directorio se han de dejar las clases compiladas.

Los objetivos pueden tener dependencias, las cuales se indican con el atributo depends. Las dependencias entre objetivos indican que un objetivo que depende de otro. Es decir, no será ejecutado sin que se haya ejecutado previamente ese otro del que depende. Veamos un ejemplo sencillo.

<!-- El objetivo empaqueta no será ejecutado sin antes ejecutar el objetivo compila.
O dicho de otra manera el objetivo empaqueta obliga a que se ejecute primero el objetivo compila --!>

<target name="empaqueta" depends="compila">

</target>

<target name="compila">  

</target>

Empaquetado del proyecto

En este apartado veremos como crear un objetivo que se encargue de empaquetar las clases compiladas en un único jar autoejecutable.

Un archivo jar es como un archivo zip, incluso con la misma compresión. En su interior contiene las clases java organizadas en paquetes y una carpeta más llamada META-INF, esa carpeta es la usada para meter en su interior meta-información sobre el archivo jar.

Dentro de la carpeta nombrada se suele encontrar un fichero llamado manifest.mf, el cual tiene en su interior un conjunto de pares clave valor que pueden ser necesarios. En nuestro ejemplo lo que indicaremos en el fichero será cual es la clase main (Main-Class) de nuestro proyecto para que se sepa por donde tiene que empezar la ejecución. Sin esto no podríamos ejecutar el archivo jar, al menos con el típico doble click, habría que hacerlo pasando por la consola y llamando directamente a la clase Main.

<target name="empaquetar" depends="compilar">
    <jar destfile="${dist}/ejemploant.jar" basedir="${destino}" includes="**">
        <manifest>
            <attribute name="Built-By" value="${user.name}" />
            <attribute name="Main-Class" value="ejemploant.ejemploant" />
        </manifest>
    </jar>
</target>

Como hemos visto, la tarea jar contiene dos atributos. El primero de ellos llamado destfile que indicará la ruta donde queremos el fichero jar generado. El atributo basedir indica donde se encuentran las clases compiladas a incluir. Finalmente el atributo includes que indicará que tipo de ficheros se incluyen dentro del fichero jar creado.

Como se puede ver, un nivel por debajo de la tarea jar nos encontramos con la etiqueta manifest, la cual, como podemos suponer, sirve para incluir conjuntos de pares clave valor dentro del fichero que comenté anteriormente. Como se observa se indica la ruta a la clase main tal y como hemos comentado.

Generación de documentación

En este apartado veremos como crear un objetivo que cree documentación javadoc sobre nuestro proyecto. Vamos a poner aquí un pequeño ejemplo y lo explicaremos posteriormente.

<target name="documentar">
    <javadoc packagenames="*"
        sourcepath="${fuente}"
        destdir="${javadoc}"
        author="true"
        version="true"
        private="true"
        locale="es"
        windowtitle="Prueba de ANT"
        doctitle="Prueba de ANT"
        bottom="Jdyb Copyright 2011">
    </javadoc>
</target>

Como vemos la tarea que se encarga de generar la documentación javadoc del proyecto tiene bastantes atributos, los primeros de ellos son los más importantes.

  • packagenames, indica los paquetes sobre los que se desea generar la documentación, esta vez hemos puesto asterisco para generar la documentación de todos ellos.
  • sourcepath es el directorio donde se encuentran los archivos fuente del proyecto.
  • destdir indica el directorio donde se van a guardar los archivos generados en el proceso de documentación (el resultado final).

Los demás atributos que observamos son de menos importancia que los anteriores ya que se refieren más a la personalización de esa documentación a generar, por ejemplo tomar en cuenta etiquetas @author o @version, el título y el pie de página del documento, etc…

Limpieza e Inicialización

Entre las muchas tareas predefinidas con las que cuenta ant podemos encontrar tareas para crear y borrar carpetas y archivos.

Puede ser que queramos borrar las clases compiladas antes de una nueva compilación para que no haya problemas, y obviamente también habrá que crear las carpetas después de borrarlas.

Veamos aquí dos ejemplos de objetivos de ejemplo para crear y borrar carpetas. En este ejemplo haremos que se borre el paquete jar autoejecutable, las clases compiladas y la documentación generada; parece lo más lógico hacerlo así para evitar problemas si hay cambios grandes en la estructura de clases y paquetes.

<target name="limpiar">
    <delete dir="${destino}" />
    <delete dir="${javadoc}" />
    <delete dir="${dist}" />
</target>

<target name="init" depends="limpiar">
    <mkdir dir="${destino}" />
    <mkdir dir="${javadoc}" />
    <mkdir dir="${dist}" />
</target>

Como podemos ver la tarea delete tiene como función borrar directorios y con los atributos se indica que es lo que se quiere borrar. Y, con la tarea mkdir se hace lo contrario, crear directorios.

Como podeis ver el objetivo init depende del objetivo limpiar. Lo que quiere decir, que llamar al objetivo init impicará borrar todo lo anterior y dejar las carpetas listas para la compilación, empaquetado y generación de documentación.

Uso de ANT con librerías externas

En este apartado veremos como trabajar con librerías externas y veremos como hacer las tareas que ya hemos realizado pero usando librerías externas.

Es posible hacer referencia a una carpeta con librerías de varias formas. Si se quiere usar esa referencia en varios lugares del fichero build.xml la mejor forma es hacer uso del elemento path, dentro del cual se selecciona la carpeta con las librerías, pudiéndose también hacer uso de filtros para seleccionar solo determinados ficheros.

<path id="path.libs">
    <pathelement location="." />
    <fileset dir="lib">
        <include name="**/*.jar" />
    </fileset>
</path>

Posteriormente habrá que usar ese elemento para incluirlo en el classpath durante la compilación y la generación de la documentación.

<classpath refid="path.libs" />

En lo que respecta a la tarea del empaquetado de las clases compiladas hay que tener en cuenta dos cosas. En primer lugar, para incluir un fichero jar de librerías en el paquete autoejecutable jar generado lo que se realiza es una descompresión del jar de las librerías para incluirlo en el jar final. Como resultado de este proceso obtendríamos la carpeta META-INF del paquete de las librerías, que de no ser excluido sobreescribiría la carpeta META-INF junto con el manifiesto creado, por lo que se perdería la meta-información que añadíamos a nuestro fichero jar final. Si esta información se perdiese la máquina virtual de JAVA no sabría cual es la clase Main para ejecutar el paquete.

Por lo tanto, para realizar estas tareas usaremos el elemento zipfileset indicando el fichero comprimido a incluir junto con un filtro para excluir la carpeta META-INF como ya hemos dicho.

<target name="empaquetar" depends="compilar">
    <jar destfile="${dist}/ejemploant.jar" basedir="${destino}" includes="**">
        <manifest>
            <attribute name="Built-By" value="${user.name}" />
            <attribute name="Main-Class" value="ejemploant.ejemploant" />
        </manifest>
        <zipfileset src="lib/jdom.jar" excludes="META-INF/*" />
    </jar>
</target>

Ejecución

En este apartado veremos como ejecutar el proyecto tanto desde una clase java como desde el proyecto empaquetado en un jar.

Ejecución desde clase Java

La tarea que nos ayudará a la ejecución es java, y usaremos el atributo classname para indicar la clase a ejecutar, para que pueda encontrar dicha clase tendremos que definir el atributo classpath con la ruta donde esta el paquete y la clase que hemos dicho que tenga que ejecutar.

Para poner los argumentos tendremos que usar arg y el argumento se pondrá en el atributo value. También hay que tener en cuenta que habrá que indicar la ruta de las librerías referenciando al elemento path que creamos en el apartado anterior.

<target name="run" depends="compilar" description="Ejecutar aplicacion con clase java" >
    <java classname="ejemploant.ejemploant" classpath="${destino}">
        <arg value="build.xml"/>
        <classpath refid="path.libs" />
    </java>
</target>

Ejecución desde un archivo jar

Para este caso seguiremos usando la tarea java, pero en este caso con el atributo jar en el que indicaremos la ubicación del archivo jar a ejecutar. Recordemos que para que la máquina virtual de java (VM) sepa que clase tiene que ejecutar tendremos que haber definido bien las propiedades en el manifiesto, como ya explicamos anteriormente.

Es obligatorio el uso del atributo fork puesto que para ejecutar este jar la VM creará una nueva instancia. Como opción podemos usar el atributo maxmemory para indicar la máxima memoria que se reservará para la nueva instancia de la VM.

El resto será igual que en el caso anterior indicando los argumentos y añadiendo al classpath el elemento path que se definió anteriormente.

<target name="runjar" depends="empaquetar" description="Ejecutar aplicacion de archivo jar" >
    <java jar="${dist}/ejemploant.jar" fork="true" maxmemory="128m">
        <arg value="build.xml"/>
        <classpath refid="path.libs" />
    </java>
</target>

Usando ANT con JFlex y CUP

Al haber preparado la presentación para una asignatura sobre procesamiento de lenguajes vamos a tener un apartado extra en este tutorial. Este añadido tratará sobre como automatizar la utilización de Jflex (Generador de analizadores léxicos) y CUP (Generador de analizadores sintácticos).

Pues bien, lo primero que tendremos que hacer es decargar las librerías de JFlex y CUP para situar sendos Jar en la carpeta de librerías.

Conviene saber que ambos generadores contienen en el interior de sus librerías una tarea de ant, por lo que nuestro trabajo consistirá únicamente en usar dichas tareas.

<taskdef classpathref="path.libs" classname="JFlex.anttask.JFlexTask" name="jflex" />
<taskdef classpathref="path.libs" classname="java_cup.anttask.CUPTask" name="cup" />

Como vemos usamos el elemento taskdef y como atributos señalamos la carpeta de librerías que ya habíamos definido anteriormente en el apartado donde veíamos como usar librerías externas. Posteriormente indicamos la clase en la que se encuentra esa tarea de ant y finalmente se indica el nombre bajo el que se usará esa tarea en el documento build.xml.

Mostramos ahora un objetivo encargado de la generación de los analizadores y posteriormente pasaremos a explicar cada uno de los atributos usados.

<target name="generateGrammar">
    <jflex file="${grammar}/reglas.flex" destdir="${fuente}"/>
    <cup srcfile="${grammar}/Gram.cup" destdir="${fuente}"/>
</target>

JFlex

En lo que se refiere a la tarea de JFlex contamos con el atributo file en el cual se indica el fichero flex del que se partirá para la generación del analizador léxico.Destdir indica el directorio en el que se generará el fichero java generado organizado en paquetes según se indique en el fichero flex.

CUP

En la tarea de CUP tenemos el atributo srcfile, equivalente al file de JFLex. Destdir también equivalente al atributo del mismo nombre en JFlex. También se pueden usar otros atributos como interface para crear una interfaz java en vez de una clase. CUP también producirá su salida organizada en el paquete que se haya especificado en el fichero cup.

Recursos

Como parte de este tutorial dejaré aquí la presentación a modo de resumen y un proyecto de ANT como ejemplo.

En el proyecto de ejemplo se ha creado un objetivo de ant para explicar su uso. Si queremos saber las opciones que tiene este fichero de configuración habrá que usar la siguiente orden en la consola situados en la carpeta del proyecto.

$ant usage

Hasta aquí este tutorial sobre ANT. Espero que os haya sido de utilidad, como siempre digo quedo abierto a todo tipo de sugerencias o adiciones; tenéis los comentarios a vuestra disposición.