Renuncia de responsabilidad:
La versión original de este artículo fue publicada por IBM
developerWorks y es propiedad de Westtech Information Services. Este
documento es una versión actualizada del artículo original y contiene
mejoras introducidas por el Equipo de Documentación de Gentoo.
Este documento carece de soporte activo.
|
Bash con ejemplos, Parte 3
1.
Explorando el sistema de ebuilds
Entra en el sistema de ebuilds
He estado deseando que llegara este capítulo final de Bash con
ejemplos, porque ahora que hemos cubierto los conceptos básicos de
la programación en bash,
Parte 1
y Parte 2,
podemos centrarnos en temas más avanzados, como el desarrollo de
aplicaciones en bash y diseño de programas. Te daré una buena dosis de
programación práctica, del mundo real, presentando un proyecto en cuya
codificación y refinamiento he invertido muchas horas: El sistema de
Ebuilds de Gentoo.
Soy el arquitecto líder de Gentoo Linux, un sistema Linux de próxima
generación, actualmente en estado beta. Una de mis principales
responsabilidades es asegurarme de que todos los paquetes binarios
(similares a los RPM) se crean correctamente, y funcionan bien en
conjunto. Como probablemente sepas, un sistema Linux estándar no está
compuesto de un solo árbol unificado de fuentes, como en el caso de
BSD, sino que está compuesto de más de 25 paquetes críticos que
funcionan juntos. Algunos de estos paquetes son:
| Paquete |
Descripción |
| linux |
El kernel actual |
| util-linux |
Una colección de programas variados relacionados con Linux |
| e2fsprogs |
Una colección de utilidades relacionadas con ext2 |
| glibc |
La librería C de GNU |
Cada paquete viene en su propio tarball, y es mantenido por
desarrolladores o equipos de desarrollo distintos. Para crear una
distribución, cada paquete debe ser descargado por separado,
compilado, y empaquetado, cada vez que el paquete necesita ser
reparado, actualizado o mejorado. Todo esto debe ser repetido (algunos
paquetes quedan desfasados realmente rápidot). Para ayudar en este
proceso tan repetitivo, he creado el sistema de ebuilds. Escrito casi
por completo en bash. Y para mejorar tu conocimiento de bash, te
enseñaré como implementé las secciones de desempaquetado y compilado
del sistema de ebuilds. Al explicar cada paso, explicaré también por
qué se hicieron ciertas decisiones. Al final de este artículo, no
solo tendrás una visión de proyectos en bash a mayor escala, sino que
también habrás implementado una buena porción de un sistama de
autocompilado.
¿Por qué bash?
Bash es un componente esencial del sistema de ebuilds de Gentoo
Linux. Fue elegido como el lenguaje primario para los ebuilds por
varias razones. En primer lugar, posee una sintaxis familiar y
asequible, muy apropiada para el uso de programas externos. Un sistema
de autocompilado es un código intermedio que automatiza la llamada a
programas externos, y bash es un lenguaje particularmente apropiado
para este tipo de aplicación. Segundo, el soporte de bash para
funciones permite al sistema de ebuilds adoptar un diseño modular,
fácil de entender. Tercero, el sistema de ebuilds saca provecho del
soporte de bash para las variables de entorno, permitiendo a los
mantenedores de paquetes y a los desarrolladores reconfigurarlo al
vuelo.
Revista al proceso de construcción
Antes de entrar en el sistema de ebuilds de lleno, tendremos que
conocer los pasos necesarios para compilar e instalar un paquete. Para
nuestro ejemplo usaremos el paquete "sed", un editor estándar de
flujos GNU que es parte integrante de todas las distribuciones de
Linux. Primero descarga el archivo con las fuentes
(sed-3.02.tar.gz) (ver Recursos). Almacenaremos este archivo en
/usr/src/distfiles, un directorio al que nos referiremos
usando la variable de entorno $DISTDIR. $DISTDIR es el
directorio donde se guardarán todos los tarball de código fuente, será
un gran almacén de código fuente.
Nuestro siguiente paso será crear un directorio
temporal work, que aloje los fuentes descomprimidos. Nos
referiremos a este directorio usando la variable $WORKDIR. Para
ésto, cambia a un directorio sobre el que tengas permiso de escritura
y escribe lo siguiente:
Listado de Código 1.1: Descomprimiendo sed en un directorio temporal |
$ mkdir work
$ cd work
$ tar xzf /usr/src/distfiles/sed-3.02.tar.gz
|
Ahora el tarball está descomprimido, habrá creado un directorio
llamado
sed-3.02, que contiene las fuentes de sed. Nos referiremos a dicho
directorio sed-3.02 más tarde usando la variable de entorno
$SRCDIR. Para compilar el programa teclea lo siguiente:
Listado de Código 1.2: Uncompressing sed into a temporary directory |
$ cd sed-3.02
$ ./configure --prefix=/usr
$ make
|
Vamos a saltarnos el paso "make install", ya que solo estamos
cubriendo los pasos de desempaquetado y compilación en este
artículo. Si quisiéramos usar un script de bash para realizar todos
estos pasos por nosotros haríamos algo como:
Listado de Código 1.3: Script bash de ejemplo para desempaquetar y compilar |
#!/usr/bin/env bash
if [ -d work ]
then
rm -rf work
fi
mkdir work
cd work
tar xzf /usr/src/distfiles/sed-3.02.tar.gz
cd sed-3.02
./configure --prefix=/usr
make
|
Generalizando el código
Aunque este script de autocompilado funciona, no es muy
flexible. Básicamente, el script contiene la lista de los comandos que
han sido escritos anteriormente en línea de comandos. Esta solución
funciona, pero sería mucho mejor tener un script más genérico que
pudiera configurar y desempaquetar cualquier paquete, quizás cambiando
solo unas pocas líneas. El trabajo para el mantenedor del paquete se
ve así disminuído, y es más fácil añadir nuevos paquetes a la
distribución. Podemos usar variables de entorno para hacer nuestro
script más genérico:
Listado de Código 1.4: Un script nuevo, más genérico |
#!/usr/bin/env bash
P=sed-3.02
A=${P}.tar.gz
export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
export SRCDIR=${WORKDIR}/${P}
if [ -z "$DISTDIR" ]
then
DISTDIR=/usr/src/distfiles
fi
export DISTDIR
if [ -d ${WORKDIR} ]
then
rm -rf ${WORKDIR}
fi
mkdir ${WORKDIR}
cd ${WORKDIR}
tar xzf ${DISTDIR}/${A}
cd ${SRCDIR}
./configure --prefix=/usr
make
|
Hemos añadido muchas variables al nuevo código, pero, básicamente,
todavía hace lo mismo. Sin embargo, ahora podemos compilar cualquier
paquete basado en GNU autoconf. Simplemente copiando este archivo con
un nuevo nombre que refleje el nombre del paquete, y cambiando los
valores de $A y $P, compilará. Las demás variables se
ajustarán automáticamente. Si bien es útil, hay aún mejoras que
podemos introducir en este código. Este código es bastante más largo
que el original. Ya que una de las tareas principales de cualquier
proyecto de programación es reducir la complejidad de cara al usuario,
estaría bien reducir un poco la longitud del cógido, o, al menos,
organizarlo un poco mejor. Podemos hacer esto con un ingenioso
truco -- separaremos el código en dos ficheros separados, guarda lo
siguiente como sed-3.02.ebuild:
Listado de Código 1.5: sed-3.02.ebuild |
P=sed-3.02
A=${P}.tar.gz
|
Nuestro primer fichero es trivial, y contiene solo variables de
entorno, que han de ser configuradas paquete por paquete, el segundo
fichero contiene el cerebro de la operación. Guárdalo como "ebuild" y
hazlo ejecutable:
Listado de Código 1.6: The ebuild script |
#!/usr/bin/env bash
if [ $# -ne 1 ]
then
echo "se esperaba un argumento."
exit 1
fi
if [ -e "$1" ]
then
source $1
else
echo "ebuild $1 no encontrado."
exit 1
fi
export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
export SRCDIR=${WORKDIR}/${P}
if [ -z "$DISTDIR" ]
then
DISTDIR=/usr/src/distfiles
fi
export DISTDIR
if [ -d ${WORKDIR} ]
then
# borra directorio antiguo si ya existía
rm -rf ${WORKDIR}
fi
mkdir ${WORKDIR}
cd ${WORKDIR}
tar xzf ${DISTDIR}/${A}
cd ${SRCDIR}
./configure --prefix=/usr
make
|
Ahora que hemos dividido nuestro sistema en dos ficheros, apuesto a
que te estarás preguntando como funciona. Fácil, para compilar sed,
escribe:
Listado de Código 1.7: Probando el ebuild |
$ ./ebuild sed-3.02.ebuild
|
Cuando "ebuild" se ejecuta, primero intenta
interpretar $1. ¿Que significa esto? Recuerda de mi anterior
artículo, que $1 es el primer argumento de línea de comandos,
en este caso sed-3.02.ebuild. En bash, el comando
"source" lee instrucciones bash de un archivo y las ejecuta como si
estuvieran dentro del fichero desde donde se usa el comando
"source". Así que "source" ${1}" causa que el script "ebuild" ejecute
los comandos contenidos en
sed-3.02.ebuild, de este modo, $P y $A son
definidos. Este cambio de diseño es realmente conveniente, porque si
queremos compilar otro programa, en lugar de sed, tan solo necesitamos
un nuevo fichero .ebuild que pasar a nuestro script
"ebuild". De este modo, los ficheros .ebuild son
realmente simples, mientras la parte complicada del sistema se
almacena en el script "ebuild". De esta forma, también se puede
mejorar o actualizar el sistema ebuild simplemente editando el script,
manteniendo los detalles de la implementación fuera de los ficheros
ebuild. Aquí hay un fichero ebuild de ejemplo para gzip:
Listado de Código 1.8: gzip-1.2.4a.ebuild |
P=gzip-1.2.4a
A=${P}.tar.gz
|
Añadiendo funcionalidad
Bien, ya hemos hecho algún progreso, pero hay funcionalidades
adicionales que me gustaría añadir. Me gustaría que el script ebuild
aceptara un segundo parámetro que será compile, unpack,
o all. Este segundo parámetro dirá al ebuild la operación que
debe realizar. De esta forma, puedo decirle a ebuild que desempaquete
el archivo pero sin compilarlo (por si necesito inspeccionar el código
fuente antes de la compilación). Para hacer esto usaremos una
estructura case, que comprobará la variable $2, y actuará de
acuerdo con su valor. El código sería algo así:
Listado de Código 1.9: ebuild, revisión 2 |
#!/usr/bin/env bash
if [ $# -ne 2 ]
then
echo "Por favor, especifique dos argumentos, el fichero .ebuild y"
echo "unpack, compile or all"
exit 1
fi
if [ -z "$DISTDIR" ]
then
DISTDIR=/usr/src/distfiles
fi
export DISTDIR
ebuild_unpack() {
cd ${ORIGDIR}
if [ -d ${WORKDIR} ]
then
rm -rf ${WORKDIR}
fi
mkdir ${WORKDIR}
cd ${WORKDIR}
if [ ! -e ${DISTDIR}/${A} ]
then
echo "${DISTDIR}/${A} no existe. Por favor, descárguelo primero."
exit 1
fi
tar xzf ${DISTDIR}/${A}
echo "Desempaquetado ${DISTDIR}/${A}."
#el código fuente está descomprimido
}
ebuild_compile() {
cd ${SRCDIR}
if [ ! -d "${SRCDIR}" ]
then
echo "${SRCDIR} no existe -- por favor, descomprima primero el paquete."
exit 1
fi
./configure --prefix=/usr
make
}
export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
if [ -e "$1" ]
then
source $1
else
echo "Ebuild $1 no encontrado."
exit 1
fi
export SRCDIR=${WORKDIR}/${P}
case "${2}" in
unpack)
ebuild_unpack
;;
compile)
ebuild_compile
;;
all)
ebuild_unpack
ebuild_compile
;;
*)
echo "Por favor, especifique unpack, compile o All como segundo argumento"
exit 1
;;
esac
|
Hemos hecho varios cambios, así que revisémoslos. Primero, hemos
puesto las órdenes para desempaquetar y compilar los paquetes en su
propia función. Las hemos llamado ebuild_compile()
y ebuild_unpack(), respectivamente. Ha sido un buen
movimiento, ya que el código se está complicando, y las funciones lo
dotan de algo más de modularidad, lo que nos ayudará a mantener el
script ordenado. En la primera línea de cada función, se cambia de
forma explícita, con cd, al directorio al que se quiere ir. Al
complicarse nuestro código es muy probably que terminemos ejecutando
algo en un directorio distinto del correcto, así, nos aseguramos de
estar en el lugar correcto antes de hacer nada, con cd, y nos
ahorraremos posible errores más adelante. Ésto es un paso importante,
sobre todo, si se borran ficheros dentro de una función.
También he añadido un test al principio de la
función ebuild_compile(). Ahora comprueba que el
directorio $SRCDIR existe, y, si no, imprime un mensaje de
error diciéndole al usuario que desempaquete el archivo y sale. Si lo
prefieres, puesdes cambiar el comportamiento de forma que,
si $SRCDIR no existe, nuestro ebuild desempaquete
automáticamente el archivo. Puedes hacerlo
cambiando ebuild_compile() por esta nueva versión:
Listado de Código 1.10: Nueva versión de ebuild_compile() |
ebuild_compile() {
if [ ! -d "${SRCDIR}" ]
then
ebuild_unpack
fi
cd ${SRCDIR}
./configure --prefix=/usr
make
}
|
Uno de los cambios más obvios en nuestro script ebuild es la
estructura case añadida al final del mismo. Dicha estructura
simplemente chequea el segundo argumento de línea de comandos, y, en
base al valor del mismo, decide la acción a realizar. Si ahora
ejecutamos esto:
Listado de Código 1.11: Acción predeterminada |
$ ebuild sed-3.02.ebuild
|
Obtendremos un mensaje de error, porque ebuild ahora necesita que le
digamos qué hacer, de esta forma:
Listado de Código 1.12: Descomprimir |
$ ebuild sed-3.02.ebuild unpack
|
or:
Listado de Código 1.13: Compilar |
$ ebuild sed-3.02.ebuild compile
|
or:
Listado de Código 1.14: Descomprimir y compilar |
$ ebuild sed-3.02.ebuild all
|
Importante:
Si se suministra un segundo parámetro distinto de los usados más arriba, se
obtiene un mensaje de error (caso *), y el programa termina.
|
Modularizando el código
Ahora que el código es más avanzado y funcional, puede que estés
pensando en crear varios ebuilds para desempaquetar y compilar tus
programas favoritos. Si lo hicieras, tarde o temprano comprobarías que
algunas fuentes no usan autoconf (./configure), sino que se
valen de otros procesos de compilación no estándar. Tenemos que
modificar el sistema de ebuilds para que se acomode a estos
programas. Pero antes de hacerlo, es bueno pararse a pensar como
conseguiremos esto.
Una de las grandes ventajas de usar
siempre ./configure --prefix=/usr; make en la fase de
compilación, es que, la mayoría de las veces funciona. Pero también
debemos hacer que el sistema de ebuilds funcione con aquellos fuentes
que no usan autoconf, o fichero Make normales. Propongo lo siguiente,
como solución a este problema:
-
Si hay un script configure en ${SRCDIR}, ejecutarlo de esta forma:
./configure --prefix=/usr. De otro modo, saltarse este paso.
- Ejecutar el comando siguiente: make
Los ebuilds solo ejecutarán configure si dicho script existe. Así
hacemos que ebuild funcione con programas que no usan autoconf, y
tienen un fichero Make estándar. Pero, ¿y si un simple "make" no
funciona con algunos fuentes? Necesitamos una forma de saltarse esta
funcionalidad predefinida, usando un código alternativo para manejar
situaciones específicas. Para esto, convertiremos nuestra función
ebuild_compile() en dos funciones. La primera de dichas funciones puede
ser vista como "padre" de la segunda, y se llamará ebuild_compile(). La
nueva función, llamada user_compile(), contendrá nuestras acciones
predeterminadas:
Listado de Código 1.15: ebuild_compile() separada en dos funciones |
user_compile() {
if [ -e configure ]
then
#run configure script if it exists
./configure --prefix=/usr
fi
#run make
make
}
ebuild_compile() {
if [ ! -d "${SRCDIR}" ]
then
echo "${SRCDIR} no existe -- por favor, descomprima primero."
exit 1
fi
cd ${SRCDIR}
user_compile
}
|
Puede que no parezca obvio el por qué de todo esto ahora mismo. Así
que, por ahora, sigamos. Si bien el código de arriba funciona de forma
idéntica a la anterior versión de ebuild, ahora podemos hacer algo que
no podiamos hacer antes. Podemos redefinir la
función user_compile() en sed-3.02.ebuild. Así,
si la predeterminada user_compile() no sirve a nuestras
necesidades, podemos redefinirla por completo en nuestro
fichero .ebuild. Como ejemplo, un fichero .ebuild
para e2fsprogs-1.18, que requiere una línea ./configure
ligeramente modificada:
Listado de Código 1.16: e2fsprogs-1.18.ebuild |
P=e2fsprogs-1.18
A=${P}.tar.gz
user_compile() {
./configure --enable-elf-shlibs
make
}
|
Ahora, e2fsprogs será compilado de la forma correcta. Para la
mayoría de los paquetes, esto no es necesario. Simplemente omitiendo
la definición de user_compile() en nuestro
fichero .ebuild, conseguiremos que se use la
función user_compile() predeterminada.
¿Como sabe el script ebuild que función user_compile() debe
usar? Muy sencillo: en el script ebuild, la
función user_compile() es definida antes de que el fichero
.ebuild e2fsprogs-1.18.ebuild sea leído. Si hay una
función user_compile() en e2fsprogs-1.18.ebuild,
dicha función sobreescribe a la versión predeterminada, definida
previamente. Si no, la primera versión es usada.
Hemos añadido una gran funcionalidad sin requerir ningún tipo de
codificación compleja. No lo explicaré aquí, pero se podría hacer algo
similar con la función ebuild_unpack(), de forma que podamos
reescribir el proceso de desempaquetado predeterminado. Esto podría
ser práctico si se tiene que hacer algún tipo de parcheo o si los
ficheros están contenido en múltiples archivos comprimidos. También
sería una buena idea modificar el código de desempaquetado de forma
que reconozca tarballs comprimidos con bzip2 por defecto.
Ficheros de configuración
Hemos cubierto varias técnicas interesantes de bash, y ahora es el
momento de aprender una más. A menudo es práctico para un programa
tener un fichero de configuración global que resida
en /etc. Afortunadamente, esto es fácil cuando se usa
bash. Simplemente crea este fichero y guárdalo como
/etc/ebuild.conf:
Listado de Código 1.17: /ect/ebuild.conf |
MAKEOPTS="-j2"
|
En este ejemplo he incluído una sola opción de configuración, pero se
podrían incluír muchas más. Una de las cosas interesantes de bash es
que el fichero se puede interpretar simplemente usando el comando
"source" sobre el mismo. Éste es un truco de diseño que funciona con
la mayoría de lenguajes interpretados. Después de
que /etc/ebuild.conf haya sido
interpretado, $MAKEOPTS está definido en nuestro script
.ebuild, y le permite al usuario pasar dichas opciones a make. En este
caso, la opción le dice al ebuild que lance una instancia paralela de
make.
Nota:
¿Qué es una instancia paralela de make? Las instancias
paralelas pueden servir para agilizar el proceso en sistema con varios
procesadores. Make soporta la compilación en paralelo. Esto significa
que, en lugar de compilar un fichero fuente en un momento dado, make
puede compilar un número de ficheros (dado por el usuario) al mismo
tiempo. En un sistema multiprocesador esto hace que se usen estos
procesadores extra. Make en paralelo se activa al interpretar la
opción -j # pasada a make, de esta forma: make -j4
MAKE="make -j4". Esto instruye a make para compilar cuatro
programas de forma simultánea. El argumento MAKE="make -j4" le
dice a make que pase la opción -j4 a cualquier proceso hijo que
lance.
|
Y aquí tenemos la versión final de ebuild:
Listado de Código 1.18: ebuild, la versión final |
#!/usr/bin/env bash
if [ $# -ne 2 ]
then
echo "Por favor, especifique fichero ebuild file y unpack, compile u all"
exit 1
fi
source /etc/ebuild.conf
if [ -z "$DISTDIR" ]
then
DISTDIR=/usr/src/distfiles
fi
export DISTDIR
ebuild_unpack() {
cd ${ORIGDIR}
if [ -d ${WORKDIR} ]
then
rm -rf ${WORKDIR}
fi
mkdir ${WORKDIR}
cd ${WORKDIR}
if [ ! -e ${DISTDIR}/${A} ]
then
echo "${DISTDIR}/${A} no existe. Por favor, descargue primero."
exit 1
fi
tar xzf ${DISTDIR}/${A}
echo "Unpacked ${DISTDIR}/${A}."
}
user_compile() {
if [ -e configure ]
then
./configure --prefix=/usr
fi
make $MAKEOPTS MAKE="make $MAKEOPTS"
}
ebuild_compile() {
if [ ! -d "${SRCDIR}" ]
then
echo "${SRCDIR} no existe -- por favor, descomprima primero."
exit 1
fi
cd ${SRCDIR}
user_compile
}
export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
if [ -e "$1" ]
then
source $1
else
echo "Fichero .ebuild $1 no encontrado."
exit 1
fi
export SRCDIR=${WORKDIR}/${P}
case "${2}" in
unpack)
ebuild_unpack
;;
compile)
ebuild_compile
;;
all)
ebuild_unpack
ebuild_compile
;;
*)
echo "Por favor, especifique unpack, compile u all como segundo argumento"
exit 1
;;
esac
|
/etc/ebuild.conf es interpretado cerca del principio del
fichero. Usamos $MAKEOPTS en nuestra user_compile()
prefabricada. Puede que te preguntes como funcionará ésto -- después
de todo, nos referimos a $MAKEOPTS antes de
interpretar /etc/ebuild.conf, que es el encargado de
definir $MAKEOPTS. Afortunadamente, ésto no es problema, porque
la expansión de variables se produce al
ejecutar user_compile(). Cuando eso
sucede, /etc/ebuild.conf ha sido ya incorporado y
$MAKEOPTS tiene un valor correcto.
Resumiendo
Hemos cubierto muchas técnicas de programación en bash en este
artículo, pero en realidad solo hemos arañado la superficie de lo que
el poder auténtico de bash representa. Por ejemplo, el sistema de
ebuilds de Gentoo no solo puede desempaquetar y compilar de forma
automática, sino que también:
-
Descarga las fuentes de forma automática si no están
en $DISTDIR
-
Verifica que las fuentes no están corruptas, usando sumas MD5
-
Si se especifica, instala el programa compilado en un sistema de
archivos, en vivo, manteniendo un listado de los ficheros
instalados de forma que el paquete pueda ser fácilmente
desinstalado en cualquier momento.
-
Si se especifica, puede empaquetar una aplicación instalada en un
tarball (comprimido) de forma que pueda ser instalada después, en
otro ordenador, o durante un proceso de instalación basado en CD
(por ejemplo, si estás contruyendo una distribución basada en
dicho medio).
De forma adicional, el sistema ebuild en producción tiene otras
opciones globales de configuración, que permiten al usuario establecer
banderas de optimización que se usan en tiempo de compilación, o el
soporte específico que se quiere en ciertas aplicaciones. Por ejemplo,
los soportes para GNOME y slang se activan de forma predeterminada en
los paquetes que lo soportan.
Bash puede hacer mucho más de lo que he tocado en esta serie de
artículos. Espero que hayas aprendido mucho sobre esta increíble
utilidad, y que estés deseando usar bash para acelerar y mejorar tus
proyectos de desarrollo.
2.
Recursos
Links de utilidad
|
|