GNU make ofrece muchas funciones útiles y numerosas capacidades avanzadas. Sin embargo, no incorpora un lenguaje de programación completo, por lo que tiene sus limitaciones. A veces estas limitaciones pueden superarse usando la función shell para invocar un programa aparte, aunque ese método suele ser ineficiente.
En los casos en que las capacidades incorporadas de GNU make no bastan para sus necesidades, hay dos opciones para extender make. Una es utilizar GNU Guile como lenguaje de scripting embebido, en los sistemas donde está disponible (véase Integración con GNU Guile). La otra, en los sistemas que admiten objetos cargables dinámicamente, es escribir su propia extensión en cualquier lenguaje (siempre que pueda compilarse a uno de esos objetos) y cargarla para proporcionar capacidades ampliadas (véase Carga de objetos dinámicos).
GNU make puede compilarse con soporte para GNU Guile como lenguaje de extensión embebido. Guile implementa el lenguaje Scheme. Una revisión de GNU Guile y del lenguaje Scheme y sus características queda fuera del alcance de este manual: consulte la documentación de GNU Guile y de Scheme.
Puede determinar si su make contiene soporte para Guile examinando la variable .FEATURES; contendrá la palabra guile si el soporte de Guile está disponible.
La integración con Guile aporta una nueva función de make: guile. La función guile toma un argumento, que primero es expandido por make del modo habitual y luego se pasa al evaluador de GNU Guile. El resultado del evaluador se convierte en una cadena y se usa como la expansión de la función guile en el makefile.
Además, GNU make expone procedimientos de Guile para usarlos en sus scripts de Guile.
En make solo existe un «tipo de datos»: la cadena. GNU Guile, en cambio, proporciona una rica variedad de tipos de datos distintos. Un aspecto importante de la interfaz entre make y GNU Guile es la conversión de los tipos de datos de Guile en cadenas de make.
Esta conversión interviene en dos lugares: cuando un makefile invoca la función guile para evaluar una expresión de Guile, el resultado de esa evaluación debe convertirse en una cadena de make para que make pueda evaluarlo a su vez. Y, en segundo lugar, cuando un script de Guile invoca uno de los procedimientos exportados por make, el argumento que se le pasa al procedimiento debe convertirse en una cadena.
La conversión de los tipos de Guile en cadenas de make es la siguiente:
#fEl valor falso (False) se convierte en la cadena vacía: en las condiciones de make, la cadena vacía se considera falsa.
#tEl valor verdadero (True) se convierte en la cadena «#t»: en las condiciones de make, toda cadena no vacía se considera verdadera.
symbolnumberUn símbolo o un número se convierte en la representación en cadena de ese símbolo o número.
characterUn carácter imprimible se convierte en ese mismo carácter.
stringUna cadena que solo contiene caracteres imprimibles se convierte en esa misma cadena.
listUna lista se convierte recursivamente según las reglas anteriores. Esto implica que toda lista estructurada quedará aplanada (es decir, un resultado de «'(a b (c d) e)» se convertirá en la cadena de make «a b c d e»).
otherCualquier otro tipo de Guile produce un error. En futuras versiones de make, es posible que se conviertan otros tipos de Guile.
La traducción de «#f» (a la cadena vacía) y de «#t» (a la cadena no vacía «#t») está pensada para que pueda usar los resultados booleanos de Guile directamente como condiciones booleanas de make. Por ejemplo:
$(if $(guile (access? "myfile" R_OK)),$(info myfile exists))
Como consecuencia de estas reglas de conversión, debe tener en cuenta el resultado de su script de Guile, ya que ese resultado se convertirá en una cadena y será analizado por make. Si el script no tiene un resultado natural (es decir, si el script existe únicamente por sus efectos secundarios), conviene añadir «#f» como última expresión para evitar errores de sintaxis en el makefile.
Además de la función guile disponible en los makefiles, make expone algunos procedimientos para usarlos en sus scripts de Guile. Al arrancar, make crea un nuevo módulo de Guile, gnu make, y exporta estos procedimientos como interfaces públicas de ese módulo:
gmk-expandEste procedimiento toma un único argumento, que se convierte en una cadena. La cadena es expandida por make usando las reglas normales de expansión de make. El resultado de la expansión se convierte en una cadena de Guile y se devuelve como resultado del procedimiento.
gmk-evalEste procedimiento toma un único argumento, que se convierte en una cadena. La cadena es evaluada por make como si fuera un makefile. Esta es la misma capacidad disponible mediante la función eval (véase La función eval). El resultado del procedimiento gmk-eval es siempre la cadena vacía.
Tenga en cuenta que gmk-eval no es exactamente lo mismo que usar gmk-expand con la función eval: en este último caso, la cadena evaluada se expande dos veces; primero mediante gmk-expand y luego de nuevo mediante la función eval.
Aquí tiene un ejemplo muy sencillo que usa GNU Guile para gestionar la escritura en un archivo. Estos procedimientos de Guile simplemente abren un archivo, permiten escribir en él (una cadena por línea) y lo cierran. Observe que, como no podemos almacenar valores complejos como los puertos de Guile en variables de make, mantendremos el puerto como una variable global en el intérprete de Guile.
Puede crear funciones de Guile con facilidad usando define/endef para construir un script de Guile y luego usar la función guile para internalizarlo:
define GUILEIO ;; A simple Guile IO library for GNU Make (define MKPORT #f) (define (mkopen name mode) (set! MKPORT (open-file name mode)) #f) (define (mkwrite s) (display s MKPORT) (newline MKPORT) #f) (define (mkclose) (close-port MKPORT) #f) #f endef # Internalize the Guile IO functions $(guile $(GUILEIO))
Si tiene una cantidad considerable de código de soporte en Guile, puede plantearse mantenerlo en un archivo aparte (por ejemplo, guileio.scm) y luego cargarlo en su makefile usando la función guile:
$(guile (load "guileio.scm"))
Una ventaja de este método es que, al editar guileio.scm, su editor entenderá que este archivo contiene sintaxis de Scheme en lugar de sintaxis de makefile.
Ahora puede usar estas funciones de Guile para crear archivos. Suponga que necesita operar con una lista muy grande que no cabe en la línea de órdenes, pero la utilidad que está usando también acepta la lista como entrada:
prog: $(PREREQS)
@$(guile (mkopen "tmp.out" "w")) \
$(foreach X,$^,$(guile (mkwrite "$(X)"))) \
$(guile (mkclose))
$(LINK) < tmp.out
Por supuesto, es posible disponer de un conjunto más completo de procedimientos para manipular archivos. Podría, por ejemplo, mantener varios archivos de salida al mismo tiempo eligiendo un símbolo para cada uno y usándolo como clave de una tabla hash, donde el valor sea un puerto, y devolviendo después el símbolo para guardarlo en una variable de make.
|
Muchos sistemas operativos ofrecen un mecanismo para cargar dinámicamente objetos compilados. Si su sistema dispone de este mecanismo, GNU make puede aprovecharlo para cargar objetos dinámicos en tiempo de ejecución, proporcionando nuevas capacidades que luego podrá invocar su makefile.
Para cargar un objeto dinámico se usa la directiva load. Una vez cargado el objeto, se invocará una función de «configuración» (setup) que permite al objeto inicializarse y registrar nuevas prestaciones en GNU make. Un objeto dinámico podría incluir nuevas funciones de make, por ejemplo, y la función de «configuración» las registraría en el sistema de gestión de funciones de GNU make.
Los objetos se cargan en GNU make colocando la directiva load en su makefile. La sintaxis de la directiva load es la siguiente:
load object-file …
o bien:
load object-file(symbol-name) …
El archivo object-file es cargado dinámicamente por GNU make. Si object-file no incluye una ruta de directorio, primero se busca en el directorio actual. Si no se encuentra allí, o si incluye una ruta de directorio, se buscará en las rutas específicas del sistema. Si la carga falla por cualquier motivo, make imprimirá un mensaje y terminará.
Si la carga tiene éxito, make invocará una función de inicialización.
Si se proporciona symbol-name, se usará como nombre de la función de inicialización.
Si no se proporciona symbol-name, el nombre de la función de inicialización se construye tomando el nombre base de object-file, hasta el primer carácter que no sea válido para un nombre de símbolo (los caracteres alfanuméricos y el guion bajo son caracteres válidos para un nombre de símbolo). A este prefijo se le añade el sufijo _gmk_setup.
Con una sola directiva load pueden cargarse varios archivos objeto, y ambas formas de los argumentos de load pueden usarse en la misma directiva.
A la función de inicialización se le proporcionan el nombre de archivo y el número de línea donde se invocó la operación load. Debe devolver un valor de tipo int, que debe ser 0 en caso de fallo y distinto de 0 en caso de éxito. Si el valor de retorno es -1, entonces GNU Make no intentará reconstruir el archivo objeto (véase Cómo se rehacen los objetos cargados).
Por ejemplo:
load ../mk_funcs.so
cargará el objeto dinámico ../mk_funcs.so. Una vez cargado el objeto, make invocará la función (que se supone definida por el objeto compartido) mk_funcs_gmk_setup.
En cambio, lo siguiente:
load ../mk_funcs.so(init_mk_func)
cargará el objeto dinámico ../mk_funcs.so. Una vez cargado el objeto, make invocará la función init_mk_func.
Por más veces que un archivo objeto aparezca en una directiva load, solo se cargará (y su función de configuración solo se invocará) una vez.
Una vez que un objeto se ha cargado con éxito, su nombre de archivo se añade a la variable .LOADED.
Si prefiere que el fallo al cargar un objeto dinámico no se notifique como error, puede usar la directiva -load en lugar de load. GNU make no fallará ni generará ningún mensaje si un objeto no logra cargarse. El objeto que falló al cargarse no se añade a la variable .LOADED, que puede consultarse entonces para determinar si la carga tuvo éxito.
Los objetos cargados se someten al mismo procedimiento de rehecho que los makefiles (véase Cómo se rehacen los makefiles). Si alguno de los objetos cargados se recrea, entonces make empezará desde cero, releerá todos los makefiles y volverá a cargar los archivos objeto. No es necesario que el objeto cargado haga nada especial para admitir esto.
Es responsabilidad del autor del makefile proporcionar las reglas necesarias para reconstruir el objeto cargado.
|
Para ser útiles, los objetos cargados deben poder interactuar con GNU make. Esta interacción incluye tanto las interfaces que el objeto cargado ofrece a los makefiles como las interfaces que make ofrece al objeto cargado para manipular el funcionamiento de make.
La interfaz entre los objetos cargados y make está definida por el archivo de cabecera en C gnumake.h. Todos los objetos cargados escritos en C deberían incluir este archivo de cabecera. Cualquier objeto cargado que no esté escrito en C deberá implementar la interfaz definida en este archivo de cabecera.
Por lo general, un objeto cargado registrará una o más funciones nuevas de GNU make usando la rutina gmk_add_function desde dentro de su función de configuración. Las implementaciones de estas funciones de make pueden hacer uso de las rutinas gmk_expand y gmk_eval para llevar a cabo sus tareas y, opcionalmente, devolver una cadena como resultado de la expansión de la función.
Toda extensión dinámica debería definir el símbolo global plugin_is_GPL_compatible para declarar que ha sido publicada bajo una licencia compatible con la GPL. Si este símbolo no existe, make emite un error fatal y termina cuando intenta cargar su extensión.
El tipo declarado del símbolo debería ser int. No obstante, no necesita estar en ninguna sección asignada concreta. El código se limita a declarar que el símbolo existe en el ámbito global. Basta con algo como esto:
int plugin_is_GPL_compatible;
gmk_flocEsta estructura representa un par de nombre de archivo y ubicación (location). Se proporciona al definir elementos, de modo que GNU make pueda informar después al usuario, si es necesario, de dónde se produjo la definición.
Actualmente solo hay una manera de que los makefiles invoquen las operaciones que ofrece el objeto cargado: a través de la interfaz de llamada de funciones de make. Un objeto cargado puede registrar una o más funciones nuevas, que luego podrán invocarse desde el makefile igual que cualquier otra función.
Use gmk_add_function para crear una nueva función de make. Sus argumentos son los siguientes:
nameEl nombre de la función. Es lo que el makefile debe usar para invocar la función. El nombre debe tener entre 1 y 255 caracteres y solo puede contener caracteres alfanuméricos, punto («.»), guion («-») y guion bajo («_»). No puede comenzar por un punto.
func_ptrUn puntero a una función que make invocará cuando expanda la función en un makefile. Esta función debe estar definida por el objeto cargado.
min_argsEl número mínimo de argumentos que aceptará la función. Debe estar entre 0 y 255. GNU make lo comprobará y fallará antes de invocar func_ptr si la función se invocó con muy pocos argumentos.
max_argsEl número máximo de argumentos que aceptará la función. Debe estar entre 0 y 255. GNU make lo comprobará y fallará antes de invocar func_ptr si la función se invocó con demasiados argumentos. Si el valor es 0, se acepta cualquier número de argumentos. Si el valor es mayor que 0, debe ser mayor o igual que min_args.
flagsIndicadores que especifican cómo operará esta función; los indicadores deseados deben combinarse con OR. Si se proporciona el indicador GMK_FUNC_NOEXPAND, los argumentos de la función no se expandirán antes de llamar a la función; de lo contrario, se expandirán primero.
Una función registrada en make debe ajustarse al tipo gmk_func_ptr. Se invocará con tres parámetros: name (el nombre de la función), argc (el número de argumentos de la función) y argv (un arreglo de punteros a los argumentos de la función). El último puntero (es decir, argv[argc]) será nulo (0).
El valor de retorno de la función es el resultado de expandir la función. Si la función no se expande a nada, el valor de retorno puede ser nulo. En caso contrario, debe ser un puntero a una cadena creada con gmk_alloc. Una vez que la función retorna, make pasa a ser el propietario de esta cadena y la liberará cuando corresponda; el objeto cargado no puede acceder a ella.
GNU make exporta algunas prestaciones para que las usen los objetos cargados. Normalmente se ejecutarían desde dentro de la función de configuración o de las funciones registradas mediante gmk_add_function, para recuperar o modificar los datos con los que trabaja make.
gmk_expandEsta función toma una cadena y la expande usando las reglas de expansión de make. El resultado de la expansión se devuelve en un búfer de cadena terminado en nulo. La persona que llama es responsable de invocar gmk_free con un puntero al búfer devuelto cuando haya terminado.
gmk_evalEsta función toma un búfer y lo evalúa como un fragmento de sintaxis de makefile. Esta función puede usarse para definir nuevas variables, nuevas reglas, etc. Es equivalente a usar la función eval de make.
Tenga en cuenta que hay una diferencia entre gmk_eval y llamar a gmk_expand sobre una cadena usando la función eval: en este último caso, la cadena se expandirá dos veces; una vez por gmk_expand y luego de nuevo por la función eval. Con gmk_eval, el búfer se expande como mucho una sola vez (cuando lo lee el analizador de make).
Algunos sistemas admiten distintos esquemas de gestión de memoria. Por ello, nunca debe pasar memoria que haya reservado usted directamente a ninguna función de make, ni debe intentar liberar directamente memoria que le haya devuelto alguna función de make. En su lugar, use las funciones gmk_alloc y gmk_free.
En particular, la cadena que una función registrada mediante gmk_add_function devuelve a make debe reservarse usando gmk_alloc, y la cadena devuelta por la función gmk_expand de make debe liberarse (cuando ya no se necesite) usando gmk_free.
gmk_allocDevuelve un puntero a un búfer recién reservado. Esta función siempre devuelve un puntero válido; si no hay suficiente memoria disponible, make terminará. gmk_alloc no inicializa la memoria reservada.
gmk_freeLibera un búfer que make le devolvió. Una vez que la función gmk_free retorna, la cadena ya no será válida. Si se pasa NULL a gmk_free, no se realiza ninguna operación.
Supongamos que quisiéramos escribir una nueva función de GNU make que cree un archivo temporal y devuelva su nombre. Nos gustaría que nuestra función tomara un prefijo como argumento. Primero podemos escribir la función en un archivo mk_temp.c:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <gnumake.h>
int plugin_is_GPL_compatible;
char *
gen_tmpfile(const char *nm, int argc, char **argv)
{
int fd;
/* Compute the size of the filename and allocate space for it. */
int len = strlen (argv[0]) + 6 + 1;
char *buf = gmk_alloc (len);
strcpy (buf, argv[0]);
strcat (buf, "XXXXXX");
fd = mkstemp(buf);
if (fd >= 0)
{
/* Don't leak the file descriptor. */
close (fd);
return buf;
}
/* Failure. */
fprintf (stderr, "mkstemp(%s) failed: %s\n", buf, strerror (errno));
gmk_free (buf);
return NULL;
}
int
mk_temp_gmk_setup (const gmk_floc *floc)
{
printf ("mk_temp plugin loaded from %s:%lu\n", floc->filenm, floc->lineno);
/* Register the function with make name "mk-temp". */
gmk_add_function ("mk-temp", gen_tmpfile, 1, 1, 1);
return 1;
}
A continuación, escribiremos un Makefile que pueda construir este objeto compartido, cargarlo y usarlo:
all:
@echo Temporary file: $(mk-temp tmpfile.)
load mk_temp.so
mk_temp.so: mk_temp.c
$(CC) -shared -fPIC -o $@ $<
En MS-Windows, debido a las peculiaridades de cómo se producen los objetos compartidos, el compilador necesita explorar la biblioteca de importación que se genera al construir make (normalmente llamada libgnumake-version.dll.a, donde version es la versión de la API de objetos cargados). Por tanto, en Windows la receta (recipe) para producir un objeto compartido tendrá este aspecto (suponiendo que la versión de la API es 1):
mk_temp.dll: mk_temp.c
$(CC) -shared -o $@ $< -lgnumake-1
Ahora, cuando ejecute make, verá algo así:
$ make mk_temp plugin loaded from Makefile:4 cc -shared -fPIC -o mk_temp.so mk_temp.c Temporary filename: tmpfile.A7JEwd