Um introdução ao make#
Discutimos brevemente o básico do make
. Esse capítulos dá ideias e estratégias de escalas make
para projetos maiores.
Antes de irmos aos detalhes com make
, considere alguns pontos:
make
é uma ferramenta Unix e pode te dar algum trabalho quando portando para plataformas não Unix. Dito isso, há outros sabores demake
disponíveis, mas nem todos podem ter as funcionalidades que você queira usar.make
te dá controle total do processo de geração de build, isso significa que você é responsável por todo o processo, e deverá especificar as regras para cada detalhe do seu projeto. Você poderá se encontrar gastando um tempo significativo escrevendo e mantendo seuMakefile
ao invés de desenvolver seu código fonte.Você pode trabalhar com seu
Makefile
, mas lembre dos outros desenvolvedores no seu projeto que podem não ser familiares commake
. Quanto tempo você espera que eles gastem para aprender seuMakefile
e eles serão capazes de debugar ou adicionar funcionalidades?make
puro não irá escalar. Cedo você adicionará programas auxiliares para gerar dinamicamente ou estaticamente seuMakefile
. Eles introduzem dependências e possíveis fontes de erros. O esforço necessário para testar e documentar essas ferramentas não deve ser subestimada.
Se você acha make
adequado para suas necessidades, então você pode começar escrevendo seu Makefile
. Para esse curso nós iremos usar exemplos do mundo real vindo do pacote de índice, o qual (no momento dessa escrita) usa sistemas de geração de build fora o make
. Esse guia apresenta uma recomendação de estilo geral para escrever make
, mas também serve como demonstração de funcionalidades úteis e interessantes.
Dica
Mesmo se você achar make
inadequado para gerar o build do seu projeto, essa é a ferramenta para automatizar fluxos de trabalho definidos pelos arquivos. Talvez você possa utilizar de seu poder em um contexto diferente.
Começando#
Para essa parte, trabalharemos com o Fortran CSV module (v1.2.0). Nosso objetivo é criar um Makefile
para compilar esse projeto em uma biblioteca estática. Comece clonando o repositório
git clone https://github.com/jacobwilliams/fortran-csv-module -b 1.2.0
cd fortran-csv-module
Dica
Para essa parte trabalharemos com o código da tag 1.2.0
para fazê-lo o mais reprodutível o possível. Sinta-se a vontade de usar a última versão ou outro projeto.
Esse projeto usa FoBiS como sistema de geração de build, e você pode chegar o build.sh
para opções com FoBiS. Nós agora iremos escrever o Makefile
para esse projeto. Primeiro, checamos a estrutura do diretório e os arquivos fonte
.
├── build.sh
├── files
│ ├── test_2_columns.csv
│ └── test.csv
├── fortran-csv-module.md
├── LICENSE
├── README.md
└── src
├── csv_kinds.f90
├── csv_module.F90
├── csv_parameters.f90
├── csv_utilities.f90
└── tests
├── csv_read_test.f90
├── csv_test.f90
└── csv_write_test.f90
Nós encontramos sete arquivos fonte Fortran diferentes; os quatros no src
devem ser compilados e adicionados na biblioteca estática enquanto que os três em src/tests
contém programas individuais que dependem da biblioteca estática.
Comece criando um Makefile
simples:
# Disable the default rules
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# Project name
NAME := csv
# Configuration settings
FC := gfortran
AR := ar rcs
LD := $(FC)
RM := rm -f
# List of all source files
SRCS := src/csv_kinds.f90 \
src/csv_module.F90 \
src/csv_parameters.f90 \
src/csv_utilities.f90
TEST_SRCS := src/tests/csv_read_test.f90 \
src/tests/csv_test.f90 \
src/tests/csv_write_test.f90
# Create lists of the build artefacts in this project
OBJS := $(addsuffix .o, $(SRCS))
TEST_OBJS := $(addsuffix .o, $(TEST_SRCS))
LIB := $(patsubst %, lib%.a, $(NAME))
TEST_EXE := $(patsubst %.f90, %.exe, $(TEST_SRCS))
# Declare all public targets
.PHONY: all clean
all: $(LIB) $(TEST_EXE)
# Create the static library from the object files
$(LIB): $(OBJS)
$(AR) $@ $^
# Link the test executables
$(TEST_EXE): %.exe: %.f90.o $(LIB)
$(LD) -o $@ $^
# Create object files from Fortran source
$(OBJS) $(TEST_OBJS): %.o: %
$(FC) -c -o $@ $<
# Define all module interdependencies
csv_kinds.mod := src/csv_kinds.f90.o
csv_module.mod := src/csv_module.F90.o
csv_parameters.mod := src/csv_parameters.f90.o
csv_utilities.mod := src/csv_utilities.f90.o
src/csv_module.F90.o: $(csv_utilities.mod)
src/csv_module.F90.o: $(csv_kinds.mod)
src/csv_module.F90.o: $(csv_parameters.mod)
src/csv_parameters.f90.o: $(csv_kinds.mod)
src/csv_utilities.f90.o: $(csv_kinds.mod)
src/csv_utilities.f90.o: $(csv_parameters.mod)
src/tests/csv_read_test.f90.o: $(csv_module.mod)
src/tests/csv_test.f90.o: $(csv_module.mod)
src/tests/csv_write_test.f90.o: $(csv_module.mod)
# Cleanup, filter to avoid removing source code by accident
clean:
$(RM) $(filter %.o, $(OBJS) $(TEST_OBJS)) $(filter %.exe, $(TEST_EXE)) $(LIB) $(wildcard *.mod)
Invocando make
deve construir a biblioteca estática e os executáveis dos testes como esperado:
gfortran -c -o src/csv_kinds.f90.o src/csv_kinds.f90
gfortran -c -o src/csv_parameters.f90.o src/csv_parameters.f90
gfortran -c -o src/csv_utilities.f90.o src/csv_utilities.f90
gfortran -c -o src/csv_module.F90.o src/csv_module.F90
ar rcs libcsv.a src/csv_kinds.f90.o src/csv_module.F90.o src/csv_parameters.f90.o src/csv_utilities.f90.o
gfortran -c -o src/tests/csv_read_test.f90.o src/tests/csv_read_test.f90
gfortran -o src/tests/csv_read_test.exe src/tests/csv_read_test.f90.o libcsv.a
gfortran -c -o src/tests/csv_test.f90.o src/tests/csv_test.f90
gfortran -o src/tests/csv_test.exe src/tests/csv_test.f90.o libcsv.a
gfortran -c -o src/tests/csv_write_test.f90.o src/tests/csv_write_test.f90
gfortran -o src/tests/csv_write_test.exe src/tests/csv_write_test.f90.o libcsv.a
Há algumas coisas para notar aqui, o build make
geralmente interlaça os artefatos do build e o código fonte, a não ser que você implemente o diretório de build. Além disso, os arquivos fonte e dependências são especificadas explicitamente, o que resulta em diversas linhas adicionais mesmo para tal projeto simples.
Dependências geradas automaticamente#
A principal desvantagem do make
para Fortran é a falta da capacidade de determinar dependências de módulo. Isso é geralmente resolvido seja adicionando à mão ou escaneando automaticamente o código fonte com uma ferramenta externa. Alguns compiladores (como o compilador Intel Fortran) também oferece a geração de dependências no formato make
.
Antes de aprofundar na geração de dependências, vamos delinear o conceito de uma medida robusta para o problema de dependência. Primeiro, queremos uma abordagem que consiga processar todos os arquivos fonte independentemente, enquanto cada arquivo fonte fornece (module
) ou requer (use
) módulos. Quando gerando as dependências apenas o nome do arquivo fonte e os arquivos módulo são conhecidos, e nenhuma informação dos nomes dos arquivos objeto deve ser necessária.
Se você checar a seção de dependência acima você notará que todas as dependências são definidas entre arquivos objeto ao invés de arquivos fonte. Para mudar isso, podemos gerar uma relação dos arquivos fonte com seus respectivos arquivos objeto:
# Define a map from each file name to its object file
obj = $(src).o
$(foreach src, $(SRCS) $(TEST_SRCS), $(eval $(src) := $(obj)))
Note a declaração do obj
como uma variável recursivamente expandida, efetivamente usamos esse mecanismo para definir a função em make
. A função foreach
nos permite fazer um laço em todos os arquivos fonte, enquanto que a função eval
nos permite gerar as declarações make
e avalia-las para esse Makefile
.
Ajustamos as dependências de acordo como definimos o nome dos arquivos objeto através dos nomes dos arquivos fonte:
# Define all module interdependencies
csv_kinds.mod := $(src/csv_kinds.f90)
csv_module.mod := $(src/csv_module.F90)
csv_parameters.mod := $(src/csv_parameters.f90)
csv_utilities.mod := $(src/csv_utilities.f90)
$(src/csv_module.F90): $(csv_utilities.mod)
$(src/csv_module.F90): $(csv_kinds.mod)
$(src/csv_module.F90): $(csv_parameters.mod)
$(src/csv_parameters.f90): $(csv_kinds.mod)
$(src/csv_utilities.f90): $(csv_kinds.mod)
$(src/csv_utilities.f90): $(csv_parameters.mod)
$(src/tests/csv_read_test.f90): $(csv_module.mod)
$(src/tests/csv_test.f90): $(csv_module.mod)
$(src/tests/csv_write_test.f90): $(csv_module.mod)
A mesma estratégia de criar uma relação já é usada para os arquivos módulo, agora é apenas expandida para os arquivos objeto também.
Para gerar a respectiva relação de dependências automaticamente usaremos um script awk
aqui
#!/usr/bin/awk -f
BEGIN {
# Fortran is case insensitive, disable case sensitivity for matching
IGNORECASE = 1
}
# Match a module statement
# - the first argument ($1) should be the whole word module
# - the second argument ($2) should be a valid module name
$1 ~ /^module$/ &&
$2 ~ /^[a-zA-Z][a-zA-Z0-9_]*$/ {
# count module names per file to avoid having modules twice in our list
if (modc[FILENAME,$2]++ == 0) {
# add to the module list, the generated module name is expected
# to be lowercase, the FILENAME is the current source file
mod[++im] = sprintf("%s.mod = $(%s)", tolower($2), FILENAME)
}
}
# Match a use statement
# - the first argument ($1) should be the whole word use
# - the second argument ($2) should be a valid module name
$1 ~ /^use$/ &&
$2 ~ /^[a-zA-Z][a-zA-Z0-9_]*,?$/ {
# Remove a trailing comma from an optional only statement
gsub(/,/, "", $2)
# count used module names per file to avoid using modules twice in our list
if (usec[FILENAME,$2]++ == 0) {
# add to the used modules, the generated module name is expected
# to be lowercase, the FILENAME is the current source file
use[++iu] = sprintf("$(%s) += $(%s.mod)", FILENAME, tolower($2))
}
}
# Match an include statement
# - the first argument ($1) should be the whole word include
# - the second argument ($2) can be everything, as long as delimited by quotes
$1 ~ /^(#:?)?include$/ &&
$2 ~ /^["'].+["']$/ {
# Remove quotes from the included file name
gsub(/'|"/, "", $2)
# count included files per file to avoid having duplicates in our list
if (incc[FILENAME,$2]++ == 0) {
# Add the included file to our list, this might be case-sensitive
inc[++ii] = sprintf("$(%s) += %s", FILENAME, $2)
}
}
# Finally, produce the output for make, loop over all modules, use statements
# and include statements, empty lists are ignored in awk
END {
for (i in mod) print mod[i]
for (i in use) print use[i]
for (i in inc) print inc[i]
}
Esse script tem algumas premissas sobre o código fonte analisado, então não funcionará para todo código Fortran (declaradamente submódulos não são suportados), mas para esse exemplo bastará.
Dica
Usando awk
O script acima usa a linguagem awk
, a qual é projetada com o propósito de processar fluxos de texto e usa uma síntaxe C-like. Em awk
você pode definir grupos que são avaliados em certos eventos, p.ex. quando uma linha corresponde a um padrão específico, geralmente espressada por uma expressão regular.
O script awk
define cinco grupos, dois usam o padrão especial BEGIN
e END
os quais rodam antes do script iniciar e depois do script finalizar, respectivamente. Antes do script iniciar fazemos o script insensitivo à caixa já que estamos lidando com código fonte Fortran aqui. Também usamos a variável especial FILENAME
para determinar qual arquivo nós estamos avaliado atualmente e permitir processar múltiplos arquivos de uma vez.
Com os três padrões definidos buscaremos as declarações module
, use
e include
como as primeiras entradas delimitadas por espaço. Com o padrão usado nem todo código Fortran válido será avaliada corretamente. Um exemplo de falha seria:
use::my_module,only:proc
Para faze-lo avaliável pelo script awk
adicionaremos outro grupo diretamente depois do grupo BEGIN
, modificando o fluxo enquanto processa ele com
{
gsub(/,|:/, " ")
}
Na teoria você precisaria de um avaliador Fortran completo para lidar com as linhas de continuação e outras dificuldades. Isso pode ser possível de implementar no awk
mas iria requerer um script enorme no final.
Além disso, tenha em mente que gerar as dependências deve ser rápido, um avaliador caro pode produzir uma despesa adicional quando gerando dependências para uma base de código grande. Tomar premissas razoáveis pode simplificar e acelerar esse passo, mas também introduzir fontes de erro nas suas ferramentas de geração de build.
Faça o script executável (chmod +x gen-deps.awk
) e teste-o com .gen-deps.awk $(find src -name
*.[fF]90)
. Você deve ver uma saída como isto:
csv_utilities.mod = $(src/csv_utilities.f90)
csv_kinds.mod = $(src/csv_kinds.f90)
csv_parameters.mod = $(src/csv_parameters.f90)
csv_module.mod = $(src/csv_module.F90)
$(src/csv_utilities.f90) += $(csv_kinds.mod)
$(src/csv_utilities.f90) += $(csv_parameters.mod)
$(src/csv_kinds.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_read_test.f90) += $(csv_module.mod)
$(src/tests/csv_read_test.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_write_test.f90) += $(csv_module.mod)
$(src/tests/csv_write_test.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_test.f90) += $(csv_module.mod)
$(src/tests/csv_test.f90) += $(iso_fortran_env.mod)
$(src/csv_parameters.f90) += $(csv_kinds.mod)
$(src/csv_module.F90) += $(csv_utilities.mod)
$(src/csv_module.F90) += $(csv_kinds.mod)
$(src/csv_module.F90) += $(csv_parameters.mod)
$(src/csv_module.F90) += $(iso_fortran_env.mod)
No que a saída do script usará variáveis recursivamente expandidas e não define nenhuma dependência ainda, por causa que a declaração de variáveis fora de ordem pode ser necessária e não queremos que crie qualquer coisa por acidente. Você pode verificar que a mesma informação como no snippet escrito acima é presente. A única exceção é a dependência adicional no iso_fortran_env.mod
, já que é uma variável indefinida e irá apenas expandir em um string vazia e não introduzirá qualquer dependência.
Agora, você pode finalmente incluir este pedaço no seu Makefile
para automatizar a geração de dependência:
# Disable the default rules
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# Project name
NAME := csv
# Configuration settings
FC := gfortran
AR := ar rcs
LD := $(FC)
RM := rm -f
GD := ./gen-deps.awk
# List of all source files
SRCS := src/csv_kinds.f90 \
src/csv_module.F90 \
src/csv_parameters.f90 \
src/csv_utilities.f90
TEST_SRCS := src/tests/csv_read_test.f90 \
src/tests/csv_test.f90 \
src/tests/csv_write_test.f90
# Add source and tests directories to search paths
vpath % .: src
vpath % .: src/tests
# Define a map from each file name to its object file
obj = $(src).o
$(foreach src, $(SRCS) $(TEST_SRCS), $(eval $(src) := $(obj)))
# Create lists of the build artefacts in this project
OBJS := $(addsuffix .o, $(SRCS))
DEPS := $(addsuffix .d, $(SRCS))
TEST_OBJS := $(addsuffix .o, $(TEST_SRCS))
TEST_DEPS := $(addsuffix .d, $(TEST_SRCS))
LIB := $(patsubst %, lib%.a, $(NAME))
TEST_EXE := $(patsubst %.f90, %.exe, $(TEST_SRCS))
# Declare all public targets
.PHONY: all clean
all: $(LIB) $(TEST_EXE)
# Create the static library from the object files
$(LIB): $(OBJS)
$(AR) $@ $^
# Link the test executables
$(TEST_EXE): %.exe: %.f90.o $(LIB)
$(LD) -o $@ $^
# Create object files from Fortran source
$(OBJS) $(TEST_OBJS): %.o: % | %.d
$(FC) -c -o $@ $<
# Process the Fortran source for module dependencies
$(DEPS) $(TEST_DEPS): %.d: %
$(GD) $< > $@
# Define all module interdependencies
include $(DEPS) $(TEST_DEPS)
$(foreach dep, $(OBJS) $(TEST_OBJS), $(eval $(dep): $($(dep))))
# Cleanup, filter to avoid removing source code by accident
clean:
$(RM) $(filter %.o, $(OBJS) $(TEST_OBJS)) $(filter %.d, $(DEPS) $(TEST_DEPS)) $(filter %.exe, $(TEST_EXE)) $(LIB) $(wildcard *.mod)
Aqui arquivos de dependência adicionais são gerados para cada arquivo fonte individualmente e então incluídos no Makefile
principal. Al[em disso, os arquivos de dependência são adicionados como dependência para os arquivos objeto para garantir que serão gerados antes do objeto ser compilado. O carácter pipe nas dependências define a ordem de regras sem uma dependência temporal, porque não é necessário recompilar um arquivo de objeto no caso de dependências serem gerem e potencialmente não modificadas.
De novo, faremos uso da função eval
para gerar as dependências em um laço foreach
em todos os arquivos objeto. Note que criamos uma relação entre os arquivos objeto e os arquivos de dependência, expandindo dep
uma vez rende o nome do arquivo objeto, expandir novamente rende os arquivos de objeto que ele depende.
Construindo seu projeto com make
deve dar uma saída similar a
./gen-deps.awk src/csv_utilities.f90 > src/csv_utilities.f90.d
./gen-deps.awk src/csv_parameters.f90 > src/csv_parameters.f90.d
./gen-deps.awk src/csv_module.F90 > src/csv_module.F90.d
./gen-deps.awk src/csv_kinds.f90 > src/csv_kinds.f90.d
gfortran -c -o src/csv_kinds.f90.o src/csv_kinds.f90
gfortran -c -o src/csv_parameters.f90.o src/csv_parameters.f90
gfortran -c -o src/csv_utilities.f90.o src/csv_utilities.f90
gfortran -c -o src/csv_module.F90.o src/csv_module.F90
ar rcs libcsv.a src/csv_kinds.f90.o src/csv_module.F90.o src/csv_parameters.f90.o src/csv_utilities.f90.o
./gen-deps.awk src/tests/csv_read_test.f90 > src/tests/csv_read_test.f90.d
gfortran -c -o src/tests/csv_read_test.f90.o src/tests/csv_read_test.f90
gfortran -o src/tests/csv_read_test.exe src/tests/csv_read_test.f90.o libcsv.a
./gen-deps.awk src/tests/csv_test.f90 > src/tests/csv_test.f90.d
gfortran -c -o src/tests/csv_test.f90.o src/tests/csv_test.f90
gfortran -o src/tests/csv_test.exe src/tests/csv_test.f90.o libcsv.a
./gen-deps.awk src/tests/csv_write_test.f90 > src/tests/csv_write_test.f90.d
gfortran -c -o src/tests/csv_write_test.f90.o src/tests/csv_write_test.f90
gfortran -o src/tests/csv_write_test.exe src/tests/csv_write_test.f90.o libcsv.a
Uma vez que os arquivos de dependência são gerados, make
irá apenas atualizá-los se a fonte mudar e não requer regerar o build novamente para cada invocação.
Dica
Com dependências corretas você pode alavancar a execução paralela no seu Makefile
, apenas use a flag -j
para criar múltiplos processos make
.
Já que dependências podem ser geradas automaticamente, não precisa especificar os arquivos fonte explicitamente, a função wildcard
pode ser usada para determiná-los dinamicamente:
# List of all source files
SRCS := $(wildcard src/*.f90) \
$(wildcard src/*.F90)
TEST_SRCS := $(wildcard src/tests/*.f90)