Let's Go Further ساخت، نسخه‌بندی و کنترل کیفیت › ایجاد و استفاده از makefileها
قبلی · فهرست مطالب · بعدی
فصل ۱۹.۱.

ایجاد و استفاده از makefileها

در این فصل اول قرار است بررسی کنیم که چگونه از ابزار make GNU و makefileها برای خودکارسازی وظایف رایج در پروژه خود استفاده کنیم.

ابزار make باید از قبل در بیشتر توزیع‌های لینوکس نصب شده باشد، اما اگر هنوز روی دستگاه شما نیست، باید بتوانید آن را از طریق مدیر بسته خود نصب کنید. به عنوان مثال، اگر سیستم عامل شما از مدیر بسته apt پشتیبانی می‌کند (مانند Debian و Ubuntu) می‌توانید آن را با نصب کنید:

$ sudo apt install make

همچنین احتمالاً اگر از macOS استفاده می‌کنید از قبل روی دستگاه شما نصب شده است، اما اگر نیست می‌توانید از brew برای نصب آن استفاده کنید:

$ brew install make

در دستگاه‌های Windows می‌توانید make را با استفاده از مدیر بسته Chocolatey با دستور نصب کنید:

> choco install make

یک makefile ساده

حالا که ابزار make روی سیستم شما نصب شده است، بیایید اولین نسخه makefile خود را ایجاد کنیم. با یک نسخه ساده شروع می‌کنیم و سپس به تدریج چیزها را اضافه می‌کنیم.

makefile اساساً یک فایل متنی است که حاوی یک یا چند قانون است که ابزار make می‌تواند آن‌ها را اجرا کند. هر قانون یک هدف دارد و شامل یک دنباله از دستورات است که هنگام اجرای قانون اجرا می‌شوند. به طور کلی، قوانین makefile دارای ساختار زیر هستند:

# comment (optional)
target: 
	command
	command
	...

اگر داشته‌اید با ما همراهی کنید، باید از قبل یک Makefile خالی در ریشه دایرکتوری پروژه خود داشته باشید. بیایید جهش کنیم و یک قانون ایجاد کنیم که دستور go run ./cmd/api را برای اجرای application API ما اجرا می‌کند. به این صورت:

فایل: Makefile
run:
	go run ./cmd/api

مطمئن شوید که Makefile ذخیره شده است و سپس می‌توانید یک قانون خاص را با اجرای $ make <target> از ترمینال خود اجرا کنید.

بیایید همین الان و make run را برای شروع API فراخوانی کنیم:

$ make run
go run ./cmd/api
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

عالی است، این کار خوب انجام شد. وقتی make run را تایپ می‌کنیم، ابزار make به دنبال فایلی به نام Makefile یا makefile در دایرکتوری فعلی می‌گردد و سپس دستورات مرتبط با هدف run را اجرا می‌کند.

یک نکته قابل اشاره — به طور پیش‌فرض make دستورات را در خروجی ترمینال بازگو می‌کند. می‌توانیم این را در کد بالا ببینیم که خط اول خروجی دستور بازگو شده go run ./cmd/api است. اگر بخواهید، امکان سرکوب کردن بازگو شدن دستورات با پیشوند دادن کاراکتر @ وجود دارد.

متغیرهای محیطی

وقتی یک قانون make را اجرا می‌کنیم، هر متغیر محیطی که در شروع make در دسترس است به یک متغیر make با همان نام و مقدار تبدیل می‌شود. سپس می‌توانیم به این متغیرها با استفاده از نحوه ${VARIABLE_NAME} در makefile خود دسترسی پیدا کنیم.

برای نشان دادن این موضوع، بیایید دو قانون اضافی ایجاد کنیم — یک قانون psql برای اتصال به پایگاه داده و یک قانون up برای اجرای مایگریشن‌های پایگاه داده. اگر داشته‌اید با ما همراهی کنید، هر دو این قوانین به مقادیر DSN پایگاه داده از متغیر محیطی GREENLIGHT_DB_DSN شما نیاز دارند.

بیایید همین الان Makefile خود را به روز کنید تا این دو قانون جدید را شامل شود، به این صورت:

فایل: Makefile
run:
	go run ./cmd/api

psql:
	psql ${GREENLIGHT_DB_DSN}

up:
	@echo 'Running up migrations...'
	migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

