Faça sabão, refaça o poder

Saudações! Quero falar sobre as principais, nem sempre óbvias, deficiências do sistema de compilação Make , que muitas vezes o torna inutilizável, e também sobre uma excelente alternativa e solução para o problema - o mais engenhoso em sua simplicidade, o sistema de redo . A ideia do famoso DJB , cuja criptografia não é usada em lugar nenhum. Pessoalmente, refazer me impressionou tanto com a simplicidade de mudança de vida, flexibilidade e desempenho muito melhor das tarefas de construção que substituí completamente o Make por ele em quase todos os meus projetos (onde não o substituí, significa que ainda não coloquei em minhas mãos), do qual não consegui encontrar nenhum um benefício ou razão para se manter vivo.





Ainda outra marca?



Muitas pessoas não estão satisfeitas com o Make, caso contrário não haveria dezenas de outros sistemas de construção e dezenas de dialetos do Make sozinho. A refazer esse mais uma alternativa? Por um lado, é claro, sim - apenas extremamente simples, mas capaz de resolver absolutamente todas as tarefas do Make. Por outro lado, temos alguma marca comum e uniforme?



A maioria dos sistemas de construção "alternativos" nasceu porque não tinha os recursos nativos do Make, faltou flexibilidade. Muitos sistemas estão preocupados apenas em gerar Makefiles, não em construí-los eles próprios. Muitos são adaptados ao ecossistema de certas linguagens de programação.



Abaixo vou tentar mostrar que refazer é um sistema muito mais notável, não apenas outra solução.



Make está sempre lá de qualquer maneira



Pessoalmente, eu ainda sempre olhei com desconfiança para toda essa alternativa, porque ela é mais complexa ou específica do ecossistema / linguagem, ou é uma dependência adicional que precisa ser definida e aprendido como usá-la. E Make é uma coisa com a qual, mais ou menos, todos estão familiarizados e sabem como usar em um nível básico. Portanto, sempre e em todo lugar eu tentei usar POSIX Make, assumindo que isso é algo que em qualquer caso, todos têm no sistema (POSIX) pronto para uso, como o compilador C. E as tarefas em Make devem ser executadas apenas para as quais se destina: execução paralelizável de metas (comandos ) levando em consideração as dependências entre eles.



Qual é o problema em apenas escrever em Make e ter certeza de que funciona em qualquer sistema? Afinal, você pode (deve!) Escrever no shell POSIX e não forçar os usuários a instalar um GNU Bash enorme e monstruoso. O único problema é que apenas o dialeto POSIX Make funcionará, o que é escasso o suficiente mesmo para muitos projetos pequenos e simples. Make em sistemas BSD modernos é mais complexo e cheio de recursos. Bem, com o GNU Make, poucos podem se comparar a ninguém, embora quase ninguém use seus recursos ao máximo e não saiba como usá-los. Mas GNU Make não suporta um dialeto de sistemas BSD modernos. Os sistemas BSD não possuem GNU Make (e são compreensíveis!).



Usar um dialeto BSD / GNU significa potencialmente forçar o usuário a instalar software adicional que não sai da caixa de qualquer maneira. Nesse caso, a possível vantagem do Make - sua presença no sistema, é anulada.



É possível usar e escrever em POSIX Make, mas difícil. Pessoalmente, lembro-me imediatamente de dois casos muito irritantes:



  • Algumas implementações de Make, ao executar $ (MAKE) -C, "vão" para o diretório onde o novo Make é executado, e outras não. É possível escrever um Makefile para que funcione da mesma forma em todos os lugares? Claro:



    tgt:
        (cd subdir ; $(MAKE) -C ...)
    


    Convenientemente? Definitivamente não. E é desagradável que tenhamos de nos lembrar constantemente dessas ninharias.
  • No POSIX Make, não há nenhuma instrução que execute uma chamada de shell e armazene seu resultado em uma variável. No GNU Make up até a versão 4.x, você pode fazer:



    VAR = $(shell cat VERSION)
    


    e começando com 4.x, bem como em dialetos BSD, você pode fazer:



    VAR != cat VERSION
    


    Não é exatamente a mesma ação que pode ser realizada:



    VAR = `cat VERSION`
    


    mas literalmente substitui essa expressão em seus comandos shell descritos nos destinos. Essa abordagem é usada em projetos sem sucção , mas é, obviamente, uma muleta.


Pessoalmente, em tais lugares, muitas vezes escrevi Makefiles para três dialetos de uma vez (GNU, BSD e POSIX):



$ cat BSDmakefile
GOPATH != pwd
VERSION != cat VERSION
include common.mk

