ایجاد و استفاده از 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 ما اجرا میکند. به این صورت:
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 خود را به روز کنید تا این دو قانون جدید را شامل شود، به این صورت:
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 جدید را شامل شود، به این صورت:
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 بدهید.
توصیه میکنم از کاراکتر / به عنوان جداکننده فضای نام استفاده کنید، به جای نقطه، خط تیره یا کاراکتر :. در واقع، کاراکتر : باید به شدت در نام اهداف اجتناب شود زیرا میتواند هنگام استفاده از پیشنیازهای هدف مشکل ایجاد کند (چیزی که لحظاتی دیگر پوشش خواهیم داد).
بیایید نام اهداف خود را به روز کنیم تا از فضاهای نام منطقی استفاده کنیم، به این صورت:
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 خود را به روز کنید به صورت زیر:
# 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 خود را به روز کنید تا به این صورت باشد:
## 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 خود را به روز کنیم تا همه قوانین ما اهداف غیرواقعی داشته باشند، به این صورت:
## 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 برای وظایف رایج به صرفهجویی در تایپ و فشار ذهنی در طول توسعه کمک میکند و — در دراز مدت — به عنوان یک نقطه ورود مفید و یادآور نحوه عملکرد چیزها عمل میکند وقتی پس از یک وقفه طولانی به پروژه بازمیگردید.