توجه کنید که چگونه از کاراکتر @ در قانون up استفاده کردیم تا از بازگو شدن خود دستور echo جلوگیری کنیم؟

بسیار خب، بیایید این را با اجرای make up برای اجرای مایگریشن‌های پایگاه داده امتحان کنیم:

$ make up
Running up migrations...
migrate -path ./migrations -database postgres://greenlight:pa55word@localhost/greenlight up
no change

باید از خروجی ببینید که مقدار متغیر محیطی GREENLIGHT_DB_DSN شما با موفقیت کشیده شده و در قانون make استفاده شده است. اگر داشته‌اید با ما همراهی کنید، نباید هیچ مایگریشن اعمال نشده‌ای باقی بماند، بنابراین این باید با موفقیت و بدون اقدام بیشتر خارج شود.

همچنین در حال دیدن مزایای استفاده از makefile هستیم — توانایی تایپ make up بهبود بزرگی نسبت به به خاطر سپردن و استفاده از دستور کامل برای اجرای مایگریشن‌های 'up' است.

به همین ترتیب، اگر بخواهید، می‌توانید اجرای make psql را نیز امتحان کنید تا با psql به پایگاه داده greenlight متصل شوید.

ارسال آرگومان‌ها

ابزار make همچنین به شما امکان می‌دهد هنگام اجرای یک قانون خاص آرگومان‌های نام‌دار ارسال کنید. برای نشان دادن این موضوع، بیایید یک قانون migration به makefile خود اضافه کنیم تا یک جفت فایل مایگریشن جدید ایجاد کند. ایده این است که وقتی این قانون را اجرا می‌کنیم، نام فایل‌های مایگریشن را به عنوان آرگومان ارسال می‌کنیم، مشابه این:

$ make migration name=create_example_table

نحوه دسترسی به مقدار آرگومان‌های نام‌دار دقیقاً مشابه نحوه دسترسی به متغیرهای محیطی است. بنابراین، در مثال بالا، می‌توانیم به نام فایل مایگریشن از طریق ${name} در makefile خود دسترسی پیدا کنیم.

بیایید همین الان makefile را به روز کنید تا این قانون migration جدید را شامل شود، به این صورت:

فایل: Makefile
run:
	go run ./cmd/api

psql:
	psql ${GREENLIGHT_DB_DSN}

migration:
	@echo 'Creating migration files for ${name}...'
	migrate create -seq -ext=.sql -dir=./migrations ${name}

up:
	@echo 'Running up migrations...'
	migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

و اگر این قانون جدید را با آرگومان name=create_example_table اجرا کنید، باید خروجی زیر را ببینید:

$ make migration name=create_example_table
Creating migration files for create_example_table ...
migrate create -seq -ext=.sql -dir=./migrations create_example_table
/home/alex/Projects/greenlight/migrations/000007_create_example_table.up.sql
/home/alex/Projects/greenlight/migrations/000007_create_example_table.down.sql

اکنون همچنین دو فایل مایگریشن خالی جدید با نام create_example_table در پوشه مایگریشن‌های خود خواهید داشت. به این صورت:

$ ls ./migrations/
000001_create_movies_table.down.sql           000004_create_users_table.up.sql
000001_create_movies_table.up.sql             000005_create_tokens_table.down.sql
000002_add_movies_check_constraints.down.sql  000005_create_tokens_table.up.sql
000002_add_movies_check_constraints.up.sql    000006_add_permissions.down.sql
000003_add_movies_indexes.down.sql            000006_add_permissions.up.sql
000003_add_movies_indexes.up.sql              000007_create_example_table.down.sql
000004_create_users_table.down.sql            000007_create_example_table.up.sql

اگر داشته‌اید با ما همراهی کنید، ما در واقع از این دو فایل مایگریشن جدید استفاده نخواهیم کرد، بنابراین راحت باشید و آن‌ها را حذف کنید:

$ rm migrations/000007*

فضای نام‌گذاری اهداف

همچنان که makefile شما رشد می‌کند، ممکن است بخواهید شروع به نام‌گذاری فضایی نام اهداف خود کنید تا تمایزی بین قوانین ایجاد شود و به سازماندهی فایل کمک کند. به عنوان مثال، در یک makefile بزرگ به جای داشتن نام هدف up واضح‌تر است که به آن نام db/migrations/up بدهید.

