เรียนรู้ Makefile พร้อมตัวอย่างที่ดีที่สุด
(makefiletutorial.com)- Makefile ใช้หลักๆ เพื่อทำงานอัตโนมัติสำหรับการ คอมไพล์ C/C++ ในโปรเจ็กต์ขนาดใหญ่ โดยจะรันเฉพาะขั้นตอนบิลด์ที่จำเป็นใหม่อีกครั้งตามการเปลี่ยนแปลงของไฟล์
- Make จะเปรียบเทียบ timestamp ของไฟล์ในระบบไฟล์ ระหว่าง target และ dependency แล้วรันคำสั่งเฉพาะเมื่อ target ยังไม่มีอยู่หรือ dependency ใหม่กว่า
- กฎพื้นฐานประกอบด้วย
targets: prerequisitesและคำสั่งที่ย่อหน้าด้วยแท็บ โดยการใช้ช่องว่างแทนการย่อหน้าสามารถทำให้makeล้มเหลวได้ - สามารถขยาย flow การบิลด์ได้ด้วยตัวแปร, wildcard, automatic variables, pattern rules, conditional, functions, recursive make,
include,.PHONY,.DELETE_ON_ERROR - ตัวอย่าง Cookbook จะสร้างผลลัพธ์ใน
build/จากซอร์ส C/C++ ในsrc/และตั้งค่า การจัดการ dependency อัตโนมัติ ด้วย-MMD -MPและ-include
สถานการณ์ที่ต้องใช้ Makefile
- Makefile ใช้เพื่อตัดสินว่าควรคอมไพล์ไฟล์ใดใหม่ในโปรแกรมขนาดใหญ่
- กรณีตัวอย่างหลักของบทเรียนคือการ คอมไพล์ไฟล์ C/C++
- นอกจากงานคอมไพล์แล้ว ยังสามารถใช้ Make กับงานที่ต้องรันชุดคำสั่งตามการเปลี่ยนแปลงของไฟล์ได้ด้วย
- มีการแนะนำเครื่องมือบิลด์อื่นสำหรับ C/C++ ได้แก่ SCons, CMake, Bazel, Ninja
- สำหรับ Java มี Ant, Maven, Gradle ส่วนภาษาอย่าง Go, Rust, TypeScript มีเครื่องมือบิลด์ของตัวเอง
- ภาษาแบบ interpreter เช่น Python, Ruby, JavaScript ล้วนๆ ไม่จำเป็นต้องคอมไพล์ใหม่เมื่อไฟล์เปลี่ยน จึงไม่ได้ต้องพึ่งเครื่องมืออย่าง Makefile เสมอไป
โมเดลการทำงานพื้นฐานของ Make
- ตัวอย่างทำงานโดยสร้างไฟล์
Makefileในเทอร์มินัลที่ติดตั้งmakeแล้วรันmakeในไดเรกทอรีนั้น - กฎพื้นฐานมีรูปแบบดังนี้
targets: prerequisitescommandที่ขึ้นต้นด้วยแท็บ
- target โดยทั่วไปคือชื่อไฟล์ และคำสั่งคือขั้นตอนสำหรับสร้าง target นั้น
- prerequisites คือไฟล์ที่ต้องมีอยู่ก่อนรันคำสั่ง และเรียกอีกอย่างว่า dependencies
- บรรทัดคำสั่งใน Makefile ต้องย่อหน้าด้วย อักขระแท็บ เท่านั้น หากใช้ช่องว่าง
makeจะล้มเหลว
การเปรียบเทียบเวลาแก้ไขของ target และ dependency
make helloจะรันคำสั่งที่เชื่อมกับ targethelloแต่หากมีไฟล์helloอยู่แล้ว ก็จะไม่รันคำสั่ง- target เชื่อมโยงโดยตรงกับชื่อไฟล์ และโดยทั่วไปคำสั่งของ target จะสร้างไฟล์ที่มีชื่อเดียวกัน
- ในกฎ
blah: blah.cคำสั่งคอมไพล์จะรันเฉพาะเมื่อblahยังไม่มีอยู่ หรือblah.cใหม่กว่าblah - Make ไม่ได้ติดตามประวัติการเปลี่ยนแปลงโดยตรง แต่ใช้ timestamp ของระบบไฟล์ เป็น heuristic
- หากแก้ไขไฟล์แล้วเปลี่ยนเวลาแก้ไขให้ย้อนกลับไปในอดีต Make อาจตัดสินผิดว่าไฟล์นั้นไม่ได้เปลี่ยน
target เริ่มต้นและ clean
- หากไม่ระบุ target ให้
makeจะรัน target แรก ใน Makefile เป็นค่าเริ่มต้น - target
allใช้สำหรับสร้างหลาย target พร้อมกัน และถ้าวางเป็นกฎแรก แค่รันmakeก็จะบิลด์ทั้งหมด cleanมักใช้เป็น target สำหรับลบผลลัพธ์จากการบิลด์ แต่ไม่ใช่คำสงวนพิเศษของ Make- ถ้า
cleanไม่ใช่ target แรกและไม่ใช่ dependency ของ target อื่น ก็ต้องเรียกแบบชัดเจน เช่นmake clean - หากมีไฟล์ชื่อ
cleanอยู่จริง targetcleanอาจไม่ถูกรัน ซึ่งจะแก้ภายหลังด้วย.PHONY
ตัวแปรและการจัดการสตริง
- ตัวแปรของ Make เก็บได้เฉพาะสตริง และบทเรียนแนะนำให้ใช้
:=เป็นหลัก - การอ้างอิงตัวแปรใช้รูปแบบ
$(name)หรือ${name} - เครื่องหมายอัญประกาศเดี่ยวและอัญประกาศคู่ไม่ได้มีความหมายพิเศษใน Make เอง แต่เป็นเพียงอักขระในค่าตัวแปร
- วิธีประกาศตัวแปรมีหลักๆ สองแบบ
=: ตัวแปรแบบ recursive ซึ่งจะขยายค่าด้วยการค้นหาตัวแปรตอนถูกใช้งาน:=: ตัวแปรแบบ simple expansion ซึ่งจะขยายเฉพาะค่าที่ถูกนิยามไว้แล้ว ณ เวลาที่ประกาศ
?=จะกำหนดค่าให้ตัวแปรเฉพาะเมื่อยังไม่ได้ตั้งค่า+=ใช้เพิ่มค่าให้ตัวแปร- ตัวแปรที่ไม่ได้ประกาศจะถูกมองเป็น สตริงว่าง
- ช่องว่างท้ายบรรทัดจะไม่ถูกลบ และหากต้องการสร้างตัวแปรที่เป็นช่องว่างหนึ่งตัว สามารถใช้เทคนิคอย่าง
$(nullstring)ได้
wildcard และ automatic variables
- ใน Make ทั้ง
*และ%ต่างก็ถูกเรียกว่า wildcard แต่มีความหมายต่างกัน *ใช้ค้นหาชื่อไฟล์ที่ตรงกันในระบบไฟล์ และโดยทั่วไปปลอดภัยกว่าหากครอบด้วย ฟังก์ชัน wildcard เช่น$(wildcard *.o)แทนการใช้ตรงๆ ในการประกาศตัวแปร- หาก
*ไม่ตรงกับไฟล์ใดเลย มันอาจคงอยู่ตามเดิมเมื่อไม่ได้อยู่ในฟังก์ชันwildcard %ใช้ในกฎและฟังก์ชันบางชนิด โดยส่วนที่จับคู่ได้จะเรียกว่า stem- automatic variables ที่ใช้บ่อยมีดังนี้
$@: ชื่อ target$?: dependency ที่ใหม่กว่า target$^: dependency ทั้งหมด$<: dependency ตัวแรก
implicit rules และ pattern rules
- Make มีกฎแบบ implicit สำหรับการคอมไพล์ C อยู่แล้ว จึงอาจบิลด์ได้แม้ไม่ได้เขียนคำสั่งคอมไพล์ไว้ชัดเจน
- ตัวอย่าง implicit rule ที่พบบ่อยมีรูปแบบดังนี้
n.oสร้างจากn.cด้วยรูปแบบ$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@n.oสร้างจากn.ccหรือn.cppด้วยรูปแบบ$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@nสร้างจากn.oด้วยคำสั่ง linker
- ตัวแปรสำคัญใน implicit rule ได้แก่
CC,CXX,CFLAGS,CXXFLAGS,CPPFLAGS,LDFLAGS - static pattern rule มีรูปแบบ
targets...: target-pattern: prereq-patterns ...โดยจะนำ stem ที่จับคู่ได้จาก target ไปแทนในแพตเทิร์นของ dependency - pattern rule จะมี
%อยู่ใน target เช่น%.o: %.cและสามารถใช้เหมือน implicit rule ที่สร้างขึ้นเอง - กฎ double colon
::ช่วยให้กำหนดหลายกฎให้ target เดียวกันได้ แต่ไม่ค่อยถูกใช้
การรันคำสั่งและการจัดการข้อผิดพลาด
- หากใส่
@ไว้หน้าคำสั่ง จะไม่แสดงตัวคำสั่งนั้นออกมา - หากรัน
make -sจะทำงานเสมือนใส่@ไว้หน้าทุกบรรทัด - แต่ละบรรทัดคำสั่งมีผลเหมือนรันในเชลล์ใหม่ ดังนั้น
cd ..ในบรรทัดหนึ่งจะไม่ส่งผลต่อบรรทัดถัดไป - หากต้องการคงสถานะเชลล์เดียวกันไว้ ควรเขียนในบรรทัดเดียว เช่น
cd ..; echo \pwd`` หรือเชื่อมบรรทัดด้วย backslash - เชลล์ปริยายคือ
/bin/shและสามารถเปลี่ยนได้ เช่นSHELL=/bin/bash - หากต้องการใช้
$ของตัวแปรเชลล์ใน Makefile ต้องเขียนเป็น$$ - การจัดการข้อผิดพลาดจะแตกต่างตามวิธีรัน
make -k: แม้เกิดข้อผิดพลาดก็จะพยายามทำงานอื่นที่ทำต่อได้-หน้าคำสั่ง: เพิกเฉยต่อข้อผิดพลาดของคำสั่งนั้นmake -i: ใช้การเพิกเฉยต่อข้อผิดพลาดกับทุกคำสั่ง
- หากหยุด Make ด้วย
ctrl+cMake จะลบ target ล่าสุดที่เพิ่งสร้างขึ้น
recursive make และตัวแปรสภาพแวดล้อม
- เมื่อต้องเรียก make ซ้ำจากใน Makefile ควรใช้ตัวแปรพิเศษ
$(MAKE)แทนmake $(MAKE)จะส่งต่อ flag ของ make และจัดการผลกระทบของ flag ได้อย่างเหมาะสม- เมื่อ Make เริ่มทำงาน มันจะสร้างตัวแปร Make จากตัวแปรสภาพแวดล้อมที่ถูกตั้งไว้ตอนรันโดยอัตโนมัติ
- คำสั่ง
exportจะตั้งค่าตัวแปรให้เข้าไปอยู่ในสภาพแวดล้อมของคำสั่งเชลล์ทุก recipe - หากต้องการใช้ตัวแปรใน make ชั้นล่างด้วย ต้อง
exportตัวแปรนั้น - หากใช้
.EXPORT_ALL_VARIABLESจะ export ตัวแปรทั้งหมดได้ - หากระบุหลาย target พร้อมกัน เช่น
make clean run testก็จะทำงานตามลำดับclean,run,test
conditional และ functions
- conditional ประกอบด้วย
ifeq,ifneq,ifdef,ifndef,else,endif ifdefจะไม่ขยายการอ้างอิงตัวแปร แต่ตรวจเพียงว่ามีการประกาศตัวแปรนั้นหรือไม่- สามารถใช้
$(MAKEFLAGS)และfindstringเพื่อตรวจว่ามีการส่ง flag อย่างmake -iมาหรือไม่ - ฟังก์ชันของ Make ใช้หลักๆ สำหรับ การประมวลผลข้อความ และเรียกในรูปแบบ
$(fn, arguments)หรือ${fn, arguments} - ฟังก์ชันหลักมีดังนี้
subst: แทนที่สตริงpatsubst: แทนที่คำตามแพตเทิร์นforeach: แปลงรายการคำที่คั่นด้วยช่องว่างif: หากอาร์กิวเมนต์แรกไม่ว่าง ให้ใช้อาร์กิวเมนต์ที่สอง ไม่เช่นนั้นใช้อาร์กิวเมนต์ที่สามcall: เรียกตัวแปรเหมือนฟังก์ชันทั่วไป และใช้พารามิเตอร์อย่าง$(1),$(2)shell: เรียกเชลล์ แต่จะแปลงขึ้นบรรทัดใหม่เป็นช่องว่างfilter: เลือกสมาชิกในรายการที่ตรงกับแพตเทิร์นที่กำหนดfilter-out: เลือกสมาชิกที่ไม่ตรงกับแพตเทิร์นที่กำหนด
include, vpath, .PHONY, .DELETE_ON_ERROR
include filenames...ใช้สำหรับอ่าน Makefile อื่นเพิ่มเติมหนึ่งไฟล์หรือมากกว่าincludeมีประโยชน์เมื่อใช้ร่วมกับ Makefile ที่สร้างจากออปชันอย่าง-Mของgccvpath <pattern> <directories>ใช้ระบุไดเรกทอรีที่จะค้นหาไฟล์ dependency บางประเภท- ตัวแปร
VPATHสามารถกำหนดพฤติกรรมเส้นทางค้นหาที่คล้ายกันในระดับ global ได้ - backslash
\ใช้สำหรับแบ่งคำสั่งยาวๆ ออกเป็นหลายบรรทัด - เมื่อเพิ่ม
.PHONYให้กับ target แล้ว Make จะไม่สับสน target นั้นกับไฟล์ชื่อเดียวกัน .DELETE_ON_ERRORจะลบ target ของกฎนั้น หากระหว่างรันกฎมีคำสั่งใดคืนค่าสถานะจบการทำงานที่ไม่ใช่ 0.DELETE_ON_ERRORไม่ได้ใช้กับเฉพาะ target ที่อยู่ก่อนหน้าเหมือนPHONYแต่มีผลกับทุก target
ตัวอย่าง Makefile Cookbook
- ตัวอย่าง Cookbook มี เทมเพลต Makefile สำหรับโปรเจ็กต์ขนาดกลาง
- หากวางไฟล์ C/C++ ไว้ในโฟลเดอร์
src/Makefile จะกำหนด target สำหรับคอมไพล์และ dependency ให้อัตโนมัติ - การตั้งค่าตัวแปรหลักมีดังนี้
TARGET_EXEC := final_programBUILD_DIR := ./buildSRC_DIRS := ./src
SRCSใช้findและ$(shell ...)เพื่อค้นหาไฟล์*.cpp,*.c,*.sOBJSจะเติมBUILD_DIRไว้หน้าซอร์สแต่ละไฟล์และเติม.oเพื่อสร้างพาธของผลลัพธ์บิลด์DEPSจะเปลี่ยน.oเป็น.dเพื่อสร้างรายการ Makefile ของ dependencyINC_DIRSจะค้นหาไดเรกทอรีทั้งหมดใต้src/และINC_FLAGSจะเติม prefix-Iให้แต่ละไดเรกทอรีCPPFLAGS := $(INC_FLAGS) -MMD -MPเป็นการตั้งค่าให้ GCC สร้างไฟล์ dependency แบบ.d- ซอร์ส C และ C++ จะใช้ pattern rule แยกกันเพื่อสร้างไฟล์
.oใต้build/และจะสร้างไดเรกทอรีที่จำเป็นด้วยmkdir -p $(dir $@) cleanถูกประกาศเป็น.PHONYและลบไดเรกทอรีบิลด์ด้วยrm -r $(BUILD_DIR)-include $(DEPS)จะ include ไฟล์.dแต่กดข้อผิดพลาดไว้หากไฟล์ยังไม่มี เช่น ในการบิลด์ครั้งแรก
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
มีแฟล็กของ
makeอยู่หลายตัวที่ไม่ค่อยเป็นที่รู้จักแต่มีประโยชน์ การซิงก์เอาต์พุต จะทำให้ stdout/stderr ถูกแสดงก็ต่อเมื่อทาร์เก็ตเสร็จแล้ว ช่วยลดปัญหาล็อกปนกันจนอ่านยากตอนบิลด์แบบขนานmake --output-sync=recurse -j10บนระบบหลายผู้ใช้ที่งานยุ่ง แค่จำนวนงานของ
-jอาจไม่พอ จึงสามารถจำกัดความขนานตามค่า load average ได้ด้วย:make -j10 --load-average=10--shuffleที่สุ่มลำดับการจัดตารางทาร์เก็ตก็มีประโยชน์ในการจับ dependency ที่ขาดหายไปใน Makefile บน CI:make --shuffle # or --shuffle=seed/reverse-Bซึ่งเป็นตัวเลือกที่สั่งให้บิลด์ใหม่ทั้งหมดแบบไม่มีเงื่อนไขmake -jทำให้เครื่องเข้าสู่สภาพ DoS อยู่หลายครั้ง จนมองว่าแทบจะเป็นบั๊กราวปี 1985 ที่ Boston University Graphics lab ผมเคยเห็นคนใช้ Makefile ขับเคลื่อนการ สร้างเรนเดอร์เรอร์ 3D สำหรับแอนิเมชัน เขาเป็นสาย Lisp และทำงานกับการสร้างเชิงกระบวนวิธีและระบบนักแสดง 3D ยุคแรก ๆ โดย Makefile มีแค่ราว 10 บรรทัดและสง่างามมาก
ใช้เพียง dependency จากวันที่ของไฟล์ธรรมดา ๆ ก็สร้างแอนิเมชันได้เป็นร้อย ๆ เรื่อง โดย Lisp สร้างรูปทรง 3D ของแต่ละเฟรม แล้ว Make ก็สร้างเฟรมนั้นขึ้นมา
ตอนนั้นเป็นปี 1985 ซึ่งสิ่งที่ทุกวันนี้เรามองว่าเป็นเรื่องปกติใน 3D และแอนิเมชันแทบยังไม่มีเลย จึงทำให้ทุกคนทึ่ง และผมจำได้ว่าเขาต่อมาใช้เรนเดอร์เรอร์ 3D ของ Iron Giant และมีบทบาทสำคัญใน Coraline ด้วย เขาคือ Brian Gardner
Make เป็นหนึ่งในเครื่องมือที่ผมรู้สึกโชคดีมากที่ได้เรียนรู้ตั้งแต่ช่วงต้นของอาชีพ ตอนนี้ไม่ได้ใช้มากแล้ว แต่เพราะมันแสดงให้เห็นพลังของ ระบบเชิงประกาศ เมื่อเทียบกับ ระบบเชิงคำสั่ง
ผมตระหนักว่าไอเดียนี้ขยายไปสู่งานอื่น ๆ ได้อย่างเป็นธรรมชาติ ถ้าดูภาพด้านบนของไซต์นี้ ผู้เขียนก็น่าจะรู้สึกคล้ายกัน คือเราสามารถเข้าใจสูตรอาหารได้ดีขึ้นเมื่อมองแบบเชิงประกาศเหมือน Makefile แทนที่จะเขียนเป็นลำดับคำสั่งแบบสคริปต์เหมือนสูตรอาหารดั้งเดิม
บทความที่เกี่ยวข้อง: https://blog.gpkb.org/posts/cooking-with-make/
ผมมักเขียนสูตรอาหารให้อ่านได้เหมือน Makefile แล้วเอาเข้าครัวเสมอ เลยสงสัยว่ามีใครเคยจัดรูปแบบหรือแสดงผลแบบนี้บ้างไหม น่าจะช่วยประหยัดเวลาได้มากเวลาอ่านสูตรใหม่ ๆ เพราะไม่ต้องแปลงสคริปต์เป็น Makefile ในหัวอีก
แน่นอนว่าราคาที่ต้องจ่ายคือคุณต้องแก้กราฟ dependency เอง ถ้าแค่อยากทำตามขั้นตอนที่ถูกเรียงลำดับไว้ล่วงหน้า ก็จะต้องรับงานเพิ่มมากขึ้น
ในบทความบอกว่าสูตรส่วนใหญ่ไม่ได้ทำเครื่องหมายเป็น
.PHONYและใช้เป็นเหตุผลทำนองว่าในทิวทอเรียลจึงไม่จำเป็นต้องพูดถึง แต่นั่นเป็นข้ออ้างที่อ่อน เครื่องมือควรสอน วิธีใช้ที่ถูกต้องในทีมเราใช้ make เป็นตัวรันงาน ผมเลยโดนบ่นเรื่องการเพิ่มและดูแล
.PHONYให้ทุกสูตรคู่มือสไตล์ Makefile ของ Clark Grubb ดีมาก: https://clarkgrubb.com/makefile-style-guide
สงสัยว่ามีใครใช้คู่มือสไตล์นี้ไหม และก็สงสัยด้วยว่าทำเครื่องหมายสูตร phony ณ จุดที่ประกาศ หรือวางเป็นรายการยักษ์ไว้บนสุดของไฟล์ ถ้ามี ลินเตอร์ ที่บังคับเรื่องนี้ได้ก็คงดี
การใช้
-o pipefailตามความเคยชินไม่ค่อยดี Pipefail มีประโยชน์ก็จริง แต่จะทำให้สิ่งที่คนทำบ่อยที่สุดใน pipeline อย่างการกรองเอาต์พุตด้วยgrepพัง ใส่เฉพาะใน recipe ที่จำเป็นเป็นรายกรณีจะดีกว่าการทำเครื่องหมายทาร์เก็ตที่ไม่ใช่ไฟล์เป็น
.PHONYนั้นถูกต้องในเชิงเคร่งครัด แต่ส่วนใหญ่ไม่จำเป็น ถ้ามีทาร์เก็ตเยอะ จะเพิ่มความเยิ่นเย้อที่ไม่จำเป็นให้ Makefile ดังนั้นผมคิดว่าควรเพิ่มเฉพาะเมื่อจำเป็นสำหรับ recipe ที่สร้างไฟล์เอาต์พุตหลายไฟล์ สมัยก่อนเมื่อ pattern rule ไม่เหมาะ ไฟล์ dummy/flag file เคยเป็นมาตรฐาน แต่ตั้งแต่ GNU Make 4.3 รองรับ group target แบบเนทีฟแล้ว และมีอยู่ใน Ubuntu 22.04 LTS ด้วย: https://www.gnu.org/software/make/manual/html_node/Multiple-...
จุดที่น่าสนใจเมื่อเร็ว ๆ นี้คือ CMake ตัดสินว่า Makefile ไม่เหมาะกับโปรเจกต์ที่ใช้ C++20 modules และ Ninja จึงเหมาะกว่า https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
โดยพื้นฐานแล้วดูเหมือนจะมองว่าเป็นเรื่องยากเกินไปหรือแทบเป็นไปไม่ได้ที่จะกำหนด dependency ของ target แบบ static ตอนนี้จึงจัดการแบบ dynamic ด้วยเครื่องมืออย่าง
clang-scan-deps: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...ที่จริง Ninja ต้องกำหนด dependency ทั้งหมดแบบ static ดังนั้นในแง่นี้จึงมีความสามารถน้อยกว่า Make ทั่วไปด้วยซ้ำ
Make ยังมีที่ทางของมันในฐานะ เครื่องมือ build สำหรับ codebase ภาษา C ขนาดใหญ่ แต่บ่อยครั้งมันถูกใช้เหมือน “ตัวรันงานรายโปรเจกต์” แบบทั่วไป ซึ่งไม่ค่อยเหมาะ เพราะแม้แต่ conditional ง่าย ๆ ก็ยังทำได้ยาก
ตัวอย่างเช่น เคยเห็นความพยายามด้วยเจตนาดีหลายครั้งที่จะครอบ Terraform ด้วย Make แต่สุดท้ายก็จบไม่สวย
เครื่องมือสมัยใหม่ที่ยอดเยี่ยมสำหรับแทนส่วนที่ทำให้ Makefile รกคือ just: https://github.com/casey/just
ถ้าเป็นแนวคิดคนละแบบโดยสิ้นเชิง ก็มี Makedown ที่เคยถูกพูดถึงบน HN เมื่อ 8 เดือนก่อน: https://news.ycombinator.com/item?id=41825344
Makefile นั้นยอดเยี่ยม แต่อย่าอินกับมันมากเกินไปจะดีกว่า หลายปีก่อนผมเคยพยายามสร้าง GNU Make framework แบบล้วน ๆ แล้วก็รู้ตัวว่ากำลังประดิษฐ์ autoconf ขึ้นมาใหม่โดยพฤตินัย ตอนนั้นเองถึงเข้าใจว่าทำไม GNU autotools ถึงถูกสร้างขึ้นมา
Makefile เป็น Turing tarpit ที่ให้ความรู้สึกคล้าย Lisp อย่างประหลาด GNU Make มีความสามารถด้าน metaprogramming ด้วย ทำให้ยากจะห้ามใจไม่ให้ metaprogram ระบบต้องห้ามบางอย่างไว้ใน Makefile อีกทั้งการที่ GNU Make ติดตั้งแพร่หลายมากก็ยิ่งเพิ่มแรงยั่วยวนนี้
ผมเป็นคนสร้างและเป็นหนึ่งในผู้ดูแล Task ซึ่งเป็นทางเลือกแทน Make มันมีมานานกว่า 8 ปีแล้วและยังพัฒนาอย่างต่อเนื่อง ถ้ากำลังมองหาอะไรใหม่ ๆ ก็อยากให้ลองใช้ดู ถามคำถามได้ตามสบาย
https://taskfile.dev/
https://github.com/go-task/task
สงสัยว่ามีใครเคยใช้ tup บ้างไหม: https://gittup.org/tup/ex_dependencies.html
มันเป็น build system ที่ตรวจหา dependency โดยอัตโนมัติจากการเข้าถึงไฟล์ระบบ จึงใช้ร่วมกับ compiler หรือเครื่องมือใด ๆ ก็ได้