Ferramentas de compilação#

Compilar seus projetos Fortran à mão pode ser um tanto complicado dependendo do número de arquivos fonte e as interdependências dentro do módulo. Suportar diferentes compiladores e linkers ou diferentes plataformas pode se tornar cada vez mais complicador a não ser que use ferramentas adequadas para automatizar essas tarefas.

Dependendo do tamanho do seu projeto e o propósito do projeto, diferentes opções para automatização de build podem ser usados.

Primeiro, um ambiente de desenvolvimento integrado provavelmente fornece um meio de realizar o build do seu programa. Uma ferramenta popular de múltiplas plataformas é o Microsoft Visual Studio Code, mas outros existem, tais como Atom, Eclipse Photran, e Code::Blocks. Eles oferecem uma interface gráfica, mas muitas vezes específicas para compilador e plataforma.

Para projetos menos, o sistema de geração de build baseada em regras make é a escolha comum. Baseadas em regras definidas, ele pode performar tarefas tais como (re)compilar arquivos de objetos a partir de arquivos fonte atualizados, criar bibliotecas e linkar executáveis. Para usar make no seu projeto é necessário que escreva as regras em um Makefile, o qual define as interdependências de todo o programa final, os arquivos objeto intermediários ou bibliotecas e os arquivos fonte atuais. Para um pequena introdução veja o guia para make.

Ferramentas de manutenção como autotools e CMake podem gerar Makefiles ou arquivos de projeto Visual Studio a partir de uma descrição de alto nível. Eles abstraem o compilador e plataformas específicas.

Qual dessas ferramentas é a melhor escolha para seus projetos depende de muitos fatores. Escolha uma ferramenta de build que você fique confortável trabalhando com ela, e que não te atrapalhe durante o desenvolvimento. Gastar muito tempo trabalhando nas ferramentas de build ao invés de trabalhar no desenvolvimento de fato pode rapidamente se tornar frustrante.

Além disso, considere a acessibilidade das suas ferramentas de build. Se é restrita a um ambiente de desenvolvimento integrado específico, todos os desenvolvedores poderão acessá-la? Se você estiver usando um sistema de geração de build específico, ela funciona em todas as plataformas que você está desenvolvendo? Qual grande é a barreira de entrada das ferramentas de build? Considere a curva de aprendizado para as ferramentas de build, a ferramenta de build perfeita não terá uso, se você tiver que aprender uma linguagem de programação complexa para adicionar um arquivo fonte novo. Finalmente, considere o que outros projetos estão usando, aquelas que você depende e aqueles que você usa (ou usará) em seu projeto como dependência.

Usando make como ferramenta de geração de build#

O sistema de geração de build mais conhecido e comumente usado é o make. Ele realiza ações seguindo regras em um arquivo de configuração chamado Makefile ou makefile, o qual geralmente leva à compilação do programa a partir de um código fonte fornecido.

Dica

Para um tutorial mais aprofundado em make olhe a sua página de informação. Há uma versão online da sua página de informações disponível.

Nós iremos começa com o básico a partir do seu diretório fonte limpo. Crie e abra um arquivo Makefile, nós começaremos com uma regra simples chamada all:

all:
	echo "$@"

Após salvar o Makefile execute-o com o comando make no mesmo diretório. Você deverá ver a seguinte saída:

echo "all"
all

Primeiro, nós notamos que make está substituindo $@ pelo nome da regra, a segunda coisa a notar é que make sempre está escrevendo o comando que está executando, finalmente, nós vemos o resultado de executar echo "all".

Nota

Nós sempre chamamos o ponto de entrada de nosso Makefile all por convenção, mas você pode escolher qualquer nome que quiser.

Nota

Você pode não ter notado se seu editor está funcionando corretamente, mas você tem que indentar o conteúdo de uma regra com tab. Em caso de você ter problemas executando o Makefile e ver um erro do tipo

Makefile:2: *** missing separator.  Stop.

