در نگاهی به صفحات قبلی، یک موضوع بارها و بارها تکرار می شود: مدیریت کد شما نسبتاً ساده است، اما مدیریت وابستگی های آن بسیار دشوارتر است. انواع وابستگیها وجود دارد: گاهی اوقات به یک کار وابستگی وجود دارد (مانند «قبل از اینکه یک نسخه کامل را علامتگذاری کنم، اسناد را فشار دهید»)، و گاهی اوقات به یک مصنوع وابستگی وجود دارد (مانند «من باید آخرین نسخه را داشته باشم». از کتابخانه بینایی کامپیوتر برای ساخت کد من"). گاهی اوقات، شما وابستگی های داخلی به بخش دیگری از پایگاه کد خود دارید، و گاهی اوقات وابستگی های خارجی به کد یا داده های متعلق به تیم دیگری (چه در سازمان شما یا یک شخص ثالث) دارید. اما در هر صورت، ایده "من قبل از اینکه بتوانم این را داشته باشم به آن نیاز دارم" چیزی است که به طور مکرر در طراحی سیستم های ساخت تکرار می شود و مدیریت وابستگی ها شاید اساسی ترین کار یک سیستم ساخت باشد.
برخورد با ماژول ها و وابستگی ها
پروژههایی که از سیستمهای ساخت مبتنی بر مصنوع مانند Bazel استفاده میکنند به مجموعهای از ماژولها تقسیم میشوند که ماژولها وابستگیهای خود را از طریق فایلهای BUILD
به یکدیگر بیان میکنند. سازماندهی مناسب این ماژولها و وابستگیها میتواند تأثیر زیادی بر عملکرد سیستم ساخت و میزان کار برای حفظ آن داشته باشد.
با استفاده از ماژول های ریز دانه و قانون 1:1:1
اولین سوالی که هنگام ساختاردهی یک ساخت مبتنی بر مصنوع مطرح می شود، تصمیم گیری است که یک ماژول منفرد باید چه مقدار عملکرد را در بر بگیرد. در Bazel، یک ماژول با یک هدف نشان داده می شود که یک واحد قابل ساخت مانند java_library
یا go_binary
را مشخص می کند. در یک افراط، کل پروژه را می توان با قرار دادن یک فایل BUILD
در ریشه و به صورت بازگشتی همه فایل های منبع آن پروژه در یک ماژول قرار داد. از طرف دیگر، تقریباً هر فایل منبع را میتوان به ماژول خاص خود تبدیل کرد، و عملاً لازم است هر فایلی هر فایل دیگری را که به آن وابسته است در یک فایل BUILD
فهرست کند.
اکثر پروژهها جایی بین این افراطها قرار میگیرند و انتخاب شامل یک مبادله بین عملکرد و قابلیت نگهداری است. استفاده از یک ماژول واحد برای کل پروژه ممکن است به این معنا باشد که شما هرگز نیازی به لمس فایل BUILD
ندارید، مگر زمانی که یک وابستگی خارجی اضافه میکنید، اما به این معنی است که سیستم ساخت همیشه باید کل پروژه را به یکباره بسازد. این به این معنی است که نمیتواند بخشهایی از ساخت را موازی یا توزیع کند، و همچنین نمیتواند قسمتهایی را که قبلاً ساخته شده است، ذخیره کند. یک ماژول در هر فایل برعکس است: سیستم ساخت حداکثر انعطافپذیری را در ذخیرهسازی و زمانبندی مراحل ساخت دارد، اما مهندسان باید تلاش بیشتری را برای حفظ فهرستی از وابستگیها صرف کنند، هر زمان که فایلها به کدام مرجع تغییر میکنند.
اگرچه جزئیات دقیق بر اساس زبان (و اغلب حتی در زبان) متفاوت است، گوگل تمایل دارد ماژولهای بسیار کوچکتری را نسبت به آنچه که معمولاً در یک سیستم ساخت مبتنی بر وظیفه بنویسد، ترجیح دهد. یک باینری تولید معمولی در گوگل اغلب به ده ها هزار هدف بستگی دارد و حتی یک تیم با اندازه متوسط می تواند چندین صد هدف را در پایگاه کد خود داشته باشد. برای زبان هایی مانند جاوا که مفهوم داخلی قوی بسته بندی دارند، هر دایرکتوری معمولاً حاوی یک بسته، هدف و فایل BUILD
است (Pants، یک سیستم ساخت دیگر مبتنی بر Bazel، این قانون را 1:1:1 می نامد). زبانهایی با قراردادهای بستهبندی ضعیفتر، اغلب اهداف متعددی را در هر فایل BUILD
تعریف میکنند.
مزایای اهداف ساخت کوچکتر واقعاً در مقیاس ظاهر می شود زیرا منجر به توزیع سریعتر ساخت و نیاز کمتر به بازسازی اهداف می شود. این مزایا پس از ورود آزمایش به تصویر قانعکنندهتر میشوند، زیرا اهداف دقیقتر به این معنی است که سیستم ساخت میتواند در اجرای تنها زیرمجموعه محدودی از آزمایشها که میتواند تحت تأثیر هر تغییری قرار گیرد بسیار هوشمندتر باشد. از آنجایی که Google به مزایای سیستمی استفاده از اهداف کوچکتر اعتقاد دارد، ما با سرمایهگذاری در ابزارهایی برای مدیریت خودکار فایلهای BUILD
برای جلوگیری از تحمیل بار توسعهدهندگان، گامهایی را در کاهش این مشکلات برداشتهایم.
برخی از این ابزارها، مانند buildifier
و buildozer
، با Bazel در دایرکتوری buildtools
در دسترس هستند.
به حداقل رساندن دید ماژول
Bazel و دیگر سیستمهای ساخت به هر هدف اجازه میدهند که یک قابلیت مشاهده را مشخص کند - ویژگی که تعیین میکند کدام اهداف دیگر ممکن است به آن وابسته باشند. یک هدف خصوصی فقط در فایل BUILD
خودش قابل ارجاع است. یک هدف ممکن است دید وسیع تری را به اهداف یک لیست مشخص از فایل های BUILD
یا در مورد دید عمومی، به هر هدف در فضای کاری بدهد.
مانند بسیاری از زبان های برنامه نویسی، معمولاً بهترین کار این است که دید را تا حد امکان به حداقل برسانید. به طور کلی، تیمهای Google فقط در صورتی اهداف را عمومی میکنند که آن اهداف، کتابخانههای پرکاربردی را که در دسترس هر تیمی در Google است، نشان دهند. تیمهایی که از دیگران میخواهند قبل از استفاده از کدشان با آنها هماهنگی کنند، فهرستی از مجوزهای هدفهای مشتری را بهعنوان نمایان شدن هدفشان حفظ میکنند. اهداف پیاده سازی داخلی هر تیم فقط به دایرکتوری های متعلق به تیم محدود می شود و اکثر فایل های BUILD
تنها یک هدف دارند که خصوصی نیست.
مدیریت وابستگی ها
ماژول ها باید بتوانند به یکدیگر ارجاع دهند. نقطه ضعف شکستن یک کد پایه به ماژولهای ریز این است که شما باید وابستگیهای آن ماژولها را مدیریت کنید (اگرچه ابزارها میتوانند به خودکارسازی این امر کمک کنند). بیان این وابستگی ها معمولاً قسمت عمده ای از محتوای یک فایل BUILD
است.
وابستگی های داخلی
در یک پروژه بزرگ که به ماژول های ریز دانه تقسیم می شود، بیشتر وابستگی ها احتمالاً داخلی هستند. یعنی روی هدف دیگری که در همان مخزن منبع تعریف و ساخته شده است. وابستگیهای داخلی با وابستگیهای خارجی تفاوت دارند زیرا از منبع ساخته میشوند نه اینکه بهعنوان یک مصنوع از پیش ساخته شده در حین اجرای بیلد دانلود شوند. این همچنین به این معنی است که هیچ مفهومی از "نسخه" برای وابستگی های داخلی وجود ندارد - یک هدف و همه وابستگی های داخلی آن همیشه در یک commit/revision در مخزن ساخته می شوند. یکی از مسائلی که باید با توجه به وابستگی های داخلی به دقت مورد بررسی قرار گیرد، نحوه برخورد با وابستگی های گذرا است (شکل 1). فرض کنید هدف A به هدف B بستگی دارد، که به یک هدف کتابخانه مشترک C بستگی دارد. آیا هدف A باید بتواند از کلاس های تعریف شده در هدف C استفاده کند؟
شکل 1 . وابستگی های گذرا
تا آنجا که به ابزارهای اساسی مربوط می شود، هیچ مشکلی با این وجود ندارد. هر دو B و C در زمان ساخت به هدف A پیوند داده می شوند، بنابراین هر نمادی که در C تعریف شده است برای A شناخته شده است. Bazel برای سال ها این اجازه را می دهد، اما با رشد گوگل، ما شروع به دیدن مشکلات کردیم. فرض کنید که B به گونه ای بازسازی شده باشد که دیگر نیازی به وابستگی به C نداشته باشد. اگر وابستگی B به C حذف شود، A و هر هدف دیگری که از C از طریق وابستگی به B استفاده می کند شکسته می شود. به طور موثر، وابستگی های یک هدف بخشی از قرارداد عمومی آن شد و هرگز نمی توانست با خیال راحت تغییر کند. این بدان معناست که وابستگیها در طول زمان انباشته شده و بیلدها در گوگل شروع به کند شدن کردند.
گوگل در نهایت این مشکل را با معرفی "حالت وابستگی گذرا سخت" در Bazel حل کرد. در این حالت، Bazel تشخیص می دهد که آیا یک هدف سعی می کند یک نماد را بدون وابستگی مستقیم به آن ارجاع دهد یا خیر و در این صورت، با یک خطا و یک فرمان پوسته که می تواند برای درج خودکار وابستگی استفاده شود شکست می خورد. اجرای این تغییر در کل پایگاه کد گوگل و بازسازی هر یک از میلیونها هدف ساخت ما برای فهرست کردن صریح وابستگیهای آنها، تلاشی چند ساله بود، اما ارزشش را داشت. با توجه به اینکه اهداف وابستگیهای غیرضروری کمتری دارند، ساختهای ما اکنون بسیار سریعتر هستند و مهندسان این اختیار را دارند که وابستگیهایی را که به آنها نیاز ندارند، بدون نگرانی در مورد شکستن اهدافی که به آنها وابسته هستند، حذف کنند.
طبق معمول، اعمال وابستگی های سخت گذرا شامل یک مبادله بود. این باعث شد فایلهای بیلد پرمخاطبتر شوند، زیرا کتابخانههای پرکاربرد در حال حاضر باید بهطور صریح در بسیاری از مکانها فهرست شوند، نه اینکه تصادفی وارد شوند، و مهندسان باید تلاش بیشتری برای افزودن وابستگیها به فایلهای BUILD
صرف کنند. از آن زمان ما ابزارهایی را توسعه دادهایم که با شناسایی خودکار بسیاری از وابستگیهای گمشده و افزودن آنها به فایلهای BUILD
بدون هیچ گونه مداخله توسعهدهنده، این زحمت را کاهش میدهند. اما حتی بدون چنین ابزارهایی، ما متوجه شدیم که این مبادله در مقیاس کد پایه ارزش آن را دارد: افزودن صریح یک وابستگی به فایل BUILD
هزینهای یکباره است، اما پرداختن به وابستگیهای گذرا ضمنی میتواند باعث ایجاد مشکلات مداوم شود. همانطور که هدف ساخت وجود دارد. Bazel به طور پیشفرض وابستگیهای گذرا را به کد جاوا اعمال میکند.
وابستگی های خارجی
اگر یک وابستگی داخلی نیست، باید خارجی باشد. وابستگی های خارجی به مصنوعاتی گفته می شود که خارج از سیستم ساخت و ساز ساخته و ذخیره می شوند. وابستگی مستقیماً از یک مخزن مصنوع (معمولاً از طریق اینترنت قابل دسترسی است) وارد می شود و به جای اینکه از منبع ساخته شود همانطور که هست استفاده می شود. یکی از بزرگترین تفاوت های وابستگی های خارجی و داخلی این است که وابستگی های خارجی دارای نسخه هایی هستند و آن نسخه ها مستقل از کد منبع پروژه وجود دارند.
مدیریت وابستگی خودکار در مقابل دستی
سیستمهای ساخت میتوانند به نسخههای وابستگیهای خارجی اجازه مدیریت دستی یا خودکار را بدهند. وقتی به صورت دستی مدیریت میشود، buildfile به صراحت نسخهای را که میخواهد از مخزن مصنوع بارگیری کند، فهرست میکند، که اغلب از یک رشته نسخه معنایی مانند 1.1.4
استفاده میکند. وقتی به صورت خودکار مدیریت می شود، فایل منبع طیفی از نسخه های قابل قبول را مشخص می کند و سیستم ساخت همیشه آخرین نسخه را دانلود می کند. به عنوان مثال، Gradle اجازه می دهد تا یک نسخه وابستگی به عنوان "1.+" اعلام شود تا مشخص کند که هر نسخه جزئی یا پچ یک وابستگی تا زمانی که نسخه اصلی 1 باشد قابل قبول است.
وابستگیهای مدیریت خودکار میتوانند برای پروژههای کوچک راحت باشند، اما معمولاً دستور العملی برای فاجعه در پروژههایی با اندازه غیرمعمول یا بیش از یک مهندس روی آنها کار میکنند. مشکل وابستگی های مدیریت شده به صورت خودکار این است که کنترلی روی زمان به روز رسانی نسخه ندارید. هیچ راهی برای تضمین اینکه طرفهای خارجی بهروزرسانیهای قطعی را انجام ندهند وجود ندارد (حتی زمانی که ادعا میکنند از نسخهسازی معنایی استفاده میکنند)، بنابراین ساختی که یک روز کار میکرد ممکن است روز بعد بدون هیچ راه آسانی برای تشخیص تغییر یا بازگرداندن آن شکسته شود. به حالت کار حتی اگر بیلد خراب نشود، ممکن است رفتار یا تغییرات عملکردی ظریفی ایجاد شود که ردیابی آن غیرممکن است.
در مقابل، از آنجایی که وابستگیهای مدیریت شده به صورت دستی نیاز به تغییر در کنترل منبع دارند، میتوان آنها را به راحتی کشف کرد و به عقب بازگرداند، و میتوان نسخه قدیمیتری از مخزن را برای ساخت با وابستگیهای قدیمیتر بررسی کرد. Bazel نیاز دارد که نسخههای همه وابستگیها به صورت دستی مشخص شوند. حتی در مقیاسهای متوسط، هزینه مدیریت نسخه دستی به دلیل ثباتی که ارائه میکند ارزش آن را دارد.
قانون یک نسخه
نسخههای مختلف یک کتابخانه معمولاً با مصنوعات مختلف نشان داده میشوند، بنابراین از نظر تئوری دلیلی وجود ندارد که نسخههای مختلف وابستگی خارجی یکسان، هر دو در سیستم ساخت با نامهای مختلف اعلام نشوند. به این ترتیب، هر هدف میتواند نسخهای از وابستگی را که میخواهد استفاده کند، انتخاب کند. این در عمل باعث مشکلات زیادی میشود، بنابراین Google یک قانون یک نسخه سختگیرانه را برای همه وابستگیهای شخص ثالث در پایگاه کد ما اعمال میکند.
بزرگترین مشکل با اجازه دادن به چندین نسخه، مسئله وابستگی الماس است. فرض کنید که هدف A به هدف B و v1 یک کتابخانه خارجی بستگی دارد. اگر هدف B بعداً برای افزودن یک وابستگی به v2 از همان کتابخانه خارجی مجدداً ساخته شود، هدف A شکسته خواهد شد زیرا اکنون به طور ضمنی به دو نسخه مختلف از همان کتابخانه وابسته است. در واقع، افزودن یک وابستگی جدید از یک هدف به هر کتابخانه شخص ثالثی با چندین نسخه هرگز ایمن نیست، زیرا هر یک از کاربران آن هدف میتواند قبلاً به نسخه دیگری وابسته باشد. پیروی از قانون یک نسخه، این تضاد را غیرممکن میکند - اگر هدفی به یک کتابخانه شخص ثالث وابستگی اضافه کند، هر وابستگی موجود قبلاً به همان نسخه خواهد بود، بنابراین میتوانند با خوشحالی همزیستی کنند.
وابستگی های خارجی گذرا
مقابله با وابستگی های گذرا یک وابستگی خارجی می تواند به ویژه دشوار باشد. بسیاری از مخازن مصنوعات مانند Maven Central به آرتیفکت ها اجازه می دهند تا وابستگی هایی را به نسخه های خاصی از دیگر مصنوعات موجود در مخزن مشخص کنند. ابزارهای بیلد مانند Maven یا Gradle اغلب به صورت بازگشتی هر وابستگی گذرا را به طور پیش فرض دانلود می کنند، به این معنی که افزودن یک وابستگی واحد به پروژه شما می تواند به طور بالقوه باعث شود که ده ها مصنوع در کل دانلود شوند.
این بسیار راحت است: هنگام افزودن یک وابستگی به یک کتابخانه جدید، ردیابی هر یک از وابستگی های متعدی آن کتابخانه و اضافه کردن همه آنها به صورت دستی دردسر بزرگی خواهد بود. اما یک نقطه ضعف بزرگ نیز وجود دارد: از آنجا که کتابخانههای مختلف میتوانند به نسخههای مختلفی از یک کتابخانه شخص ثالث وابسته باشند، این استراتژی لزوماً قانون یک نسخه را نقض میکند و منجر به مشکل وابستگی الماس میشود. اگر هدف شما به دو کتابخانه خارجی وابسته است که از نسخههای متفاوتی از یک وابستگی استفاده میکنند، نمیتوان گفت کدام یک را دریافت خواهید کرد. این همچنین به این معنی است که اگر نسخه جدید شروع به کشیدن نسخه های متضاد برخی از وابستگی های خود کند، به روز رسانی یک وابستگی خارجی می تواند باعث خرابی های ظاهرا نامرتبط در کل پایگاه کد شود.
به همین دلیل، Bazel به طور خودکار وابستگی های متعدی را دانلود نمی کند. و، متأسفانه، هیچ گلوله نقره ای وجود ندارد - جایگزین Bazel این است که به یک فایل سراسری نیاز دارد که تک تک وابستگی های خارجی مخزن را فهرست کند و یک نسخه صریح که برای آن وابستگی در سراسر مخزن استفاده می شود. خوشبختانه، Bazel ابزارهایی را ارائه می دهد که قادر به تولید خودکار چنین فایلی حاوی وابستگی های انتقالی مجموعه ای از مصنوعات Maven هستند. این ابزار می تواند یک بار برای تولید فایل WORKSPACE
اولیه برای یک پروژه اجرا شود و سپس آن فایل می تواند به صورت دستی برای تنظیم نسخه های هر وابستگی به روز شود.
با این حال، انتخاب در اینجا بین راحتی و مقیاس پذیری است. پروژه های کوچک ممکن است ترجیح دهند که نگران مدیریت وابستگی های گذرا نباشند و ممکن است بتوانند با استفاده از وابستگی های گذرای خودکار کنار بیایند. با رشد سازمان و پایگاه کد، این استراتژی کمتر و کمتر جذاب می شود و تضادها و نتایج غیرمنتظره بیشتر و بیشتر می شوند. در مقیاس های بزرگتر، هزینه مدیریت دستی وابستگی ها بسیار کمتر از هزینه رسیدگی به مسائل ناشی از مدیریت خودکار وابستگی است.
ذخیره سازی نتایج ساخت با استفاده از وابستگی های خارجی
وابستگی های خارجی اغلب توسط اشخاص ثالث ارائه می شود که نسخه های پایدار کتابخانه ها را منتشر می کنند، شاید بدون ارائه کد منبع. برخی از سازمانها همچنین ممکن است برخی از کدهای خود را به عنوان مصنوع در دسترس قرار دهند، و به سایر قطعات کد اجازه میدهند که بهعنوان شخص ثالث به جای وابستگی داخلی به آنها وابسته شوند. اگر آرتیفکتها دیر ساخته شوند اما سریع دانلود شوند، این میتواند از نظر تئوری ساختها را افزایش دهد.
با این حال، این امر همچنین سربار و پیچیدگی زیادی را به همراه دارد: شخصی باید مسئول ساخت هر یک از آن مصنوعات و آپلود آنها در مخزن مصنوع باشد، و مشتریان باید اطمینان حاصل کنند که با آخرین نسخه به روز می مانند. اشکالزدایی نیز بسیار دشوارتر میشود، زیرا بخشهای مختلف سیستم از نقاط مختلف مخزن ساخته شدهاند، و دیگر دیدگاه ثابتی از درخت منبع وجود ندارد.
یک راه بهتر برای حل مشکل ساخت مصنوعات، استفاده از یک سیستم ساخت است که از حافظه پنهان از راه دور پشتیبانی می کند، همانطور که قبلا توضیح داده شد. چنین سیستم ساختی، مصنوعات به دست آمده را از هر ساختنی در مکانی که بین مهندسان به اشتراک گذاشته شده است، ذخیره می کند، بنابراین اگر یک توسعه دهنده به مصنوعی که اخیراً توسط شخص دیگری ساخته شده است وابسته باشد، سیستم ساخت به جای ساختن آن، به طور خودکار آن را دانلود می کند. این همه مزایای عملکردی را فراهم می کند که مستقیماً به مصنوعات بستگی دارد و در عین حال اطمینان می دهد که ساخت ها به همان اندازه سازگار هستند که گویی همیشه از یک منبع ساخته شده اند. این استراتژی به صورت داخلی توسط گوگل استفاده میشود و Bazel را میتوان برای استفاده از حافظه پنهان از راه دور پیکربندی کرد.
امنیت و قابلیت اطمینان وابستگی های خارجی
بسته به مصنوعات از منابع شخص ثالث ذاتاً خطرناک است. اگر منبع شخص ثالث (مانند مخزن مصنوع) از کار بیفتد، خطر در دسترس بودن وجود دارد، زیرا اگر نتواند یک وابستگی خارجی را دانلود کند ممکن است کل ساخت شما متوقف شود. همچنین یک خطر امنیتی وجود دارد: اگر سیستم شخص ثالث توسط یک مهاجم به خطر بیفتد، مهاجم می تواند مصنوع ارجاع شده را با یکی از طراحی های خود جایگزین کند و به آنها اجازه می دهد کد دلخواه را به ساخت شما تزریق کنند. هر دو مشکل را می توان با انعکاس هر آرتیفکتی که به آن وابسته هستید در سرورهایی که کنترل می کنید کاهش داد و سیستم ساخت خود را از دسترسی به مخازن مصنوعات شخص ثالث مانند Maven Central مسدود کرد. معاوضه این است که نگهداری این آینه ها نیازمند تلاش و منابع است، بنابراین انتخاب استفاده از آنها اغلب به مقیاس پروژه بستگی دارد. همچنین میتوان با نیاز به درهمسازی هر مصنوع شخص ثالث در مخزن منبع، با هزینه کمی از مشکل امنیتی کاملاً جلوگیری کرد و در صورت دستکاری آرتیفکت باعث از کار افتادن ساخت میشود. جایگزین دیگری که مشکل را کاملاً کنار می گذارد، فروش وابستگی های پروژه شما است. هنگامی که یک پروژه وابستگی های خود را عرضه می کند، آنها را به عنوان منبع یا باینری در کنار کد منبع پروژه در کنترل منبع بررسی می کند. این به طور موثر به این معنی است که تمام وابستگی های خارجی پروژه به وابستگی های داخلی تبدیل می شوند. Google از این رویکرد به صورت داخلی استفاده میکند و هر کتابخانه شخص ثالثی را که در سرتاسر Google به third_party
از شخص ثالث در ریشه درخت منبع Google ارجاع شده است، بررسی میکند. با این حال، این در Google فقط به این دلیل کار میکند که سیستم کنترل منبع Google برای مدیریت یک monorepo بسیار بزرگ ساخته شده است، بنابراین فروشندگی ممکن است گزینهای برای همه سازمانها نباشد.