Cmake

Junho 5, 2011

Conforme eu tinha comentado em um post anterior, minha tarefa dessa semana era desenvolver um shader para interfacear com o OSL. A primeira etapa dessa tarefa é permitir a chamada de funções do OSL. Apanhei bastante para fazer isso.

Recentemente o BRL-CAD migrou seu sistema de construção do autotools para o cmake. O pouco que sei sobre cmake, é que ele é um sistema multi-plataforma para compilar um projeto. Você descreve, em um nível mais alto, os CMakeLists.txt em cada diretório do seu projeto. No caso mais básico, executa o comando cmake no diretório raíz e serão gerados os Makefile‘s. Basta então executar make para compilar e make install para instalar.

Eu venho usando o cmake de maneira bem cut and paste no meu mestrado por causa do CGAL. Decidi estudar um pouco mais sobre esse sistema e escrever sobre as principais componentes.

Geração de executável

A principal função presente no cmake é a de gerar executáveis. Dados vários arquivos de código-fonte, por exemplo a.c, b.c e c.c, sendo que o main está em a.c, uma maneira de gerar um executável usando o gcc é o seguinte:

gcc -c b.c c.c
gcc a.c b.o c.o -o executavel

Com o cmake isso simplifica para

add_executable(executavel a.c b.c c.c)

Geração de biblioteca estática

Outra possibilidade é gerar bibliotecas estáticas. Se tivermos funções definidas nos arquivos a.c e b.c e quisermos disponibilizá-las para outros programas utilizarem, podemos fazer:

gcc -c a.c b.c
ar -cvq libab.a a.o b.o

Para que um programa c.c possa usá-la, fazemos a linkagem da seguinte maneira:

gcc c.c -o executavel -L/caminho/para/libab/ -lab

que é equivalente a:

gcc c.c -o executavel /caminho/para/libab/libab.a

No cmake essas tarefas podem ser realizadas pelos seguintes comandos

add_library(libab a.c b.c)
add_executable(executavel c.c)
target_link_libraries(executavel libab)

Uma excelente referência sobre bibliotecas no linux é a do yolinux.com

Dependências

Agora, suponha que queremos linkar o nosso código a uma biblioteca já compilada. Vamos supor que essa biblioteca, chamada libxyz.a, esteja em /home/jose/programa/lib e o cabeçalho com as funções, chamado xyz.h, esteja em /home/jose/programa/include.

Fazendo manualmente ficaríamos com algo do tipo

gcc -I/home/jose/programa/include -L/home/jose/programa/lib -lxyz c.c -o executavel

Uma das maneiras de se encontrar uma biblioteca usando o cmake é da seguinte maneira:

find_library(xyz_library NAMES xyz PATHS /home/jose/programa/lib)

No exemplo acima, xyz_library é um nome qualquer para nos referenciarmos à biblioteca. Em NAMES colocamos o nome da biblioteca sem o prefixo lib e a extensão (por exemplo, libxyz.a vira xyz) em PATHS o caminho para essa biblioteca (PS.: podemos colocar vários caminhos se a priori não sabemos onde a biblioteca libzyx.a está).

Para fazer a ligação basta fazer:

target_link_libraries(executavel ${xyz_library})

Para encontrar o cabeçalho com as funções, usamos

find_path(xyz_include NAMES xyz.h PATHS /home/jose/programa/include)

Para informar ao compilador o caminho de xyz.h, basta fazer

include_directories(${xyz_includes})

Geralmente a busca por bibliotecas externas é modularizada para haver re-uso de código. Para cada biblioteca externa, podemos definir um arquivo com nome descritivo e extensão cmake. Por exemplo, suponha que queremos encontrar a biblioteca Boost. Podemos definir um arquivo FindBOOST.cmake contendo código para encontrar as bibliotecas e os cabeçalhos.

Cada um desses arquivos são postos em um diretório separado, que deverá ser salvo na variável interna do cmake chamada CMAKE_MODULE_PATH. Um modo de fazer isso é:

set(CMAKE_MODULE_PATH "/caminho/para/os/arquivos/Find/")

Para executar os comandos desse arquivo, dentro de qualquer CMakeLists.txt dentro do projeto, basta fazer algo como:

include(FindBOOST)

Sub-diretórios

Em geral cria-se um arquivo CMakeLists.txt por diretório. Para que o cmake processe esses arquivos, os diretórios pais devem informá-lo com o comando add_subdirectories. Se por exemplo quisermos incluir os diretórios sub1 e sub2 filhos do diretório atual, basta incluir as seguintes linhas:

add_subdirectories(sub1)
add_subdirectories(sub2)

Compilação de código

É possível controlar o modo como a compilação é feita com o cmake. Um dos principais meios para se fazer isso é através das variáveis CMAKE_C_FLAGS (para C) e CMAKE_CXX_FLAGS (para C++).