A indentação provavelmente não está correta. Nesse caso, substitua a indentação na segunda linha por um tab.

Agora nós queremos nossas regras mais complicadas, portanto adicionamos outra regra:

PROG := my_prog

all: $(PROG)
	echo "$@ depends on $^"

$(PROG):
	echo "$@"

Note como declaramos as variáveis em make, você deve sempre declarar suas variáveis locais com :=. Para acessar o conteúdo de uma variável nós usamos o $(...), note que nós temos que fechar o nome da variável dentro de parênteses.

Nota

A declaração das variáveis é geralmente feita com :=, mas make suporta variáveis recursively expanded assim como com =. Normalmente, o primeiro tipo de declaração é exigido, já que são mais previsíveis e não precisam de um overhead de execução em expansão recursiva.

Nós introduzimos uma dependência na regra all, a saber o conteúdo da variável PROG, também modificamos a impressão, nós queremos ver todas as dependências dessa regra, as quais estão armazenadas na variável $^. Agora para a nova regra que nomeamos com o valor da variável PROG, ela fará a mesma coisa que fizemos anteriormente para a regra all, note como o valor de $@ é dependente da regra à qual é utilizada.

De novo executando o make, você deverá ver:

echo "my_prog"
my_prog
echo "all depends on my_prog"
all depends on my_prog

A dependência foi corretamente solucionada e avaliada antes de executar qualquer ação na regra all. Vamos executar a segunda regra: escreva make my_prog e você verá apenas as duas primeiras linhas no seu terminal.

O próximo passo é executar algumas ações real com make, nós usamos o código fonte do capítulo anterior aqui e adicionamos novas regras ao nosso Makefile:

OBJS := tabulate.o functions.o
PROG := my_prog

all: $(PROG)

$(PROG): $(OBJS)
	gfortran -o $@ $^

$(OBJS): %.o: %.f90
	gfortran -c -o $@ $<

Nós definimos OBJS para arquivos de objeto, nosso programa depende desses OBJS e para cada arquivo de objeto nós criamos a regra para construí-los a partir do código fonte. A última regra que introduzimos é uma regra de pattern matching, % é um padrão comum entre tabulate.o e tabulate.f90, o qual conecta nosso arquivo de objeto tabulate.o ao código fonte tabulate.f90. Com esse conjunto, nós executamos nosso compilador, aqui gfortran e traduzimos o arquivo fonte no arquivo de objeto, nós não criamos um executável ainda devido a flag -c. Note o uso de $< para o primeiro elemento nas dependências aqui.

Após compilar todos os arquivos de objeto nós tentaremos linkar o programa, nós não usamos um linker diretamente, mas o gfortran para criar o executável.

Agora nós executamos o script de build com make:

gfortran -c -o tabulate.o tabulate.f90
tabulate.f90:2:7:

    2 |    use user_functions
      |       1
Fatal Error: Cannot open module file ‘user_functions.mod’ for reading at (1): No such file or directory
compilation terminated.
make: *** [Makefile:10: tabulate.f90.o] Error 1

Lembramos que temos dependências entre nossos arquivos fonte, portando nós adicionamos essas dependências explicitamente no Makefile com

tabulate.o: functions.o

Agora nós tentamos novamente e descobrimos que o build ocorre corretamente. A saída deve ser parecida com

gfortran -c -o functions.o functions.f90
gfortran -c -o tabulate.o tabulate.f90
gfortran -o my_prog tabulate.o functions.o

Você deve encontrar quatro novos arquivos no seu diretório agora. Execute my_prog para garantir que tudo funciona como esperado. Agora execute make de novo:

make: Nothing to be done for 'all'.

Usando os timestamps do executável make foi possível determinar que é mais novo que o tabulate.o e functions.o, os quais são mais novos que tabulate.f90 e functions.f90. Portanto, o programa está atualizado com o último código e nenhuma ação é necessária.

Por fim, nós veremos o Makefile completo.

