راهنمای کوچک 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 میسازم که مثل یک اسفنج عمل میکند و تمام دستورات ریز و درشتی که به آن پروژه مربوط است را به تدریج به خودش جذب میکند.
امیدورام مفید فایده شده باشد.