راهنمای کوچک GNU Make

از زمانی که تصمیم به ساده‌سازی گرفته‌ام چند ابزار خیلی به کارم آمده است. یکی از آنها GNU Make است که در ادامه شرح می‌دهم.

کم و بیش طبق شرح وبسایت خودش:

GNU Make ابزاری است که تولید خروجی‌های اجرایی و غیراجرایی از سورس‌های یک برنامه را کنترل می‌کند.

اگر سورس برنامه‌ای را دانلود و کامپایل کرده باشید احتمالا به دستوراتی مانند make و make install برخورد کرده‌اید. لازمه‌ی کار کردن این دستورات هم وجود فایلی بنام Makefile در فولدر مربوطه است.

در حقیقت Make ابزار ساده ایست که می‌تواند دستوراتی که ما تعریف می‌کنیم اجرا کند. ما به هر گروه از دستورات یک نام اختصاص می‌دهیم و Make آنها را اجرا می‌کند و از خودش چیزی ندارد، حتی دستور install فقط یک نام سنتی است و باید توسط برنامه‌ساز تعریف شود.

برای استفاده از Make باید تعدادی «قانون» یا Rule داخل فایلی بنام Makefile نوشت. دقت کنید که تورفتگی‌های داخل این فایل حتما باید با TAB باشد. ساختار این فایل اینگونه است:

# Rule No 1
target: dependencies ...
	commands
	...

هر قانون Make از سه بخش تشکیل شده است:

  • نام فایل خروجی یا همان target
  • نام فایل‌های مبداء یا همان dependencyها
  • دستوراتی برای تولید فایل خروجی از روی فایل‌های مبداء

هر Makefile هم می‌تواند تعداد دلخواهی قانون داشته باشد. وقتی در کامندلاین دستور make را وارد می‌کنیم اولین قانون داخل فایل اجرا می‌شود، فارغ از نام target. از طرفی هم می‌توان اسم یک قانون را وارد کرد مثلا make install، یعنی اجرای قانون install.

فرض کنید یک فایل C بنام dorood.c را می‌خواهیم کامپایل کنیم:

#include <stdio.h>

int main (int argc, char* argv[]) {
		puts("Hello World!");
		return 0;
}

می‌توانیم Makefile زیر را برای برنامه‌مان بنویسیم:

dorood: dorood.c
	gcc -o dorood dorood.c

حالا اگر در کامندلاین make یا make dorood وارد کنیم این دستورات اجرا می‌شوند:

$ make
gcc -o dorood dorood.c

اما اگر دوباره make وارد کنیم:

$ make
make: 'dorood' is up to date.

چه اتفاقی افتاد؟ Make از کجا فهمید که دستور اجرا شده؟ روش کار ساده است. بار اول فایل خروجی بنام dorood وجود نداشت پس Make دستور را اجرا کرد. بار دوم اما این فایل ساخته شده بود بنابراین نیازی به اجرای دوباره‌ی دستور نبود.

حالا بگذارید فایل dorood.c را تغییر بدهیم و دوباره Make را اجرا کنیم. من اینکار را با تغییر آخرین زمان تغییر این فایل به کمک دستور touch انجام می‌دهم:

$ touch dorood.c
$ make
gcc -o dorood dorood.c

اینبار چه شد؟ Make تاریخ آخرین تغییر خروجی را با ورودی‌ها مقایسه می‌کند. اگر خروجی جدیدتر باشد یعنی همه چیز روبراه است. اگر خروجی قدیمی‌تر از هر یک از وابستگی‌ها باشد یعنی باید دوباره ساخته شود. به این ترتیب Make در وقت کامپایل صرفه‌جویی می‌کند.

همه دستورات هم نیاز نیست مروبط به کامپایل باشند. ما می‌توانیم قوانین “phony” داشته باشیم، یعنی «الکی». بیایید یک قانون clean اضافه کنیم:

.PHONY: clean

dorood: dorood.c
	gcc -o dorood dorood.c

clean:
	rm dorood

دستورات «الکی» لزومی ندارد سورس‌کد بخوانند و خروجی بسازند. یعنی Make دنبال فایل ورودی و خروجی برای آنها نمی‌گردد و اگر دستور بدهیم همیشه اجرا می‌شوند. مثلا برای پاک کردن خروجی‌ها دستور زیر کافیست:

$ make clean
rm dorood

قوانین Make را می‌توان به یکدیگر زنجیر کرد. مثلا فرض کنید ما می‌خواهیم فایل بالا را در دو مرحله تولید کنیم. اول سورس را کامپیال می‌کنیم به object file و بعد آن را لینک می‌کنیم و خروجی اجرایی را می‌سازیم. برای این منظور یک قانون جدید نیاز داریم:

.PHONY: clean

dorood: dorood.o
	gcc -o dorood dorood.o

dorood.o: dorood.c
	gcc -c dorood.c

clean:
	rm dorood dorood.o

و حالا اجرا:

$ make clean
rm dorood dorood.o

$ make
gcc -c dorood.c
gcc -o dorood dorood.o

به صورت سنتی Make برای کامپایل سورس‌کد استفاده شده است، هرچند من از Make در هر پروژه برای تجمیع دستورات پراکنده‌ای استفاده می‌کنم که معمولا باید در هیستوری ترمینالم به دنبال آنها بگردم. مثلا بیایید نگاهی به Makefile ساخت وبسایتم بیندازیم:

DBPATH=mehdix.db
all: build
.PHONY: init serve publish clean

init: init_db
	bundle config set path vendor/bundle
	bundle install
	pip install -r scripts/requirements.txt

build: comments
	bundle exec jekyll build

comments:
	@echo rebuilding alef comments
	python scripts/rebuild_comments.py

serve: build
	bundle exec jekyll serve

publish: build
	rsync -vr _site/* mehdix.ir:/var/www/mehdix.ir/

clean:
	rm -rf _site
	rm -rf **/.jekyll-cache

init_db: scripts/schema.sql
	sqlite3 ${DBPATH} < scripts/schema.sql

مجال شرح این دستورات نیست، ولی تمام اینها را در دوران پیش از Make در حافظه‌ی ترمینالم جستجو می‌کردم و گاهی فراموش می‌کردم. حالا در ابتدای ساخت هر پروژه همواره یک Makefile می‌سازم که مثل یک اسفنج عمل می‌کند و تمام دستورات ریز و درشتی که به آن پروژه مربوط است را به تدریج به خودش جذب می‌کند.

امیدورام مفید فایده شده باشد.