# Disable all of make's built-in rules (similar to Fortran's implicit none)
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# configuration
FC := gfortran
LD := $(FC)
RM := rm -f
# list of all source files
SRCS := tabulate.f90 functions.f90
PROG := my_prog

OBJS := $(addsuffix .o, $(SRCS))

.PHONY: all clean
all: $(PROG)

$(PROG): $(OBJS)
	$(LD) -o $@ $^

$(OBJS): %.o: %
	$(FC) -c -o $@ $<

# define dependencies between object files
tabulate.f90.o: functions.f90.o user_functions.mod

# rebuild all object files in case this Makefile changes
$(OBJS): $(MAKEFILE_LIST)

clean:
	$(RM) $(filter %.o, $(OBJS)) $(wildcard *.mod) $(PROG)

Já que você está começando com make recomendamos fortemente que você a primeira linha, como no implicit none do Fortran não queremos regras implícitas bagunçando nosso Makefile de formas surpreendentes e prejudiciais.

Em seguida, nós temos uma seção de configuração onde nós definimos variáveis, em caso de você querer mudar o seu compilador, é fácil realizar aqui. Também introduzimos a variável SRCS que contem todos os arquivos fonte, o qual é mais intuitivo que especificar os arquivos de objeto. Podemos facilmente criar os arquivos de objeto ao anexar o sufixo .o usando a função addsuffix. O .PHONY é uma regra especial, o qual deve ser usada para todos os pontos de entrada no seu Makefile, aqui definimos dois pontos de entrada, já sabemos o all, e a nova regra clean a qual deleta todos os artefatos de build novamente para começarmos com um diretório limpo.

Além disso, nós mudamos um pouco a regra de build para os arquivos de objeto para garantir a inserção do sufixo .o ao invés de substitui-lo. Perceba que nós ainda precisamos definir explicitamente as interdependências no Makefile. Nós também precisamos adicionar uma dependência para os arquivo de objeto no próprio Makefile, em caso de mudar o compilador, isso permitirá o rebuild com segurança.

Agora você sabe o suficiente sobre make para usá-lo na construção de pequenos projetos. Caso você planeje usar make de maneira mais extensiva, nós compilamos algumas dicas para você.

Dica

Nesse guia, evitamos e desabilitamos várias funcionalidades make comuns usadas que podem ser particularmente problemáticas se não usadas corretamente, nós recomendamos fortemente que se mantenha longe de regras internas e variáveis se você não se sentir confiante ao trabalhar com make, mas explicitamente declarar todas as variáveis e regras.

Você descobrirá que o make é uma ferramenta capacitada para automatizar curtos fluxos de trabalho interdependentes e para realizar o build de pequenos projetos. Para projetos grandes, você provavelmente irá se encontrar com algumas limitações. Geralmente, make não é usada sozinha mas combinada com outras ferramentas para gerar o Makefile completamente ou em partes.

Variáveis expandidas recursivamente#

Comumente visto em vários projetos são variáveis expandidas recursivamente (declaradas com = ao invés de :=). Expansão recursiva de suas variáveis permite declarações fora de ordem e outros truques legais com make, já que eles são definidas como regras, os quais são expandidas em tempo de execução, ao invés de serem definidas durante interpretação.

Por exemplo, declarando e usando suas flags Fortran com esse trecho de código irá funcionar completamente bem:

all:
	echo $(FFLAGS)

FFLAGS = $(include_dirs) -O
include_dirs += -I./include
include_dirs += -I/opt/some_dep/include

Você deve ver a saída esperada (ou talvez inesperada) após usar o comando make

echo -I./include -I/opt/some_dep/include -O
-I./include -I/opt/some_dep/include -O

Dica

adicionar com += a uma variável indefinida irá produzir uma variável recursivamente expandida com esse estado sendo herdado para todas as futuras adições.

Enquanto parece uma funcionalidade interessante de usar, ele leva a resultados surpreendes e inesperados. Geralmente, quando definindo variáveis como o seu compilador, há pouca razão para se utilizar a expansão recursiva.

