أنظمة البناء المستندة إلى العناصر

تتناول هذه الصفحة أنظمة الإنشاء المستندة إلى العناصر والفلسفة التي تستند إلى إبداعها. Bazel هو نظام إصدار يستند إلى العناصر. تُعدّ أنظمة الإصدار المستندة إلى المهام خطوة جيدة فوق النصوص البرمجية، ولكنها توفّر قدرًا كبيرًا من القوة للمهندسين الفرديين من خلال السماح لهم بتحديد مهامهم الخاصة.

تحتوي أنظمة الإنشاء المستندة إلى العناصر على عدد صغير من المهام التي يحدّدها النظام والتي يمكن للمهندسين إعدادها بطريقة محدودة. لا يزال المهندسون يخبرون النظام بالعناصر التي يجب إنشاؤها، ولكن نظام الإصدار يحدّد كيفية إنشائه. كما هو الحال في أنظمة الإنشاء المستندة إلى المهام، لا تزال أنظمة الإنشاء المستندة إلى العناصر، مثل Bazel، تتضمن ملفات بنية، ولكن محتوى ملفات الإصدار هذه مختلف جدًا. فبدلاً من اتّخاذ إجراء ضروري لتنفيذ مجموعة من الأوامر بلغة مستنِدة إلى لغة "تورينغ"، توضّح كيفية إنتاج النتائج، فإنّ ملفات build في Bazel هي بيان يتضمّن بيانًا يتضمّن مجموعة من القطع الأثرية التي تعتمد على التصميم واعتمادياته ومجموعة محدودة من الخيارات التي تؤثر في طريقة إنشائها. عند تشغيل المهندسين لـ bazel في سطر الأوامر، يحدّدون مجموعة من الأهداف لإنشاء (ما)، ويكون Bazel مسؤولاً عن ضبط خطوات التجميع وتشغيلها وجدولتها (كيفية). ولأن نظام التصميم أصبح لديه الآن إمكانية التحكّم الكامل في الأدوات التي يتم تشغيلها عند تشغيله، يمكنه تحقيق ضمانات أكثر فعالية تسمح له بأن يكون أكثر فعالية مع ضمان السلامة في الوقت نفسه.

منظور وظيفي

من السهل إجراء مقارنة بين أنظمة التصميم المستندة إلى العناصر والبرمجة الوظيفية. تحدّد لغات البرمجة الإمبراطورية التقليدية (مثل Java وC و Python) قوائم العبارات التي سيتم تنفيذها واحدة تلو الأخرى، بالطريقة نفسها التي تسمح بها أنظمة التصميم المستندة إلى المهام للبرمجة في تحديد سلسلة من الخطوات. أمّا لغات البرمجة الوظيفية (مثل Haskell وML)، فتشبه تنظيمها أقرب إلى سلسلة من المعادلات الرياضية. في اللغات الوظيفية، يصف المبرمج عملية حسابية لتنفيذها، ولكنه يترك تفاصيل وقت تنفيذ الحوسبة وكيفية تنفيذها بالضبط على برنامج التجميع.

ويؤدي ذلك إلى ربط فكرة بيان البيان في نظام إصدار قائم على العناصر والسماح للنظام بمعرفة كيفية تنفيذ الإصدار. لا يمكن التعبير عن الكثير من المشاكل بسهولة باستخدام الوظائف الوظيفية، ولكنها تستفيد كثيرًا منها: غالبًا ما تكون اللغة قادرة على موازاة مثل هذه البرامج وتقديم ضمانات قوية حول صحتها التي قد تكون مستحيلة في اللغة الإمبراطورية. إنّ أسهل ما يمكن التعبير عنه باستخدام البرمجة الوظيفية هي المشاكل التي تنطوي على تحويل جزء من البيانات إلى جزء آخر باستخدام سلسلة من القواعد أو الدوال. وهذا بالضبط هو نظام التصميم: النظام بأكمله عبارة عن وظيفة حسابية تأخذ الملفات المصدر (والأدوات، مثل برنامج التجميع) كإدخالات وتنشئ برامج ثنائية كمخرجات. لذلك، ليس من الغريب أن تعمل هذه الأداة على أساس إنشاء نظام بناء على مبادئ البرمجة الوظيفية.

فهم أنظمة البناء المستندة إلى العناصر

كان "بليز" نظام تصميم Google الذي يُعد أول نظام بناء قائم على العناصر. Bazel هو إصدار مفتوح المصدر من Blaze.

في ما يلي الشكل الذي يظهر به ملف build (يُسمّى عادةً BUILD) في Bazel:

