बेहतरीन उदाहरणों के साथ Makefile सीखें
(makefiletutorial.com)- Makefile फ़ाइल में हुए बदलाव के आधार पर सिर्फ़ ज़रूरी build steps को दोबारा चलाता है, और मुख्यतः बड़े प्रोजेक्ट्स में C/C++ compilation को automate करने के लिए इस्तेमाल होता है
- Make target और dependency के filesystem timestamps की तुलना करता है, और command तभी चलाता है जब target मौजूद न हो या dependency उससे नई हो
- बुनियादी नियम
targets: prerequisitesऔर tab से indented command से मिलकर बनता है; spaces से indentation करने परmakefail हो सकता है - variables, wildcards, automatic variables, pattern rules, conditionals, functions, recursive make,
include,.PHONY,.DELETE_ON_ERRORके ज़रिए build flow को बढ़ाया जा सकता है - Cookbook example
src/के C/C++ source सेbuild/outputs बनाता है और-MMD -MPतथा-includeके साथ automatic dependency management सेट करता है
Makefile की ज़रूरत कब पड़ती है
- Makefile बड़े प्रोग्राम्स में यह तय करने के लिए इस्तेमाल होता है कि किन फ़ाइलों को दोबारा compile करना है
- ट्यूटोरियल का मुख्य उदाहरण C/C++ files compile करना है
- फ़ाइल में बदलाव के हिसाब से commands की एक श्रृंखला चलानी हो, तो Make को compilation के अलावा भी इस्तेमाल किया जा सकता है
- C/C++ के दूसरे build tools के रूप में SCons, CMake, Bazel, Ninja का परिचय दिया गया है
- Java के लिए Ant, Maven, Gradle हैं, और Go, Rust, TypeScript जैसी भाषाओं के अपने build tools होते हैं
- Python, Ruby, और pure JavaScript जैसी interpreted languages में फ़ाइल बदलने पर दोबारा compile करने की ज़रूरत नहीं होती, इसलिए Makefile जैसे tools अनिवार्य नहीं होते
Make का बुनियादी execution model
- उदाहरण
makeinstalled terminal मेंMakefileफ़ाइल बनाकर, उस directory मेंmakeचलाने के तरीके से काम करते हैं - बुनियादी नियम नीचे दिए गए रूप का पालन करता है
targets: prerequisites- tab से शुरू होने वाला
command
- target आम तौर पर एक फ़ाइल नाम होता है, और command उस target को बनाने का step होता है
- prerequisites वे फ़ाइलें हैं जो command चलने से पहले मौजूद होनी चाहिए; इन्हें dependencies भी कहा जाता है
- Makefile की command lines को अनिवार्य रूप से tab character से indent करना चाहिए; spaces इस्तेमाल करने पर
makefail हो जाएगा
target और dependency के समय की तुलना
make hellohellotarget से जुड़ी command चलाता है, लेकिन अगरhelloफ़ाइल पहले से मौजूद है तो command नहीं चलती- target और फ़ाइल नाम सीधे जुड़े होते हैं, और आम तौर पर target की command उसी नाम की फ़ाइल बनाती है
blah: blah.crule में compilation command तभी चलती है जबblahमौजूद न हो याblah.c,blahसे नया हो- Make बदलावों का इतिहास सीधे track नहीं करता, बल्कि filesystem timestamps को heuristic की तरह इस्तेमाल करता है
- अगर फ़ाइल को modify करने के बाद उसका modified time अतीत में बदल दिया जाए, तो Make ग़लत तरीके से मान सकता है कि फ़ाइल नहीं बदली
default target और clean
- अगर
makeमें कोई target न दिया जाए, तो Makefile का पहला target default target की तरह चलता है alltarget कई targets को एक साथ बनाने के लिए इस्तेमाल होता है, और इसे पहला rule बना देने पर सिर्फ़makeचलाने से पूरा build हो जाता हैcleanआम तौर पर build outputs हटाने के लिए इस्तेमाल होने वाला target है, लेकिन यह Make का कोई विशेष reserved word नहीं है- अगर
cleanन तो पहला target हो और न किसी दूसरे target की dependency, तो इसेmake cleanकी तरह explicitly चलाना होगा - अगर वास्तव में
cleanनाम की फ़ाइल मौजूद हो, तोcleantarget नहीं चल सकता; इसे आगे.PHONYसे हल किया जाता है
variables और string processing
- Make variables सिर्फ़ strings रख सकते हैं, और ट्यूटोरियल आम तौर पर
:=इस्तेमाल करने की सलाह देता है - variable reference के लिए
$(name)या${name}format इस्तेमाल होता है - single quotes और double quotes का Make में अपने-आप कोई विशेष अर्थ नहीं होता; वे variable के भीतर सिर्फ़ characters हैं
- variable definition के दो मुख्य तरीके हैं
=: recursive variable, जो इस्तेमाल के समय variables को expand करता है:=: simply expanded variable, जो definition के समय सिर्फ़ पहले से defined values को expand करता है
?=सिर्फ़ उन variables के लिए value सेट करता है जो अभी set नहीं हुए हैं+=variable में value जोड़ता है- undefined variables को empty string माना जाता है
- line ending पर spaces हटाए नहीं जाते; एक-character space variable बनाने के लिए
$(nullstring)जैसी तकनीक इस्तेमाल की जा सकती है
wildcards और automatic variables
- Make में
*और%दोनों को wildcard कहा जाता है, लेकिन इनके अर्थ अलग हैं *filesystem में matching filenames ढूँढता है, और इसे सीधे variable definition में रखने के बजाय$(wildcard *.o)जैसी wildcard function में wrap करना ज़्यादा सुरक्षित है- अगर
*किसी फ़ाइल से match नहीं करता, तोwildcardfunction के बाहर वह ज्यों का त्यों रह सकता है %rules और कुछ functions में इस्तेमाल होता है, और match हुआ हिस्सा stem कहलाता है- अक्सर इस्तेमाल होने वाले automatic variables ये हैं
$@: target का नाम$?: target से नई dependencies$^: सभी dependencies$<: पहली dependency
implicit rules और pattern rules
- Make के पास C compilation के लिए implicit rules होते हैं, इसलिए compilation command को explicitly लिखे बिना भी build चल सकता है
- प्रमुख implicit rules का रूप इस तरह है
n.oकोn.cसे$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@के रूप में बनाया जाता हैn.oकोn.ccयाn.cppसे$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@के रूप में बनाया जाता हैnकोn.oसे linker command द्वारा बनाया जाता है
- implicit rules में महत्वपूर्ण variables हैं
CC,CXX,CFLAGS,CXXFLAGS,CPPFLAGS,LDFLAGS - static pattern rule का रूप
targets...: target-pattern: prereq-patterns ...होता है, और target में%से match हुए stem को dependency pattern में डाला जाता है - pattern rule में target में
%शामिल होता है, जैसे%.o: %.c, और इसे अपने बनाए implicit rule की तरह इस्तेमाल किया जा सकता है - double-colon rule
::एक ही target के लिए कई rules define कर सकता है, लेकिन इसका इस्तेमाल कम होता है
command execution और error handling
- command के आगे
@लगाने से वह command खुद print नहीं होती make -sचलाने पर यह हर line के आगे@लगे होने जैसा व्यवहार करता है- हर command line नए shell में चलने जैसा प्रभाव देती है, इसलिए एक line में
cd ..अगली line को प्रभावित नहीं करता - वही shell state बनाए रखने के लिए
cd ..; echo \pwd`` की तरह एक line में लिखना या backslash से जोड़ना चाहिए - default shell
/bin/shहोता है, जिसेSHELL=/bin/bashकी तरह बदला जा सकता है - shell variable का
$Makefile में लिखने के लिए$$इस्तेमाल करना होता है - error handling execution mode के अनुसार बदलती है
make -k: error होने पर भी संभव दूसरे काम जारी रखता है- command के आगे
-: उस command की error को ignore करता है make -i: सभी commands पर error ignore लागू करता है
ctrl+cसे Make रोकने पर, Make अभी-अभी बनाया गया सबसे नया target हटा देता है
recursive make और environment variables
- Makefile के भीतर दोबारा make बुलाते समय
makeकी जगह special variable$(MAKE)इस्तेमाल करना चाहिए $(MAKE)make flags आगे भेजता है और flags के प्रभाव को सही तरीके से संभालता है- Make शुरू होते ही execution के समय set environment variables अपने-आप Make variables के रूप में बन जाते हैं
exportdirective variables को सभी recipe shell commands के environment में सेट करता है- child make में भी variable इस्तेमाल करना हो तो उस variable को
exportकरना होगा .EXPORT_ALL_VARIABLESइस्तेमाल करने पर सभी variables export किए जा सकते हैंmake clean run testकी तरह कई targets एक साथ देने पर वेclean,run,testक्रम में चलते हैं
conditionals और functions
- conditionals
ifeq,ifneq,ifdef,ifndef,else,endifसे बनते हैं ifdefvariable reference को expand नहीं करता, बल्कि सिर्फ़ यह देखता है कि variable defined है या नहीं$(MAKEFLAGS)औरfindstringका इस्तेमाल करकेmake -iजैसे flags के pass होने की जाँच की जा सकती है- Make functions मुख्यतः text processing के लिए इस्तेमाल होती हैं, और इन्हें
$(fn, arguments)या${fn, arguments}रूप में call किया जाता है - प्रमुख functions ये हैं
subst: string replacementpatsubst: pattern के अनुसार word replacementforeach: space-separated words की list को transform करनाif: पहला argument खाली न हो तो दूसरा argument, नहीं तो तीसरा argumentcall: variable को base function की तरह call करना और$(1),$(2)जैसे parameters इस्तेमाल करनाshell: shell को call करता है, लेकिन newlines को spaces में बदल देता हैfilter: list में से किसी खास pattern से match करने वाले elements चुनता हैfilter-out: किसी खास pattern से match न करने वाले elements चुनता है
include, vpath, .PHONY, .DELETE_ON_ERROR
include filenames...एक या अधिक दूसरे Makefile पढ़ने देता हैincludeतब उपयोगी होता है जबgccके-Mजैसे options से generated Makefile पढ़ना होvpath <pattern> <directories>बताता है कि किसी खास dependency file को किन directories में ढूँढना हैVPATHvariable से इसी तरह का search path behavior global रूप से सेट किया जा सकता है- backslash
\लंबी commands को कई lines में बाँटकर लिखने के लिए इस्तेमाल होता है - किसी target को
.PHONYमें जोड़ने पर Make उसे उसी नाम की फ़ाइल के साथ confuse नहीं करता .DELETE_ON_ERRORrule चलते समय अगर command non-zero exit status लौटाए, तो उस rule का target हटा देता है.DELETE_ON_ERRORPHONYकी तरह सिर्फ़ ठीक पहले वाले target पर लागू नहीं होता, बल्कि सभी targets पर लागू होता है
Makefile Cookbook उदाहरण
- Cookbook example मध्यम आकार के प्रोजेक्ट्स के लिए Makefile template देता है
src/फ़ोल्डर में C/C++ files रखने पर Makefile compile targets और dependencies अपने-आप तय कर लेता है- मुख्य variable configuration इस प्रकार है
TARGET_EXEC := final_programBUILD_DIR := ./buildSRC_DIRS := ./src
SRCSfindऔर$(shell ...)का इस्तेमाल करके*.cpp,*.c,*.sfiles ढूँढता हैOBJSहर source file के आगेBUILD_DIRजोड़कर और.oलगाकर build output path बनाता हैDEPS.oको.dमें बदलकर dependency Makefile की list बनाता हैINC_DIRSsrc/के नीचे की सभी directories ढूँढता है, औरINC_FLAGSहर directory के आगे-Iprefix जोड़ता हैCPPFLAGS := $(INC_FLAGS) -MMD -MPGCC को.ddependency files बनाने के लिए configure करता है- C और C++ source के लिए अलग-अलग pattern rules
build/के नीचे.ofiles बनाते हैं, और ज़रूरी directoriesmkdir -p $(dir $@)से बनाई जाती हैं cleanको.PHONYघोषित किया जाता है औरrm -r $(BUILD_DIR)से build directory हटाई जाती है-include $(DEPS).dfiles को include करता है, लेकिन पहली build की तरह file न होने पर आने वाली error को दबा देता है
1 टिप्पणियां
Hacker News की राय
makeके कुछ कम-ज्ञात लेकिन उपयोगी flags हैं। Output synchronization target खत्म होने के बाद ही stdout/stderr दिखाता है, जिससे parallel build के दौरान logs आपस में मिलकर पढ़ने में मुश्किल होने की समस्या कम होती हैmake --output-sync=recurse -j10व्यस्त multi-user system में सिर्फ
-jjobs की संख्या काफी नहीं हो सकती, इसलिए load average के आधार पर parallelism सीमित भी किया जा सकता है:make -j10 --load-average=10target scheduling order को randomize करने वाला
--shuffleभी CI में Makefile की छूटी हुई dependencies पकड़ने में उपयोगी है:make --shuffle # or --shuffle=seed/reverse-Bइस्तेमाल करता हूं, यह option बिना शर्त सब कुछ फिर से build कराता हैmake -jको कई बार machine को DoS state में डालते देखा है, इसलिए इसे लगभग bug जैसा मानता हूं1985 के आसपास Boston University Graphics lab में मैंने किसी को Makefile से animation के लिए 3D renderer generation चलाते देखा था। वे Lisp side के व्यक्ति थे और शुरुआती procedural generation और 3D actor systems पर काम कर रहे थे; Makefile कुल मिलाकर करीब 10 lines का था और बहुत elegant था
सिर्फ simple file date dependencies से सैकड़ों animations generate किए जाते थे; Lisp हर frame का 3D form बनाता था और Make frames generate करता था
1985 में, आज 3D और animation में जिन चीजों को हम obvious मानते हैं, वे लगभग मौजूद नहीं थीं, इसलिए इसने सबको चौंका दिया। बाद में उन्होंने Iron Giant का 3D renderer लिखा और, जहां तक मुझे याद है, Coraline में भी key role निभाया। वे Brian Gardner थे
Make उन tools में से है जिन्हें career की शुरुआत में सीख लेने पर मुझे सच में खुशी होती है। आजकल मैं इसे ज्यादा इस्तेमाल नहीं करता, लेकिन इसने imperative systems की तुलना में declarative systems की ताकत दिखाई
मुझे एहसास हुआ कि यह idea दूसरे कामों तक भी स्वाभाविक रूप से फैलता है। इस site के ऊपर की image देखकर लगता है कि author भी कुछ ऐसा ही महसूस करते हैं: traditional recipe की तरह script-style command sequence में लिखने के बजाय, Makefile की तरह declarative रूप में देखने पर cooking recipes ज्यादा अच्छी तरह समझ आती हैं
संबंधित लेख: https://blog.gpkb.org/posts/cooking-with-make/
मैं recipes को हमेशा Makefile की तरह पढ़ने लायक लिखकर kitchen में ले जाता हूं, और सोचता हूं कि क्या किसी ने उन्हें इस तरह typeset या display किया है। नई recipe पढ़ते समय script को दिमाग में Makefile में बदलना न पड़े, तो काफी समय बचेगा
बेशक इसकी कीमत यह है कि dependency graph खुद solve करना पड़ता है। जब आप बस पहले से serialized sequential steps follow करना चाहते हैं, तो आपके ऊपर ज्यादा काम आ जाता है
लेख में कहा गया है कि ज्यादातर recipes को
.PHONYके रूप में mark नहीं किया जाता, और इसे tutorial में अलग से न समझाने की वजह जैसा इस्तेमाल किया गया है, लेकिन यह कमजोर बहाना है। tools को सही usage सिखाना चाहिएहमारी team में make को task runner की तरह इस्तेमाल किया जाता है, इसलिए हर recipe में
.PHONYजोड़ने और maintain करने पर मुझे टोका गया थाClark Grubb की Makefile style guide अच्छी है: https://clarkgrubb.com/makefile-style-guide
सोचता हूं कि क्या कोई इस style guide का इस्तेमाल करता है। यह भी जानना चाहूंगा कि phony recipes को declaration point पर mark करते हैं या file के top पर एक बड़ी list में रखते हैं। अच्छा होगा अगर इसे enforce करने वाला कोई linter हो
-o pipefailको आदतन इस्तेमाल करना मुझे ठीक नहीं लगता। Pipefail उपयोगी है, लेकिन pipelines में लोग जो सबसे आम काम करते हैं—grepसे output filter करना—उसे तोड़ देता है। बेहतर है कि इसे सिर्फ उन recipes में अलग से लगाया जाए जहां जरूरत होnon-file targets को
.PHONYके रूप में mark करना strict sense में सही है, लेकिन आमतौर पर जरूरी नहीं होता। targets ज्यादा हों तो Makefile में अनावश्यक verbosity बढ़ती है, इसलिए मेरे हिसाब से जरूरत पड़ने पर ही जोड़ना बेहतर हैकई output files बनाने वाली recipes में, पहले जब pattern rules fit नहीं होते थे तो dummy files/flag files standard तरीका थे, लेकिन GNU Make 4.3 से group targets का native support है। यह Ubuntu 22.04 LTS में भी शामिल है: https://www.gnu.org/software/make/manual/html_node/Multiple-...
हाल में दिलचस्प बात यह है कि CMake ने तय किया कि C++20 modules इस्तेमाल करने वाले projects के लिए Makefile उपयुक्त नहीं है और Ninja सही है। https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
मूल रूप से लगता है कि target dependencies को statically define करना बहुत कठिन या असंभव है। अब इसे
clang-scan-depsजैसे tools से dynamically handle किया जाता है: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...दरअसल Ninja में सभी dependencies statically define करनी पड़ती हैं, इसलिए इस मामले में यह सामान्य Make से कम सक्षम है
बड़े C codebase के build tool के रूप में Make की अपनी जगह है। लेकिन इसे अक्सर general-purpose “project-specific task runner” की तरह इस्तेमाल किया जाता है, और वहां यह ज्यादा फिट नहीं बैठता। क्योंकि साधारण conditionals भी कठिन हैं
उदाहरण के लिए Terraform को Make से wrap करने की नेकनीयत कोशिशें मैंने कई बार देखी हैं, लेकिन उनका अंत अच्छा नहीं हुआ
Makefile के messy हिस्सों की जगह लेने के लिए एक बेहतरीन modern tool just है: https://github.com/casey/just
बिल्कुल अलग concept के तौर पर Makedown भी है, जिसे 8 महीने पहले HN पर cover किया गया था: https://news.ycombinator.com/item?id=41825344
Makefile शानदार हैं, लेकिन उनमें बहुत ज्यादा डूबना ठीक नहीं। कुछ साल पहले pure GNU Make framework बनाने की कोशिश करते हुए मुझे एहसास हुआ कि मैं असल में autoconf को फिर से invent कर रहा हूं, और तभी समझ आया कि GNU autotools क्यों बनाए गए थे
Makefile अजीब तरह से Lisp जैसी Turing tarpit है। GNU Make में metaprogramming features तक हैं, इसलिए Makefile के अंदर कोई अपवित्र system metaprogram करने की इच्छा रोकना मुश्किल है। GNU Make का इतना व्यापक रूप से installed होना भी इस लालच को बढ़ाता है
मैं Make के alternative Task का creator और maintainers में से एक हूं। यह 8 साल से ज्यादा समय से मौजूद है और लगातार विकसित हो रहा है, इसलिए अगर आप कुछ नया खोज रहे हैं तो इसे आजमा कर देखना अच्छा होगा। सवाल भी बेझिझक पूछ सकते हैं
https://taskfile.dev/
https://github.com/go-task/task
सोच रहा हूं कि क्या किसी ने tup इस्तेमाल किया है: https://gittup.org/tup/ex_dependencies.html
यह file system access के आधार पर dependencies अपने आप identify करने वाला build system है, इसलिए किसी भी compiler या tool के साथ इस्तेमाल किया जा सकता है