Pakai sabun, ulangi tenaga

Salam pembuka! Saya ingin berbicara tentang kekurangan utama, tidak selalu jelas, dari sistem Make build , yang sering membuatnya tidak dapat digunakan, dan juga berbicara tentang alternatif dan solusi yang sangat baik untuk masalah - yang paling cerdik dalam kesederhanaannya, sistem redo . Ide DJB terkenal , yang kriptografinya tidak digunakan di mana pun. Secara pribadi, redo sangat mengesankan saya dengan kesederhanaan yang mengubah hidup, fleksibilitas, dan performa yang jauh lebih baik dari tugas build yang saya gantikan sepenuhnya dengan Make di hampir semua project saya (di mana saya tidak menggantinya, itu berarti saya belum mendapatkannya), yang tidak dapat saya temukan. satu manfaat atau alasan untuk tetap hidup.





Make lain?



Banyak orang tidak puas dengan Make, jika tidak, tidak akan ada lusinan sistem build lain dan puluhan dialek Make saja. Sebuah mengulang satu ini alternatif lain? Di satu sisi, tentu saja, ya - hanya sangat sederhana, namun mampu memecahkan benar-benar semua tugas yang sama seperti Membuat. Di sisi lain, apakah kita memiliki beberapa Make yang umum dan seragam?



Kebanyakan sistem build "alternatif" lahir karena mereka tidak memiliki kemampuan Make asli, tidak memiliki fleksibilitas. Banyak sistem hanya mementingkan pembuatan Makefile, bukan membangunnya sendiri. Banyak yang disesuaikan dengan ekosistem bahasa pemrograman tertentu.



Di bawah ini saya akan mencoba menunjukkan pengulangan itu adalah sistem yang jauh lebih penting, bukan hanya solusi lain.



Make selalu ada



Secara pribadi, saya masih selalu memandang curiga pada keseluruhan alternatif ini, karena lebih kompleks, atau ekosistem / bahasa-spesifik, atau ketergantungan tambahan yang perlu diatur dan dipelajari bagaimana menggunakannya. Dan Make adalah hal yang, plus atau minus, semua orang sudah familiar dan tahu bagaimana menggunakan pada tingkat dasar. Oleh karena itu, selalu dan di mana pun saya mencoba menggunakan POSIX Make, dengan asumsi bahwa ini adalah sesuatu yang setiap orang miliki di sistem (POSIX) di luar kotak, seperti kompiler C. Dan tugas di Make untuk melakukan hanya yang dimaksudkan: eksekusi tujuan yang dapat diparalelkan (perintah ) dengan mempertimbangkan ketergantungan di antara mereka.



Apa masalahnya hanya dengan menulis di Make dan memastikannya berfungsi di sistem apa pun? Lagi pula, Anda dapat (harus!) Menulis di shell POSIX dan tidak memaksa pengguna untuk menginstal GNU Bash yang sangat besar. Satu-satunya masalah adalah bahwa hanya dialek POSIX Make yang akan berfungsi, yang cukup langka bahkan untuk banyak proyek kecil yang sederhana. Membuat sistem BSD modern lebih kompleks dan penuh fitur. Nah, dengan GNU Make, hanya sedikit yang dapat dibandingkan dengan siapa pun, meskipun hampir tidak ada yang menggunakan kemampuan penuhnya dan tidak tahu bagaimana menggunakannya. Tetapi GNU Make tidak mendukung dialek sistem BSD modern. Sistem BSD tidak memiliki GNU Make di dalamnya (dan dapat dimengerti!).



Menggunakan dialek BSD / GNU berarti berpotensi memaksa pengguna untuk menginstal perangkat lunak tambahan yang tidak dikeluarkan dari kotaknya. Dalam hal ini, keuntungan yang mungkin dari Make - keberadaannya di sistem, dibatalkan.