java_binary(
    name = "MyBinary",
    srcs = ["MyBinary.java"],
    deps = [
        ":mylib",
    ],
)
java_library(
    name = "mylib",
    srcs = ["MyLibrary.java", "MyHelper.java"],
    visibility = ["//java/com/example/myproduct:__subpackages__"],
    deps = [
        "//java/com/example/common",
        "//java/com/example/myproduct/otherlib",
    ],
)

في تطبيق Bazel، يحدّد ملفّا BUILD الأهداف، وهما نوعَا الاستهدافات هنا: java_binary وjava_library. ويتوافق كل هدف مع عنصر يمكن إنشاؤه من خلال النظام: تنتج الأهداف الثنائية برامج ثنائية يمكن تنفيذها مباشرةً، في حين تهدف استهدافات المكتبة إلى إنشاء مكتبات يمكن استخدامها من خلال برامج ثنائية أو مكتبات أخرى. يحتوي كل هدف على:

  • name: كيف تتم الإشارة إلى الهدف في سطر الأوامر والأهداف الأخرى
  • srcs: الملفات المصدر التي تم تجميعها لإنشاء عناصر للعنصر
  • deps: الأهداف الأخرى التي يجب إنشاؤها قبل هذا الهدف وربطه بها

يمكن أن تكون المهام التابعة ضمن الحزمة نفسها (مثل اعتمادية MyBinary على :mylib) أو على حزمة مختلفة في التدرج الهرمي نفسه المصدر (مثل اعتمادية mylib على //java/com/example/common).

كما هو الحال مع أنظمة التصميم المستندة إلى المهام، يمكنك إنشاء الإصدارات باستخدام أداة سطر الأوامر في Bazel. لإنشاء الهدف MyBinary، عليك تشغيل bazel build :MyBinary. بعد إدخال هذا الأمر للمرة الأولى في مستودع نظيف، يُجري Bazel ما يلي:

  1. يحلل كل ملف BUILD في مساحة العمل لإنشاء رسم بياني للاعتماديات بين العناصر.
  2. ويستخدم الرسم البياني لتحديد المهام التابعة الانتقالية لـ MyBinary. بمعنى أن كل هدف يعتمد عليه MyBinary وكل هدف تعتمد عليه هذه الأهداف بشكل متكرر.
  3. تنشئ كل اعتمادية من هذه التبعيات بالترتيب. يبدأ Bazel بإنشاء كل هدف لا يحتوي على أي تبعيات أخرى ويتتبّع المهام التابعة التي لا تزال بحاجة إلى إنشائها لكل هدف. بمجرد إنشاء كل المهام التابعة الهدف، يبدأ Bazel في إنشاء ذلك الهدف. وتستمر هذه العملية حتى يتم إنشاء كل اعتماديات MyBinary المباشرة.
  4. يتم إنشاء MyBinary لإنشاء برنامج ثنائي نهائي قابل للتنفيذ يربط جميع المهام التابعة التي تم إنشاؤها في الخطوة 3.

في الأساس، قد يبدو أن ما يحدث هنا يختلف كثيرًا عن ما حدث عند استخدام نظام إصدار مستند إلى المهام. في الواقع، النتيجة النهائية هي برنامج ثنائي واحد، وكانت عملية تحليله تتطلب تحليل مجموعة من الخطوات للعثور على تبعيات بينها، ثم تنفيذ هذه الخطوات بالترتيب. ولكن هناك اختلافات ملحّة. ويظهر الأمر الأول في الخطوة 3: بما أنّ Bazel يعرف أنّ كل هدف يؤدي إلى إنشاء مكتبة Java، يعني ذلك أنّه ليس مطلوبًا سوى تشغيل برنامج التجميع بلغة Java بدلاً من النص البرمجي الذي يحدّده المستخدم، لذا يدرك أنّه من الآمن تشغيل هذه الخطوات في الوقت نفسه. ويمكن أن يؤدي ذلك إلى ترتيب تحسّن كبير في الأداء عند إنشاء الاستهدافات واحدًا تلو الآخر على جهاز متعدّد النواة، ولا يمكن ذلك إلا لأنّ النهج المستند إلى العناصر الاصطناعية يترك نظام الإصدار مسؤولاً عن استراتيجية التنفيذ الخاصة به حتى يتمكّن من تقديم ضمانات أقوى بشأن التوازي.

ومع ذلك، فإنّ المزايا تتعدّى إلى جانب التوازي. بعد ذلك، يتضح لنا هذا الأسلوب عندما يكتب مطوّر البرامج bazel build :MyBinary مرة ثانية بدون إجراء أي تغييرات، حيث يخرج البازل من الصفحة في أقل من ثانية وتظهر رسالة تفيد بأن الهدف محدّث. وقد يكون ذلك ممكنًا بسبب نموذج البرمجة الوظيفي الذي تحدّثنا عنه في وقت سابق. ومع ذلك، يدرك Bazel أنّ كل هدف ناتج عن تشغيل برنامج تحويل بلغة Java، ويدرك أن النتائج من برنامج التجميع بلغة Java تعتمد فقط على البيانات التي يستخدمها، طالما أنّ مصادر الإدخال لم تتغيّر. ويعمل هذا التحليل على كل مستوى. وإذا تغيّر MyBinary.java، يعرف "بازل" إعادة تصميم MyBinary مع إعادة استخدام mylib. في حال تغيير ملف المصدر الخاص بـ //java/com/example/common، يعرف Bazel إعادة إنشاء هذه المكتبة، mylib وMyBinary، مع إعادة استخدام //java/com/example/myproduct/otherlib. ولأنّ "بازل" يعرف خصائص الأدوات التي يتم تشغيلها في كل خطوة، فإنّه قادر على إعادة بناء الحدّ الأدنى من مجموعة القطع الأثرية في كل مرة مع ضمان أنّه لن يؤدي إلى إنشاء أي إصدارات قديمة.

إن إعادة تصميم عملية الإنشاء من حيث العناصر بدلاً من المهام هي عملية بسيطة ولكنها فعّالة في الوقت نفسه. من خلال تقليل المرونة التي تظهر للمبرمج، يمكن لنظام الإنشاء معرفة المزيد حول ما يتم تنفيذه في كل خطوة من الإصدار. ويمكنه الاستفادة من هذه المعلومات في تحقيق مزيد من الكفاءة في الإصدار من خلال موازاة عمليات الإنشاء وإعادة استخدام النتائج. هذه ليست سوى الخطوة الأولى، ويمثّل هذا الوحدات الأساسية للموازاة وإعادة الاستخدام أساسًا لنظام توزيع موزع وقابل للتطور بشكلٍ كبير.

حيل أنيقة أخرى في لعبة Bazel

تحل أنظمة الإنشاء المستندة إلى العناصر بشكل أساسي مع المشاكل المتعلقة بالتوازي وإعادة الاستخدام التي تكون بطبيعتها في أنظمة الإنشاء المستندة إلى المهام. ولكن لا تزال هناك بعض المشاكل التي لم نتناولها سابقًا. لدى "بازل" طرق ذكية لحل كلّ منها، ويجب مناقشة هذه المشاكل قبل المضي قدمًا.

الأدوات كاعتماديات

إحدى المشاكل التي واجهناها سابقًا هي أنّ الإصدارات تمّ الاعتماد عليها وتزداد المشكلة صعوبة عند استخدام المشروع لغات تتطلب أدوات مختلفة استنادًا إلى النظام الأساسي المُطوَّر عليها أو الذي يتم تجميعها لها (مثل نظام التشغيل Windows مقابل Linux)، وتتطلب كل منصّة من هذه الأنظمة الأساسية مجموعة مختلفة قليلاً من الأدوات لتنفيذ الوظيفة نفسها.

يحل Bazel الجزء الأول من هذه المشكلة من خلال التعامل مع الأدوات على أنها اعتماديات لكل هدف. يعتمد كل java_library في مساحة العمل ضمنيًا على برنامج تحويل لغة Java، والذي يتم ضبطه تلقائيًا على مجمِّع برامج معروف. عندما ينشئ Bazel رمز java_library، يتحقّق من توفّر برنامج التجميع المحدَّد في موقع جغرافي معروف. تمامًا مثل أي اعتمادية أخرى، في حال تغيير مجمِّع لغة Java، تتم إعادة إنشاء كل عنصر يعتمد عليه.

يحل Bazel الجزء الثاني من المشكلة، وهو استقلالية المنصة، عن طريق إعداد عمليات إعداد. بدلاً من الاستهدافات استنادًا إلى أدواتها مباشرةً، تعتمد على أنواع عمليات الضبط التالية:

  • ضبط المضيف: إنشاء أدوات تعمل أثناء الإصدار
  • ضبط الاستهداف: إنشاء البرنامج الثنائي الذي طلبته في النهاية

تمديد نظام التشغيل

يقدّم Bazel أهدافًا للعديد من لغات البرمجة الرائجة على الفور، غير أنّ المهندسين يريدون دائمًا إنجاز المزيد من المهام، ويعود الفضل في ذلك إلى فوائد الأنظمة المستندة إلى المهام إلى مرونتها في دعم أي نوع من عمليات البناء، لذا من الأفضل عدم التخلّي عن ذلك في نظام إصدار يستند إلى العناصر. ولحسن الحظ، تسمح قناة Bazel بتوسيع أنواع الاستهداف المتوافقة من خلال إضافة قواعد مخصصة.

لتحديد قاعدة في Bazel، يؤكّد مؤلف القاعدة الإدخالات التي تتطلّبها القاعدة (في شكل سمات يتم تمريرها في ملف BUILD) والمجموعة الثابتة من النتائج التي تنتجها القاعدة. ويحدّد المؤلف أيضًا الإجراءات التي سيتم إنشاؤها بواسطة هذه القاعدة. ويعلن كل إجراء عن مدخلاته ومخرجاته، أو يشغِّل سلسلة تنفيذية معينة أو يكتب سلسلة معيّنة إلى ملف، ويمكن ربطه بإجراءات أخرى عبر مدخلاته ومخرجاته. هذا يعني أنّ الإجراءات هي أقل وحدة تركيبية في نظام التصميم، ويمكن أن ينفّذ أي إجراء طالما أنّه يستخدم فقط المدخلات والمخرجات المعلَنَة، علمًا أنّ Bazel يتولّى جدولة الإجراءات وتخزين النتائج مؤقتًا حسب الحاجة.

لا يضمن النظام أنّه ما مِن طريقة لمنع مطوّر برامج معيّنة من تنفيذ أي إجراء، مثل تقديم عملية غير محدّدة كجزء من الإجراء الذي يتّخذه المستخدم. وهذا لا يحدث كثيرًا في كثير من الأحيان، كما أن تقليل احتمالات إساءة الاستخدام إلى أن تصل إلى مستوى الإجراءات يؤدي إلى تقليل فرص حدوث الأخطاء بشكلٍ كبير. تتوفّر القواعد التي تتوافق مع العديد من اللغات والأدوات الشائعة على نطاق واسع على الإنترنت، ولن تحتاج معظم المشاريع مطلقًا إلى تحديد قواعدها الخاصة. حتى أولئك الذين يجرون ذلك، تحتاج تعريفات القواعد إلى تحديدها في مكان مركزي واحد في المستودع، ما يعني أن معظم المهندسين سيتمكنون من استخدام تلك القواعد بدون القلق بشأن تنفيذها.

عزل البيئة

تبدو الإجراءات كما لو أنها قد تواجه مشاكل مماثلة للمهام التي في الأنظمة الأخرى، ألا يزال من غير الممكن كتابة الإجراءات التي تتم كتابتها في الوقت نفسه مع الملف نفسه وينتهي بها الأمر مع بعضها البعض؟ في الواقع، يعمل Bazel على جعل هذه التعارضات أمرًا مستحيلاً باستخدام وضع الحماية. على الأنظمة المتوافقة، يتم عزل كل إجراء عن كل إجراء آخر عبر وضع الحماية في ملف النظام. بشكل فعّال، يمكن لكل إجراء الاطّلاع فقط على عرض محدود لنظام الملفات الذي يتضمّن المدخلات التي أعلن عنها وأي مخرجات أدّاها. ويتم فرض ذلك من خلال أنظمة مثل LXC على نظام التشغيل Linux، وهو التقنية ذاتها خلف الإرساء. وهذا يعني أنّه من المستحيل تنفيذ إجراءات ضد بعضها البعض لأنّه يتعذّر عليه قراءة أي ملفات لا يعلنها، وأنّ أي ملفات يكتبها بدون أن يفصح عنها سيتم تجاهلها عند الانتهاء من الإجراء. يستخدم Bazel أيضًا وضع الحماية لحظر الإجراءات من الاتصال عبر الشبكة.

تحديد العناصر التابعة الخارجية بشكل نهائي

لا تزال هناك مشكلة واحدة متبقية: غالبًا ما تحتاج أنظمة الإنشاء إلى تنزيل العناصر التابعة (سواء الأدوات أو المكتبات) من مصادر خارجية بدلاً من إنشائها مباشرةً. يمكن الاطّلاع على هذا المثال في الاعتمادية @com_google_common_guava_guava//jar، التي ينزّل ملف JAR من Maven.

بناءً على الملفات خارج مساحة العمل الحالية، تنطوي على خطورة. ويمكن أن تتغيّر هذه الملفات في أي وقت، ما قد يتطلّب أن يتحقّق نظام الإصدار باستمرار مما إذا كانت جديدة. في حال تغيير ملف عن بُعد بدون تغيير مقابل في رمز مصدر مساحة العمل، يمكن أن يؤدي ذلك أيضًا إلى إصدارات غير قابلة للتكرار، وقد يعمل الإصدار يومًا ما وقد يفشل في المرة التالية بدون سبب واضح بسبب حدوث تغيير غير ملحوظ في الاعتماد. وأخيرًا، يمكن أن تؤدي الاعتمادية الخارجية إلى خطر ضخم أمني إذا كانت مملوكة لجهة خارجية: إذا كان بإمكان المهاجم اختراق الخادم التابع لجهة خارجية، يمكنه استبدال ملف الاعتمادية بتصميم من تصميمه، ما يوفر له إمكانية التحكُّم الكامل في بيئتك والإصدار.

تكمن المشكلة الأساسية في أننا نريد أن يكون نظام الإصدار على علم بهذه الملفات بدون الحاجة إلى فحصها في عنصر التحكّم في المصدر. يجب أن يكون تعديل الاعتمادية خيارًا واعيًا، ولكن يجب أن يتم تحديد هذا الخيار مرة واحدة في مكان مركزي بدلاً من إدارته من قِبل مهندسين فرديين أو من خلال النظام تلقائيًا. ويرجع ذلك إلى أنّه حتى باستخدام نموذج "بث مباشر مباشر"، لا نزال نريد أن تكون الإصدارات قابلة لتحديدها، ما يعني أنّه في حال الاطّلاع على عملية تنفيذ سابقة من الأسبوع الماضي، من المفترض أن ترى اعتمادياتك كما كانت في السابق.

تعالج Bazel بعض أنظمة التصميم الأخرى هذه المشكلة من خلال طلب ملف بيان على مستوى مساحة العمل يتضمّن تجزئة مشفّرة لكل اعتمادية خارجية في مساحة العمل. التجزئة هي طريقة مختصرة لتمثيل الملف بشكل فريد بدون فحص الملف بأكمله في عنصر التحكم في المصدر. عند الإشارة إلى تبعية خارجية جديدة من مساحة عمل، تتم إضافة تجزئة الاعتمادية هذه إلى البيان، سواء يدويًا أو تلقائيًا. عندما يعمل Bazel على إصدار، يتحقق من التجزئة الفعلية لاعتمادية ذاكرة التخزين المؤقت مقابل التجزئة المتوقعة المحددة في البيان ويعيد تنزيل الملف فقط إذا كانت التجزئة تختلف.

إذا كانت تجزئة العنصر التي ننزّلها تحتوي على تجزئة مختلفة عن التجزئة المُحدّدة في البيان، سيتعذّر الإصدار ما لم يتم تعديل التجزئة في البيان. ويمكن تنفيذ ذلك تلقائيًا، ولكن يجب الموافقة على هذا التغيير وفحصه في عنصر التحكّم في المصدر قبل قبول الإصدار الاعتمادية الجديدة. وهذا يعني أن هناك دائمًا سجلاً عندما يتم تحديث اعتمادية، ولا يمكن تغيير اعتمادية خارجية بدون تغيير مقابل في مصدر مساحة العمل. ويعني ذلك أيضًا أنه عند الاطّلاع على إصدار قديم من رمز المصدر، يمكن ضمان استخدام الإصدار نفسه للاعتماديات التي كان يستخدمها في الوقت الذي تم فيه تسجيل الدخول إلى الإصدار الجديد، وإلا سيتعذّر استخدامه إذا لم تكن هذه الارتباطات التابعة متاحة.

وقد تكون هناك مشكلة بالطبع إذا أصبح الخادم البعيد غير متاح أو بدأ في عرض البيانات التالفة، ما قد يؤدي إلى تعذّر بدء جميع الإصدارات إذا لم يكن لديك نسخة أخرى من الاعتمادية. لتجنّب هذه المشكلة، ننصحك بإظهار جميع اعتمادياتها على أي خوادم أو خدمات تثق بها وتتحكّم فيها في أي مشروع غير حصري. بخلاف ذلك، ستكون دائمًا في برأي طرف ثالث لمدى توفّر نظام الإصدار الذي تستخدمه، حتى إذا كانت علامات التجزئة التي تم تسجيل الوصول إليها تضمن أمانها.