Ao fazer:

set(CMAKE_C_FLAGS -Wall -Werror)

A próxima vez que o um programa C for compilado, essas flags serão adicionadas à compilação. Para verificar isso, uma flag muito interessante a ser setada no comando make, é o VERBOSE. Ao setar essa variável, toda vez que você executar o comando make, ele exibirá os comandos de compilação que estão sendo executados.

make VERBOSE=1

Ele é bastante útil também para “debugar” um arquivo do tipo CMakeLists.txt, para ver se ele está gerando comandos de compilação do jeito que esperamos.

Integração do cmake com o emacs

(Seção adicionada em 6 de Junho de 2011)

Nesse wiki do cmake há dicas para configuração do emacs para colorir corretamente arquivos do tipo CMakeLists.txt e .cmake. Geralmente o emacs vem configurado por padrão, mas em todo caso, basta baixar o arquivo cmake-mode.el e adicionar as seguintes linhas no arquivo de configuração:

; Add cmake listfile names to the mode list.
(setq auto-mode-alist
	  (append
	   '(("CMakeLists\\.txt\\'" . cmake-mode))
	   '(("\\.cmake\\'" . cmake-mode))
	   auto-mode-alist))

(autoload 'cmake-mode "~/Downloads/cmake-mode.el" t)

Troque Downloads pelo local onde você baixou o cmake-mode.el.

Se você usa abas no emacs, ao abrir vários arquivos CMakeLists.txt, eles serão chamados de CMakeLists.txt, CMakeLists.txt<2>, CMakeLists.txt<3> e assim por diante. Isso é bem incômodo, pois você sempre tem que olhar o conteúdo para ver de qual CMakeLists se trata.

Para resolver esse problema, adicione a seguinte rotina no seu arquivo de configurações. Ela adiciona um prefixo contendo o caminho de cada arquivo. Muito útil!

;; uniquify.el is a helper routine to help give buffer names a better unique name.
(when (load "uniquify" 'NOERROR)
  (require 'uniquify)
  (setq uniquify-buffer-name-style 'forward)
  ;(setq uniquify-buffer-name-style 'post-forward)
)

Leitura Complementar

Ao invés de reunir uma série de referências, deixo apenas esse post do stackoverflow para maior abrangência: cmake tutorial

Anúncios

Python como alternativa a Bash Script

Fevereiro 18, 2011

Sempre que precisava automatizar alguma tarefa no linux, apelava para scripts bash. O problema é que escrevo scripts com pouca frequência e somada à sintaxe peculiar dos comandos bash, sempre preciso consultar referências e acabo perdendo bastante tempo.

Pra piorar, algumas tarefas como processamento de strings são complicadas de fazer em bash script e então normalmente usam-se ferramentas auxiliares como sed e awk. A sintaxe delas é bem concisa e elegante, mas acho difícil dominá-las usando-as tão pouco.

Foi por isso que decidi investir um tempo pesquisando modos de substituir bash script por python.

No meu ponto de vista, há um tradeoff entre usar bash script e python. O primeiro faz comunicação direta com o sistema operacional, sendo trivial executar programas, redirecionar entrada/saída ou manipular diretórios, mas é complicado para se fazer processamentos em geral. Já python é o oposto: é bem simples de se programar mas para comunicar com o sistema operacional é necessário usar API’s.

Módulo os

Algumas tarefas de manipulação de diretórios ficam relativamente simples usando o módulo os. Comandos como mkdir, chmod, chown têm suas versões homônimas nesse módulo. Outras comuns mas com nomes ligeiramente diferentes são

* ls — os.listdir(path) — retorna uma lista com os nomes dos arquivos no diretório apontado por path.

* pwd — os.getcwd() — retorna o nome do diretório corrente.

Além dessas, temos também algumas de manipulação de caminhos no módulo os.path. As que eu mais usei até agora,

* os.path.join — Sintaxe: os.path.join(path1[, path2[, …]])

Recebe um conjunto de strings e as concatena em ordem para formar um caminho. Exemplo:

print os.path.join('/home', 'joao/', 'musica')

Vai imprimir

/home/joao/musica

Nota: como está dito no manual, se alguma das strings representar um caminho absoluto, ele ignora todas as strings anteriores. No linux o caminho absoluto é iniciado por /. Por exemplo:

print os.path.join('/home', 'joao/', '/musica')

Vai imprimir

/musica

* os.path.exists — Sintaxe: os.path.exists(path)

Esse comando retorna True se o caminho path existe e False caso contrário.

Nota: esse comando não diferencia arquivos e diretórios. Assim, se existir o diretório foo/ mas você estiver procurando um arquivo chamado foo, o comando mencionado vai retornar True mesmo que o arquivo não exista. Para obter o comportamento esperado, deve-se usar o comando os.path.isfile (ou os.path.isdir na situação oposta).

Módulo subprocess

A criação de processos dentro de scripts python ficava no módulo os, mas está depreciada desde a versão 2.6. Essa tarefa agora está implementada no módulo subprocess.

O principal componente desse módulo é a classe Popen, que recebe vários parâmetros. O primeiro deles é uma lista contendo o nome do programa a ser executado e os argumentos. Depois tem diversos outros, mas os que achei mais importantes são stdin, stderr, stdout.

Exemplo 1 — Executando um processo com argumentos

Imagine um programa chamado ./print que imprime o primeiro e o segundo parâmetro que ele receber. Em C++ pode ser algo como:

#include <iostream>
int main (int argc, char **argv){
    std::cout << argv[1] << " -- " << argv[2] << "\n"; 
}

No bash, se quisermos enviar um argumento para o programa ./print que contenha espaço, devemos pô-lo entre aspas. Por exemplo,

./print "ola mundo" "tudo bem?"

Vai imprimir: ola mundo -- tudo bem?

A vantagem de se usar uma lista de strings é que podemos especificar qual é argumento é qual. Podemos fazer assim:

import subprocess
p = subprocess.Popen(['./print', 'ola mundo',
                      'tudo bem?'])
p.wait()

Exemplo 2 — Processando stdout

No exemplo anterior, a string será impressa na saída padrão (stdout), mas o código python não terá acesso a ela. Se quisermos processar a string impressa por ./print, podemos redirecionar a saída usando pipes, como no bash.

Para fazer isso, usa-se a variável especial subprocess.PIPE. Por exemplo:

import subprocess
import sys
p = subprocess.Popen(['./print', 'oi', 'mundo'],
                     stdout=subprocess.PIPE)
saida = p.communicate()
sys.stdout.write(saida[0].upper())

Nesse caso, o método communicate() serve para fazer a comunicação com o processo. Ele retorna uma lista de dois elementos [stdout, stderr] e também pode receber como argumento a string que será redirecionada para o stdin do processo. No exemplo, só estamos redirecionando o stdout para o objeto, então saida[1] = None, enquanto que saida[0] contém a linha impressa por ./print. Estamos convertendo tudo para caixa alta, então o programa acima imprime:

OI -- MUNDO

Exemplo 3 — Usando stdin e stderr

Para completar, um exemplo onde usamos o stdin e o stdout. Seja ./read um programa que lê um inteiro da entrada padrão e imprima na saída de erro esse número multiplicado por 2, como o código C++ a seguir:

#include <iostream>
int main(){
    int n;
    std::cin >> n;
    std::cerr << 2*n << '\n';
}

Queremos executar esse programa, enviar um número na entrada padrão dele e ler da saída de erro. Podemos fazer isso com o seguinte código:

import subprocess
import sys
p = subprocess.Popen(['./read'],
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE)
sys.stdout.write(p.communicate('2')[1])

Nesse caso redirecionamos o stdin e o stderr. Conforme eu havia dito, o método communicate recebe como parâmetro os dados que serão enviados para a entrada padrão do processo. Além disso, o conteúdo da saída de erro é o segundo elemento da lista retornada.

Exemplo 4 — Redirecionamento para arquivo

Em bash script é comum redirecionarmos saída para arquivos com o operador ‘>’. Para fazer isso usando subprocess, temos que abrir o arquivo explicitamente com open e passar o objeto retornado como parâmetro para o stdout do Popen. Exemplo:

import subprocess
f = open('tmp.txt', 'w')
subprocess.call(['ls'], stdout=f)
f.close()

É comum redirecionarmos o stderr para o /dev/null para que ele não seja impresso no terminal. Se quisermos fazer isso com subprocess, basta abrir o arquivo '/dev/null' e passar o objeto para stderr.

Funções auxiliares

Adicionalmente há algumas funções para simplificar a tarefa dependendo das suas necessidades.

subprocess.call — executa um comando, espera o processo terminar e devolve o código retornado pelo processo. Seria adequado para o Exemplo 1.

subprocess.check_output — Executa um comando e retorna uma string com o conteúdo da saída padrão — só está disponível a partir da versão 2.7. Seria adequado para o Exemplo 2.

Nota: Em geral, eu adiciono permissão de execução para meus scripts bash, por exemplo script.sh, e o executo como ./script.sh. Porém, usando subprocess, tem que executá-lo usando o /bin/bash:

p = subprocess.Popen(['/bin/bash', 'script.sh'])

Conclusão

Com esses módulos mencionados, tenho conseguido me virar usando python como script. Agora só uso bash scripting mesmo quando preciso executar uma lista de comandos sem ter que fazer processamento algum.

Referências

[1] 15.1 – Miscellaneous operating system interfaces
[2] 17.1 – Subprocess management
[3] Stackoverflow — How do I check if a file exists using Python?