توصیه می‌کنم از کاراکتر / به عنوان جداکننده فضای نام استفاده کنید، به جای نقطه، خط تیره یا کاراکتر :. در واقع، کاراکتر : باید به شدت در نام اهداف اجتناب شود زیرا می‌تواند هنگام استفاده از پیش‌نیازهای هدف مشکل ایجاد کند (چیزی که لحظاتی دیگر پوشش خواهیم داد).

بیایید نام اهداف خود را به روز کنیم تا از فضاهای نام منطقی استفاده کنیم، به این صورت:

فایل: Makefile
run/api:
	go run ./cmd/api

db/psql:
	psql ${GREENLIGHT_DB_DSN}

db/migrations/new:
	@echo 'Creating migration files for ${name}...'
	migrate create -seq -ext=.sql -dir=./migrations ${name}

db/migrations/up:
	@echo 'Running up migrations...'
	migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

و باید بتوانید قوانین را با تایپ نام کامل هدف هنگام اجرای make اجرا کنید. به عنوان مثال:

$ make run/api 
go run ./cmd/api
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

یک ویژگی خوب استفاده از کاراکتر / به عنوان جداکننده فضای نام این است که تکمیل خودکار در ترمینال هنگام تایپ نام اهداف دریافت می‌کنید. به عنوان مثال، اگر make db/migrations/ را تایپ کنید و سپس دکمه tab را روی صفحه کلید خود فشار دهید، اهداف باقیمانده در زیر فضای نام لیست می‌شوند. به این صورت:

$ make db/migrations/
new  up

اهداف پیش‌نیاز و درخواست تأیید

نحوه کلی برای یک قانون makefile که در ابتدای این فصل ارائه کردم کمی ساده‌سازی شده بود، زیرا همچنین امکان مشخص کردن اهداف پیش‌نیاز وجود دارد.

target: prerequisite-target-1 prerequisite-target-2 ...
	command
	command
	...

وقتی یک هدف پیش‌نیاز برای یک قانون مشخص می‌کنید، دستورات متناظر با اهداف پیش‌نیاز قبل از اجرای دستورات واقعی هدف اجرا می‌شوند.

بیایید این قابلیت را بهره‌برداری کنیم تا از کاربر تأیید برای ادامه قبل از اجرای قانون db/migrations/up خود بپرسیم.

برای این کار، یک هدف جدید confirm ایجاد می‌کنیم که از کاربر Are you sure? [y/N] می‌پرسد و اگر y وارد نکند با خطا خارج می‌شود. سپس از این هدف جدید confirm به عنوان پیش‌نیاز برای db/migrations/up استفاده می‌کنیم.

بیایید همین الان Makefile خود را به روز کنید به صورت زیر:

فایل: Makefile
# Create the new confirm target.
confirm:
	@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ]

run/api:
	go run ./cmd/api

db/psql:
	psql ${GREENLIGHT_DB_DSN}

db/migrations/new:
	@echo 'Creating migration files for ${name}...'
	migrate create -seq -ext=.sql -dir=./migrations ${name}

# Include it as prerequisite.
db/migrations/up: confirm
	@echo 'Running up migrations...'
	migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

کد در هدف confirm از این پست StackOverflow گرفته شده است. اساساً، آنچه اینجا اتفاق می‌افتد این است که از کاربر Are you sure? [y/N] می‌پرسیم و سپس پاسخ را می‌خوانیم. سپس از کد [ $${ans:-N} = y ] برای ارزیابی پاسخ استفاده می‌کنیم — اگر کاربر y وارد کند true و اگر هر چیز دیگری وارد کند false برمی‌گرداند. اگر یک دستور در makefile false برگرداند، make اجرای قانون را متوقف کرده و با پیام خطا خارج می‌شود — اساساً قانون را در مسیر خود متوقف می‌کند.

همچنین، به طور مهم، توجه کنید که confirm را به عنوان پیش‌نیاز برای هدف db/migrations/up تنظیم کرده‌ایم؟

بیایید این را امتحان کنیم و ببینیم وقتی y وارد می‌کنیم چه اتفاقی می‌افتد:

$ make db/migrations/up 
Are you sure? [y/N] y
Running up migrations...
migrate -path ./migrations -database postgres://greenlight:pa55word@localhost/greenlight up
no change

