Faire du savon, refaire le pouvoir

Salutations! Je veux parler des principales lacunes, pas toujours évidentes, du système Make build , qui le rendent souvent inutilisable, et aussi parler d'une excellente alternative et solution au problème - le plus ingénieux dans sa simplicité, le système de redo . L'idée du célèbre DJB , dont la cryptographie n'est utilisée nulle part. Personnellement, refaire m'a tellement impressionné par la simplicité, la flexibilité et les performances bien meilleures des tâches de construction que j'ai complètement remplacé Make avec dans presque tous mes projets (là où je ne l'ai pas remplacé, cela signifie que je ne l'ai pas encore mis la main), dont je n'ai pas pu en trouver un avantage ou une raison de rester en vie.





Encore une autre marque?



Beaucoup de gens ne sont pas satisfaits de Make, sinon il n'y aurait pas des dizaines d'autres systèmes de construction et des dizaines de dialectes de Make seul. Un refaire celui-ci encore une autre alternative? D'une part, bien sûr, oui - extrêmement simple, mais capable de résoudre absolument toutes les mêmes tâches que Make. D'un autre côté, avons-nous une marque commune et uniforme?



La plupart des systèmes de construction «alternatifs» sont nés parce qu'ils n'avaient pas les capacités natives de Make, manquaient de flexibilité. De nombreux systèmes se préoccupent uniquement de générer des Makefiles, pas de les construire eux-mêmes. Beaucoup sont adaptés à l'écosystème de certains langages de programmation.



Ci-dessous, je vais essayer de montrer que refaire est un système beaucoup plus remarquable, pas seulement une autre solution.



Make est toujours là de toute façon



Personnellement, j'ai toujours regardé avec méfiance toute cette alternative, car elle est soit plus complexe, soit spécifique à un écosystème / à une langue, soit une dépendance supplémentaire qui doit être définie et appris à l'utiliser. Et Make est une chose telle que, plus ou moins, tout le monde est familier et sait comment l'utiliser à un niveau de base. Par conséquent, toujours et partout, j'ai essayé d'utiliser POSIX Make, en supposant que c'est quelque chose que tout le monde a dans le système (POSIX) prêt à l'emploi, comme le compilateur C. Et les tâches de Make à effectuer uniquement pour lesquelles il est destiné: exécution parallélisée des objectifs (commandes ) en tenant compte des dépendances entre eux.



Quel est le problème avec le simple fait d'écrire dans Make et de s'assurer qu'il fonctionne sur n'importe quel système? Après tout, vous pouvez (devez!) Écrire dans le shell POSIX et ne pas forcer les utilisateurs à installer un énorme GNU Bash monstrueux. Le seul problème est que seul le dialecte POSIX Make fonctionnera, ce qui est assez rare même pour de nombreux petits projets simples. Faire sur les systèmes BSD modernes est plus complexe et complet. Eh bien, avec GNU Make, peu de gens peuvent se comparer à n'importe qui, même si presque personne n'utilise ses capacités au maximum et ne sait pas comment les utiliser. Mais GNU Make ne prend pas en charge un dialecte des systèmes BSD modernes. Les systèmes BSD n'ont pas GNU Make en eux (et ils sont compréhensibles!).



Utiliser un dialecte BSD / GNU signifie potentiellement forcer l'utilisateur à installer un logiciel supplémentaire qui ne sort pas de la boîte de toute façon. Dans ce cas, l'avantage éventuel de Make - sa présence dans le système, est annulé.



Il est possible d'utiliser et d'écrire dans POSIX Make, mais difficile. Personnellement, je me souviens immédiatement de deux cas très ennuyeux:



  • Certaines implémentations Make, lors de l'exécution de $ (MAKE) -C, "vont" dans le répertoire où le nouveau Make est exécuté, et d'autres non. Est-il possible d'écrire un Makefile pour qu'il fonctionne de la même manière partout? Bien sûr:



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


    Idéalement? Définitivement pas. Et il est désagréable de devoir constamment se souvenir de telles bagatelles.
  • Dans POSIX Make, il n'y a aucune instruction qui exécute un appel shell et stocke son résultat dans une variable. Dans GNU Make up jusqu'à la version 4.x, vous pouvez faire:



    VAR = $(shell cat VERSION)
    


    et à partir de 4.x, ainsi que dans les dialectes BSD, vous pouvez faire:



    VAR != cat VERSION
    


    Pas tout à fait la même action peut être effectuée:



    VAR = `cat VERSION`
    


    mais il remplace littéralement cette expression dans vos commandes shell décrites dans les cibles. Cette approche est utilisée dans les projets sans succion , mais c'est, bien sûr, une béquille.


Personnellement, dans de tels endroits, j'ai souvent écrit des Makefiles pour trois dialectes à la fois (GNU, BSD et POSIX):



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

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


Idéalement? Loin de là! Bien que les tâches soient extrêmement simples et courantes. Il s'avère donc que soit:



  • Écrivez en parallèle pour plusieurs dialectes Make. Échangez le temps des développeurs pour la commodité de l'utilisateur.
  • En gardant à l'esprit beaucoup de nuances et d'anecdotes, peut-être avec des substitutions inefficaces ( `cmd ...` ), essayez d'écrire dans POSIX Make. Pour moi personnellement, avec de nombreuses années d'expérience avec GNU / BSD Make, cette option est la plus longue (c'est plus facile d'écrire dans plusieurs dialectes).
  • Écrivez dans l'un des dialectes Make, forçant l'utilisateur à installer un logiciel tiers.


Faire des problèmes techniques



Mais tout est bien pire car tout Make ne dit pas qu'il fait (bien) face aux tâches qui lui sont assignées.



  • 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, . ?


Admettons honnêtement: combien de fois et combien avez-vous dû faire nettoyer ou reconstruire sans parallélisation, parce que quelque chose n'a pas été compilé ou pas reconstruit contrairement aux attentes? Dans le cas général, bien sûr, cela n'est pas dû à des Makefiles idéalement corrects, correctement et entièrement écrits, qui parlent de la complexité de leur écriture compétente et efficace. L'outil devrait aider.



Refaire les exigences



Pour passer à la description de redo , je vais d'abord vous dire ce que c'est en tant qu'implémentation et ce que l '«utilisateur» (le développeur qui décrit les objectifs et les dépendances entre eux) devra apprendre.



  • 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



Les règles de construction cible sont un script shell POSIX normal dans nom_cible.do . Permettez-moi de vous rappeler pour la dernière fois qu'il peut s'agir de n'importe quel autre langage (si vous ajoutez un shebang) ou simplement d'un fichier binaire exécutable, mais par défaut c'est un shell POSIX. Le script est exécuté avec l' ensemble -e et trois arguments:



  • $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 , .
    • Il m'a fallu plus d'un mois pour désapprendre le réflexe de refaire le nettoyage , car c'est déjà une habitude après Faire que quelque chose ne se (re) rassemble pas.


    Je recommande la documentation d' implémentation apenwarr / redo , avec des tonnes d' exemples et d'explications.



    Sergey Matveev , cypherpunk , développeur Python / Go / C, spécialiste en chef de FSUE STC Atlas.



All Articles