6 คะแนน โดย GN⁺ 2025-06-21 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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: prerequisites
    • command ที่ขึ้นต้นด้วยแท็บ
  • target โดยทั่วไปคือชื่อไฟล์ และคำสั่งคือขั้นตอนสำหรับสร้าง target นั้น
  • prerequisites คือไฟล์ที่ต้องมีอยู่ก่อนรันคำสั่ง และเรียกอีกอย่างว่า dependencies
  • บรรทัดคำสั่งใน Makefile ต้องย่อหน้าด้วย อักขระแท็บ เท่านั้น หากใช้ช่องว่าง make จะล้มเหลว

การเปรียบเทียบเวลาแก้ไขของ target และ dependency

  • make hello จะรันคำสั่งที่เชื่อมกับ target hello แต่หากมีไฟล์ 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 อยู่จริง target clean อาจไม่ถูกรัน ซึ่งจะแก้ภายหลังด้วย .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+c Make จะลบ 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 ของ gcc
  • vpath <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_program
    • BUILD_DIR := ./build
    • SRC_DIRS := ./src
  • SRCS ใช้ find และ $(shell ...) เพื่อค้นหาไฟล์ *.cpp, *.c, *.s
  • OBJS จะเติม BUILD_DIR ไว้หน้าซอร์สแต่ละไฟล์และเติม .o เพื่อสร้างพาธของผลลัพธ์บิลด์
  • DEPS จะเปลี่ยน .o เป็น .d เพื่อสร้างรายการ Makefile ของ dependency
  • INC_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 ความคิดเห็น

 
