فا نیکسی

پارامترهای بسته و بازنویسی‌ها با callPackage

نیکس (Nix) همراه با یک زبان برنامه‌نویسی منظورخاص برای ایجاد بسته‌ها و پیکربندی‌ها عرضه می‌شود: زبان Nix. از این زبان برای ساخت مجموعه‌ی بسته‌های نیکس (Nixpkgs)، که به نام Nixpkgs شناخته می‌شود، استفاده می‌گردد.

زبان Nix از آنجا که کاملاً تابعی / pure functional است، اجازه می‌دهد توابع سفارشی را برای انتزاع الگوهای رایج تعریف کنید. یکی از برجسته‌ترین الگوها در Nixpkgs، پارامتری‌سازی دستورالعمل‌های ساخت بسته است.

بررسی اجمالی

Nixpkgs به خودی خود یک پروژه نرم‌افزاری بزرگ است، با قراردادها و اصطلاحاتی که طی سال‌ها شکل گرفته‌اند. این پروژه قراردادی را بنا نهاده است که بر اساس آن، بسته‌های پارامتری‌شده با تنظیمات خودکار از طریق تابعی به نام callPackage ترکیب می‌شوند. این آموزش نحوه استفاده از آن و مزایایش را نشان می‌دهد.

چه چیزی خواهید آموخت؟

  • استفاده از callPackage برای فراخوانی دستورالعمل‌های ساخت بسته که از قراردادهای Nixpkgs پیروی می‌کنند
  • بازنویسی پارامترهای بسته
  • ایجاد مجموعه‌های بسته وابسته به یکدیگر

به چه چیزی نیاز دارید؟

چقدر زمان می‌برد؟

  • ۴۵ دقیقه

فراخوانی‌های خودکار توابع

یک فایل جدید به نام hello.nix ایجاد کنید که می‌تواند یک دستورالعمل ساخت بسته نمونه باشد که معمولاً در Nixpkgs یافت می‌شود: تابعی که یک مجموعه ویژگی (attribute set) دریافت می‌کند، با صفت (attribute)هایی که متناظر با درایویشن‌ها در مجموعه بسته‌های سطح بالا هستند، و یک درایویشن برمی‌گرداند.

hello.nix

{ writeShellScriptBin }:
writeShellScriptBin "hello" ''
 echo "Hello, world!"
''

توضیح تفصیلی فایل hello.nix تابعی را اعلام می‌کند که یک مجموعه ویژگی با یک صفت writeShellScriptBin را به عنوان آرگومان می‌پذیرد. تابع writeShellScriptBin به‌طور اتفاقی در Nixpkgs وجود دارد؛ یک کمک‌رسان ساخت که یک derivation برمی‌گرداند. خروجی derivation در اینجا شامل یک اسکریپت شل اجرایی در مسیر $out/bin/hello است که هنگام اجرا عبارت "Hello world" را چاپ می‌کند.

اکنون فایلی به نام default.nix با محتوای زیر ایجاد کنید:

default.nix

let
 pkgs = import <nixpkgs> { };
in
pkgs.callPackage ./hello.nix { }

درایویشن را در default.nix محقق (Realise) کرده و فایل اجرایی تولیدشده را اجرا کنید:

$ nix-build
$ ./result/bin/hello
Hello, world!

آرگومان writeShellScriptBin هنگامی که تابع موجود در hello.nix ارزیابی می‌شود، به‌طور خودکار پر می‌شود. برای هر صفت (attribute) در آرگومان تابع، callPackage یک صفت از مجموعه ویژگی pkgs را (در صورت وجود) ارسال می‌کند.

ممکن است ایجاد فایل اضافی hello.nix برای بسته در چنین راه‌اندازی ساده‌ای کمی دست‌وپاگیر به‌نظر برسد. ما این کار را انجام داده‌ایم زیرا این دقیقاً همان روش سازمان‌دهی Nixpkgs است: هر دستورالعمل ساخت (recipe) بسته، فایلی است که یک تابع را اعلام می‌کند. این تابع وابستگی‌های بسته را به‌عنوان آرگومان دریافت می‌کند.

ساخت‌های پارامترمحور

فایل default.nix را تغییر دهید تا یک مجموعه ویژگی از derivationها را تولید کند، به‌طوری که صفت hello شامل derivation اصلی باشد:

default.nix

let
 pkgs = import <nixpkgs> { };
in
{
 hello = pkgs.callPackage ./hello.nix { };
}

هنگام ساخت صفت hello، با دسترسی به آن توسط گزینه -A / --attr، نتیجه مانند قبل خواهد بود:

