Cada entorno de desarrollo y/o lenguaje de programación maneja por sus propios medios la manera de definir un proyecto de software y el proceso de construcción del mismo. Sin embargo make sigue siendo ampliamente utilizado para este propósito, especialmente en los sistemas UNIX y derivados.
Make
Para usar make cada proyecto debe ir acompañado de un fichero Makefile
donde se incluyen las reglas para la compilación y enlazado de las librerías y ejecutables del mismo. Estas reglas fijan que es lo que hay que hacer —que comandos ejecutar y como hacerlo— para obtener cada producto del proceso de construcción, así como las dependencias de estos con respecto a otros productos o a los distintos archivos de código fuente del proyecto. Además se pueden incluir reglas para automatizar tareas tales como generar la documentación; instalar o desplegar los programas, las librerías, la documentación y otros productos generados; o limpiar el proyecto, borrando archivos temporales y subproductos de la compilación.
Cuando la construcción de un proyecto ha sido automatizada adecuadamente con make
, la compilación del mismo se reduce a ejecutar el comando:
make
en el directorio del proyecto. Siendo su instalación igual de sencilla:
sudo make install
Obviamente entornos integrados de desarrollo como Eclise, KDevelop, Code::Blocks o Visual Studio incorporan sus propias herramientas para automatizar la compilación de proyectos, que además se integran perfectamente con estos entornos gráficos. Sin embargo make suele estar disponible en cualquier sistema y puede ser utilizado independientemente de las preferencias de cada desarrollador acerca del entorno integrado con el que trabajar.
Además make es una herramienta que fácilmente puede ser utilizada desde otras de más alto nivel. Por ejemplo, un sistema de construcción de paquetes como el que utilizan las distribuciones basadas en Debian automatiza todos los pasos, desde la descarga del código fuente hasta la generación del archivo .deb
, pasando por invocar a make
para compilar y a make install
para que los productos de dicha construcción se instalen en su ubicación definitiva, de donde son tomados para conformar el contenido del paquete del proyecto.
En realidad, durante el proceso de construcción de un paquete .deb
, el proyecto, una vez compilado, se instala, pero no en la raíz del sistema donde está teniendo lugar la compilación, sino que se confina el comando make install
a un subdirectorio temporal. Así make deposita los archivos en sus ubicaciones predefinidas —por ejemplo /usr/bin
, /usr/lib
, /etc
, /var/lib
, etc.— solo que relativas a dicho subdirectorio —por ejemplo /ruta/al/subdirectorio/usr/bin
, /ruta/al/subdirectorio/etc
, /ruta/al/subdirectorio/usr/lib
, /ruta/al/subdirectorio/var/lib
, etc.—. Para obtener el contenido del paquete sólo hace falta tomar el contenido del subdirectorio temporal, evitando que el proceso de construcción ensucie el sistema donde se está ejecutando.
De forma muy similar funciona Bitbake, la herramienta de construcción que utiliza el proyecto Yocto para generar distribuciones de Linux para sistemas empotrados, y así una larga lista de herramientas similares. Para todas ellas el poder automatizar la construcción de proyectos de software es fundamental, siendo éste un campo donde la solución orientada a línea de comandos de make se muestra mucho más flexible que las soluciones de interfaz gráfica integradas de los distintos entornos de desarrollo.
Qmake
Aunque make es una herramienta muy flexible, resulta muy compleja de utilizar, si no imposible, cuando se quiere crear software portable. Diferentes sistemas operativos pueden tener distintos compiladores, ya sean de diferentes fabricantes o en distintas versiones, u ofrecer a las aplicaciones diferentes funcionalidades —o las mismas pero de manera distintas—. Además un proyecto de software puede depender de otras librerías o programas, que nuevamente pueden ser de versiones diferentes o distintos desarrolladores. Y a todo eso hay que unir que según el sistema operativo la ubicación de librerías, programas y herramientas de desarrollo puede diferir.
Enfrentar estas situaciones con make es extremadamente difícil, por lo que los desarrolladores suelen utilizar otras utilidades que se encarguen de buscar las herramientas de desarrollo y las dependencias del programa, detectar las funcionalidades del sistema y generar un archivo Makefile
ajustado al sistema concreto donde se va a compilar.
Herramientas de este tipo existen muchas. Por ejemplo Autotools, CMake, SCons y qmake. Siendo esta última la que se usa preferentemente con las aplicaciones desarrolladas en Qt, ya que sabe manejar perfectamente las singularidades de este framework. GNU Autotools, por ejemplo, es la herramienta que está detrás de tener que ejecutar ./configure
antes de compilar con make muchos programas y librerías libres.
Con qmake las información requerida para construir un proyecto se define en un archivo de proyecto que generalmente tienen extensión .pro
. Al ejecutar qmake
dentro del directorio de un proyecto, éste interpreta los archivos de proyecto y genera el Makefile
correspondiente. Después sólo es necesario ejecutar el comando make
para iniciar la compilación del mismo:
qmake
make
sudo make install
Reglas para make install
Realmente el último make install
no sirve de mucho porque qmake por defecto no añade tareas a dicha regla en el archivo Makefile
.
Si queremos que al ejecutar make install
se instalen los archivos de nuestra aplicación en las ubicaciones adecuadas, debemos instruir a qmake a través del archivo de proyecto acerca de como se hace:
unix { # Esta configuración es específica de Linux y UNIX
# Variables
#
isEmpty(PREFIX) {
PREFIX = /usr/local
}
BINDIR = $$PREFIX/bin
DATADIR = $$PREFIX/share
CONFDIR = /etc
isEmpty(VARDIR) {
VARDIR = /var/lib/$${TARGET}
}
# Install
#
INSTALLS += target config desktop icon32 vardir
## Instalar ejecutable
target.path = $$BINDIR
## Instalar archivo de configuración
config.path = $$CONFDIR
config.files += $${TARGET}.conf
## Instalar acceso directo en el menú del escritorio
desktop.path = $$DATADIR/applications
desktop.files += $${TARGET}.desktop
## Instalar icono de aplicación
icon32.path = $$DATADIR/icons/hicolor/32x32/apps
icon32.files += ./data/32x32/$${TARGET}.png
## Crear directorio de archivos variables
vardir.path = $$VARDIR
vardir.commands = true
}
La variable INSTALLS
debe contener una lista de los recursos que queremos que sean instalados con make install
. De tal forma que cada elemento de la lista incorpora atributos que proporcionan información sobre dónde van a ser instalados.
Por ejemplo, el elemento target
en la línea 16 representa a los ficheros resultado de la construcción del proyecto. Asignando una ruta a target.path
—línea 19— estamos indicando donde queremos que sean instalados. De forma similar, el elemento icon32
representa al archivo del icono de la aplicación en el escritorio. Asignando un valor a icon32.path
en la línea 30 estamos diciendo donde queremos que sea instalado, mientras que el valor del atributo icon32.files
en la línea 31 indica donde podemos encontrar el archivo o archivos del icono respecto al directorio del proyecto. En el caso del recurso target
no hace falta usar target.files
porque se sobreentiende que se sabe dónde el compilador ha dejado los ejecutables que deben ser copiados a target.path
.
En teoría podemos especificar cualquier ubicación como destino de nuestros archivos, aunque es muy recomendable seguir el Filesystem Hierarchy Standard de los sistemas Linux, UNIX y derivados.
Extras y commands
En las líneas de la 33 a las 35 se puede observar como hacer para crear un directorio en la ruta almacenada en $$VARDIR
. Asignando un valor a vardir.commands
le estamos diciendo el comando que debe ejecutar make
para construir esa parte del proyecto. Por lo tanto lo que va a ocurrir es lo siguiente:
Durante la construcción del proyecto
make
ejecutará el comandotrue
, que no hace nada y siempre termina con éxito.Durante la instalación,
make install
creará la ruta indicada envardir.path
si no existe. Posteriormente, intentará copiar los archivos indicados envardir.files
envardir.path
pero comovardir.files
no contiene nada, no se copiará nada.
Por eso esta es una manera sencilla de crear directorios durante la instalación. Sin embargo no es la única. Una forma alternativa es la siguiente:
## Crear directorio de archivos variables
vardir.extra = mkdir -p $$VARDIR
puesto que por medio de .extra
podemos indicar comandos personalizados que serán invocados durante la ejecución de make install
.
Definición de macros del preprocesador
Las variables que hemos definido dentro del archivo de proyecto establecen la ubicación de los recursos del programa tras su instalación. Sería más complicado cometer errores si en nuestro código fuente usáramos directamente las rutas tal y como se definen en el archivo de proyecto, sin tener que volver a definirlas en C++. Además eso daría pie a modificar la ruta de los archivos mediante la redefinición de variables en la línea de comandos de qmake, sin tener por ello que modificar el código fuente. Por ejemplo:
qmake PREFIX=/usr
ejecuta qmake usando el valor indicado para la variable PREFIX
.
Para conseguir esto sólo tenemos que utilizar la variable DEFINES
, que nos permite listar un conjunto de macros del preprocesador que queremos que sean pasadas al compilador. Las macros del preprocesador son aquellas que generalmente se definen en C/C++ mediante la directiva #define
.
unix { # Esta es configuración específica de Linux y UNIX
# Variables
#
isEmpty(PREFIX) {
PREFIX = /usr/local
}
BINDIR = $$PREFIX/bin
DATADIR = $$PREFIX/share
CONFDIR = /etc
isEmpty(VARDIR) {
VARDIR = /var/lib/$${TARGET}
}
## Se escapan las comillas con \\\" para asegurar que en el
## compilador es equivalente al siguiente código:
## #define APP_CONFFILE="/ruta/al/archivo.conf"
DEFINES += APP_DATADIR=\\\"$$DATADIR\\\"
DEFINES += APP_VARDIR=\\\\"$$VARDIR\\\\"
DEFINES += APP_CONFFILE=\\\\"$$CONFDIR/$${TARGET}.conf\\\\"
# Install
#
INSTALLS += target config desktop icon32 vardir
## Instalar ejecutable
target.path = $$BINDIR
## Instalar archivo de configuración
config.path = $$CONFDIR
config.files += $${TARGET}.conf
## Instalar acceso directo en el menú del escritorio
desktop.path = $$DATADIR/applications
desktop.files += $${TARGET}.desktop
## Instalar icono de aplicación
icon32.path = $$DATADIR/icons/hicolor/32x32/apps
icon32.files += ./data/32x32/$${TARGET}.png
## Crear directorio de archivos variables
vardir.path = $$VARDIR
vardir.commands = true
}
Así, por ejemplo, dentro del código fuente se puede usar la macro APP_CONFFILE
con la ruta al archivo de configuración de la aplicación para acceder a él mediante QSettings:
QSettings settings(APP_CONFFILE, QSettings::IniFormat);
Referencias
Filesystem Hierarchy Standard — Wikipedia