Dimungkinkan untuk menggunakan dan menulis di POSIX Make, tetapi sulit. Secara pribadi, saya langsung teringat dua kasus yang sangat mengganggu:



  • Beberapa Membuat implementasi, saat menjalankan $ (MAKE) -C, "pergi" ke direktori tempat Make baru dijalankan, dan beberapa tidak. Apakah mungkin untuk menulis Makefile agar berfungsi sama di mana-mana? Tentu saja:



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


    Nyaman? Tentu saja tidak. Dan sungguh tidak menyenangkan bahwa seseorang harus selalu mengingat tentang hal-hal sepele seperti itu.
  • Dalam POSIX Make, tidak ada pernyataan yang mengeksekusi panggilan shell dan menyimpan hasilnya dalam variabel. Di GNU Make up to versi 4.x Anda dapat melakukan:



    VAR = $(shell cat VERSION)
    


    dan mulai dengan 4.x, serta dalam dialek BSD, Anda dapat melakukan:



    VAR != cat VERSION
    


    Tidak cukup tindakan yang sama dapat dilakukan:



    VAR = `cat VERSION`
    


    tetapi secara harfiah menggantikan ekspresi ini dalam perintah shell Anda yang dijelaskan dalam target. Pendekatan ini digunakan dalam suckless proyek, tetapi, tentu saja, kruk.


Secara pribadi, di tempat-tempat seperti itu, saya sering menulis Makefiles untuk tiga dialek sekaligus (GNU, BSD dan POSIX):



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

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


Nyaman? Jauh dari itu! Meskipun tugasnya sangat sederhana dan umum. Jadi ternyata:



  • Tulis secara paralel untuk beberapa dialek Make. Trading waktu pengembang untuk kenyamanan pengguna.
  • Mengingat banyak nuansa dan hal sepele, mungkin dengan substitusi yang tidak efisien ( `cmd ...` ), coba tulis di POSIX Make. Bagi saya pribadi, dengan pengalaman bertahun-tahun dengan GNU / BSD Make, opsi ini paling memakan waktu (lebih mudah untuk menulis dalam beberapa dialek).
  • Tulis di salah satu dialek Make, yang memaksa pengguna untuk menginstal perangkat lunak pihak ketiga.


Buat masalah teknis



Tapi semuanya jauh lebih buruk karena Make mana pun tidak mengatakan bahwa itu (baik) mengatasi tugas yang diberikan padanya.



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


Mari kita akui dengan jujur: seberapa sering dan seberapa banyak yang harus Anda lakukan untuk membersihkan atau membangun kembali tanpa paralelisasi, karena ada sesuatu yang tidak dikompilasi atau tidak dibangun ulang bertentangan dengan harapan? Dalam kasus umum, tentu saja, ini bukan karena Makefile yang ditulis dengan benar, benar dan lengkap, yang berbicara tentang kompleksitas tulisan mereka yang kompeten dan efisien. Alat itu akan membantu.



Ulangi persyaratan



Untuk melanjutkan ke deskripsi redo , pertama-tama saya akan memberi tahu Anda apa itu sebagai implementasi dan apa yang harus dipelajari "pengguna" (pengembang menjelaskan tujuan dan ketergantungan di antara mereka).



  • 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



Aturan build target adalah skrip shell POSIX reguler di target_name.do . Izinkan saya mengingatkan Anda untuk terakhir kalinya bahwa itu bisa berupa bahasa lain (jika Anda menambahkan shebang) atau hanya file biner yang dapat dieksekusi, tetapi secara default itu adalah shell POSIX. Skrip dijalankan dengan set -e dan tiga argumen:



  • $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 , .
    • Butuh waktu lebih dari satu bulan untuk menghilangkan refleks melakukan redo clean , karena sudah menjadi kebiasaan setelah Make bahwa sesuatu tidak akan berkumpul (kembali).


    Saya merekomendasikan dokumentasi implementasi apenwarr / redo , dengan banyak contoh dan penjelasan.



    Sergey Matveev , cypherpunk , Python / Go / C-developer, kepala spesialis FSUE STC Atlas.



All Articles