O mesmo pode ser facilmente obtido usando a declaração :=:

all:
	echo $(FFLAGS)

include_dirs := -I./include
include_dirs += -I/opt/some_dep/include
FFLAGS := $(include_dirs) -O

Importante

sempre pense no Makefile com um grande conjunto de regras, ele sempre deve ser interpretado completamente antes de qualquer regra ser avaliada.

Você sempre pode usar qualquer tipo de variável que preferir, mesclá-las deve ser feito com parcimônia, claro. É importante estar atento às diferenças entre dois tipos e as respectivas implicações.

Comentários e espaços em branco#

Há algumas advertências com espaços em branco e comentários, os quais podem aparecer de tempos em tempos quando usando make. Primeiro make não conhece qualquer tipo de dado com exceção de strings e o separator padrão é só um espaço. Isso significa que make dará algumas dificuldades para fazer o build de um projetos que tem espaço no nome dos arquivos. Se você encontrar tal caso, renomear o arquivo provavelmente é a solução mais fácil.

Outro problema comum é buscar e encontrar espaços em branco, uma vez introduzido, make irá carregar isso por todo o caminho e isso de fato faz diferença quando comparando strings em make.

Isso pode ser introduzido por comentários como

prefix := /usr  # path to install location
install:
	echo "$(prefix)/lib"

Enquanto o comentário irá ser corretamente removido pelo make, os dois espaços agora são parte do conteúdo da variável. Use o comando make e cheque se esse é de fato o caso:

echo "/usr  /lib"
/usr  /lib

Para resolver esse problema, você pode mover o comentário, ou cortar os espaços em branco com a função strip. Alternativamente, você pode tentar usar join nas strings.

prefix := /usr  # path to install location
install:
	echo "$(strip $(prefix))/lib"
	echo "$(join $(join $(prefix), /), lib)"

Em todo caso, nenhuma dessas soluções fará o Makefile mais legível, portanto, é prudente de ter uma atenção extra à espaços em branco e comentário quando escrevendo e usando make.

O sistema de build meson#

Após ter aprendido o básico do make, o qual podemos chamar de sistema de geração de build de baixo nível, agora introduziremos meson, um sistema de geração de build de alto nível. Enquanto você especifica em um sistema de build de baixo nível para realizar o build do seu programa, você pode usar um sistema de build de alto nível para especificar o que irá para a geração. Um sistema de geração de build de alto nível irá lidar para você como gerar o arquivo de sistema de build para sistemas de geração de build de baixo nível.

Há vários sistemas de geração de build de alto nível disponíveis, mas iremos no meson porque ele é construído para ser particularmente «user friendly». O sistema de geração de build de baixo nível padrão do meson é chamado de ninja.

Vamos olhar no arquivo meson.build completo:

project('my_proj', 'fortran', meson_version: '>=0.49')
executable('my_prog', files('tabulate.f90', 'functions.f90'))

E agora estamos prontos, a próxima etapa é configurar o nosso sistema de geração de build de baixo nível com meson setup build, você deverá ver uma saída parecida com isso

The Meson build system
Version: 0.53.2
Source dir: /home/awvwgk/Examples
Build dir: /home/awvwgk/Examples/build
Build type: native build
Project name: my_proj
Project version: undefined
Fortran compiler for the host machine: gfortran (gcc 9.2.1 "GNU Fortran (Arch Linux 9.2.1+20200130-2) 9.2.1 20200130")
Fortran linker for the host machine: gfortran ld.bfd 2.34
Host machine cpu family: x86_64
Host machine cpu: x86_64
Build targets in project: 1

Found ninja-1.10.0 at /usr/bin/ninja

A informação provida até esse ponto já é mais detalhada do que qualquer coisa que provemos em um Makefile, agora vamos rodar o build com ninja-C build, o qual deve mostrar algo como