این خوب به نظر می‌رسد — دستورات در قانون db/migrations/up ما همانطور که انتظار داشتیم اجرا شده‌اند.

در مقابل، بیایید دوباره همان کار را امتحان کنیم اما هر حرف دیگری را هنگام درخواست تأیید وارد کنیم. این بار، make باید بدون اجرای چیزی در قانون db/migrations/up خارج شود. به این صورت:

$ make db/migrations/up 
Are you sure? [y/N] n
make: *** [Makefile:3: confirm] Error 1

استفاده از یک قانون confirm به عنوان هدف پیش‌نیاز به این صورت واقعاً یک الگوی قابل استفاده مجدد عالی است. هر زمان که یک قانون makefile دارید که کاری مضر یا خطرناک انجام می‌دهد، اکنون می‌توانید confirm را به عنوان هدف پیش‌نیاز اضافه کنید تا از کاربر تأیید برای ادامه بپرسید.

نمایش اطلاعات راهنما

یک چیز کوچک دیگر که می‌توانیم انجام دهیم تا makefile خود را کاربرپسندتر کنیم، شامل کردن برخی نظرات و قابلیت راهنما است. به طور خاص، هر قانون در makefile خود را با یک نظر در قالب زیر پیشوند می‌دهیم:

## <example target call>: <help text>

سپس یک قانون جدید help ایجاد می‌کنیم که خود makefile را پارس کرده، متن راهنما را از نظرات با استفاده از sed استخراج کرده، آن‌ها را در یک جدول قالب‌بندی کرده و سپس به کاربر نمایش می‌دهد.

اگر داشته‌اید با ما همراهی کنید، همین الان Makefile خود را به روز کنید تا به این صورت باشد:

فایل: Makefile
## help: print this help message
help:
	@echo 'Usage:'
	@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' |  sed -e 's/^/ /'

confirm:
	@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ]

## run/api: run the cmd/api application
run/api:
	go run ./cmd/api

## db/psql: connect to the database using psql
db/psql:
	psql ${GREENLIGHT_DB_DSN}

## db/migrations/new name=$1: create a new database migration
db/migrations/new:
	@echo 'Creating migration files for ${name}...'
	migrate create -seq -ext=.sql -dir=./migrations ${name}

## db/migrations/up: apply all up database migrations
db/migrations/up: confirm
	@echo 'Running up migrations...'
	migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

و اگر اکنون هدف help را اجرا کنید، باید پاسخی دریافت کنید که همه اهداف موجود و متن راهنمای متناظر را فهرست می‌کند. مشابه این:

$ make help
Usage: 
  help                        print this help message
  run/api                     run the cmd/api application
  db/psql                     connect to the database using psql
  db/migrations/new name=$1   create a new database migration
  db/migrations/up            apply all up database migrations 

همچنین باید اشاره کنم که قرار دادن قانون help به عنوان اولین چیز در Makefile یک حرکت عمدی است. اگر make را بدون مشخص کردن هدف اجرا کنید، به طور پیش‌فرض اولین قانون در فایل را اجرا می‌کند.

بنابراین این به این معنی است که اگر سعی کنید make را بدون هدف اجرا کنید، اکنون اطلاعات راهنما به شما نمایش داده می‌شود، به این صورت:

$ make
Usage: 
  help                        print this help message
  run/api                     run the cmd/api application
  db/psql                     connect to the database using psql
  db/migrations/new name=$1   create a new database migration
  db/migrations/up            apply all up database migrations 

اهداف غیرواقعی

در این فصل از make برای اجرای اقدامات استفاده کرده‌ایم، اما یک هدف دیگر (و به طور قابل بحث، هدف اصلی) make کمک به ایجاد فایل‌ها روی دیسک است، جایی که نام یک هدف نام فایلی است که توسط قانون ایجاد می‌شود.

اگر از make عمدتاً برای اجرای اقدامات استفاده می‌کنید، مانند ما، این می‌تواند مشکلی ایجاد کند اگر فایلی در دایرکتوری پروژه شما با همان مسیر به عنوان نام یک هدف وجود داشته باشد.

اگر بخواهید، می‌توانید این مشکل را با ایجاد فایلی به نام ./run/api در ریشه دایرکتوری پروژه خود نشان دهید، به این صورت:

$ mkdir run && touch run/api