$ cat GNUmakefile
GOPATH = $(shell pwd)
VERSION = $(shell cat VERSION)
include common.mk


Convenientemente? Longe disso! Embora as tarefas sejam extremamente simples e comuns. Então, acontece que:



  • Escreva em paralelo para vários dialetos Make. Negociar o tempo do desenvolvedor para conveniência do usuário.
  • Tendo em mente muitas nuances e trivialidades, talvez com substituições ineficientes ( `cmd ...` ), tente escrever no POSIX Make. Para mim, pessoalmente, com muitos anos de experiência com GNU / BSD Make, esta opção é a mais demorada (é mais fácil de escrever em vários dialetos).
  • Escreva em um dos dialetos Make, forçando o usuário a instalar software de terceiros.


Faça problemas técnicos



Mas tudo é muito pior porque qualquer Make não diz que (bem) dá conta das tarefas que lhe são atribuídas.



  • mtime , Make mtime, . , , Make . mtime ! mtime , , ! mtime — , . FUSE mtime . mmap mtime… -, msync ( POSIX ). NFS? , Make : ( ), , FUSE/NFS/mmap/VCS.

  • . ? Make . :



    tgt-zstd:
        zstd -d < tgt-zstd.zst > tgt
    
    tgt-fetch:
        fetch -o tgt-fetch SOME://URL
    


    , , Make , , , , Make, .



    :



    tgt-zstd:
        zstd -d < tgt-zstd.zst > tgt-zstd.tmp
        fsync tgt-zstd.tmp
        mv tgt-zstd.tmp tgt-zstd
    


    tmp/fsync/mv ? , Make-, tgt.tmp.
  • . ( ) Makefile, Make ? . - $(CFLAGS)? .



    Makefile! . Makefile , , . , , - , .



    Makefile :



    $ cat Makefile
    include tgt1.mk
    include tgt2.mk
    ...
    


    . ? !

  • , . Recursive Make Considered Harmful , Makefile-, Makefile- - , , Make , . Makefile — . ? , Makefile.



    ? , . FreeBSD , , , , .

  • . , #include «tgt.h», .c tgt.h, .c - sed .



    tgt.o: tgt.c `sed s/.../ tgt.c`
    


    . .mk Makefile include. ? Make, : .mk , , Makefile- include-.

  • Makefile- shell, , - , \\$, , .sh , Make. Make /, shell shell, . ?


Vamos admitir honestamente: quantas vezes e quanto você teve que fazer para limpar ou reconstruir sem paralelização, porque algo estava incompleto ou não reconstruiu ao contrário do esperado? No caso geral, é claro, isso não se deve a Makefiles idealmente corretos, corretos e completos, o que demonstra a complexidade de sua escrita competente e eficiente. A ferramenta deve ajudar.



Requisitos de refazer



Para passar para a descrição de redo , primeiro direi o que é uma implementação e o que o "usuário" (o desenvolvedor que descreve os objetivos e dependências entre eles) terá que aprender.



  • redo, , - . redo . POSIX shell . Python . : , , .
  • redo : POSIX shell, GNU bash, Python, Haskell, Go, C++, Inferno Shell. .
  • C , SHA256, 27KB. POSIX shell 100 . , POSIX shell redo tarball- .
  • Make-, ( ).


redo