[1/4] Compiling Fortran object 'my_prog@exe/functions.f90.o'.
[2/4] Dep hack
[3/4] Compiling Fortran object 'my_prog@exe/tabulate.f90.o'.
[4/4] Linking target my_prog.

Encontre e teste seu programa em build/my_prog para garantir que funciona corretamente. Notamos que os passos que ninja realizou são os mesmos que programaríamos em um Makefile (incluindo a dependência), ainda assim nós não tivemos que especificá-los, olhe o arquivo meson.build novamente:

project('my_proj', 'fortran', meson_version: '>=0.49')
executable('my_prog', files('tabulate.f90', 'functions.f90'))

Nós especificamos somente que temos um projeto Fortran (que por sua vez requer uma certa versão do meson para suporte à Fortran) e dissemos ao meson para realizar o build do executável my_prog dos arquivos tabulate.f90 e functions.f90. Não dissemos ao meson como realizar o build do projeto, ele descobriu por si só.

Nota

meson é um sistema de geração de build cross plataforma, o projeto que você especificou para o seu programa pode ser usado para compilar binários para o seu sistema operacional nativo ou cross compilar seu projeto para outras plataformas. Semelhante, o arquivo meson.build é portável e irá funcionar também em diferentes plataformas.

A documentação do meson pode ser encontrada em meson-build webpage.

Criando um projeto CMake#

Semelhante ao meson, CMake é também um sistema de geração de build de alto nível e comumente usado para realizar o build de projetos Fortran.

Nota

CMake segue uma estratégia levemente diferente e te fornece uma linguagem de programação completa para criar seus arquivos de build. Isso tem a vantagem de que você pode fazer quase tudo com CMake, mas seus arquivos de build CMake poderá ficar tão complexo quanto o programa que você está construindo.

Comece criando um arquivo CMakeLists.txt com o conteúdo

cmake_minimum_required(VERSION 3.7)
project("my_proj" LANGUAGES "Fortran")
add_executable("my_prog" "tabulate.f90" "functions.f90")

Semelhante ao meson nós já terminamos com nosso arquivo de build CMake. Configuramos os arquivos de geração de build de baixo nível com cmake -B build -G Ninja, você deverá ver uma saída semelhante a isto

-- The Fortran compiler identification is GNU 10.2.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
-- Check for working Fortran compiler: /usr/bin/f95 - skipped
-- Checking whether /usr/bin/f95 supports Fortran 90
-- Checking whether /usr/bin/f95 supports Fortran 90 - yes
-- Configuring done
-- Generating done
-- Build files have been written to: /home/awvwgk/Examples/build

você pode ficar surpreso que o CMake tenta usar o compilador f95, por sorte isso é só um link simbólico para gfortran na maioria dos sistemas e não o compilador f95 de fato. Para fazer o CMake dar melhor retorno você pode exportar a variável de ambiente FC=gfortran rodar novamente deverá mostrar o nome correto do compilador agora

-- The Fortran compiler identification is GNU 10.2.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
-- Check for working Fortran compiler: /usr/bin/gfortran - skipped
-- Checking whether /usr/bin/gfortran supports Fortran 90
-- Checking whether /usr/bin/gfortran supports Fortran 90 - yes
-- Configuring done
-- Generating done
-- Build files have been written to: /home/awvwgk/Example/build

De maneira similar, você pode usar o compilador Intel Fortran para realizar o build do seu projeto (use FC=ifort).

CMake dá suporte para diversos arquivos de geração de build de baixo nível, já que o padrão é específica de plataforma, iremos usar ninja já que já usamos com o meson. Como antes, realize o build do seu projeto com ninja -C build:

[1/6] Building Fortran preprocessed CMakeFiles/my_prog.dir/functions.f90-pp.f90
[2/6] Building Fortran preprocessed CMakeFiles/my_prog.dir/tabulate.f90-pp.f90
[3/6] Generating Fortran dyndep file CMakeFiles/my_prog.dir/Fortran.dd
[4/6] Building Fortran object CMakeFiles/my_prog.dir/functions.f90.o
[5/6] Building Fortran object CMakeFiles/my_prog.dir/tabulate.f90.o
[6/6] Linking Fortran executable my_prog