و سپس اگر make run/api را اجرا کنید، به جای شروع application API ما پیام زیر را دریافت خواهید کرد:

$ make run/api 
make: 'run/api' is up to date. 

از آنجایی که قبلاً فایلی روی دیسک در ./run/api داریم، ابزار make در نظر می‌گیرد که این قانون قبلاً اجرا شده است و بنابراین پیامی که در بالا می‌بینیم را بدون انجام هیچ اقدام بیشتری برمی‌گرداند.

برای دور زدن این مشکل، می‌توانیم اهداف makefile خود را به عنوان اهداف غیرواقعی اعلام کنیم:

یک هدف غیرواقعی هدفی است که واقعاً نام یک فایل نیست؛ بلکه فقط نامی برای یک قانون است که اجرا شود.

برای اعلام یک هدف به عنوان غیرواقعی، می‌توانید آن را پیش‌نیاز هدف ویژه .PHONY کنید. نحوه به این صورت است:

.PHONY: target
target: prerequisite-target-1 prerequisite-target-2 ...
	command
	command
	...

بیایید همین الان Makefile خود را به روز کنیم تا همه قوانین ما اهداف غیرواقعی داشته باشند، به این صورت:

فایل: Makefile
## help: print this help message
.PHONY: help
help:
	@echo 'Usage:'
	@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' |  sed -e 's/^/ /'

.PHONY: confirm
confirm:
	@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ]

## run/api: run the cmd/api application
.PHONY: run/api
run/api:
	go run ./cmd/api

## db/psql: connect to the database using psql
.PHONY: db/psql
db/psql:
	psql ${GREENLIGHT_DB_DSN}

## db/migrations/new name=$1: create a new database migration
.PHONY: db/migrations/new
db/migrations/new:
	@echo 'Creating migration files for ${name}...'
	migrate create -seq -ext=.sql -dir=./migrations ${name}

## db/migrations/up: apply all up database migrations
.PHONY: db/migrations/up
db/migrations/up: confirm
	@echo 'Running up migrations...'
	migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

اگر دوباره make run/api را اجرا کنید، اکنون باید به درستی آن را به عنوان یک هدف غیرواقعی تشخیص دهد و قانون را برای ما اجرا کند:

$ make run/api 
go run ./cmd/api -db-dsn=postgres://greenlight:pa55word@localhost/greenlight
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development 

ممکن است فکر کنید که فقط در صورت داشتن نام فایل مغایر اعلام اهداف به عنوان غیرواقعی ضروری است، اما در عمل اعلام نکردن یک هدف به عنوان غیرواقعی وقتی واقعاً هست می‌تواند منجر به باگ‌ها یا رفتارهای گیج‌کننده شود. به عنوان مثال، تصور کنید اگر در آینده کسی ناآگاهانه فایلی به نام confirm در ریشه دایرکتوری پروژه ایجاد کند. این به این معنی است که قانون confirm ما هرگز اجرا نمی‌شود، که به نوبه خود منجر به اجرای قوانین خطرناک یا مضر بدون تأیید می‌شود.

برای جلوگیری از این نوع باگ، اگر یک قانون makefile دارید که یک اقدام انجام می‌دهد (به جای ایجاد فایل) بهتر است عادت کنید آن را به عنوان غیرواقعی اعلام کنید.

اگر داشته‌اید با ما همراهی کنید، می‌توانید محتوای پوشه run را که تازه ایجاد کردیم حذف کنید. به این صورت:

$ rm -rf run/

در مجموع، این در حال شکل‌گیری خوبی است. makefile ما شروع به حاوی برخی قابلیت‌های مفید کرده است و در فصول بعدی این کتاب به اضافه کردن موارد بیشتر به آن ادامه خواهیم داد.

اگرچه این یکی از آخرین کارهایی است که در این ساخت انجام می‌دهیم، ایجاد یک makefile در ریشه دایرکتوری پروژه معمولاً یکی از اولین کارهایی است که هنگام شروع پروژه انجام می‌دهم. من متوجه شده‌ام که استفاده از makefile برای وظایف رایج به صرفه‌جویی در تایپ و فشار ذهنی در طول توسعه کمک می‌کند و — در دراز مدت — به عنوان یک نقطه ورود مفید و یادآور نحوه عملکرد چیزها عمل می‌کند وقتی پس از یک وقفه طولانی به پروژه بازمی‌گردید.