$ nix-build -A hello
$ ./result/bin/hello
Hello, world!

همچنین hello.nix را به گونه‌ای تغییر دهید که یک پارامتر اضافی audience با مقدار پیش‌فرض "world" اضافه کند:

hello.nix

{
 writeShellScriptBin,
 audience ? "world",
}:
writeShellScriptBin "hello" ''
 echo "Hello, ${audience}!"
''

این کار نیز نتیجه را تغییر نمی‌دهد.

وقتی فایل default.nix را طوری تغییر می‌دهید که از این آرگومان جدید استفاده کند، همه‌چیز جذاب‌تر می‌شود. پارامتر audience را در آرگومان دوم به callPackage ارسال کنید:

default.nix

 let
 pkgs = import <nixpkgs> { };
 in
 {
- hello = pkgs.callPackage ./hello.nix { };
+ hello = pkgs.callPackage ./hello.nix { audience = "people"; };
 }

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

آن را امتحان کنید:

$ nix-build -A hello
$ ./result/bin/hello
Hello, people!

این الگو به طور گسترده در Nixpkgs استفاده می‌شود: برای مثال، توابعی که برنامه‌های Go را نمایش می‌دهند اغلب دارای یک پارامتر buildGoModule هستند. یافتن عبارت‌هایی مانند callPackage ./go-program.nix {'{'} buildGoModule = buildGo116Module; {'}'} برای تغییر نسخه پیش‌فرض کامپایلر Go امری رایج است. بنابراین Nixpkgs صرفاً یک کتابخانه بزرگ از بسته‌های از‌پیش‌تنظیم‌شده نیست، بلکه مجموعه‌ای از توابع دستورالعمل‌های بسته‌بندی، برای سفارشی‌سازی بسته‌ها و حتی کل بوم‌سازگان‌ها (برای مثال «تمام بسته‌های پایتون با استفاده از مفسر سفارشی من») به صورت آنی و بدون تکرار کد است.

بازنشانی‌ها

callPackage با اجازه دادن به سفارشی‌سازی پارامترها پس از وقوع با استفاده از تابع override درایویشن بازگردانده‌شده، راحتی بیشتری را فراهم می‌کند.

یک صفت سوم به نام hello-folks به default.nix اضافه کنید و آن را روی hello.override تنظیم کنید که با مقدار جدیدی برای audience فراخوانی شده است:

default.nix

 let
 pkgs = import <nixpkgs> { };
 in
