เรียนรู้ Makefile พร้อมตัวอย่างที่ดีที่สุด
(makefiletutorial.com)- Makefile เป็นเครื่องมือที่ช่วยให้การทำ build อัตโนมัติและการจัดการ dependency ของ C/C++ ง่ายขึ้น
- ใช้วิธี ตรวจจับไฟล์ที่เปลี่ยนแปลงด้วย timestamp เพื่อคอมไพล์เฉพาะเมื่อจำเป็นเท่านั้น
- อธิบาย โครงสร้างหลัก อย่าง rule, command และ prerequisite พร้อมตัวอย่าง
- ครอบคลุมฟีเจอร์ขั้นสูงอย่าง automatic variables, pattern rules และ variable expansion ในเชิงปฏิบัติ
- แนะนำความสำคัญของการขยายระบบและการดูแลรักษาผ่าน เทมเพลต Makefile ใช้งานจริง สำหรับโปรเจ็กต์ขนาดกลาง
แนะนำคู่มือสอน Makefile
- Makefile เป็นเครื่องมือหลักสำหรับการทำ build อัตโนมัติและจัดการ dependency ของโปรเจ็กต์
- แม้จะดูซับซ้อนสำหรับผู้เริ่มต้นเพราะมีทั้งกฎและสัญลักษณ์แฝงหลายแบบ แต่คู่มือนี้สรุปประเด็นสำคัญด้วยตัวอย่างที่กระชับและนำไปรันได้ทันที
- แต่ละส่วนสามารถทำความเข้าใจได้ผ่านตัวอย่างแบบลงมือปฏิบัติ
เริ่มต้นใช้งาน
จุดประสงค์ของ Makefile
- Makefile ใช้เพื่อ คอมไพล์ซ้ำเฉพาะส่วนที่มีการเปลี่ยนแปลง ในโปรแกรมขนาดใหญ่
- นอกจาก C/C++ แล้ว ภาษาต่าง ๆ ก็มีเครื่องมือ build เฉพาะของตนเอง แต่ Make ยังใช้ได้กับสถานการณ์การ build ทั่วไป
- แกนสำคัญคือ ตรวจจับไฟล์ที่เปลี่ยนและรันเฉพาะงานที่จำเป็น
ระบบ build ทางเลือกของ Make
- สาย C/C++: มีตัวเลือกอย่าง SCons, CMake, Bazel, Ninja เป็นต้น
- สาย Java: Ant, Maven, Gradle เป็นต้น
- Go, Rust, TypeScript ก็มีเครื่องมือ build ของตัวเองเช่นกัน
- ภาษาแบบ interpreter อย่าง Python, Ruby, JavaScript ไม่ต้องคอมไพล์ จึงมีความจำเป็นน้อยกว่าที่จะต้องจัดการแยกด้วย Makefile
เวอร์ชันและชนิดของ Make
- แม้จะมี Make หลาย implementation แต่คู่มือนี้ปรับให้เหมาะกับ GNU Make (ใช้เป็นหลักบน Linux และ MacOS)
- ตัวอย่างทั้งหมดใช้ได้กับ GNU Make เวอร์ชัน 3 และ 4
วิธีรันตัวอย่าง
- ติดตั้ง make ในเทอร์มินัล จากนั้นบันทึกแต่ละตัวอย่างเป็นไฟล์
Makefileแล้วรันคำสั่งmake - บรรทัดคำสั่งใน Makefile ต้องย่อหน้าด้วย อักขระแท็บ เท่านั้น
ไวยากรณ์พื้นฐานของ Makefile
โครงสร้างของกฎ (Rule)
-
target: prerequisite(s)- command
- command
-
target: ชื่อไฟล์ผลลัพธ์จากการ build (ปกติมีหนึ่งไฟล์)
-
command: shell script ที่ถูกรันจริง (ขึ้นต้นด้วยแท็บ)
-
prerequisite: รายชื่อไฟล์ที่ต้องพร้อมก่อนจะ build target
แก่นของ Make
ตัวอย่าง Hello World
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
- target
helloไม่มี dependency และรันคำสั่ง 2 บรรทัด - เมื่อรัน
make helloหากยังไม่มีไฟล์helloคำสั่งจะถูกรัน แต่ถ้ามีไฟล์อยู่แล้วจะไม่รัน - โดยทั่วไปมักเขียนให้ target ตรงกับชื่อไฟล์
ตัวอย่างพื้นฐานของการคอมไพล์ไฟล์ C
- สร้างไฟล์
blah.c(มีเนื้อหาint main() { return 0; }) - เขียน Makefile ดังนี้
blah:
cc blah.c -o blah
- เมื่อรัน
makeหากยังไม่มี targetblahก็จะคอมไพล์และสร้างไฟล์blah - แต่ถึง
blah.cจะถูกแก้ไข ก็จะไม่คอมไพล์ใหม่อัตโนมัติ → ต้องเพิ่ม dependency
วิธีเพิ่ม dependency
blah: blah.c
cc blah.c -o blah
- ตอนนี้ถ้า
blah.cมีการเปลี่ยนแปลงใหม่ targetblahจะถูก build อีกครั้ง - ใช้ timestamp ของไฟล์เป็นเกณฑ์ในการตรวจจับการเปลี่ยนแปลง
- หากแก้ timestamp เอง อาจทำให้การทำงานไม่เป็นไปตามที่ตั้งใจ
ตัวอย่างเพิ่มเติม
ตัวอย่าง target และ dependency แบบเชื่อมโยงกัน
blah: blah.o
cc blah.o -o blah
blah.o: blah.c
cc -c blah.c -o blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
- ระบบจะไล่ dependency ตามโครงสร้างแบบต้นไม้ และทำแต่ละขั้นตอนโดยอัตโนมัติ
ตัวอย่าง target ที่รันทุกครั้ง
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
- เนื่องจาก
other_fileไม่ได้ถูกสร้างเป็นไฟล์จริง คำสั่งของsome_fileจึงถูกรันทุกครั้ง
Make clean
- target
cleanมักใช้สำหรับลบผลลัพธ์ที่ได้จากการ build - ไม่ใช่คำสงวนพิเศษของ Make จึงต้องนิยามคำสั่งเอง
- หากมีไฟล์ชื่อ
cleanอาจทำให้เกิดความสับสน จึงแนะนำให้ใช้.PHONY
ตัวอย่าง:
some_file:
touch some_file
clean:
rm -f some_file
การจัดการตัวแปร
- ตัวแปรเป็น สตริง เสมอ
- โดยทั่วไปแนะนำให้ใช้
:=และยังมีรูปแบบการกำหนดค่าอื่น ๆ เช่น=,?=,+= - ตัวอย่างการใช้งาน:
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
- การอ้างอิงตัวแปรใช้ได้ทั้ง
$(variable)หรือ${variable} - เครื่องหมายคำพูดใน Makefile ไม่มีความหมายสำหรับ Make เอง (แต่จำเป็นในคำสั่ง shell)
การจัดการ target
target all
- หากต้องการรันหลาย target พร้อมกัน ให้กำหนด target แรก (ค่าเริ่มต้น) ให้รวมสิ่งที่ต้องการไว้
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
หลาย target และ automatic variables
- สามารถรันคำสั่งแยกให้แต่ละ target ได้ โดย
$@จะเก็บชื่อ target ปัจจุบัน
all: f1.o f2.o
f1.o f2.o:
echo $@
Automatic variables และ wildcard
wildcard *
*ใช้ค้นหาชื่อไฟล์จากไฟล์ซิสเต็มโดยตรง- แนะนำให้ครอบด้วยฟังก์ชัน
wildcardเสมอเมื่อนำมาใช้
print: $(wildcard *.c)
ls -la $?
- ไม่ควรใช้
*ตรง ๆ ในการกำหนดตัวแปร
thing_wrong := *.o
thing_right := $(wildcard *.o)
wildcard %
- มักใช้ใน pattern rules เพื่อดึงและขยายตามแพตเทิร์นที่กำหนด
Fancy Rules
กฎแบบ implicit
- Make มี กฎพื้นฐานแบบซ่อน หลายอย่างในตัวสำหรับการ build C/C++
- ตัวแปรที่พบบ่อย:
CC,CXX,CFLAGS,CPPFLAGS,LDFLAGSเป็นต้น - ตัวอย่าง C:
CC = gcc
CFLAGS = -g
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
Static Pattern Rules
- เขียนหลายกฎที่มีแพตเทิร์นเดียวกันให้กระชับขึ้นได้
objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
$(objects): %.o: %.c
$(CC) -c $^ -o $@
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
Static Pattern Rules + ฟังก์ชัน filter
- ใช้ filter เพื่อเลือกเฉพาะรายการที่ตรงกับแพตเทิร์นนามสกุลที่ต้องการได้
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
.PHONY: all
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
มีคนหนึ่งเล่าถึงประสบการณ์ที่เคยเห็นด้วยตาตัวเองในปี 1985 ที่ห้องแล็บกราฟิกของ Boston University ตอนที่มีคนใช้ Makefile สร้าง 3D renderer สำหรับงานแอนิเมชัน คนคนนั้นเป็นโปรแกรมเมอร์ Lisp กำลังทำระบบสร้าง procedure ระยะแรกและระบบ 3D actor อยู่ และได้เขียน Makefile ที่สง่างามมากยาวแค่ประมาณ 10 บรรทัด โครงสร้างนี้ใช้ dependency จากเวลาแก้ไขไฟล์แบบเรียบง่ายเพื่อสร้างแอนิเมชันหลายร้อยชุดโดยอัตโนมัติ รูปร่าง 3D ของแต่ละเฟรมสร้างด้วย Lisp แล้วให้ Make เป็นตัวสร้างเฟรม ในปี 1985 นั้นต่างจากทุกวันนี้ที่ 3D และแอนิเมชันเป็นเรื่องปกติ ทุกคนในตอนนั้นตื่นตะลึงกันมาก และเขาจำได้ว่าคนคนนั้นคือ Brian Gardner ผู้ที่ต่อมารับผิดชอบ 3D renderer ของ Iron Giant และ Coraline
มีคนสงสัยว่าคนที่พูดถึงคือคนใน 3d-consultant.com/bio.html หรือไม่
มีคนถามยืนยันว่าหมายถึงภาพยนตร์เรื่อง Coraline ใช่ไหม
มีการแนะนำแฟล็กที่มีประโยชน์แต่ไม่ค่อยเป็นที่รู้จักบางตัวตอนใช้ Make
--output-sync=recurse -j10: เป็นแฟล็กที่รวบรวม stdout/stderr ไว้จนกว่างานของแต่ละ target จะเสร็จแล้วค่อยแสดงผล ไม่อย่างนั้น log จะปนกันจนวิเคราะห์ได้ยาก--load-averageแทน-jเพื่อควบคุมภาระของระบบระหว่างประมวลผลแบบขนานได้ (make -j10 --load-average=10)--shuffleซึ่งสุ่มลำดับตารางงานของ build target มีประโยชน์ในการจับปัญหา dependency ภายใน Makefile ในสภาพแวดล้อม CIมีการพูดถึงไอเดียว่า ถ้ารวบรวมตัวเลือกต่าง ๆ ของ make อย่างเป็นทางการแล้วใส่ไว้ในโปรแกรมในรูปแบบข้อความหรือเอกสาร ก็จะช่วยให้เข้าถึงการใช้งานได้ง่ายขึ้น
มีคนอธิบายว่าออปชันที่ตัวเองใช้บ่อยคือแฟล็ก
-Bสำหรับบังคับ build ใหม่ทั้งหมดมีคนบอกว่าตัวเองเห็นปัญหาที่เกิดจาก
make -jบนเครื่อง DOS บ่อยมากจนมองว่าปรากฏการณ์นั้นเป็นบั๊กมีคนถามว่าในระบบที่งานเยอะหรือมีผู้ใช้หลายคน ปัญหาเรื่อง parallelization ไม่ควรเป็นสิ่งที่ OS scheduler จัดการหรือ
มีคนแนะนำว่าแม้จะเป็นแฟล็กที่มีประโยชน์ แต่ตัวเลือกเหล่านี้ไม่ portable ดังนั้นไม่ควรใช้ยกเว้นในโปรเจกต์ส่วนตัวที่ไม่เปิดเผยของตัวเอง
มีความเห็นว่าการข้าม
.PHONYในบทสอนเพียงเพราะไม่ได้ใช้ เป็นข้ออ้างที่อ่อนเกินไป ควรสอนวิธีใช้เครื่องมือให้ถูกต้องมากกว่า.PHONYให้ทุก recipe.PHONYแยกตาม recipe กับการรวบรวมไว้ครั้งเดียวด้านบนของไฟล์ และหวังว่าจะมีการบังคับด้วย linter-o pipefailแบบไม่คิดหน้าคิดหลังเป็นปัญหา และอาจพังเมื่อใช้ grep หรือคำสั่งอื่นใน pipe จึงแนะนำให้ใช้ตามบริบท.PHONYอาจเข้มงวดถูกต้องก็จริง แต่แทบไม่จำเป็นและทำให้ Makefile เยิ่นเย้อเกินไป จึงมองว่าควรใช้เมื่อจำเป็นเท่านั้นมีคนอ้างว่า Make เป็นเครื่องมือที่ออกแบบมาเฉพาะสำหรับการ build codebase C ขนาดใหญ่
มีความเห็นว่า Make ไม่ได้เป็น job runner แต่เป็น shell tool ทั่วไปที่แปลง shell script แบบเชิงเส้นให้เป็นรูป dependency แบบประกาศ
มีคนแย้งว่ามุมมองที่เห็น Make เป็นแค่เครื่องมือ build สำหรับ codebase C อย่างเดียวไม่ถูกต้องอีกต่อไป และชี้ว่าตลอด 20 ปีที่ผ่านมาได้มีระบบ build ที่แข็งแรงและชัดเจนกว่านี้ถูกพัฒนาขึ้นแล้ว จึงควรอัปเดตมุมมอง
มีคนถามว่า job runner ที่ดีคืออะไร พร้อมขอโทษว่าตัวเองอาจเข้าใจความหมายของคำว่า job runner สับสน
มีคนแนะนำ just ว่าเป็นเครื่องมือสมัยใหม่ที่มาแทนส่วนที่ Makefile มักซับซ้อน
แต่ก็มีคนบอกว่า just เหมาะกับการแทนรายการ shell script เท่านั้น และไม่สามารถแทนความสามารถแก่นของ Make ที่คือ “รันเฉพาะกฎที่จำเป็นต้องรันใหม่” ได้
ทางเลือกอื่น ๆ ก็มี
มีคนมองว่าเครื่องมือทางเลือกต่างก็ประกาศตัวว่าเป็น Make replacement แต่จริง ๆ แล้วแตกต่างกันโดยสิ้นเชิงจนเทียบกันยาก แก่นของ Make คือการสร้าง artifact และไม่ build ซ้ำสิ่งที่สร้างไปแล้ว ขณะที่ just ทำหน้าที่เป็นตัวรันคำสั่งอย่างง่ายเท่านั้น
ข้อดีของการใช้ Make เป็นตัวรันคำสั่งคือความมั่นคงในฐานะเครื่องมือมาตรฐานที่ติดตั้งอยู่แทบทุกที่ ต่อให้ทางเลือกอื่นจะออกแบบมาดีกว่า ก็ยังไม่รู้สึกว่าคุ้มจะต้องติดตั้งเพิ่ม
มีคนบอกว่าใช้ Task กับโปรเจกต์งานอดิเรกเล็ก ๆ ที่เขียนด้วย C ได้ดี แต่ยังตัดสินไม่ได้ว่าเหมาะกับโปรเจกต์ขนาดใหญ่ด้วยหรือไม่ (หน้าเว็บทางการของ Task)
มีคนเห็นว่าน่าสนใจที่ช่วงหลัง CMake เลือก ninja เป็นค่าเริ่มต้น เพราะมองว่า Makefile ไม่เหมาะกับการรองรับ C++20 modules (คู่มือ CMake)
clang-scan-deps(สไลด์ทางเทคนิค)มีคนแย้งว่าข้อจำกัดนี้น่าจะเป็นการตัดสินใจฝั่ง CMake หรือเป็นปัญหาที่ไม่มีอาสาสมัครมาช่วยทำให้ generator ของ Makefile รองรับมากกว่า เพราะ ninja เองก็ยังไม่รองรับ C++ modules โดยตรง (ประเด็นที่เกี่ยวข้อง) และจริง ๆ แล้ว ninja มีความสามารถน้อยกว่า Make อีกทั้งยังบังคับให้ระบุ dependency ทั้งหมดแบบ static ด้วย
มีคนแสดงความเห็นว่าการนำ modules เข้ามาเองก็ซับซ้อนและชวนสับสน
มีคนถามว่าใครเคยมีประสบการณ์ใช้ tup บ้างไหม (เอกสารทางการ)
มีคนแนะนำตัวว่าเป็นผู้สร้างและผู้ดูแลหลักของ Task ซึ่งเป็นเครื่องมือทางเลือกแทน Make โดยพัฒนามานานกว่า 8 ปีและยังพัฒนาต่อเนื่อง
มีคนแนะนำ just เช่นกันว่าเป็นอีกหนึ่งทางเลือกแทน Make (just GitHub)
มีคนบอกว่าเป็นเรื่องบังเอิญน่าสนุก เพราะตัวเองใช้ Task บ่อย และเช้าวันนี้ก็เพิ่ง เปิด issue
มีความเห็นว่าบทสอนนี้มีปัญหาที่อันตรายและละเอียดอ่อนอยู่
ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))มีคนบอกว่าตัวเองติดนิสัยใส่ Makefile ไว้ในทุก GitHub repo เสมอ
makeก็ทำงานตามที่คาดหวังของแต่ละโปรเจกต์ได้ทันทีโดยไม่ต้องจำอะไรเพิ่ม