As regras de construção de destino são um script de shell POSIX regular em target_name.do . Deixe-me lembrá-lo pela última vez que pode ser qualquer outra linguagem (se você adicionar um shebang) ou apenas um arquivo binário executável, mas por padrão é um shell POSIX. O script é executado com set -e e três argumentos:



  • $1

    $2 — ( )

    $3



    redo . stdout $3 . ? - , - stdout. redo:



    $ cat tgt-zstd.do
    zstd -d < $1.zst
    
    $ cat tgt-fetch.do
    fetch -o $3 SOME://URL
    


    , fetch stdout. stdout , $3. , fsync . ! , fsync — .



    , (make) clean, , . redo , . , all .



    default



    . POSIX Make .c:



    .c:
        $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<
    


    redo default.do , default.---.do. Make :



    $ cat default.c.do
    $CC $CLFAGS $LDFLAGS -o $3 $1
    


    $2 , $1 «» redo . default- :



    a.b.c.do       -> $2=a.b.c
    default.do     -> $2=a.b.c
    default.c.do   -> $2=a.b
    default.b.c.do -> $2=a
    


    , , . cd dir; redo tgt redo dir/tgt. .do . , .



    -.do , default.do . , .do ../a/b/xtarget.y :



    ./../a/b/xtarget.y.do
    ./../a/b/default.y.do
    ./../a/b/default.do
    ./../a/default.y.do
    ./../a/default.do
    ./../default.y.do
    ./../default.do
    


    2/3 redo .





    redo-ifchange :



    $ cat hello-world.do
    redo-ifchange hello-world.o ../config
    . ../config
    $CC $CFLAGS -o $3 hello-world.o
    
    $ cat hello-world.o.do
    redo-ifchange hw.c hw.h ../config
    . ../config
    $CC $CFLAGS -c -o $3 hw.c
    
    $ cat ../config
    CC=cc
    CFLAGS=-g
    
    $ cat ../all.do
    #       , ,  <em>redo</em>,  
    # hw/hello-world   
    redo-ifchange hw/hello-world
    
    #    
    $ cat ../clean.do
    redo hw/clean
    
    $ cat clean.do
    rm -f *.o hello-world
    


    redo : state. . redo-ifchange , - , - , , , , . .do . , config hello-world .



    state? . - TSV-like -.do.state, - , .redo , - SQLite3 .redo .



    stderr - , - state, « - ».



    state? redo : , FUSE/mmap/NFS/VCS, . ctime, inode number, — , .



    state lock- Make — . ( ) state lock- . .





    , redo-ifchange - , . — . redo-ifchange , :



    redo-ifchange $2.c
    gcc -o $3 -c $2.c -MMD -MF $2.deps
    read deps < $2.deps
    redo-ifchange ${deps#*:}
    


    , include-:



    $ cat default.o.do
    deps=`sed -n 's/^#include "\(.*\)"$/\1/p' < $2.c`
    redo-ifchange ../config $deps
    [...]
    


    *.c?



    for f in *.c ; do echo ${f%.c}.o ; done | xargs redo-ifchange
    


    .do (....do.do ) . .do $CC $CFLAGS..., « »:



    $ cat tgt.do
    redo-ifchange $1.c cc
    ./cc $3 $1.c
    
    $ cat cc.do
    redo-ifchange ../config
    . ../config
    cat > $3 <<EOF
    #!/bin/sh -e
    $CC $CFLAGS $LDFLAGS -o \$1 \$@ $LDLIBS
    EOF
    chmod +x $3
    


    compile_flags.txt Clang LSP ?



    $ cat compile_flags.txt.do
    redo-ifchange ../config
    . ../config
    echo "$PCSC_CFLAGS $TASN1_CFLAGS $CRYPTO_CFLAGS $WHATEVER_FLAGS $CFLAGS" |
        tr " " "\n" | sed "/^$/d" | sort | uniq
    


    $PCSC_CFLAGS, $TASN1_CFLAGS? , pkg-config, autotools!



    $ cat config.do
    cat <<EOF
    [...]
    PKG_CONFIG="${PKG_CONFIG:-pkgconf}"
    
    PCSC_CFLAGS="${PCSC_CFLAGS:-`$PKG_CONFIG --cflags libpcsclite`}"
    PCSC_LDFLAGS="${PCSC_LDFLAGS:-`$PKG_CONFIG --libs-only-L libpcsclite`}"
    PCSC_LDLIBS="${PCSC_LDLIBS:-`$PKG_CONFIG --libs-only-l libpcsclite`}"
    
    TASN1_CFLAGS="${TASN1_CFLAGS:-`$PKG_CONFIG --cflags libtasn1`}"
    TASN1_LDFLAGS="${TASN1_LDFLAGS:-`$PKG_CONFIG --libs-only-L libtasn1`}"
    TASN1_LDLIBS="${TASN1_LDLIBS:-`$PKG_CONFIG --libs-only-l libtasn1`}"
    [...]
    EOF
    


    - .do , Makefile:



    foo: bar baz
        hello world
    
    .c:
        $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<
    


    :



    $ cat default.do
    case $1 in
    foo)
        redo-ifchange bar baz
        hello world
        ;;
    *.c)
        $CC $CFLAGS $LDFLAGS -o $3 $1
        ;;
    esac
    


    , default.do . .o ? special.o.do, fallback default.o.do default.do .





    redo , , « , !?» ( default ). , , , , . suckless ( , CMake, GCC, pure-C redo — ).



    • - .
    • (*BSD vs GNU) — POSIX shell , (Python, C, shell) redo .
    • / Makefile-.
    • .
    • ( ) , , .
    • — , , l **.do.


    /?



    • Make , .
    • Levei mais de um mês para desaprender o reflexo de refazer limpar , já que é um hábito depois de fazer que algo não (re) se reúna


    Eu recomendo a documentação de implementação apenwarr / redo , com toneladas de exemplos e explicações.



    Sergey Matveev , cypherpunk , desenvolvedor Python / Go / C, especialista chefe do FSUE STC Atlas.



All Articles