-{
+rec {
 hello = pkgs.callPackage ./hello.nix { audience = "people"; };
+ hello-folks = hello.override { audience = "folks"; };
 }

نکته

مجموعه ویژگی حاصل‌اکنون بازگشتی است (با استفاده از کلیدواژه rec). 즉، مقادیر ویژگی می‌توانند به نام‌های درون همان مجموعه ویژگی ارجاع دهند.

تابع override مقدار audience را به تابع اصلی موجود در hello.nix ارسال می‌کند - این کار هر آرگومان دیگری را که در callPackage اصلی برای تولید درایویشن hello ارسال شده بود، بازنشانی (override) می‌کند. سایر پارامترها دست‌نخورده باقی خواهند ماند. این ویژگی بسیار کاربردی است و اغلب در بسته‌هایی مشاهده می‌شود که گزینه‌های متعددی برای سفارشی‌سازی یک بسته فراهم می‌کنند.

ساخت صفت hello-folks و اجرای فایل اجرایی حاصل، بار دیگر نسخه جدیدی از اسکریپت را تولید خواهد کرد:

$ nix-build -A hello-folks
$ ./result/bin/hello
Hello, folks!

یک نمونه‌ی دنیای واقعی، دستور پخت بسته neovim است که دارای آرگومان‌های قابل‌بازنشانی مانند extraLuaPackages، extraPythonPackages یا withRuby می‌باشد. در حال حاضر، این پارامترها تنها با خواندن کد منبع قابل‌کشف هستند؛ کد منبعی که با دنبال کردن پیوند 📦 Source در search.nixos.org/packages پیدا می‌شود.

مجموعه‌های بسته‌ی وابسته به یکدیگر

شما در واقع می‌توانید نسخه خودتان از callPackage را بسازید! این کار برای مجموعه‌های بسته‌ای که دستور پخت‌هایشان به یکدیگر وابسته هستند، بسیار مفید است.

نکته

مثال‌های زیر فایل‌های «فراخوانی‌شده» را نشان نمی‌دهند، زیرا برای درک اصل موضوع نیازی به آن‌ها نیست.

مجموعه ویژگی بازگشتی زیر از derivationها را در نظر بگیرید:

default.nix

let
 pkgs = import <nixpkgs> { };
in
rec {
 a = pkgs.callPackage ./a.nix { };
 b = pkgs.callPackage ./b.nix { inherit a; };
 c = pkgs.callPackage ./c.nix { inherit b; };
 d = pkgs.callPackage ./d.nix { };
 e = pkgs.callPackage ./e.nix { inherit c d; };
}

نکته

در اینجا، inherit a; معادل a = a; است.

درایویشن‌های از‌پیش‌تعریف‌شده‌ از طریق callPackage به عنوان آرگومان به سایر درایویشن‌ها پاس داده می‌شوند.

در این حالت باید به خاطر داشته باشید که تمام آرگومان‌های مورد نیاز هر بسته را که در Nixpkgs وجود ندارند، به طور دستی در Nix file مربوطه مشخص کنید. اگر فایل ./b.nix به یک آرگومان a نیاز داشته باشد اما هیچ pkgs.a وجود نداشته باشد، فراخوانی تابع یک خطا تولید خواهد کرد. این کار می‌تواند به سرعت بسیار خسته‌کننده شود، به‌ویژه برای مجموعه‌های بسته‌ی بزرگ‌تر.

از lib.callPackageWith برای ساخت callPackage اختصاصی خودتان بر اساس یک مجموعه ویژگی استفاده کنید.

default.nix

let
 pkgs = import <nixpkgs> { };
 callPackage = pkgs.lib.callPackageWith (pkgs // packages);
 packages = {
 a = callPackage ./a.nix { };
 b = callPackage ./b.nix { };
 c = callPackage ./c.nix { };
 d = callPackage ./d.nix { };
 e = callPackage ./e.nix { };
 };
in
packages

این نیازمند مقداری توضیح است.

پیش از هر چیز، توجه داشته باشید که به جای یک مجموعه ویژگی بازگشتی، نام‌هایی که روی آن‌ها کار می‌کنیم اکنون در یک اتصالات let مقداردهی می‌شوند. این ویژگی مشابه مجموعه‌های بازگشتی را دارد: نام‌های سمت چپ را می‌توان در عبارت‌های سمت راست علامت مساوی (=) استفاده کرد. به این ترتیب است که وقتی محتوای packages را با مجموعه ویژگی از‌پیش‌موجود pkgs با استفاده از عملگر // ادغام می‌کنیم، می‌توانیم به packages ارجاع دهیم.

تابع callPackages سفارشی شما اکنون تمام صفات موجود در pkgs و packages را برای تابع بسته فراخوانی‌شده در دسترس قرار می‌دهد (نام‌های مشابه از packages اولویت دارند) و packages با هر فراخوانی به صورت بازگشتی ساخته می‌شود.

بخش آخر ممکن است سر شما را گیج بیاورد. این ساختار تنها به این دلیل امکان‌پذیر است که زبان Nix به صورت تنبل ارزیابی می‌شود. 즉، مقادیر تنها زمانی محاسبه می‌شوند که واقعاً مورد نیاز باشند. این امکان را فراهم می‌کند که packages بدون اینکه به‌طور کامل تعریف شده باشد، منتقل شود.

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

البته این مثال کوچک هنوز در شکل اصلی خود قابل مدیریت است. و گونه‌ی بازگشتی ضمنی می‌تواند ساختار را برای توسعه‌دهندگان نرم‌افزاری که با ارزیابی تنبل آشنا نیستند مبهم کند و خواندن آن را نسبت به قبل دشوارتر سازد. اما این مزیت واقعاً برای ساختارهای بزرگ نتیجهخش است؛ جایی که حجم کد است که ساختار را مبهم می‌کند و تغییرات دستی در آن دشوار و خطاپذیر می‌شود.

خلاصه

استفاده از callPackage نه تنها از قراردادهای Nixpkgs پیروی می‌کند (که باعث می‌شود کد شما برای کاربران باتجربه‌‌ی Nix راحت‌تر دنبال شود)، بلکه مزایایی را نیز به رایگان برای شما به ارمغان می‌آورد:

  1. ساخت‌های پارامتری‌شده
  2. ساخت‌های قابل‌بازنویسی
  3. پیاده‌سازی مختصر مجموعه‌های بسته‌های وابسته به یکدیگر

منابع

گام‌های بعدی

  • file-sets-tutorial - یادگیری نحوه بسته‌بندی پروژه‌های خود با Nix
  • module-system-deep-dive - یادگیری تسلط بر جادوی برنامه‌نویسی تابعی پشت سر NixOS

nix.dev/tutorials/callpackage

نیکسی · یادداشت‌های فارسی Nix local fonts