Encontre e teste seu programa em build/my_prog para garantir que está funcionando corretamente. Os passos que ninja executou são um tanto diferentes por que geralmente há mais de um jeito de escrever arquivos de build de baixo nível para realizar as tarefas de gerar o build do projeto. Felizmente, nós não precisamos nos preocupar já que o sistema de geração de build dá conta desses detalhes por nós.

Finalmente, vamos recapitular brevemente nosso CMakeLists.txt completo para especificar nosso projeto:

cmake_minimum_required(VERSION 3.7)
project("my_proj" LANGUAGES "Fortran")
add_executable("my_prog" "tabulate.f90" "functions.f90")

Nós especificamos que nós temos um projeto Fortran e dissemos ao CMake para criar um executável my_prog a partir dos arquivos tabulate.f90 e functions.f90. CMake sabe os detalhes de como gerar o build do executável a partir das fontes especificadas, assim não precisamos nos preocupar sobre o passos no processo de geração de build.

Dica

CMake’s official reference can be found at the CMake webpage. It is organised in manpages, which are also available with your local CMake installation as well using man cmake. While it covers all functionality of CMake, it sometimes covers them only very briefly.

The SCons build system#

SCons is one another high-level cross-platform build system with automatic dependency analysis that supports Fortran projects. SCons configuration files are executed as Python scripts but it could be successfully used without knowledge about Python programming. The using of Python scripting if needed allows to handle build process and file naming in more sophisticated manner.

Nota

SCons doesn’t automatically passes the external environment variables such as PATH therefore it will not find programs and tools that installed into non-standard locations unless are specified or passed via appropriate variables. That guaranties that build isn’t affected by external (especially user’s) environment variables and build is repeatable. The most of such variables and compiler options and flags are required to be configured within special «isolated» Environments (please refer to User Guide for additional information).

The SCons doesn’t use external low-level build systems and relies on own build system. The ninja support as external tool to generate ninja.build file is highly experimental (available since scons 4.2) and required to be enabled explicitly by additional configuration.

The simple SCons project is SConstruct file that contains:

Program('my_proj', ['tabulate.f90', 'functions.f90'])

The next step is to build our project with command scons:

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
gfortran -o functions.o -c functions.f90
gfortran -o tabulate.o -c tabulate.f90
gfortran -o my_proj tabulate.o functions.o
scons: done building targets.

or with scons -Q to disable extended output:

gfortran -o functions.o -c functions.f90
gfortran -o tabulate.o -c tabulate.f90
gfortran -o my_proj tabulate.o functions.o

Find and test your program my_prog at the same directory as source files (by default) to ensure it works correctly.

To cleanup the build artifacts run scons -c (or scons -Qc):

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed functions.o
Removed user_functions.mod
Removed tabulate.o
Removed my_proj
scons: done cleaning targets.

In our SCons SConstruct file

Program('my_proj', ['tabulate.f90', 'functions.f90'])

we specified executable target name my_proj (optional, if omitted the first source file name is used) and list of source files ['tabulate.f90', 'functions.f90']. There is no need to specify project source files language – it’s detected automatically by SCons for supported languages.

The list of source files could be specified with SCons Glob function:

Program('my_proj', Glob('*.f90'))

or using SCons Split function:

Program('my_proj', Split('tabulate.f90 functions.f90'))

or in more readable form by assigning variable:

src_files = Split('tabulate.f90 functions.f90')
Program('my_proj', src_files)

In case of Split function the multiple lines could be used with Python «triple-quote» syntax:

src_files = Split("""tabulate.f90
                     functions.f90""")
Program('my_proj', src_files)

Dica

SCons official webpage provides: User Guide with extended description of various aspects how to handle the build process, Frequently Asked Questions page and man page.