viernes, 2 de diciembre de 2016

Construyendo el programa

La mayoría de los programas se construyen con una secuencia simple de dos comandos:

./configure
make

El programa configure es un script de shell que es proporcionado por el árbol de código fuente. Su trabajo es analizar el entorno de construcción. La mayoría del código fuente se diseña para ser portable. Es decir, se diseña para construirse en más de un tipo de sistema tipo Unix. Pero para hacer eso, el código fuente puede necesitar someterse a leves ajustes durante la construcción para adaptarse a las diferencias entre sistemas. configure también comprueba que se instalen las herramientas externas y los componentes necesarios. Ejecutemos configure. Como configure no está localizado donde el shell espera normalmente que estén almacenados los programas, tenemos que decirle al shell explícitamente su localización precediendo el comando con ./ para indicar que el programa se localiza en el directorio de trabajo actual:

[me@linuxbox diction-1.11]$ ./configure

configure producirá un montón de mensajes a medida que prueba y configura la construcción. Cuando termina, tendrá un aspecto como este:

checking libintl.h presence... yes
checking for libintl.h... yes
checking for library containing gettext... none required
configure: creating ./config.status
config.status: creating Makefile
config.status: creating diction.1
config.status: creating diction.texi
config.status: creating diction.spec
config.status: creating style.1
config.status: creating test/rundiction
config.status: creating config.h
[me@linuxbox diction-1.11]$

Lo importante aquí es que no hay mensajes de error. Si los hubo, la configuración falló, y el programa no se construirá hasta que se corrijan los errores.

Vemos que configure ha creado varios archivos nuevos en nuestro directorio fuente. El más importante es Makefile. Makefile es un archivo de configuración que indica al programa make cómo construir exactamente el programa. Sin él, make no funcionará. Makefile es un archivo de texto ordinario, así que podemos verlo:

[me@linuxbox diction-1.11]$ less Makefile

El programa make toma como entrada un makefile (que normalmente se llama Makefile), que describe las relaciones y dependencias entre los componentes que componen el programa finalizado.

La primera parte de makefile define variables que son sustituidas en secciones posteriores del makefile. Por ejemplo vemos la línea:

CC=           gcc

que define que el compilador C será gcc. Más adelante en el makefile, vemos una instancia donde se usa:

diction: diction.o sentence.o misc.o getopt.o getopt1.o $(CC) -o $@ $(LDFLAGS) diction.o sentence.o misc.o \ getopt.o getopt1.o $(LIBS)

Aquí se realiza una sustitución, y el valor $(CC) se reemplaza por gcc en el momento de la ejecución.

La mayoría del makefile consiste en líneas, que definen un objetivo, en este caso el archivo ejecutable diction, y los archivos de los que depende. Las líneas restantes describen el/los comando/s necesarios para crear el objetivo desde sus componentes. Vemos en este ejemplo que el archivo ejecutable diction (uno de los productos finales) depende de la existencia de diction.o, sentence.o, misc.o, getop.o y gestopt1.o. Más adelante aún, en el makefile, vemos las definiciones de cada uno de estos objetivos:

diction.o:  diction.c config.h getopt.h misc.h sentence.h
getopt.o:   getopt.c getopt.h getopt_int.h
getopt1.o:  getopt1.c getopt.h getopt_int.h
misc.o:     misc.c config.h misc.h
sentence.o: sentence.c config.h misc.h sentence.h
style.o:    style.c config.h getopt.h misc.h sentence.h

Sin embargo, no vemos ningún comando especificado para ellos. Esto es gestionado por un objetivo general, anteriormente en el archivo, que describe el comando usado para compilar cualquier archivo .c en un archivo .o:

.c.o:
            $(CC) -c $(CPPFLAGS) $(CFLAGS) $<

Todo esto parece muy complicado. ¿Por qué no listamos simplemente todos los pasos para compilar las partes y terminamos? La respuesta a esto se aclarará en un momento. Mientras tanto, ejecutemos make y construyamos nuestros programas:

[me@linuxbox diction-1.11]$ make

El programa make se ejecutará, usando los contenidos de Makefile para guiar sus acciones. Producirá un montón de mensajes.

Cuando termine, veremos que todos los objetivos están presentes ahora en nuestro directorio:

[me@linuxbox diction-1.11]$ ls
config.guess  de.po           en           install-sh  sentence.c
config.h      diction         en_GB        Makefile    sentence.h
config.h.in   diction.1       en_GB.mo     Makefile.in sentence.o
config.log    diction.1.in    en_GB.po     misc.c      style
config.status diction.c       getopt1.c    misc.h      style.1
config.sub    diction.o       getopt1.o    misc.o      style.1.in
configure     diction.pot     getopt.c     NEWS        style.c
configure.in  diction.spec    getopt.h     nl          style.o
COPYING       diction.spec.in getopt_int.h nl.mo       test
de            diction.texi    getopt.o     nl.po
de.mo         diction.texi.in INSTALL      README

Entre los archivos, vemos diction y style, los programas que elegimos construir. ¡Tengo que darte la enhorabuena! ¡Acabamos de compilar nuestros primeros programas desde código fuente!

Pero sólo por curiosidad, ejecutemos make de nuevo:

[me@linuxbox diction-1.11]$ make
make: Nothing to be done for `all'.

Sólo produce un extraño mensaje. ¿Qué está pasando? ¿Por qué no ha construido el programa de nuevo? Ah, esta es la magia de make. En lugar de simplemente construirlo todo de nuevo, make sólo construye lo que necesita construirse. Con todos los objetivos presentes, make ha determinado que no hay nada que hacer. Podemos demostrar esto eliminando uno de los objetivos y ejecutando make de nuevo para ver qué hace. Deshagámonos de uno de los objetivos intermedios:

[me@linuxbox diction-1.11]$ rm getopt.o
[me@linuxbox diction-1.11]$ make

Vemos que make reconstruye y reenlaza los programas diction y style, ya que dependen del módulo perdido. Este comportamiento también indica otra característica importante de make: mantiene los objetivos actualizados. make insiste en que los objetivos sean más nuevos que sus dependencias. Esto tiene mucho sentido, como programador a menudo actualizarás algo de código fuente y luego usarás make para construir una nueva versión del producto finalizado. make se asegura que se construya todo lo que se necesita construir basándose en el código actualizado. Si usamos el programa touch para "actualizar" uno de los archivos de código fuente, podemos ver lo que ocurre:

[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:14 diction
-rw-r--r-- 1 me me 33125 2007-03-30 17:45 getopt.c
[me@linuxbox diction-1.11]$ touch getopt.c
[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:14 diction
-rw-r--r-- 1 me me 33125 2009-03-05 06:23 getopt.c
[me@linuxbox diction-1.11]$ make

Después de que make se ejecute, vemos que ha restaurado el objetivo para que sea más nuevo que la dependencia:

[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:24 diction
-rw-r--r-- 1 me me 33125 2009-03-05 06:23 getopt.c

La capacidad de make de construir inteligentemente sólo lo que necesita ser construido es un gran beneficio para los programadores. Aunque el ahorro de tiempo no es muy evidente en nuestro pequeño proyecto, es muy significativo para proyectos más grandes. Recuerda, el kernel Linux (un programa sometido a modificaciones y mejoras constantes) contiene varios millones de líneas de código.

No hay comentarios:

Publicar un comentario