GN⁺ 2025-06-21
ความคิดเห็นจาก 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

    • ถ้า “ไม่ค่อยเป็นที่รู้จัก” ก็น่าจะดีถ้าผู้เขียน make รวบรวมรายการตัวเลือกไว้ที่ไหนสักแห่งแล้วแจกไปพร้อมกับโปรแกรม ถ้าอยู่ในรูปแบบไฟล์ข้อความที่ผู้ใช้เปิดอ่านได้ หรือภาษาสำหรับจัดพิมพ์อะไรทำนองนั้น การเข้าถึงความรู้น่าจะดีขึ้นมาก
    • ที่ใช้บ่อยที่สุดคือ -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 และกลายเป็น ผู้ลงมือรัน เอง จึงมีอำนาจควบคุมมากขึ้น ถ้ามองจากตัวอย่างการทำอาหาร โครงสร้างเชิงประกาศจะแสดงให้เห็นชัดขึ้นว่าตรงไหนที่ยังเปิดให้เลือกได้ว่าจะทำอะไรต่อ และให้อิสระในการสะท้อนข้อจำกัดภายนอกที่ไม่ได้เขียนอย่างเป็นทางการใน 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...

    • ถ้าผมเข้าใจผิดก็ช่วยแก้ด้วย แต่ข้อจำกัดเกี่ยวกับ Makefile นี้ดูเหมือนจะเป็น การเลือกของ CMake ล้วน ๆ หรืออย่างน้อยก็เป็นผลจากการขาดอาสาสมัครที่จะเพิ่มการรองรับให้ตัวสร้าง Makefile ตัว Ninja เองก็ไม่ได้รองรับ C++ modules: https://github.com/ninja-build/ninja/issues/2457
      ที่จริง Ninja ต้องกำหนด dependency ทั้งหมดแบบ static ดังนั้นในแง่นี้จึงมีความสามารถน้อยกว่า Make ทั่วไปด้วยซ้ำ
    • พูดตรง ๆ ว่า modules แทบจะเป็นหายนะ
  • Make ยังมีที่ทางของมันในฐานะ เครื่องมือ build สำหรับ codebase ภาษา C ขนาดใหญ่ แต่บ่อยครั้งมันถูกใช้เหมือน “ตัวรันงานรายโปรเจกต์” แบบทั่วไป ซึ่งไม่ค่อยเหมาะ เพราะแม้แต่ conditional ง่าย ๆ ก็ยังทำได้ยาก
    ตัวอย่างเช่น เคยเห็นความพยายามด้วยเจตนาดีหลายครั้งที่จะครอบ Terraform ด้วย Make แต่สุดท้ายก็จบไม่สวย

    • Make ไม่ใช่ตัวรันงานทั่วไป มันใกล้เคียงกับวิธีทั่วไปในการเปลี่ยน shell script แบบลำดับขั้นให้กลายเป็น dependency เชิงประกาศ เป็นเครื่องมือทั่วไปสำหรับ shell
    • ผมคิดว่าคำว่าเป็น build tool สำหรับ codebase ภาษา C ขนาดใหญ่ก็ไม่ถูกต้องแล้วเหมือนกัน ตลอด 20 ปีที่ผ่านมา มี build system ที่แข็งแรงกว่าและนิยามชัดเจนกว่าถูกสร้างขึ้นมาแล้ว ถึงเวลาต้องอัปเดตมุมมอง
    • มีตัวรันงานทั่วไปดี ๆ ไหม? แก้ไข: ดูเหมือนผมจะเข้าใจคำว่า “ตัวรันงาน” ผิดไปโดยสิ้นเชิง
  • เครื่องมือสมัยใหม่ที่ยอดเยี่ยมสำหรับแทนส่วนที่ทำให้ Makefile รกคือ just: https://github.com/casey/just

    • มันทดแทนด้าน “รายการ shell script สั้น ๆ” ของ Make ได้ แต่ไม่ได้ทดแทนส่วนที่มีประโยชน์จริง ๆ คือความสามารถในการ “รันเฉพาะ rule ที่ต้องรันซ้ำ”
    • ยังมีทางเลือกอื่นด้วย: Task(Go) https://github.com/go-task/task, Cake(C#) https://github.com/cake-build/cake, Rake(Ruby) https://github.com/ruby/rake
      ถ้าเป็นแนวคิดคนละแบบโดยสิ้นเชิง ก็มี Makedown ที่เคยถูกพูดถึงบน HN เมื่อ 8 เดือนก่อน: https://news.ycombinator.com/item?id=41825344
    • แม้เครื่องมือเหล่านี้จะวางตำแหน่งตัวเองเป็นทางเลือกแทน make แต่โดยส่วนตัวแล้วผมมองว่ามันแตกต่างกันโดยสิ้นเชิงและไม่ใช่สิ่งที่จะเอามาเทียบกันได้ make เน้น การสร้าง output/artifact และการไม่ build สิ่งที่สร้างไว้แล้วซ้ำ ส่วน just เป็นตัวรันคำสั่ง
    • ข้อดีหลักของการใช้ Make เป็นตัวรันคำสั่งคือมันเป็นเครื่องมือมาตรฐานที่ติดตั้งอยู่ “แทบทุกที่” ต่อให้เครื่องมือทางเลือกดูใช้ง่ายกว่า ผมก็ไม่เคยรู้สึกว่ามันให้ประโยชน์มากพอที่จะทำให้การติดตั้งเครื่องมือเพิ่มดูสมเหตุสมผล
    • Task ก็เป็นอีกทางเลือกหนึ่งเช่นกัน แต่โดยส่วนตัวเคยใช้แค่กับโปรเจกต์งานอดิเรกภาษา C แบบง่าย ๆ เลยพูดยากว่ามันจะรับมือได้ดีไหมเมื่อขนาดใหญ่ขึ้น: https://taskfile.dev/
  • 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

    • อีกทางเลือกหนึ่งคือ just: https://github.com/casey/just
    • เป็นเรื่องบังเอิญที่น่าสนใจ เพราะผมใช้มันบ่อย และเพิ่งเปิด issue ไปก่อนหน้านี้วันนี้เอง: https://github.com/go-task/task/issues/2303
  • สงสัยว่ามีใครเคยใช้ tup บ้างไหม: https://gittup.org/tup/ex_dependencies.html
    มันเป็น build system ที่ตรวจหา dependency โดยอัตโนมัติจากการเข้าถึงไฟล์ระบบ จึงใช้ร่วมกับ compiler หรือเครื่องมือใด ๆ ก็ได้