# Tentando entender o sistema de montagem do Heirloom
Já faz um certo tempo que postei algo aqui com os artigos ainda não terminados,
mas enfim. Resolvi começar esse blog comentando sobre algo que eu venho
trabalhando há um tempo — desde 2021, o que já seria um “caso sério”? Só que
ao invés de noites em claro por estar empapuçado de amor, são noites em claro
resolvendo bugs como transbordamento de dados, falhas de segmentação e afins
—, esse que é uma bifurcação do Heirloom Toolchest para a nova geração,
também conhecido como “Heirloom New Generation”.
A história está completa no website, então não me estenderei tanto no que se
tange à origem do projeto Heirloom Toolchest original e o porquê dessa
bifurcação.
“Se o projeto é, agora, seu, qual a dificuldade em entender o sistema de montagem?”
Em suma, desde quando comecei até hoje, eu e toda a equipe praticamente trabalhamos apenas na parte em C do projeto, fazendo pouca ou nenhuma alteração no sistema de montagem em si — que é escrito puramente em Makefile (usando do “dialeto”, ou melhor, extensões do GNU) e shell, na boa e velha maneira de se fazer um sistema de montagem — afinal, não havia um porquê de ir mais a fundo e entender além do que já era descrito no README do projeto original desde pelo menos 2004:
The first thing to understand is the build system. This is actually quite simple: Every directory contains a file named Makefile.mk that includes the directory-specific make instructions. To generate the real Makefile, configuration settings are prepended to the directory-specific file. You have to edit these configuration settings before you start compiling; they are located in the file build/mk.config which is also in make syntax. Follow the descriptions in this file and select appropriate values for your system.
Em suma, isto basicamente nos diz que os Makefile
s para cada programa são
gerados a partir da matriz Makefile.mk
, essa específica de cada programa, e
somados (ou contatenados, chame como preferir) ao arquivo de configuração
mk.config
, presente na pasta build/
.
Sim, a documentação do projeto resume-se basicamente a esse README e não vai
mais a fundo do que isso, apenas o necessário para que você, caso quisesse,
conseguisse mexer em algo antes de compilar (já que não usamos ./configure
s
nem nada do tipo) e/ou não estivesse numa plataforma que usasse o RPM.
“Por que mexer justo agora, 20 anos depois?”
Entretanto, depois de muita insistência de algumas pessoas que acompanham o
projeto, eu resolvi que seria bom começar a implementar uma suíte de testes —
até porque, mesmo que o Heirloom NG continuasse sendo um mero conjunto de
patches feitos especificamente para que funcionasse como userspace do
Copacabana e nada mais nem nada menos,
continuaria sendo bom ter certeza de que os programas estão rodando
corretamente e se os patches aplicados realmente ajeitaram algo
ou se foram placebo por simplesmente criar um binário não-funcional.
Então temos um problema: como criar um novo “alvo” do Make para testes? Como
chamá-los? Como saber se não estaremos a resolver tudo com uma “foice e um
martelo” e deixando a precisão e os padrões do projeto de lado?
Simples ma non troppo: alquimia. Vulgo estudar o sistema de montagem
quebrando-o em partes e analisando o que for possível em todas as frentes,
fazendo anotações e corrigindo quando necessário.
Destilação: analisando o produto e o subproduto do gmake
Primeiramente, vamos analisar quais arquivos compõem o sistema de montagem em si. Numa árvore de código-fonte limpa, essa é a saída que deveremos ter:
S145% cd /usr/home//luiz/projetos/heirloom-toolchest/
S145% find . -type f \( -name '*.mk' -o -name 'mk.*' -o -name '[M,m]akefile' \) -print
# [...]
./build/mk.head
./build/mk.tail
./build/Makefile.mk
./build/mk.config
# [...]
./Makefile.mk
./makefile
Além dos arquivos que são específicos para cada programa e biblioteca — e que
eu cortei da saída, afinal são literalmente mais que cem —, os Makefile.mk
,
temos basicamente os arquivos dentro de build/
, um Makefile.mk
e um
arquivo que passa praticamente batido, makefile
.
O movimento natural a partir disso, ignorando a parte de configuração
customizada do build/mk.config
, é executar o GNU Make. Logo, faremos-o, mas
com a partir do strace
(1) e iremos parar antes de começarmos a compilar algo
de fato.
S145% pwd
/usr/home/luiz/projetos/heirloom-toolchest
S145% strace gmake
Certo, é muita coisa na tela, melhor passar para um arquivo e analisar de lá.
S145% strace gmake &> '/tmp/strace make Heirloom.txt'
^CS145% pg /tmp/strace\ make\ Heirloom.txt
As primeiras linhas podem ser ignoradas por nós, afinal elas tratam de
procedimentos internos do sistema para carregar bibliotecas dinâmicas, além de
configurar o conjunto de sinais e ações que devem ser tomadas caso algum seja
detectado (sigaction(2)
e seus amigos). O que nos interessa é ver qual
arquivo Makefile é aberto, afinal, após a primeira compilação, o Heirloom gera
um outro arquivo chamado Makefile
(com “M” maiúsculo) na raiz da pasta,
então, o que está acontecendo?
^CS145% pg /tmp/strace\ make\ Heirloom.txt
# [...]
newfstatat(AT_FDCWD, ".", {st_mode=S_IFDIR|0775, st_size=1458, ...}, 0) = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0775, st_size=1458, ...}) = 0
brk(0x5c5a91380000) = 0x5c5a91380000
getdents64(3, 0x5c5a913577d0 /* 150 entries */, 32768) = 4208
getdents64(3, 0x5c5a913577d0 /* 0 entries */, 32768) = 0
close(3) = 0
openat(AT_FDCWD, "makefile", O_RDONLY) = 3
fcntl(3, F_GETFD) = 0
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=4164, ...}) = 0
read(3, "SHELL = /bin/sh\r\n\r\nSUBDIRS = bui"..., 4096) = 4096
read(3, "igraphs \\\r\n\t\t\t-Wuninitialized -W"..., 4096) = 68
read(3, "", 4096) = 0
close(3) = 0
newfstatat(AT_FDCWD, "RCS", 0x7ffd276bf100, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "SCCS", 0x7ffd276bf100, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "makefile", {st_mode=S_IFREG|0644, st_size=4164, ...}, 0) = 0
clock_gettime(CLOCK_REALTIME, {tv_sec=1713877589, tv_nsec=474742533}) = 0
newfstatat(AT_FDCWD, "dummy", 0x7ffd276bcec0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "makefiles", 0x7ffd276bcec0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "Makefile", 0x7ffd276bcd50, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "Makefile.mk", {st_mode=S_IFREG|0644, st_size=3438, ...}, 0) = 0
fstat(1, {st_mode=S_IFREG|0644, st_size=13507, ...}) = 0
:█
Perceberam isso? Olhem com cuidado:
Primeiro, abrimos a pasta atual, representada como .
, para um descritor de
arquivo, que então é informado à função fstat
(2) junto com o endereço de
uma struct
que conterá informações sobre a pasta atual e também a
getdents
(2), essa que, de fato, vai nos dar uma lista dos arquivos na pasta
informada em forma de um array de struct
s do tipo dirent
.
Sim, isso soa confuso, mas não se preocupem, iremos olhar o código diretamente
agora ao invés das chamadas de sistema que são feitas, afinal, muitas delas
estão escondidas por funções-porcelana.
Podemos ver qual parte do código de fato faz isso ao procurarmos não por uma
chamada da função getdents
ou fstat
, mas sim onde a struct
é
declarada. Por uma questão de formalidade, iremos estudar o código do GNU Make
3.81, versão que foi contemporânea do último lançamento formal do Heirloom
Toolchest de 15 de Julho de 2007.
Para isso, basta que baixemos o arquivo do servidor HTTPS do GNU e extrairmos-o.
S145% curl -Lo- https://ftp.gnu.org/gnu/make/make-3.81.tar.gz \
| gzip -cd - \
| tar -xvf - -C /usr/src
Então, podemos realizar a busca utilizando o find
(1), xargs
(1)
e grep
(1):
S145% pwd
/usr/src/make-3.81
S145% find . -type f -name '*.c' -print | xargs grep -n 'readdir'
./dir.c: 678: ENULLLOOP (d, readdir (dir->dirstream));
./dir.c: 1202: gl->gl_readdir = read_dirstream;
./read.c: 223: /* all lower case since readdir() (the vms version) 'lowercasifies' */
./vmsfunctions.c: 78:readdir (DIR *dir)
./glob/glob.c: 250:# define readdir(str) __readdir (str)
./glob/glob.c: 1317: ? (*pglob->gl_readdir) (stream)
./glob/glob.c: 1318: : readdir ((DIR *) stream));
./w32/compat/dirent.c: 109:readdir(DIR* pDir)
./w32/compat/dirent.c: 131: /* bump count for next call to readdir() or telldir() */
./w32/compat/dirent.c: 160: /* reset members which control readdir() */
./w32/compat/dirent.c: 181: /* return number of times readdir() called */
./w32/compat/dirent.c: 199: for (--nPosition; nPosition && readdir(pDir); nPosition--);
Curioso, o único arquivo que não menciona a função diretamente é o read.c
…
S145% sed 9q read.c
/* Reading and parsing of makefiles for GNU Make.
Copyright (C) 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997,
1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006 Free Software
Foundation, Inc.
This file is part of GNU Make.
GNU Make is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 2, or (at your option) any later version.
Ha, touché! Achamos o arquivo que lê os Makefile
s dentro do próprio GNU
Make.
Vejamos, aquele comentário refere-se a uma questão do VMS, mais especificamente
o fato de que esse sistema tem problemas em lidar com maiúsculas e minúsculas no
nome dos arquivos. Isso não nos interessa agora, mas é uma pista de que há algum
tipo de lista de arquivos que o comando gmake
/make
precisa procurar
para ler e executar por padrão.
Linhas 219 até 234, arquivo read.c
:
/* If there were no -f switches, try the default names. */
if (num_makefiles == 0)
{
static char *default_makefiles[] =
#ifdef VMS
/* all lower case since readdir() (the vms version) 'lowercasifies' */
{ "makefile.vms", "gnumakefile.", "makefile.", 0 };
#else
#ifdef _AMIGA
{ "GNUmakefile", "Makefile", "SMakefile", 0 };
#else /* !Amiga && !VMS */
{ "GNUmakefile", "makefile", "Makefile", 0 };
#endif /* AMIGA */
#endif /* VMS */
register char **p = default_makefiles;
while (*p != 0 && !file_exists_p (*p))
++p;
/* [...] Mais 2904 linhas... */
Podemos ver que há a declaração de um array chamado *default_makefiles[]
.
Um nome autodescritivo: em ordem de prioridade, tem uma lista de arquivos que
devem ser lidos por padrão pelo programa, como tínhamos proposto antes enquanto
hipótese. Limpando toda a “crosta” de compatibilidade feita por #ifdef
s e
focando apenas em sistemas UNIX-compatíveis, temos isso:
Linha 229, arquivo read.c
(adaptado):
static char *default_makefiles[] = { "GNUmakefile", "makefile", "Makefile", 0 };
Touché, encore. Era a prova que precisávamos para saber que o arquivo
“makefile
” na raiz do Heirloom era executado primeiro e que o
“Makefile
” gerado jamais seria executado se não fosse explicitado.
Poderíamos simplesmente ter lido a página da especificação POSIX no site da OpenGroup em coisa de 10 minutos ao invés de investir pelo menos algumas horas em todo esse processo? Claro, mas o processo “alquímico” nos mostra mais sobre como as coisas funcionam.
Logo, volta a dúvida que, parafraseando Dante Alighieri, tenciona-me a cabeça
na última semana desde o pull-request
nº 49: para que
serve o “Makefile
” gerado? E o que o “makefile
” de fato faz para
gerá-lo? Pois bem, como dito anteriormente, qualquer arquivo com a extensão
.mk
terá o conteúdo dos arquivos da pasta build/
pré-anexados antes do
seu conteúdo na síntese final, o que pode ser visto além das palavras aqui:
Linhas 23 até 34, arquivo makefile
:
.DEFAULT:
+ for i in $(SUBDIRS) ;\
do \
(cd "$$i" && $(MAKE) $@) || exit ; \
done
$(MAKE) -f Makefile $@
.SUFFIXES: .mk
.mk:
cat build/mk.head build/mk.config $< build/mk.tail >$@
makefiles: Makefile $(SUBDIRS:=/Makefile)
Segundo a documentação da OpenGroup, .SUFFIXES
contém uma lista de sufixos
que devem ser utilizados pelas “regras” — que são, basicamente, quando
declara-se um alvo — e sua ordem define como serão de fato usados. Que honra
seja feita, a documentação específica do GNU é melhor para que isso seja
compreendido, além do código em si.
Basicamente, isso faz com que o alvo, que é um arquivo, tenha seu sufixo —
desde que na lista .SUFFIXES
— removido e isso é passado para outra
variável, no caso $*
.
Linhas 628 até 657, arquivo file.c
:
void
snap_deps (void)
{
struct file *f;
struct file *f2;
struct dep *d;
struct file **file_slot_0;
struct file **file_slot;
struct file **file_end;
/* Perform second expansion and enter each dependency
name as a file. */
/* Expand .SUFFIXES first; it's dependencies are used for
$$* calculation. */
for (f = lookup_file (".SUFFIXES"); f != 0; f = f->prev)
expand_deps (f);
/* We must use hash_dump (), because within this loop
we might add new files to the table, possibly causing
an in-situ table expansion. */
file_slot_0 = (struct file **) hash_dump (&files, 0, 0);
file_end = file_slot_0 + files.ht_fill;
for (file_slot = file_slot_0; file_slot < file_end; file_slot++)
for (f = *file_slot; f != 0; f = f->prev)
{
if (strcmp (f->name, ".SUFFIXES") != 0)
expand_deps (f);
}
free (file_slot_0);
Ele contatena o nome do alvo (representado por $@
), nomes esses que
foram declarados na regra makefiles
, com o sufixo de dependência,
resultando no nome do arquivo de dependência (representado por $<
),
e isso pode ser visto aqui, onde nós primeiro passamos pelos alvos e guardamos
os nomes dos alvos a serem gerados numa struct
chamada *deps
, dentro da
struct
f
, essa que é atribuída por meio do macro
record_waiting_files()
:
Linhas 472 até 486, arquivo read.c
:
#define record_waiting_files() \
do \
{ \
if (filenames != 0) \
{ \
fi.lineno = tgts_started; \
record_files (filenames, pattern, pattern_percent, deps, \
cmds_started, commands, commands_idx, two_colon, \
&fi); \
} \
filenames = 0; \
commands_idx = 0; \
no_targets = 0; \
if (pattern) { free(pattern); pattern = 0; } \
} while (0)
Esse macro não aparece pelo gdb, mas sim a função que o dito chama,
record_files()
, essa sim que irá guardar os nomes.
Linha 34, arquivo makefile
:
makefiles: Makefile $(SUBDIRS:=/Makefile)
Backtrace do GDB:
record_files (filenames=<optimized out>, pattern=<optimized out>, pattern_percent=<optimized out>, deps=<optimized out>, cmds_started=<optimized out>,
commands=<optimized out>, commands_idx=<optimized out>, two_colon=<optimized out>, flocp=<optimized out>) at read.c:1971
1971 if (f->double_colon)
(gdb)
1977 if (cmds != 0 && cmds == f->cmds)
(gdb)
1995 f->is_target = 1;
(gdb)
1998 if (f == default_file && this == 0 && cmds == 0)
(gdb)
2005 if (f == suffix_file && this == 0)
(gdb) print f->name
$3 = 0x5555555932d0 "makefiles"
(gdb) print f
$4 = (struct file *) 0x555555594a40
(gdb) ptype f
type = struct file {
char *name;
char *hname;
char *vpath;
struct dep *deps;
struct commands *cmds;
int command_flags;
char *stem;
struct dep *also_make;
uintmax_t last_mtime;
uintmax_t mtime_before_update;
struct file *prev;
struct file *last;
struct file *renamed;
struct variable_set_list *variables;
struct variable_set_list *pat_variables;
struct file *parent;
struct file *double_colon;
short update_status;
enum cmd_state command_state : 2;
unsigned int precious : 1;
unsigned int low_resolution_time : 1;
unsigned int tried_implicit : 1;
unsigned int updating : 1;
unsigned int updated : 1;
unsigned int is_target : 1;
unsigned int cmd_target : 1;
unsigned int phony : 1;
unsigned int intermediate : 1;
unsigned int secondary : 1;
unsigned int dontcare : 1;
unsigned int ignore_vpath : 1;
unsigned int pat_searched : 1;
unsigned int considered : 1;
}
Note-se o uso da função expand_deps()
, que procura por arquivos dentro da
pasta que estejam dentro da lista de alvos utilizando a lookup_file()
Esses nomes de arquivos ficam guardados dentro de uma struct
do tipo dep
chamada d1
, num formado de lista encadeada. O Make, então, verifica se o
alvo já existe e, se não, ele guarda o nome do alvo numa outra lista chamada
files
em forma de hash e, posteriormente, executa a regra do sufixo em cada
um para que sejam criados.
Pode-se ver isso ao executar o programa gmake
utilizando o gdb
e marcando
pontos de interrupção na execução das funções snap_deps()
, expand_deps()
,
lookup_file()
e enter_file()
.
Backtrace do GDB:
expand_deps (f=f@entry=0x555555594a40) at file.c:584
584 d1->name = 0;
(gdb)
577 for (d1 = new; d1 != 0; d1 = d1->next)
(gdb)
579 d1->file = lookup_file (d1->name);
(gdb)
Breakpoint 3.1, lookup_file (name=0x555555591510 "chroot/Makefile") at file.c:77
77 {
(gdb)
84 assert (*name != '\0');
(gdb)
Breakpoint 3.2, lookup_file (name=0x555555591510 "chroot/Makefile") at file.c:105
105 while (name[0] == '.' && name[1] == '/' && name[2] != '\0')
(gdb)
125 file_key.hname = name;
(gdb)
126 f = (struct file *) hash_find_item (&files, &file_key);
(gdb)
lookup_file (name=<optimized out>) at file.c:131
131 return f;
(gdb)
expand_deps (f=f@entry=0x555555594a40) at file.c:580
580 if (d1->file == 0)
(gdb)
581 d1->file = enter_file (d1->name);
(gdb)
Breakpoint 4, enter_file (name=0x555555591510 "chroot/Makefile") at file.c:136
136 {
(gdb)
145 assert (*name != '\0');
(gdb)
167 file_key.hname = name;
(gdb)
168 file_slot = (struct file **) hash_find_slot (&files, &file_key);
(gdb)
170 if (! HASH_VACANT (f) && !f->double_colon)
(gdb)
179 new = (struct file *) xmalloc (sizeof (struct file));
(gdb)
180 bzero ((char *) new, sizeof (struct file));
(gdb)
181 new->name = new->hname = name;
(gdb)
186 new->last = new;
(gdb)
187 hash_insert_at (&files, new, file_slot);
(gdb)
A contatenação em si é feita com uma função chamada convert_to_pattern()
,
como pode ser visto no gdb
após o término da execução da função
snap_deps()
, que é executada antes da install_default_implicit_rules()
para que os sufixos definidos no Makefile tenham prioridade sob os padrões.
Backtrace do GDB:
main (argc=<optimized out>, argv=<optimized out>, envp=<optimized out>) at main.c:1763
1763 convert_to_pattern ();
(gdb)
1770 install_default_implicit_rules ();
(gdb)
1774 count_implicit_rule_limits ();
(gdb)
1778 build_vpath_lists ();
(gdb)
1784 if (old_files != 0)
(gdb)
1794 if (!restarts && new_files != 0)
(gdb)
1804 remote_setup ();
(gdb)
1806 if (read_makefiles != 0)
(gdb)
1814 int orig_db_level = db_level;
(gdb)
1817 if (! ISDB (DB_MAKEFILES))
(gdb)
1818 db_level = DB_NONE;
(gdb)
1828 while (d != 0)
(gdb)
1831 if (f->double_colon)
(gdb)
1863 makefile_mtimes = (FILE_TIMESTAMP *)
(gdb)
1866 makefile_mtimes[mm_idx++] = file_mtime_no_search (d->file);
(gdb)
1868 d = d->next;
(gdb)
1828 while (d != 0)
(gdb)
1874 define_makeflags (1, 1);
(gdb)
1876 rebuilding_makefiles = 1;
(gdb)
1877 status = update_goal_chain (read_makefiles);
(gdb)
“Tá, mas por que ele não faz isso com outras regras além do makefiles
? Tem a
ou8
, ps2diet
, world
…“ Pois então, é “tentativa e erro”, o Make
verifica se o arquivo com o sufixo existe ou não antes de executar a regra do
sufixo.
Backtrace do GDB:
2146 if (goals == 0)
(gdb)
2148 if (**default_goal_name != '\0')
(gdb)
2150 if (default_goal_file == 0 ||
(gdb)
2178 goals = alloc_dep ();
(gdb)
2179 goals->file = default_goal_file;
(gdb)
2196 DB (DB_BASIC, (_("Updating goal targets....\n")));
(gdb)
2198 switch (update_goal_chain (goals))
(gdb)
Breakpoint 3.1, lookup_file (name=name@entry=0x5555555a03e0 "dummy.mk") at file.c:77
77 {
(gdb)
84 assert (*name != '\0');
(gdb)
Breakpoint 3.2, lookup_file (name=0x5555555a03e0 "dummy.mk") at file.c:105
105 while (name[0] == '.' && name[1] == '/' && name[2] != '\0')
(gdb)
125 file_key.hname = name;
(gdb)
126 f = (struct file *) hash_find_item (&files, &file_key);
(gdb)
lookup_file (name=name@entry=0x5555555a03e0 "dummy.mk") at file.c:131
131 return f;
(gdb)
pattern_search (file=file@entry=0x555555592fc0, archive=archive@entry=0, depth=depth@entry=1, recursions=recursions@entry=0) at implicit.c:706
706 vname = name;
(gdb)
707 if (vpath_search (&vname, (FILE_TIMESTAMP *) 0))
(gdb)
723 if (intermed_ok)
(gdb)
766 if (failed)
(gdb)
771 free_idep_chain (deps);
(gdb)
772 deps = 0;
(gdb)
781 if (i < nrules)
(gdb)
433 for (intermed_ok = 0; intermed_ok == !!intermed_ok; ++intermed_ok)
(gdb)
439 for (i = 0; i < nrules; i++)
(gdb)
452 if (rule == 0)
(gdb)
457 if (intermed_ok && rule->terminal)
(gdb)
462 rule->in_use = 1;
(gdb)
466 stem = filename
(gdb)
468 stemlen = namelen - rule->lens[matches[i]] + 1;
(gdb)
469 check_lastslash = checked_lastslash[i];
(gdb)
470 if (check_lastslash)
(gdb)
476 DBS (DB_IMPLICIT, (_("Trying pattern rule with stem `%.*s'.\n"),
(gdb)
479 strncpy (stem_str, stem, stemlen);
(gdb)
480 stem_str[stemlen] = '\0';
(gdb)
484 file->stem = stem_str;
(gdb)
488 for (dep = rule->deps; dep != 0; dep = dep->next)
(gdb)
503 p = get_next_word (dep->name, &len);
(gdb)
510 if (p == 0)
(gdb)
515 for (p2 = p; p2 < p + len && *p2 != '%'; ++p2)
(gdb)
518 if (dep->need_2nd_expansion)
(gdb)
566 if (p2 < p + len)
(gdb)
568 register unsigned int i = p2 - p;
(gdb)
569 bcopy (p, depname, i);
(gdb)
570 bcopy (stem_str, depname + i, stemlen);
(gdb)
571 bcopy (p2 + 1, depname + i + stemlen, len - i - 1);
(gdb)
572 depname[len + stemlen - 1] = '\0';
(gdb)
574 if (check_lastslash)
(gdb)
585 p2 = depname;
(gdb)
594 for (; *id_ptr; id_ptr = &(*id_ptr)->next)
(gdb)
597 *id_ptr = (struct idep *)
(gdb)
608 if (order_only || add_dir || had_stem)
(gdb)
610 unsigned long l = lastslash - filename + 1;
(gdb)
612 for (d = *id_ptr; d != 0; d = d->next)
(gdb)
614 if (order_only)
(gdb)
617 if (add_dir)
(gdb)
629 if (had_stem)
(gdb)
630 d->had_stem = 1;
(gdb)
612 for (d = *id_ptr; d != 0; d = d->next)
(gdb)
634 if (!order_only && *p2)
(gdb)
645 p = get_next_word (p, &len);
(gdb)
510 if (p == 0)
(gdb)
488 for (dep = rule->deps; dep != 0; dep = dep->next)
(gdb)
651 file->stem = 0;
(gdb)
656 for (d = deps; d != 0; d = d->next)
(gdb)
658 char *name = d->name;
(gdb)
660 if (file_impossible_p (name))
(gdb)
676 DBS (DB_IMPLICIT,
(gdb)
685 for (expl_d = file->deps; expl_d != 0; expl_d = expl_d->next)
(gdb)
686 if (streq (dep_name (expl_d), name))
(gdb)
685 for (expl_d = file->deps; expl_d != 0; expl_d = expl_d->next)
(gdb)
686 if (streq (dep_name (expl_d), name))
(gdb)
685 for (expl_d = file->deps; expl_d != 0; expl_d = expl_d->next)
(gdb)
699 if (((f = lookup_file (name)) != 0 && f->is_target)
(gdb)
Breakpoint 3.1, lookup_file (name=name@entry=0x5555555946c0 "dummy.mk") at file.c:77
77 {
(gdb)
84 assert (*name != '\0');
(gdb)
Breakpoint 3.2, lookup_file (name=0x5555555946c0 "dummy.mk") at file.c:105
105 while (name[0] == '.' && name[1] == '/' && name[2] != '\0')
(gdb)
125 file_key.hname = name;
(gdb)
126 f = (struct file *) hash_find_item (&files, &file_key);
(gdb)
lookup_file (name=name@entry=0x5555555946c0 "dummy.mk") at file.c:131
131 return f;
(gdb)
pattern_search (file=file@entry=0x555555592fc0, archive=archive@entry=0, depth=depth@entry=1, recursions=recursions@entry=0) at implicit.c:706
706 vname = name;
(gdb)
707 if (vpath_search (&vname, (FILE_TIMESTAMP *) 0))
(gdb)
723 if (intermed_ok)
(gdb)
725 if (intermediate_file == 0)
(gdb)
726 intermediate_file
(gdb)
729 DBS (DB_IMPLICIT,
(gdb)
733 bzero ((char *) intermediate_file, sizeof (struct file));
(gdb)
734 intermediate_file->name = name;
(gdb)
735 if (pattern_search (intermediate_file,
(gdb)
752 if (intermediate_file->variables)
(gdb)
753 free_variable_set (intermediate_file->variables);
(gdb)
754 file_impossible (name);
(gdb)
766 if (failed)
(gdb)
771 free_idep_chain (deps);
(gdb)
772 deps = 0;
(gdb)
781 if (i < nrules)
(gdb)
433 for (intermed_ok = 0; intermed_ok == !!intermed_ok; ++intermed_ok)
(gdb)
789 if (rule == 0)
(gdb)
978 free_idep_chain (deps);
(gdb)
979 free (tryrules);
(gdb)
981 return rule != 0;
(gdb)
Executando com o gdb, vemos que, quando o arquivo existe, “failed
” é
redefinido como falso e o arquivo é “atualizado” utilizando a função
remake_file()
, mas apenas se estiver na lista de dependências (da
qual falamos antes). Em resumo, quando tudo dá certo, essa é a saída que
deveremos ter:
Backtrace do GDB:
750 DBF (DB_BASIC, _("Must remake target `%s'.\n"));
(gdb)
754 if (!streq(file->name, file->hname))
(gdb)
761 remake_file (file);
(gdb)
make: Entering directory `/usr/home/luiz/projetos/heirloom-toolchest'
cat build/mk.head build/mk.config Makefile.mk build/mk.tail >Makefile
[Detaching after vfork from child process 16777]
Isso também apareceu pelo strace
, mas de forma menos clara. Podemos ver que,
em vários momentos, chamadas do fstat
(2) são feitas para se obter
informações sobre a existência do arquivo e data de modificação:
S145% pg /tmp/strace\ make\ Heirloom.txt
# [...]
newfstatat(AT_FDCWD, "RCS", 0x7ffe8ba615e0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "SCCS", 0x7ffe8ba615e0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "makefile", {st_mode=S_IFREG|0644, st_size=4164, ...}, 0) = 0
clock_gettime(CLOCK_REALTIME, {tv_sec=1713909508, tv_nsec=329059299}) = 0
newfstatat(AT_FDCWD, "dummy", 0x7ffe8ba5f4a0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "makefiles", 0x7ffe8ba5f4a0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "Makefile", 0x7ffe8ba5f3a0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "Makefile.mk", {st_mode=S_IFREG|0644, st_size=3438, ...}, 0) = 0
fstat(1, {st_mode=S_IFREG|0644, st_size=7226, ...}) = 0
write(1, "cat build/mk.head build/mk.confi"..., 70cat build/mk.head build/mk.config Makefile.mk build/mk.tail >Makefile
) = 70
pipe2([3, 4], 0) = 0
close(4) = 0
:█
Entretanto, caso eu não tivesse feito esse registro utilizando o gdb
e o
strace
, eu faria uma boa aposta que é pelo fato de, além deles não terem
nenhum correspondente com o sufixo como, por exemplo, “world.mk
”, eles
também não têm nenhuma dependência listada, ao contrário do makefiles
.
Eu já teria acertado apenas pela primeira, de qualquer forma, mesmo a segunda
não sendo o motivo.
Se isso tudo ainda lhe é um bocado confuso, creio que agora fica um pouco
mais claro quando executa-se o programa gmake
com a opção --trace
:
S145% gmake --trace &> '/tmp/trace make Heirloom.txt'
S145% pg '/tmp/trace make Heirloom.txt'
makefile:31: update target 'Makefile' due to: target does not exist
cat build/mk.head build/mk.config Makefile.mk build/mk.tail >Makefile
makefile:31: update target 'build/Makefile' due to: target does not exist
cat build/mk.head build/mk.config build/Makefile.mk build/mk.tail >build/Makefile
makefile:31: update target 'libwchar/Makefile' due to: target does not exist
cat build/mk.head build/mk.config libwchar/Makefile.mk build/mk.tail >libwchar/Makefile
makefile:31: update target 'libcommon/Makefile' due to: target does not exist
cat build/mk.head build/mk.config libcommon/Makefile.mk build/mk.tail >libcommon/Makefile
makefile:31: update target 'libuxre/Makefile' due to: target does not exist
cat build/mk.head build/mk.config libuxre/Makefile.mk build/mk.tail >libuxre/Makefile
makefile:31: update target '_install/Makefile' due to: target does not exist
cat build/mk.head build/mk.config _install/Makefile.mk build/mk.tail >_install/Makefile
makefile:31: update target 'banner/Makefile' due to: target does not exist
cat build/mk.head build/mk.config banner/Makefile.mk build/mk.tail >banner/Makefile
makefile:31: update target 'basename/Makefile' due to: target does not exist
cat build/mk.head build/mk.config basename/Makefile.mk build/mk.tail >basename/Makefile
makefile:31: update target 'bc/Makefile' due to: target does not exist
cat build/mk.head build/mk.config bc/Makefile.mk build/mk.tail >bc/Makefile
makefile:31: update target 'bdiff/Makefile' due to: target does not exist
cat build/mk.head build/mk.config bdiff/Makefile.mk build/mk.tail >bdiff/Makefile
makefile:31: update target 'bfs/Makefile' due to: target does not exist
cat build/mk.head build/mk.config bfs/Makefile.mk build/mk.tail >bfs/Makefile
makefile:31: update target 'cal/Makefile' due to: target does not exist
cat build/mk.head build/mk.config cal/Makefile.mk build/mk.tail >cal/Makefile
makefile:31: update target 'calendar/Makefile' due to: target does not exist
cat build/mk.head build/mk.config calendar/Makefile.mk build/mk.tail >calendar/Makefile
makefile:31: update target 'cat/Makefile' due to: target does not exist
cat build/mk.head build/mk.config cat/Makefile.mk build/mk.tail >cat/Makefile
:█
Veem? É claro que o programa nos diz “makefile:31: update target 'Makefile'
due to: target does not exist
” antes de criá-lo pelo remake_file()
.
Agora que já sabemos que para adicionar algo novo no projeto como, por exemplo,
uma suite de testes, é melhor criar um arquivo independente com instruções
para aquela pasta e só adicionar em SUBDIRS
para aquele Makefile.mk
possa virar um Makefile
definitivo e… $(MAKE)
? .DEFAULT
?
Linhas 23 até 28, arquivo makefile
:
.DEFAULT:
+ for i in $(SUBDIRS) ;\
do \
(cd "$$i" && $(MAKE) $@) || exit ; \
done
$(MAKE) -f Makefile $@
Pois bem, presumo que essa parte será a mais simples de explicar até agora.
Basicamente, o .DEFAULT
é um alvo especial, assim como o .SUFFIXES
e outros cujos nomes são precedidos por um ponto, que é usado para definir
comandos que serão executados pelo Make caso não hajam outras regras definidas
para se montar um determinado alvo. É literalmente uma “regra padrão”, mais um
caso de nome autodescritivo.
Linhas 477 até 481 e 1583 até 1586, arquivo main.c
:
/* Pointer to structure for the file .DEFAULT
whose commands are used for any file that has none of its own.
This is zero if the makefiles do not define .DEFAULT. */
struct file *default_file;
/* Define the default variables. */
define_default_variables ();
default_file = enter_file (".DEFAULT");
Nesse caso, o .DEFAULT
executa um laço for em todas as pastas que estão
em SUBDIRS
— inclusive, a confusão mais comum é pensar que o
Makefile.mk
era transformado em Makefile
por meio desse laço, o que se
mostrou não ser verdade agora há pouco —, esse que entra em cada pasta e
executa o $(MAKE)
e, em seguida, ao fim do laço,
$(MAKE) -f Makefile $@
. A partir daqui, vamos por partes para explicar o
que é o “Makefile
” e o que é o $(MAKE)
.
Antes de tudo, lembremos que, pela ordem de prioridade, o arquivo makefile
sempre será executado antes de qualquer outro, então o Makefile
precisaria
ser explicitado que o Make o executasse e, claro, o makefile
já faz isso por
si só, como vimos.
Certo, mas o que é o $(MAKE)
? Em resumo, é uma forma do Make “se chamar”
recursivamente, mas com os mesmos parâmetros.
Nota de revisão, 26 de Abril (Sexta-feira): O Make já estaria sendo
executado recursivamente a partir da declaração do laço for, pois pode-se
ver um +
antes, o que já ativaria a “bandeira” de recursão no código-fonte,
como é mostrado mais à frente.
Não precisaria se ir muito longe para se ver como funciona, apenas limpar a árvore de código-fonte do Heirloom depois de uma primeira compilação:
S145% gmake mrproper
rm -f .foo .FOO Makefile
for i in build libwchar libcommon libuxre _install banner basename bc bdiff bfs cal calendar cat chmod chown chroot cksum cmp col comm copy cp cpio csplit cut date dc dd deroff diff diff3 dircmp dirname df du echo ed env expand expr factor file find fmt fmtmsg fold getconf getopt grep groups hd head hostname id join kill line listusers ln logins logname ls mail man mesg mkdir mkfifo mknod more mvdir nawk news nice nl nohup oawk od paste pathchk pg pgrep pr printenv printf priocntl ps psrinfo pwd random renice rm rmdir sdiff sed setpgrp shl sleep sort spell split stty su sum sync tabs tail tapecntl tar tcopy tee test time touch tr true tsort tty ul uname uniq units users wc what who whoami whodo xargs yes ;\
do \
(cd "$i" && gmake mrproper) || exit ; \
done
gmake[1]: Entering directory '/usr/home/luiz/projetos/heirloom-toolchest/build'
rm -f core log *~
rm -f Makefile maninst crossln genintro
gmake[1]: Leaving directory '/usr/home/luiz/projetos/heirloom-toolchest/build'
gmake[1]: Entering directory '/usr/home/luiz/projetos/heirloom-toolchest/libwchar'
# [...]
Achou que eu não ia buscar como isso é feito pelo próprio GNU Make no código-fonte? Achou errado. Vamos lá de novo.
S145% find /usr/src/make-3.81/ -type f -name '*.c' -print | xargs grep -n '$(MAKE)'
/usr/src/make-3.81/commands.c: 355: && (strstr (p, "$(MAKE)") != 0 || strstr (p, "${MAKE}") != 0))
/usr/src/make-3.81/main.c: 1128: done before $(MAKE) is figured out so its definitions will not be
O que temos aqui… O arquivo commands.c
, que define comandos, variáveis e
“macros” internos do GNU Make e o que parece ser um comentário no arquivo
main.c
. Vamos analisar o commands.c
primeiro, porque tem umas coisas
interessantes a se notar.
Linhas 353 até 356, arquivo commands.c
:
/* If no explicit '+' was given, look for MAKE variable references. */
if (!(flags & COMMANDS_RECURSE)
&& (strstr (p, "$(MAKE)") != 0 || strstr (p, "${MAKE}") != 0))
flags |= COMMANDS_RECURSE;
Bem, primeiramente se verifica se o Make já não estaria sendo executado
recursivamente ao se verificar se COMMANDS_RECURSE
já não está presente no
valor de flags
e, então, se procuraria por “${MAKE}
” ou $(MAKE)
—
que é mais comumente usado — na string p
utilizando a função
strstr()
. Para quem não está acostumado, essa função é o equivalente de se
usar as expressões regulares embutidas de KornShell 93 (ou do GNU Broken-Again)
para encontrar uma string dentro d’outra, a exemplo:
if [[ "$p" =~ (.*\$[\(,\{]MAKE[\),\}].*) ]]; then
Isso é mais uma curiosidade e não seria muito relevante de se comentar a fundo;
entretanto, é de se notar que o Make apenas aumenta o valor de um inteiro para
dizer que se trata de uma recursão e nada mais, o que é interessante porque nos
dá motivo para ver onde esse inteiro é usado no código do programa e como ele
atua. A menção do “$(MAKE)” no arquivo main.c
apenas nos diz que o programa
deve ler as variáveis de ambiente antes de chamar sua recursão para que as
próximas definições não venham do ambiente… Confuso, mas tranquilo porque não
é o que queremos saber por ora.
Bem, onde mais essa COMMANDS_RECURSE
é mencionada? Além do commands.c
,
também a vemos em job.c
e remake.c
.
Algo que é digno de alguma curiosidade é que a maioria das verificações por esse
valor são negativas, ou seja, “faça algo apenas se COMMANDS_RECURSE
não
esteja definida”. O que me chamou a atenção é que esse algo é, de fato, a
“limpeza” da variável argv[]
. Quem programa em C, Nawk, Perl, PHP, Python,
JavaScript, enfim, qualquer linguagem que tenha bebido muito da fonte de C,
saberá que essa variável contém os parâmetros que são passados para o programa.
Em resumo, quando o Make não executa recursivamente, os parâmetros (alvos e
afins) passados a ele são limpos da memória.
Linhas 1032 até 1079, arquivo job.c
:
/* If -q was given, say that updating `failed' if there was any text on the
command line, or `succeeded' otherwise. The exit status of 1 tells the
user that -q is saying `something to do'; the exit status for a random
error is 2. */
if (argv != 0 && question_flag && !(flags & COMMANDS_RECURSE))
{
#ifndef VMS
free (argv[0]);
free ((char *) argv);
#endif
child->file->update_status = 1;
notice_finished_file (child->file);
return;
}
if (touch_flag && !(flags & COMMANDS_RECURSE))
{
/* Go on to the next command. It might be the recursive one.
We construct ARGV only to find the end of the command line. */
#ifndef VMS
if (argv)
{
free (argv[0]);
free ((char *) argv);
}
#endif
argv = 0;
}
if (argv == 0)
{
next_command:
#ifdef __MSDOS__
execute_by_shell = 0; /* in case construct_command_argv sets it */
#endif
/* This line has no commands. Go to the next. */
if (job_next_command (child))
start_job_command (child);
else
{
/* No more commands. Make sure we're "running"; we might not be if
(e.g.) all commands were skipped due to -n. */
set_command_state (child->file, cs_running);
child->file->update_status = 0;
notice_finished_file (child->file);
}
return;
}
Isso ainda se repete mais para frente, mas é uma função exclusiva do EMX, uma extensão para o MS-DOS e o IBM OS/2 que mantém os descritores de arquivos dos “trabalhos” do Make abertos caso seja chamado recursivamente, então acredito que não caiba comentar muito além aqui.
Finalmente, conseguiremos chegar onde queríamos: para que serve o bendito
Makefile
gerado a partir do Makefile.mk
? E por que ele é chamado apenas
no final do laço for que compila todos os programas e bibliotecas do Heirloom?
Quando analisamos o arquivo Makefile.mk
, vemos que ele contém regras
específicas para criar a estrutura de pastas onde o Heirloom será instalado
— essa que pode ser alterada em build/mk.config
, como já falamos
anteriormente —, a directories
, e para fazer ligações no sistema de
arquivos, a links
, duas regras que são “escondidas” por uma
“regra-porcelana” chamada install
. Entretanto, como o makefile
é o
primeiro arquivo a ser chamado, ele executa o Makefile
dessa maneira:
Linhas 35 até 41, arquivo makefile
:
install:
$(MAKE) -f Makefile directories
+ for i in $(SUBDIRS) ;\
do \
(cd "$$i" && $(MAKE) $@) || exit ; \
done
$(MAKE) -f Makefile links
Perceba-se que cada programa ainda tem sua própria regra para ser instalado,
afinal, como pode-se ver no próprio
site do Heirloom, alguns têm
variantes específicas para cada especificação, logo precisariam ser instalados
em outro diretório além do padrão informado no build/mk.config
como
DEFBIN
.
Primeiro, cria-se os diretórios, então instala-se cada programa a partir de sua
própria regra e, por fim, façam-se as ligações no sistema de arquivos.
Por padrão, caso não seja executado o alvo install
, esse arquivo meramente
gera o intro.1
, que é uma página de manual para o pacote Heirloom em si, ao
passar o arquivo de dependência intro.1.in
para o script em shell
genintro.sh
.
Linhas 1 até 4, arquivo Makefile.mk
:
all: intro.1
intro.1: intro.1.in
$(SHELL) build/genintro intro.1.in >intro.1
Ou seja, esse arquivo não faz nada, absolutamente nada de vital além de instalar
o pacote; o que é até justificável, considerando que o arquivo contém diversas
listas de comandos e adicionar tudo em um arquivo apenas seria deselegante e,
além de tudo, atrapalharia quem quisesse estudar o sistema de montagem por si só
ao aumentar o tamanho do makefile
em pelo menos 90 linhas.
Bem, fico por aqui. Esse artigo me tomou dois dias de pesquisa e quebração de
cuca para entender o que estava acontecendo em diversos momentos, mas meu único
arrependimento foi não ter conseguido lançar ontem, Dia de São Jorge.
O próximo passo vai ser implementar a suíte de testes, espero trazer algo novo
até lá.