Daniel Robbins
President and CEO, Gentoo Technologies, Inc.
Abril de 2000
Neste artigo final da série Bash em Exemplos, Daniel Robbins dá uma boa olhada no sistema ebuild do gentoo Linux, um excelente exemplo do poder do bash. Passo a passo, ele mostra como o sistema ebuild foi implementado, e toca em muitas técnicas úteis do bash, e estratégias de projeto. No fim do artigo, você terá uma boa idéia do que está envolvido na produção de uma aplicação completa baseada no bash, bem como terá iniciado a codificação de seu próprio sistema de auto-construção.
Eu realmente estava olhando para diante para este terceiro e último artigo Bash em Exemplos, por que agora que já cobrimos os fundamentos da programação bash na Parte 1 e Parte 2, podemos nos direcionar para tópicos mais avançados, como desenvolvimento de aplicações bash e projeto de pogramas. Para este artigo, eu vou dar a vocês uma boa dose de experiência de desenvolvimento prática e real de bash, apresentando um projeto em que gastei muitas horas codificando e refinando: o sistema ebuild do Gentoo Linux.
Sou o arquiteto chefe do Gentoo Linux, um Linux OS avançado, atualmente em estágio beta. Uma das minhas responsabilidades primárias é garantir que todos os pacotes binários (similares a pacotes RPM) sejam criados propriamente e funcionem juntos. Como você provavelmente sabe, um sistema padrão Linux não é composto de uma única árvore de fontes unificada (como o BSD), mas é feito de cerca de mais de 25 pacotes principiais que trabalham juntos. Alguns dos pacotes incluem:
Pacote | Descrição |
---|---|
linux | O kernel |
util-linux | Uma coleção de programas miscelânea relacionados ao Linux |
e2fsprogs | Uma coleção de utilitários relacionados ao filesystem ext2 |
glibc | A biblioteca GNU C |
Cada pacote está em seu próprio tarball e é mantido por desenvolvedores independentes ou times de desenvolvedores. Para criar uma distribuição, cada pacote tem que ser separadamente baixado, compilado, e empacotado. Cada vez que um pacote deve ser corrigido, atualizado, ou melhorado, os passos de compilação e empacotamento devem ser repetidos (e eles ficam velhos realmente rápido). Para ajudar a eliminar os passos repetitivos envolvidos na criação e atualização de pacotes, eu criei o sistema ebuild, escrito quase que inteiramente em bash. Para melhorar seu conhecimento de bsh, irei mostrar como eu implementei as porções que fazem o desenpacotamento e compilação do sistema ebuild, passo a passo. Conforme eu explico cada passo, também irei discutir pro que certas decisões de projeto foram feitas. No fim do artigo, não somente você terá um excelente entendimento de projetos de programação bash de larga escala, mas você também terá implementado uma boa porção de um sistema de auto criação completa.
O bash é um componente essencial do sistema ebuild do Gentoo Linux. Ele foi escolhido como linguagem primária do ebuild por várias razões. Primeiro, ele possui uma sintaxe simples e familiar que é especialmente apropriada para chamar programas externos. Um sistema auto-build é a "cola' que automatiza a chamada de programas externos, e o bash é bastante apropriado para este tipo de aplicação. Segundo, o suporte do bash para funções permite que o sistema ebuild tenha um código modular e fácil de entender. Terceiro, o sistema ebuild aproveita-se do suporte do bash para variáveis de ambiente, permitindo que mantenedores de pacotes e desenvolvedores o configurem facilmente, enquanto está rodando.
antes de olharmos no sistema ebuild, vamos revisar o que está envolvido em fazer com que um pacote esteja compilado e instalado. Para nosso exemplo, iremos olhar o pacote "sed", um utilitário de edição de textos em linha padrão do GNU, que é parte de todas as distribuições do Linux. Primeiro, baixe o tarball do fonte (sed-3.02.tar.gz) (veja Recursos). Iremos armazenar este arquivo em /usr/src/distfiles, um diretório ao qual iremos nos referir usando a variável de ambiente "$DISTDIR". "$DISTDIR" é o diretório em que todos os tarballs de fontes originais residem. É um grande depósito de código fonte.
Nosso próximo passo é criar um diretório temporário chamado "work", que irá abrigar os fontes descompactados. Iremos nos referir a este diretório mais tarde usando a variável de ambiente "$WORKDIR". Para isto, iremos trocar de diretório para um em que tenhamos permissão de escrita, e iremos escrever o seguinte:
$ mkdir work $ cd work $ tar xzf /usr/src/distfiles/sed-3.02.tar.gz
O tarball é então descompactado, criando um diretório chamado sed-3.02 que contém todo o código fonte. Iremos nos referir ao diretório sed-3.02 mais tarde usando a variável de ambiente "$SRCDIR". Para compilar o programa, usamos o seguintes comandos:
$ cd sed-3.02 $ ./configure --prefix=/usr (autoconf gera os arquivos makefile apropriados, pode demorar um pouco) $ make (o pacote é compilado a partir dos fontes, também pode demorar um pouco)
Vamos omitir o passo final, o "make install", já que estamos apenas cobrindo a descompactação e compilação neste artigo. Se quisermos escrever um scrpt bash para executar todos estes passos para nós, ele poderia se parecer com o seguinet:
#!/usr/bin/env bash if [ -d work ] then # remove old directory if it exists 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
Apesar deste script de autocompilação funcionar, ele não é muito flexível. Basicamente, o script só contém a listagem de todos os comandos que escrevemos na linha de comando. Apesar desta solução funcionar, seria legal fazer um script mais genérico que possa ser configurado rapidamente para descompactar e compilar qualquer pacote pela alteração de algumas linhas. Desta forma, é menos trabalho para o administrador do pacote acrescentar novos pacotes à distribuição. Vamos dar o primeiro passo nesta direção usando bastante variáveis de ambiente, tornando nosso script mais genérico:
#!/usr/bin/env bash # P is the package name P=sed-3.02 # A is the archive name A=${P}.tar.gz export ORIGDIR=`pwd` export WORKDIR=${ORIGDIR}/work export SRCDIR=${WORKDIR}/${P} if [ -z "$DISTDIR" ] then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfiles fi export DISTDIR if [ -d ${WORKDIR} ] then # remove old work directory if it exists rm -rf ${WORKDIR} fi mkdir ${WORKDIR} cd ${WORKDIR} tar xzf ${DISTDIR}/${A} cd ${SRCDIR} ./configure --prefix=/usr make
Acrescentamos muitas variáveis de ambiente ao código, mas ele ainda faz basicamente a mesma coisa. Entretanto, agora, para compilar qualquer tarball de fonte padrão GNU baseado no autoconf, podemos simplesmente copiar este arquivo para um novo arquivo (com um nome apropriado para refletir o nome do novo pacote que ele compila), e então trocar os valores de "$A" e "$P" para os novos valores. Todas as outras variáveis de ambiente automaticamente são ajustadas para as configurações corretas, e o script irá funcionar como esperado. Enquanto isto é útil, existe ainda alguns melhoramentos que podem ser feitos ao código. Este códito em particular é muito maior que o script de "transcrição" que criamos. Como um dos objetivos de qualquer projeto de programação deve ser a redução da complexidade para o usuário, seria legal diminuir dramaticamente o código, ou, pelo menos, organizá-lo melhor. Podemos fazer isto com um truque legal -- dividir o código em dois arquivos separados. Salve este arquivo como "sed-3.02.ebuild":
#the sed ebuild file -- very simple! P=sed-3.02 A=${P}.tar.gz
Nosso primeiro arquivo é trivial, e contém somente as variáveis de ambiente que devem ser configuradas por pacote. Aqui está o seguindo script, que contém o cérebro da operação. Salve este como "ebuild" e torne-o executável:
#!/usr/bin/env bash if [ $# -ne 1 ] then echo "one argument expected." exit 1 fi if [ -e "$1" ] then soruce $1 else echo "ebuild file $1 not found." exit 1 fi export ORIGDIR=`pwd` export WORKDIR=${ORIGDIR}/work export SRCDIR=${WORKDIR}/${P} if [ -z "$DISTDIR" ] then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfiles fi export DISTDIR if [ -d ${WORKDIR} ] then # remove old work directory if it exists rm -f ${WORKDIR} fi mkdir ${WORKDIR} cd ${WORKDIR} tar xzf ${DISTDIR}/${A} cd ${SRCDIR} ./configure --prefix=/usr make
Agora que dividimos nosso sistema em dois arquivos, eu aposto que você está tentando imaginar como ele funciona. Basicamente, para compilar o sed, escreva:
$ ./ebuild sed-3.02.ebuild
Quando o "ebuild" é executado, ele primeiro tenta fazer um "source" de "$1". O que isto significa? Do meu artigo anterior, lembre-se que "$1" é o primeiro argumento da linha de comando -- neste caso, "sed-3.02.ebuild". No bash, o comando "source" lê declarações bash de um arquivo, e executa elas como se elas estivessem no lugar em que o comando "source" está. Assim, "source ${1}" faz com que o script "ebuild" execute os comandos em "sed-3.02.ebuild", que faz com que "$P" e "$A" sejam definidas. Esta alteração de projeto é realmente útil, por que se queremos compilar outro programa ao invés do sed, simplesmente criamos um novo arquivo .ebuild e passamos o mesmo como um argumento para nosso script "ebuild". Desta forma, os arquivos .ebuild são realmente simples, enquanto o cérebro complicado do sistema ebuild fica armazenado em um lugar -- nosso script "ebuild". Desta forma, podemos atualizar ou melhorar o sistema ebuild simplesmente editando o script "ebuild", mantendo os detalhes de implementação fora dos arquivos ebuild. Segue um arquivo ebuild exemplo para o gzip:
#another really simple ebuild script! P=gzip-1.2.4a A=${P}.tar.gz
OK, estamos fazendo algum progresso. Mas existe uma funcionalidade adicional que eu gostaria de acrescentar. Eu gostaria que o script ebuild aceitasse um segundo argumento de linha de comando, que será "compile", "unpack", ou "all". Este segundo argumento de linha de comando informa ao script ebuild qual passo em particular do script eu quero executar. Desta forma, eu posso informar ao ebuild para descompactar o arquivo, mas não compilar ele (caso eu queira inspecionar o código fonte antes que a compilação inicie). Para isto, irei acrescentar uma declaração case que irá testar a variável "$2", e fazer coisas diferentes baseado em seu valor. Aqui está o nosso novo código:
#!/usr/bin/env bash if [ $# -ne 2 ] then echo "Please specify two args - .ebuild file and unpack, compile or all" exit 1 fi if [ -z "$DISTDIR" ] then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfiles fi export DISTDIR ebuild_unpack() { #make sure we're in the right directory cd ${ORIGDIR} if [ -d ${WORKDIR} ] then rm -rf ${WORKDIR} fi mkdir ${WORKDIR} cd ${WORKDIR} if [ ! -e ${DISTDIR}/${A} ] then echo "${DISTDIR/${A} does not exist. Please download first." exit 1 fi tar xzf ${DISTDIR/${A} echo "Unpacked ${DISTDIR}/${A}." #source is now correctly unpacked } ebuild_compile() { #make sure we're in the right directory cd ${SRCDIR} if [ ! -d "${SRCDIR}" ] then echo "${SRCDIR} does not exist -- please unpack first." exit 1 fi ./configure --prefix=/usr make } export ORIGDIR=`pwd` export WORKDIR=${ORIGDIR}/work if [ -e "$1" ] then source $1 else echo "Ebuild file $1 not found." exit 1 fi export SRCDIR=${WORKDIR}/${P} case "${2}" in unpack) ebuild_unpack ;; compile) ebuild_compile ;; all) ebuild_unpack ebuild_compile ;; *) echo "Please specify unpack, compile or all as the second arg" exit 1 ;; esac
Fizemos muitas alterações, então vamos primeiro revisá-las. Primeiro, colocamos os passos de compilação e desarquivamento em suas próprias funções, chamadas ebuild_compile() e ebuild_unpack(), respectivamente. Este foi um movimento inteligenes, uma vez que o código vai ficando mais complicado, e as novas funções dão uma modularidade, que ajuda a manter as coisas organizadas. Na primeira linha de cada função, eu explicitamente fiz um "cd" para o diretório que queria estar por que, conforme nosso código está ficando mais modular que linear, é mais provável que cometamos um deslize e executemos uma função no diretório de trabalho errado. O comando "cd" explicitamente coloca-nos no lugar certo, e evita que cometamos um erro mais tarde -- um passo importante -- especialmente se você irá excluir arquivos em funções.
Além disto, eu acrescentei uma checagem útil no início da função ebuild_compile(). Agora, ela checa para certificar-se que "$SRCDIR" exista, e, caso não exista, imprime uma mensagem de erro informando o usuário para primeiro desempacotar o arquivo, e então sai. Se você quiser, pode alterar este comportamento, de forma que se "$SRCDIR" não existir, nosso script ebuild irá descompactar o arquivo fonte automaticamente. Você pdoe fazer isto substituindo o código do ebuild_compile() pela seguinte versão:
ebuild_compile() { #make sure we're in the right directory if [ ! -d "${SRCDIR}" ] then ebuild_unpack fi cd ${SRCDIR} ./configure --prefix=/usr make }
Uma das alterações mais óbvias em nossa segunda versão do script ebuild é a nova declaração case no fim do código. Esta declaração case simplesmente checa o segundo argumento da linha de comando, e executa a ação correta, dependendo de seu valor. Se agora escrevermos:
$ ebuild sed-3.02.ebuild
iremos obter uma mensagem de erro. O ebuild agora quer que digamos a ele o que fazer, como abaixo:
ebuild sed-3.02.ebuild unpack
ou
ebuild sed-3.02.ebuild compile
ou
ebuild sed-3.02.ebuild all
Se você fornecer um segundo argumento diferente de qualquer destas opções listadas, receberá uma mensagem de erro (a cláusula *), e o programa terminará.
Agora que o código está bastante avançado e funcional, você pode ficar tentado a criar vários scripts ebuild para descompactar e compilar seus programas favoritos. Se você o fizer, cedo ou tarde irá cruzar com alguns fontes que não usam o autoconf ("./configure") ou possivelmente outros que possuam processos de compilação não-padrão. Precisamos nos certificar de fazer mais algumas alterações para o sistema ebuild para acomodar estes programas. Mas, antes disto, é uma boa idéia pensar um pouco sobre como fazer isto.
Uma das coisas boas de ter escrito explicitamente "./configure --prefix=/usr; make' em nosso estágio de compilação é que, na maior parte do tempo, isto funciona. Mas precisamos que o sistema ebuild acomode fontes que não usem o autoconf ou Makefiles normais. Para resolver este problema, eu proponho que nosso script ebuil deve, por padrão, fazer o seguinte:
./configure --prefix=/usrCaso contrário, não executar este passo
make
Uma vez que o ebuild somente executa o ebuild se ele realmente existir, podemos acomodar automaticamente os programas que não usam o autoconf e possuem makefiles padrão. Mas se um simples "make" não fizer o mesmo truque para alguns fontes? Precisamos uma forma de contornar nossos valores defaults com algum código específico para tratar estas situações. Para isto, iremos transformar nossa função ebuild_compile() em duas funções. A primeira função, que pode ser vista como uma função "pai", ainda será chamada de ebuild_compile(). Entretanto, teremos uma nova função, chamada user_compile(), que contém somente nossas ações de compilação razoáveis:
user_compile() { #we're already in ${SRCDIR} 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} does not existe -- please unpack first." exit 1 fi #make sure we're in the right directory cd ${SRCDIR} user_compile }
Pode não parecer óbvio o que eu estou fazendo agora, mas confie em mim. Enquanto o código funciona de forma quase idêntica a nossa versão anterior do ebuild, podemos fazer agora algo que não poderíamos fazer antes -- podemos sobrescrever o user_compile() no sed-3.02.ebuild. Assim, se o user_compile() padrão não atende nossas necessidades, podemos definir um novo em nosso arquivo .ebuild que contém os comandos necessários para compilar o pacote. Por exemplo, aqui temos um ebuild para o e2fsprogs-1.18, que requer uma linha "./configure" um pouco diferente:
#este arquivo ebuild sobrescreve o user_compile() padrão P=e2fsprogs-1.18 A=${P}.tar.gz user_compile() { ./configure --enable-elf-shlibs make }
Agora, o e2fsprogs será compilado exatamente da forma que queremos. Mas, na maioria dos pacotes, poderemos omitir qualquer função user_compile() no arquivo .ebuild, e a função default user_compile() será usada.
Como exatamente o script ebuild sabe qual função user_compile() usar? Isto é, na verdade, bastante simples. No script ebuild, a função padrão user_compile() é definida antes que o arquivo e2fsprogs-1.18.ebuild seja "sourced". Se houver uma função user_compile() no e2fsprogs-1.18.ebuild, ela sobrescreve a versão default definida anteriormente. Se não, a função default user_compile() é usada.
Isto é muito legal, acrescentamos bastante flexibilidade sem precisar de nenhum código complexo desnecessário. Não iremos cobrir isto aqui, mas você pode fazer modificações similares ao ebuild_unpack() de forma que os usuários consigam contornar o processo padrão de desempacotamento. Isto pode ser útil se qualquer patch deve ser aplicado, ou se os arquivos estão contidos em múltiplos arquivos. Também é uma boa idéia modificar nosso código de desempacotamento de forma que ele reconheça tarballs compactados pelo bzip2 por default.
Já cobrimos bastantes técnicas do bash, e agora é hora de cobrirmos mais uma. Geralmente, é útil para um programa ter um arquivo de configurações globais que resida no /etc. Felizmente, isto é fácil de fazer usando o bash. Simplesmente crie o seguinte arquivo e salve-o como /etc/ebuild.conf:
# /etc/ebuild.conf: set system-wide ebuild options in this file # MAKEOPTS are options passed to make MAKEOPTS="-j2"
Neste exemplo, incluímos apenas uma opção de configuração, mas você poderia ter incluído muitas mais. Uma das coisas bonitas sobre o bash é que este arquivo pode ser executado via o "source", de forma muito simples. Este é um truque de projeto que funciona com a maioria das linguagens interpretadas. Depois que /etc/ebuild.conf é "sourced", "$MAKEOPTS" está definida dentro de seu script ebuild. Iremos usar ele para permitir que o usuário passe opções para o make. Normalmente, esta opção pode ser utilizada para permitir ao usuário informar ao ebuild para fazer um make em paralelo.
Para aumentar a velocidade de compilação em máquinas multiprocessadas, o make suporta a compilação em paralelo de programas. Isto significa que, em vez de somente compilar um código fonte por vez, o make compila um número especificado pelo usuário de arquivos fonte simultaneamente (assim aqueles processadores extras em máquinas multiprocessadas são usadas). O make paralelo é habilitado passando a opção -j # para o make, conforme abaixo:
make -j4 MAKE="make -j4"
Este código instrui o make a compilar quatro programas simultaneamente. O argumento MAKE="make -j4" diz ao make para passar a opção -j4 para qualquer processo-filho que o make venha a lançar.
Aqui está a versão final do nosso programa ebuild:
#!/usr/bin/env bash if [ $# -ne 2 ] then echo "Please specify ebuild file and unpack, compile or all" exit 1 fi source /etc/ebuild.conf if [ -z "$DISTDIR" ] then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfiles fi export DISTDIR ebuild_unpack() { #make sure we're in the right directory cd ${ORIGDIR} if [ -d ${WORKDIR} ] then rm -rf ${WORKDIR} fi mkdir ${WORKDIR} cd ${WORKDIR} if [ ! -e ${DISTDIR}/${A} ] then echo "${DISTDIR}/${A} does not exist. Please download first." exit 1 fi tar xzf ${DISTDIR}/${A} echo "Unpacked ${DISTDIR}/${A}." #ource is now correctly unpacked } user_compile() { #we're already in ${SRCDIR} if [ -e configure ] then #run configure script if it exists ./configure --prefix=/usr fi #run make make $MAKEOPTS MAKE="make $MAKEOPTS" } ebuild_compile() { if [ ! -d "${SRCDIR}" ] then echo "${SRCDIR} does not exist -- please unpack first." exit 1 fi #make sure we're in the right directory cd ${SRCDIR} user_compile } export ORIGDIR=`pwd` export WORKDIR=${ORIGDIR}/work if [ -e "$1" ] then source $1 else echo Ebuild file $1 not found." exit 1 fi export SRCDIR=${WORKDIR}/${P} case "${2}" in unpack) ebuild_unpack ;; compile) ebuild_compile ;; all) ebuild_unpack ebuild_compile ;; *) echo "Please specify unpack, compile or all as the second arg" exit 1 ;; esac
Note que o "sourcing" de /etc/ebuild.conf é feito perto do início do arquivo. Note também que usamos "$MAKEOPTS" em nossa função user_compile() default.Você pode estar se perguntando como isto irá funcionar -- afinal de contas, nós estamos nos referindo a "$MAKEOPTS" antes de fazer o "source" em /etc/ebuild.conf, que é o que define "$MAKEOPTS" em princípio. Para nossa sorte, isto estáOK, por que a expansão de variáveis somente acontece quando user_compile() é executado. Na hora que user_compile() é executado, /etc/ebuild.conf já foi "sourced", e "$MAKEOPTS" está configurada com o valor correto.
Nós cobrimos muitas técnicas de programação bash neste artigo, mas somente tocamos na superfície do poder do bash. Por exemplo, o sistema ebuild em produção no Gentoo Linux não somente desempacota e compila cada pacote, mas ele também:
Além disto, o sistema ebuild em produção possui várias outras opções de configuração global, permitindo que o usuário especifique opções como quais as flags de otimização utilizadas durante a compilação, e se o suporte opcional a pacotes como o GNOME e o slang devem ser habilitados por padrão nos pacotes que suportam esta opção.
Está claro que o bash pode realizar muito mais do que o que foi tocado nesta série de artigos. Espero que você tenha aprendido bastante sobre esta incrível ferramenta, e está excitado sobre o uso do bash para tornar mais rápido e melhorar seus projetos de desenvolvimento.
Residindo em Albuquerque, Novo México, Daniel Robbins é o Chief Architect do Gentoo Project, CEO da Gentoo Technologies, Inc., o mentor do Linux Advanced Multimedia Project (LAMP), e um autor-contribuinte dos livros da Macmillan Caldera OpenLinux Unleashed, SuSE Linux Unleashed, e Samba Unleashed. Daniel está envolvido com computadores de alguma forma desde o segundo grau, quando ele foi exposto pela primeira vez à linguagem de programação Logo, bem como a uma dose podencialmente perigosa de Pac Man. Isto provavelmente explica por que ele tem servido desde então como Lead Graphic Artist na SONY Eletronic Publishing/Psygnosis. Daniel gosta de passar o tempo com sua esposa, Mary, que está esperando uma criança para esta primavera. Ele pode ser encontrado no email drobbins@gentoo.org.