قاعدة رموز Bazel

يقدّم هذا المستند وصفًا لقاعدة الرموز وآلية تنظيم Bazel. وهو مخصص للأشخاص الذين يرغبون في المساهمة في Bazel، وليس للمستخدمين.

المقدمة

إنّ قاعدة الرموز في Bazel كبيرة (رمز إنتاج حوالى 350 KLOC ورمز اختبار 260 KLOC) ولا أحد يعرف المناظر الطبيعية بالكامل، فكل شخص يعلم واديه على وجه الخصوص، ولكن القليل يعرف الكثير من المعلومات عن التلال في كل اتجاه.

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

تتوفّر النسخة العلنية من رمز Bazel المصدر على GitHub على github.com/bazelbuild/bazel. وهذا ليس "مصدر الحقيقة"، بل هو مصدر من شجرة بيانات داخلية في Google تحتوي على وظائف إضافية ليست مفيدة خارج Google. والهدف الطويل الأمد هو تحويل GitHub إلى مصدر للحقيقة.

ويتم قبول المساهمات من خلال آلية طلب سحب GitHub العادية، ويتم استيرادها يدويًا من قِبل موظف Google إلى شجرة المصدر الداخلية، ثم يُعاد تصديرها إلى GitHub.

بنية العميل/الخادم

يتوفّر الجزء الأكبر من Bazel في عملية الخادم التي تظل في ذاكرة الوصول العشوائي (RAM) بين الإصدارات. يسمح ذلك لمنصة Bazel بالحفاظ على حالتها بين الإصدارات.

ولهذا السبب يحتوي سطر الأوامر في Bazel على خيارَين: بدء التشغيل والأمر. في سطر الأوامر على النحو التالي:

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

توجد بعض الخيارات (--host_jvm_args=) قبل اسم الأمر الذي يتم تشغيله، والبعض الآخر بعد (-c opt)، النوع السابق هو يُسمى "start;optionupquot" ويؤثر في عملية الخادم بالكامل، بينما يؤثر النوع الثاني، "command;option"، في أمر واحد فقط.

يحتوي كل مثيل خادم على شجرة مصدر واحدة مرتبطة ("workspace") وعادةً ما تحتوي كل مساحة عمل على مثيل خادم نشط واحد. يمكن التحايل على هذا من خلال تحديد قاعدة مخرجات مخصّصة (راجع القسم "الدليل، تنسيق الدليل&quot، وللحصول على المزيد من المعلومات).

يتم توزيع Bazel كملف ELF تنفيذي واحد وهو أيضًا ملف .zip صالح. عند كتابة bazel، يتم تنفيذ عنصر ELF التنفيذي أعلاه في C++ ( &"client"). يتم إعداد عملية مناسبة للخادم باستخدام الخطوات التالية:

  1. التحقّق مما إذا كان قد استخرج نفسه من قبل إذا لم يكن الأمر كذلك، سينطبق ذلك. هذا هو المكان الذي يأتي منه تنفيذ الخادم.
  2. يتحقّق مما إذا كان هناك مثيل نشط لخادم يعمل: فهو قيد التشغيل، ويحتوي على خيارات بدء التشغيل المناسبة ويستخدم دليل مساحة العمل الصحيح. تعثر على الخادم قيد التشغيل من خلال البحث عن الدليل $OUTPUT_BASE/server الذي يحتوي على ملف قفل مع المنفذ الذي يستمع إليه الخادم.
  3. إذا لزم الأمر، سيتم إنهاء عملية الخادم القديمة
  4. بدء عملية خادم جديدة، إذا لزم الأمر

بعد تجهيز عمليّة خادم مناسبة، يتم تلقائيًا توجيه الأمر الذي يجب تشغيله عبر واجهة gRPC، ثم يتم توجيه إخراج Bazel إلى الوحدة الطرفية. يمكن تشغيل أمر واحد فقط في الوقت نفسه. ويتم تنفيذ ذلك باستخدام آلية قفل معقدة مع أجزاء في C++ وأجزاء في Java. هناك بعض البنية الأساسية لتشغيل أوامر متعددة في الوقت نفسه، حيث إن عدم القدرة على تنفيذ bazel version في الوقت نفسه باستخدام أمر آخر محرج. أداة الحظر الرئيسية هي دورة حياة BlazeModule وبعض الحالات في BlazeRuntime.

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

وعند الضغط على Ctrl-C، يترجمها العميل إلى طلب إلغاء على اتصال gRPC، وهو ما يحاول إنهاء الأمر في أقرب وقت ممكن. بعد مفتاح Ctrl-C الثالث، يرسل البرنامج رمز SIGKILL إلى الخادم بدلاً من ذلك.

تكون رمز مصدر البرنامج تحت src/main/cpp والبروتوكول المستخدَم للاتصال بالخادم في src/main/protobuf/command_server.proto .

نقطة الإدخال الرئيسية للخادم هي BlazeRuntime.main() وسيعالج GrpcServerImpl.run() طلبات gRPC.

تنسيق الدليل

ينشئ Bazel مجموعة مُعقّدة إلى حدٍّ ما من الأدلة أثناء إنشاء مبنى. ويتوفر الوصف الكامل في تنسيق دليل الناتج.

إن "workspace"الشجرة هي مصدر مجموعة Bazel المصدر. وعادةً ما تتوافق مع شيء ما تحققت منه من تحكم المصدر.

تضع Bazel جميع بياناتها ضمن &&;;;; الخاصة] إلى مكان الاستمتاع للمستخدم. ويكون هذا عادةً $HOME/.cache/bazel/_bazel_${USER}، ولكن يمكن إلغاءه باستخدام خيار بدء التشغيل --output_user_root.

تُستخرج ";;;;; العملية الأساسية] نيابةً عن Bazel. يتم تنفيذ ذلك تلقائيًا ويحصل كل إصدار من Bazel على دليل فرعي استنادًا إلى المجموع الاختباري أسفل قاعدة التثبيت. يتم عرضها في $OUTPUT_USER_ROOT/install بشكل تلقائي ويمكن تغييرها باستخدام خيار سطر الأوامر --install_base.

إنّ &;;;;;قاعدة وإخراج: هي المكان الذي تتم الكتابة إليه في الصفحة. يجب أن يكون لدى كل قاعدة مخرجات خادم Bazel واحد على الأكثر يتم تشغيله في أي وقت. السعر المعتاد هو $OUTPUT_USER_ROOT/<checksum of the path to the workspace>. ويمكن تغييره باستخدام خيار بدء التشغيل --output_base، والذي يعد من بين أمور أخرى مفيدًا للتغلب على القيد الذي لا يمكن تشغيله إلا في مثيل Bazel واحد في أي مساحة عمل في أي وقت.

يحتوي دليل المخرجات على أمور أخرى، من بينها:

  • تم جلب المستودعات الخارجية في $OUTPUT_BASE/external.
  • الجذر exe، وهو دليل يحتوي على روابط تؤدي إلى جميع رموز المصدر للإصدار الحالي. سيكون هذا المكان في $OUTPUT_BASE/execroot. أثناء استخدام الإصدار، يكون دليل العمل هو $EXECROOT/<name of main repository>. نخطط لتغيير هذه الحالة إلى $EXECROOT، على الرغم من أنها خطة طويلة الأمد لأنها تغيير غير متوافق على الإطلاق.
  • الملفات التي تم إنشاؤها أثناء الإصدار.

عملية تنفيذ أحد الأوامر

بعد أن يحصل خادم Bazel على التحكم ويتم إبلاغه بأمر يحتاج إلى تنفيذ، يحدث التسلسل التالي للأحداث:

  1. تم إعلام BlazeCommandDispatcher بالطلب الجديد. يحدِّد الأمر ما إذا كان الأمر يحتاج إلى تشغيل مساحة عمل (في كل أمر تقريبًا باستثناء الأوامر التي لا علاقة لها برمز المصدر، مثل الإصدار أو المساعدة) وما إذا كان الأمر قيد التشغيل أم لا.

  2. تم العثور على الأمر الصحيح. يجب أن ينفّذ كل أمر الواجهة BlazeCommand وأن يتضمّن التعليق التوضيحي @Command (قد يكون هذا أمرًا قديمًا إلى حد ما، وسيكون من المفيد أن تصف كل البيانات الوصفية التي يحتاج إليها الأمر في أساليب على BlazeCommand).

  3. يتم تحليل خيارات سطر الأوامر. يحتوي كل أمر على خيارات مختلفة لسطر الأوامر، وهي موضّحة في تعليق @Command التوضيحي.

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

  5. ويتم التحكّم في الأمر. الأوامر الأكثر أهمية هي تلك التي تشغّل إصدارًا: إصدار، واختبار، وتنفيذ، وتغطية، وما إلى ذلك: يتم تنفيذ هذه الوظيفة من خلال BuildTool.

  6. يتم تحليل مجموعة من الأنماط المستهدفة في سطر الأوامر ويتم التعامل مع أحرف البدل مثل //pkg:all و//pkg/.... تمّ تنفيذ هذه السياسة في AnalysisPhaseRunner.evaluateTargetPatterns() وتمت إعادة ضبطها في Skyframe باسم TargetPatternPhaseValue.

  7. يتم تنفيذ مرحلة التحميل/التحليل لإنشاء رسم بياني للإجراء (رسم بياني موجّه موجّه للأوامر التي يجب تنفيذها للإصدار).

  8. مرحلة التنفيذ قيد التشغيل. وهذا يعني تنفيذ كل إجراء مطلوب لإنشاء أهداف المستوى الأعلى المطلوبة.

خيارات سطر الأوامر

يتم وصف خيارات سطر الأوامر لاستدعاء Bazel في كائن OptionsParsingResult، والذي بدوره يتضمن خريطة من "option;option classes" إلى قيم الخيارات. تمثّل فئة "option&option ويتعلّق بفئة معيّنة] فئة فرعية من OptionsBase وتحدِّد خيارات سطر الأوامر معًا وترتبط ببعضها البعض. مثلاً:

  1. الخيارات المرتبطة بلغة البرمجة (CppOptions أو JavaOptions). يجب أن تكون هذه فئة فرعية من FragmentOptions ويتم تضمينها في النهاية في عنصر BuildOptions.
  2. الخيارات المتعلقة بطريقة تنفيذ Bazel للإجراءات (ExecutionOptions)

تم تصميم هذه الخيارات ليتم استهلاكها في مرحلة التحليل (وإما من خلال RuleContext.getFragment() في Java أو ctx.fragments في Starlark). تتم قراءة بعضها (على سبيل المثال، سواء كان C++ يتضمن المسح أم لا) في مرحلة التنفيذ، ولكنها تتطلب دائمًا أعمال سباكة صريحة لأن BuildConfiguration غير متاح في ذلك الوقت. لمزيد من المعلومات، راجِع قسم "عمليات الضبط".

تحذير: نودّ التظاهر بأنّ مثيلات OptionsBase غير قابلة للتغيير ولا يمكن استخدامها من جديد (مثل جزء من SkyKeys)، لكنّ ذلك ليس صحيحًا، فتعديلها يُعدّ طريقة جيدة لاختراق بازل بطرق سهلة يصعب تصحيحها. ويُرجى العِلم أنّ جعل هذه الأهداف غير قابلة للتغيير هو محاولة كبيرة. (يمكن تعديل FragmentOptions مباشرةً بعد الإنشاء قبل أي شخص آخر للحصول على فرصة للاحتفاظ بمرجع له وقبل الاتصال بـ equals() أو hashCode().)

يتعرّف Bazel على فئات الخيارات بالطرق التالية:

  1. نوع من الأسلاك الثابتة هو Bazel (CommonCommandOptions)
  2. من التعليق التوضيحي @Command في كل أمر Bazel
  3. من ConfiguredRuleClassProvider (هذه هي خيارات سطر الأوامر المرتبطة بلغات برمجة فردية)
  4. يمكن أن تحدّد قواعد Starlark خياراتها الخاصة (اطّلِع على هذه الصفحة)

يمثّل كل خيار (باستثناء الخيارات التي تحدّدها Starlar) متغيّرًا في فئة فرعية من FragmentOptions يتضمن التعليق التوضيحي @Option الذي يحدّد اسم ونوع خيار سطر الأوامر إلى جانب بعض نصوص المساعدة.

عادةً ما يكون نوع Java لقيمة خيار سطر الأوامر أمرًا بسيطًا (سلسلة أو عدد صحيح أو قيمة منطقية أو تصنيف أو غير ذلك). ومع ذلك، فإننا نوفّر أيضًا خيارات لأنواع أكثر تعقيدًا. وفي هذه الحالة، تقع مهمة التحويل من سلسلة سطر الأوامر إلى نوع البيانات ضمن عملية تنفيذ com.google.devtools.common.options.Converter.

شجرة المصدر، كما يراها بازل

تعمل Bazel في مجال إنشاء البرامج، وهو ما يحدث من خلال قراءة رمز المصدر وتفسيره. يُعرَف إجمالي رمز المصدر الذي تستخدمه Bazel باسم "workspace;quot; ويتم تنظيمه في مستودعات وحِزم وقواعد.

مستودعات

A "repository&quot: هي شجرة مصدر يعمل عليها مطوّر البرامج، وعادةً ما تمثّل مشروعًا واحدًا. الأصل "بليز" من "بازل" يعمل على أحادي المستوى، أي شجرة مصدر واحدة تحتوي على كل رموز المصدر المستخدَمة لتشغيل المبنى. على الرغم من ذلك، يتيح Bazel المشاريع التي يمتد رمز مصدرها إلى عدة مستودعات.

يتم وضع علامة على المستودع باستخدام ملف باسم WORKSPACE (أو WORKSPACE.bazel) في الدليل الجذري له. يحتوي هذا الملف على معلومات " "global;quot;للإصدار بالكامل، مثل مجموعة المستودعات الخارجية المتاحة. ويعمل مثل ملف Starlark عادي، ما يعني أنّه يمكن للمستخدم load() جمع ملفّات Starlark أخرى. ويُستخدم هذا الحقل عادةً في استخدام المستودعات التي يطلبها المستودع الذي يشير إليه صراحةً هذا الاسم (يُطلق على هذه العملية اسم "deps.bzl).

يتم ربط رمز المستودعات الخارجية أو تنزيله ضمن $OUTPUT_BASE/external.

عند تشغيل المبنى، يجب تجميع شجرة المصدر الكاملة معًا، ويتم ذلك من خلال SymlinkForest، وهو الذي يربط كل حزمة في المستودع الرئيسي بـ $EXECROOT وكل مستودع خارجي إلى $EXECROOT/external أو $EXECROOT/.. (وبالطبع، يتعذّر الاحتفاظ بحزمة تُسمى external في المستودع الرئيسي).

الطرود

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

الحزم مستقلة عن بعضها بعضًا: لا يمكن أن تؤدي التغييرات في ملف BUILD للحزمة إلى تغيير حِزم أخرى. ويمكن أن تؤدي إضافة ملفات BUILD أو إزالتها __ إلى تغيير الحِزم الأخرى، بما أنّ الكرات متكرّرة تتوقف عند حدود الحزمة، وبالتالي يؤدي إيقاف ملف BUILD إلى إيقاف التكرار.

يُسمى تقييم ملف BUILD باسم &"تحميل حزم&quot. يتم التنفيذ في الصف PackageFactory، ويعمل عن طريق استدعاء مترجم فوري وهو يتطلب معرفة مجموعة فئات القواعد المتاحة. نتيجة تحميل الحزمة هي عنصر Package. ويتم في الغالب ربط الخريطة من سلسلة (اسم هدف) بالهدف نفسه.

جزء كبير من التعقيد أثناء تحميل الحزمة هو الكرة الأرضية: لا يتطلب Bazel إدراج كل ملف مصدر بشكل صريح، ويمكنه بدلاً من ذلك تشغيل نظام globs (مثل glob(["**/*.java"])). وعلى عكس واجهة الأوامر، إنه يتوافق مع الكرات المتحركة المتكررة التي تنقسم إلى أدلة فرعية (ولكن ليس في حِزم فرعية). ويتطلب ذلك أيضًا الوصول إلى نظام الملفات، وقد يكون هذا الإجراء بطيئًا، فننفّذ جميع أنواع الحيل لتشغيله بالتوازي قدر الإمكان.

يتم تنفيذ الكرة الأرضية في الفئات التالية:

  • LegacyGlobber، جبل سريع وممتع
  • SkyframeHybridGlobber، وهو إصدار يستخدم Skyframe ويتم الرجوع إلى التسلسل العالمي القديم لتجنب"عمليات إعادة تشغيل Skysky" (كما هو موضح أدناه)

تحتوي الفئة Package نفسها على بعض الأعضاء الذين يتم استخدامهم بشكل حصري لتحليل ملف WORKSPACE والذي لا يكون مفيدًا للحزم الفعلية. هذا خطأ في التصميم لأنّ العناصر التي تصف الحِزم العادية يجب ألا تحتوي على حقول تصف شيئًا آخر. وتشمل هذه المشاكل ما يلي:

  • تعيينات المستودع
  • سلاسل الأدوات المسجّلة
  • منصّات التنفيذ المسجّلة

في الحالة المثالية، سيكون هناك فاصل أكبر بين تحليل ملف WORKSPACE من تحليل الحِزم العادية حتى لا يحتاج Package إلى تلبية احتياجات كل منهما. وللأسف، يصعب تنفيذ هذا الإجراء لأنهما متداخلان إلى حد كبير.

التصنيفات والأهداف والقواعد

تتألف الحِزَم من الاستهدافات التي تحتوي على الأنواع التالية:

  1. الملفات: عناصر تمثل الإدخال أو مخرجات الإصدار. أمّا في لغة Bazel، فنطلق عليها اسم العناصر (التي تمت مناقشتها في الأقسام الأخرى). لا تُعد جميع الملفات التي تم إنشاؤها خلال الإصدار هدفًا، ومن الشائع أن يكون لمخرجات Bazel تصنيفًا مرتبطًا بها.
  2. القواعد: تصف خطوات استخراج النتائج من مدخلاتها. ترتبط هذه اللغات بشكل عام بلغة برمجة (مثل cc_library أو java_library أو py_library)، ولكن هناك بعض اللغات غير المختلفة (مثل genrule أو filegroup).
  3. مجموعات الحِزم: تمت مناقشتها في قسم مستوى العرض.

ويُطلق على اسم الهدف اسم تصنيف. بنية التصنيفات هي @repo//pac/kage:name، حيث يكون repo هو اسم المستودع الذي يتضمنه التصنيف، وpac/kage هو الدليل الذي يوجد فيه ملف BUILD، وname هو مسار الملف (إذا كان التصنيف يشير إلى ملف مصدر) بالنسبة إلى دليل الحزمة. عند الإشارة إلى هدف في سطر الأوامر، يمكن حذف بعض أجزاء التصنيف:

  1. وفي حال حذف المستودع، يتم اعتبار التصنيف في المستودع الرئيسي.
  2. إذا تم حذف جزء الحزمة (مثل name أو :name)، سيتم أخذ التصنيف ليكون في حزمة دليل العمل الحالي (المسارات النسبية التي تحتوي على مراجع ذات مستوى أعلى (.))

يُسمّى نوع قاعدة (مثل &"C++ Library") &&;;;;قاعدة؟؟&؟ قد يتم تنفيذ فئات القواعد إما في Starlark (الدالة rule()) أو في Java (تُعرف باسم "القواعد الأصلية"، النوع RuleClass). على المدى البعيد، سيتم تنفيذ كل قاعدة لغة في Starlark، ولكن لا تزال بعض مجموعات القواعد القديمة (مثل Java أو C++ ) في Java في الوقت الحالي.

يجب استيراد فئات قواعد Starlark في بداية ملفات BUILD باستخدام عبارة load()، في حين أن فئات قاعدة Java هي &&nnately" معروفة باسم Bazel، بموجب التسجيل في ConfiguredRuleClassProvider.

تحتوي فئات القواعد على معلومات مثل:

  1. سماته (مثل srcs وdeps): أنواعها وقيمها التلقائية وقيودها وما إلى ذلك
  2. عمليات نقل الإعداد والجوانب المرفقة بكل سمة، في حال توفّرها
  3. تنفيذ القاعدة
  4. ينشئ موفّرو المعلومات العابرون القاعدة "&&;;;&;;; خاصة"

ملاحظة المصطلحات: في قاعدة الرمز، غالبًا ما نستخدم "القاعدة" للإشارة إلى الهدف الذي أنشأته فئة قاعدة. أمّا في Starlark وفي المستندات الموجّهة للمستخدمين، فيجب استخدام "Rule" بشكل حصري للإشارة إلى فئة القاعدة نفسها، لأن الهدف هو مجرد "هدف". وعلى الرغم من إدخال RuleClass "class" في اسمه، لا تتوفر علاقة اكتساب من Java بين فئة القاعدة والأهداف من هذا النوع.

الإطار الأفقي

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

تُسمى العُقد في الرسم البياني SkyValues ويُطلق على أسمائها SkyKeys. كلاهما غير قابل للتغيير إلى حد كبير، ولا يمكن الوصول إلى أي كائنات غير قابلة للتغيير إلا من خلالها. يكون هذا المتغير ثابتًا دائمًا، وفي حال عدم تمكّنه من ذلك (على سبيل المثال، فئات الخيارات الفردية BuildOptions، التي تكون عضوًا في BuildConfigurationValue وSkyKey الخاصة بها)، نحاول جاهدًا عدم تغييرها أو تغييرها بطرق لا يمكن ملاحظتها من الخارج. بناءً على ذلك، يجب أن تكون كل العناصر التي تم احتسابها ضمن Skyframe (مثل الأهداف التي تم ضبطها) غير قابلة للتغيير أيضًا.

إنّ الطريقة الأكثر ملاءمةً لمراقبة الرسم البياني لـ Skyframe هي تشغيل bazel dump --skyframe=detailed، الذي يسحب الرسم البياني، واحد SkyValue في كل سطر. ومن الأفضل تنفيذ ذلك بالنسبة إلى المباني الصغيرة، لأنّ حجمها قد يكون كبيرًا جدًا.

يعيش Skyframe في حزمة com.google.devtools.build.skyframe. تحتوي الحزمة com.google.devtools.build.lib.skyframe التي تحمل الاسم نفسه على تطبيق Bazel أعلى Skyframe. يتوفر المزيد من المعلومات حول Skyframe هنا.

لتقييم SkyKey معيّن في SkyValue، سيستدعي Skyframe SkyFunction المقابل لنوع المفتاح. أثناء التقييم، قد يتطلب الأمر ارتباطات أخرى من Skyframe من خلال استدعاء الحِمل الزائدة المختلفة لـ SkyFunction.Environment.getValue(). ويترافق ذلك مع الآثار الجانبية لتسجيل هذه الارتباطات التابعة في الرسم البياني الداخلي لبرنامج Skyframe' ، ولذلك ستعرف قناة Skyframe إعادة تقييم الدالة عند تغيير أي من تبعياتها. وبعبارة أخرى، تعمل ذاكرة التخزين المؤقت لـ Skyframe' والحوسبة المتزايدة بدقة SkyFunction وSkyValues.

عندما يطلب SkyFunction اعتمادية غير متاحة، سيعرض getValue() قيمة فارغة. من المفترض أن تعرض الدالة بعد ذلك التحكم مرة أخرى في Skyframe من خلال إرجاع السمة فارغة. في وقت لاحق، سيقيّم Skyframe الاعتمادية غير المتاحة، ثم يعيد تشغيل الدالة من البداية. وفي هذه المرة فقط، ستنجح مكالمة getValue() مع نتيجة غير فارغة.

ونتيجة لذلك، يجب تكرار أي عملية حسابية تم إجراؤها داخل SkyFunction قبل إعادة التشغيل. ولا يشمل ذلك العمل الذي تم تنفيذه لتقييم الاعتمادية SkyValues المخزّنة مؤقتًا. لذا، نعمل عادةً على حلّ هذه المشكلة من خلال:

  1. التعريف بالاعتماديات على دفعات (باستخدام getValuesAndExceptions()) للحد من عدد عمليات إعادة التشغيل
  2. تقسيم SkyValue إلى أجزاء منفصلة تم احتسابها من خلال SkyFunction مختلفة، بحيث يمكن حسابها وتخزينها مؤقتًا بشكل مستقل. ويجب اتخاذ هذا الإجراء بشكل استراتيجي، لأنّه من المحتمل أن يزيد من استخدام الذاكرة.
  3. تخزين الحالة بين عمليات إعادة التشغيل، إما باستخدام SkyFunction.Environment.getState()، أو الاحتفاظ بذاكرة التخزين المؤقت الثابتة &quot:

نحتاج بشكل أساسي إلى هذه الأنواع من الحلول، نظرًا لأن هناك مئات الآلاف من عُقد Skyframe التي يتم تشغيلها أثناء الطيران، ولا تدعم Java سلاسل المحادثات البسيطة.

ستارلارك

Starlark هي اللغة الخاصة بالنطاق التي يستخدمها المستخدمون لضبط ميزة Bazel وتوسيع نطاقها. ويتم تصنيفه كمجموعة فرعية محدودة من لغة Python تتضمّن أنواعًا أقل بكثير، وقيودًا إضافية على تدفق التحكّم، والأهم من ذلك، تضمن قدرًا كبيرًا من قابلية التكيّف لإتاحة عمليات القراءة المتزامنة. هذه الميزة ليست بالكامل في خدمة "تورينغ"، ما يمنع بعض المستخدمين (وليس الكل) من محاولة إنجاز مهام البرمجة العامة داخل اللغة.

تم تنفيذ Starlark في حزمة net.starlark.java. ويتضمّن الموقع الإلكتروني أيضًا عملية تنفيذ Go مستقلّة هنا. يتطلّب تطبيق لغة Java المستخدَمة في Bazel ترجمة فورية في الوقت الحالي.

يتم استخدام Starlark في عدة سياقات، بما في ذلك:

  1. اللغة BUILD. هذا هو المكان الذي يتم فيه تحديد القواعد الجديدة. في حال استخدام رمز Starlark في هذا السياق، يمكن فقط الوصول إلى محتوى ملف BUILD نفسه وملفَين (.bzl) تم تحميلهما باستخدامه.
  2. تعريفات القواعد: وهذه هي الطريقة التي يتم من خلالها تحديد القواعد الجديدة (مثل إتاحة لغة جديدة). يمكن الوصول إلى رمز Starlark الذي يتم تنفيذه في هذا السياق من خلال الإعدادات والبيانات التي تقدّمها تبعياته المباشرة (المزيد حول هذا الموضوع لاحقًا).
  3. ملف WORKSPACE. وهذا هو المكان الذي يتم فيه تحديد المستودعات الخارجية (الرمز الذي لا يمثّل ذلك في شجرة المصدر الرئيسية).
  4. تعريفات قواعد المستودع. هذا هو المكان الذي يتم فيه تحديد أنواع المستودعات الخارجية الجديدة. عند تشغيل رمز Starlark في هذا السياق، يمكن تشغيل رمز عشوائي على الجهاز حيث يتم تشغيل Bazel والوصول إلى خارج مساحة العمل.

تختلف اللهجات المتاحة لملفات BUILD و.bzl قليلاً لأنها تعبّر عن أمور مختلفة. وتتوفّر قائمة بالاختلافات هنا.

يمكنك الحصول على مزيد من المعلومات حول Starlark هنا.

مرحلة التحميل/التحليل

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

ويُطلق على هذه العملية اسم &&;;quot;التحميل/التحليل / مرحلة التحليل لأنّه يمكن تقسيمه إلى جزأين مختلفين كانا متسلسلان ولكن يمكن أن يتداخلا الآن مع الوقت:

  1. تحميل الحِزم، وهذا يعني تحويل ملفات BUILD إلى كائنات Package التي تمثّلها.
  2. تحليل الأهداف التي تم ضبطها، أي تنفيذ قواعد إنشاء الرسم البياني للإجراء

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

  1. الإعدادات: ("how" لإنشاء هذه القاعدة؛ على سبيل المثال، النظام الأساسي المستهدف إلى جانب عناصر مثل خيارات سطر الأوامر التي يريد المستخدم أن يتم تمريرها إلى برنامج التجميع C++)
  2. الاعتماديات المباشرة: مقدّمو المعلومات الانتقاليون متاحون للقاعدة التي يتم تحليلها. ويُطلق عليها مثل ذلك لأنها توفِّر &&;;term;up" ; للمعلومات في الخانة الانتقالية للهدف المستهدف، مثل جميع ملفات .jar على مسار الصف أو جميع الملفات التي تتضمّن .o التي تحتاج إلى ربطها بملف ثنائي C++).
  3. الهدف نفسه. هذه هي نتيجة تحميل الحزمة التي تم استهدافها. وبالنسبة إلى القواعد، تتضمّن هذه السمة سماتها.
  4. تنفيذ الاستهداف الذي تم ضبطه. بالنسبة إلى القواعد، يمكن أن تكون إما في Starlark أو Java. يتم تنفيذ كل الأهداف التي لم يتم ضبطها على قاعدة في Java.

نتيجة تحليل هدف تم إعداده هي:

  1. يمكن لمزوّدي المعلومات المعلوماتية الذين أعدّوا الاستهدافات التي تعتمد عليه الوصول إلى هذه المعلومات.
  2. العناصر التي يمكن إنشاؤها والإجراءات التي تنتجها.

واجهة برمجة التطبيقات التي تم تقديمها لقواعد Java هي RuleContext، أي ما يعادل وسيطة ctx لقواعد Starlark. واجهة برمجة التطبيقات الخاصة بها أكثر قوة، ولكن في الوقت نفسه، يكون من الأسهل تنفيذ مهام سيئة، على سبيل المثال كتابة الرمز الذي يكون فيه مقياس الوقت أو القدر المعقّد هو تعقيد (أو أسوأ) لهذا السبب، بسبب تعطُّل خادم Bazel باستخدام استثناءات Java أو مخالفة الثوابت (مثل تعديل مثيل Options أو الهدف من ضبط مثيل عن طريق الخطأ).

الخوارزمية التي تحدّد الارتباطات المباشرة لهدف مستهدف تم إعداده في DependencyResolver.dependentNodeMap().

الإعدادات

وتكوّن عمليات الضبط "&how;how" من تصميم هدف: مع أي نظام أساسي، مع خيارات سطر الأوامر، وما إلى ذلك.

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

من الناحية النظرية، الإعدادات هي مثيل BuildOptions. ومع ذلك، في الممارسة العامة، يتضمّن السمة BuildConfiguration السمة BuildOptions التي توفّر وظائف إضافية متنوعة. ويظهر من أعلى الرسم البياني للمهام التابعة إلى أسفل الصفحة. وإذا تم تغييره، يجب إعادة تحليل الإصدار.

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

عندما يحتاج تنفيذ قاعدة إلى جزء من الضبط، عليه الإعلان عن تعريفه باستخدام RuleClass.Builder.requiresConfigurationFragments() . ويهدف ذلك إلى تجنب الأخطاء (مثل قواعد Python باستخدام جزء Java) وتسهيل قطع الإعدادات، بحيث إذا تغيّرت خيارات Python، لا يلزم +C++ الاستهدافات إعادة التحليل.

إنّ ضبط القاعدة ليس بالضرورة أن يكون مطابقًا لضبط قاعدةها "parent;quot; . وتُسمّى عملية تغيير الإعدادات في حافة تبعية و"نقل الإعدادات"؛ ويمكن أن يحدث ذلك في موضعين:

  1. على حافة اعتمادية. يتم تحديد عمليات النقل هذه في Attribute.Builder.cfg() وهي تعمل من Rule (حيث يحدث النقل) وBuildOptions (الضبط الأصلي) إلى واحدة أو أكثر من BuildOptions (ضبط الإخراج).
  2. على أي حافة واردة لاستهداف تم إعداده. ويتم تحديدها في RuleClass.Builder.cfg().

الصفان ذوا الصلة هما TransitionFactory وConfigurationTransition.

يتم استخدام عمليات نقل الإعداد، مثل:

  1. للإشارة إلى أنّه اعتمادية معيّنة تُستخدم أثناء التصميم، وبالتالي يجب بناؤها في بنية التنفيذ
  2. للإشارة إلى أنّه يجب إنشاء تبعية معيّنة لبنية متعددة (مثل الرمز الأصلي في ملفات APK لنظام التشغيل Android الدهنية).

إذا نتج عن تغيير في عملية الضبط عدة إعدادات، تُسمّى عملية نقل مقسّمة.

يمكن أيضًا تنفيذ عمليات نقل الإعداد في Starlark (المستندات هنا).

مقدّمو المعلومات الانتقالية

إنّ مقدّمي المعلومات الانتقالية هم الطريقة (و_الطريق__الخاص) فقط للأهداف التي تم ضبطها. لإدارة المحتوى حول الاستهدافات الأخرى التي تم ضبطها والتي تعتمد عليه. والسبب في ذلك هو أن اسم الإجراء "النقل&&في الأغنية" هو اسمه عادةً أن هذا الإجراء عبارة عن عرض إجمالي للإغلاق الانتقالي لهدف مستهدف تم ضبطه.

وهناك بشكل عام مراسلات 1:1 بين مقدّمي المعلومات الانتقالية في Java و"ستارلاك" (الاستثناء هو DefaultInfo، وهو عبارة عن توليف من FileProvider وFilesToRunProvider وRunfilesProvider لأنّ واجهة برمجة التطبيقات هذه اعتبرت أكثر وضوحًا من StarStark مقارنةً بتحويل لغة Java مباشرةً). ويتمثل مفتاحها في أحد الأمور التالية:

  1. عنصر من فئة Java. وهذا متاح فقط لمقدمي الخدمة الذين لا يمكن الوصول إليهم من خلال Starlark. ومقدّمو الخدمات هؤلاء هم فئة فرعية من TransitiveInfoProvider.
  2. سلسلة. وقد كان هذا أمرًا صعبًا منذ العام 2021 نظرًا لأنه ليس عرضة لالتعارض بين الأسماء. إنّ مقدّمي المعلومات المعلوماتيين المباشرين هؤلاء هم فئات فرعية مباشرة من build.lib.packages.Info .
  3. رمز مزوّد الخدمة يمكن إنشاء هذا الإعلان من Starlark باستخدام الدالة provider()، وهي الطريقة المُقترحة لإنشاء مقدّمي خدمة جدد. ويتم تمثيل الرمز من خلال مثيل Provider.Key في لغة Java.

يجب تنفيذ مقدّمي الخدمة الجدد الذين تم تنفيذها في Java باستخدام BuiltinProvider. تم إيقاف العمل بأداة NativeProvider نهائيًا (ليس لدينا وقت لإزالته بعد) ولا يمكن الوصول إلى TransitiveInfoProvider فئة فرعية من Starlark.

الأهداف التي تم ضبطها

يتم تنفيذ الأهداف التي تم ضبطها على أنها RuleConfiguredTargetFactory. هناك فئة فرعية لكل فئة قاعدة تم تنفيذها في Java. يتم إنشاء الأهداف Starlark التي تم ضبطها من خلال StarlarkRuleConfiguredTargetUtil.buildRule() .

يجب أن تستخدم المصانع المستهدفة التي تم إعدادها RuleConfiguredTargetBuilder لتحديد قيمة الإرجاع. تتألف هذه الميزة من العناصر التالية:

  1. تمثّل هذه السمة filesToBuild، وهي المفهوم الضبابي لمجموعة الملفات "&"، والتي تمثّل هذه القاعدة. وهذه هي الملفات التي يتم إنشاؤها عندما يكون الهدف الذي تم ضبطه في سطر الأوامر أو في عناصر تحكّم القاعدة.
  2. ملفات الجري والعادية والبيانات.
  3. مجموعات النتائج هذه مجموعات متنوعة من الملفات &quot،ويمكن إنشاء القاعدة. ويمكن الوصول إليها باستخدام السمةإخراج_مجموعة لقاعدة مجموعة الملفات في BUILD واستخدام موفّر OutputGroupInfo في Java.

ملفات التشغيل

تحتاج بعض البرامج الثنائية إلى تشغيل ملفات البيانات. ومن الأمثلة البارزة على ذلك الاختبارات التي تحتاج إلى ملفات إدخال. ويتمثل ذلك في Bazel من خلال مفهوم "run;runfiles". إنّ A"runfilesشجرة" هي شجرة دليل لملفات البيانات الخاصة ببرنامج ثنائي معيّن. ويتم إنشاء هذا النظام في نظام الملفات كشجرة ربط رمزي تحتوي على روابط رمزية فردية تشير إلى الملفات في مصدر أشجار الإخراج.

ويتم تمثيل مجموعة من ملفات التشغيل على شكل مثيل Runfiles. وهي بمثابة خريطة من الناحية النظرية من مسار ملف في العرض التدرّجي للملفات إلى مثيل Artifact الذي يمثّله. الأمر أكثر تعقيدًا من Map واحد لسببين:

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

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

ويتم تمثيل ملفات البرامج الثنائية على أنها نسخة افتراضية من RunfilesSupport. ويختلف ذلك عن Runfiles لأن RunfilesSupport يتيح الإنشاء بشكل فعلي (على عكس Runfiles، وهو مجرد عملية ربط). ويجب إضافة المكوّنات الإضافية التالية:

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

جوانب

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

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

ويتم تحديد مجموعة الجوانب التي تم نشرها في الرسم البياني للاعتمادية لكل سمة باستخدام الدالة Attribute.Builder.aspects(). وهناك بعض الدروس التي تحمل اسمًا مربكًا وتشارك في العملية:

  1. AspectClass هو تنفيذ الجانب. يمكن أن يكون إما في فئة Java (في هذه الحالة، هي فئة فرعية) أو في Starlark (في هذه الحالة، 's هي نسخة افتراضية من StarlarkAspectClass). وهي تشبه RuleConfiguredTargetFactory.
  2. ويُقصد بـ AspectDefinition تعريف الجانب الذي يتضمّن مقدّمي الخدمة الذين يطلبونه ومقدّمي الخدمات الذين يتضمّنون فيه المرجع ويحتوي على مرجع لتنفيذه، مثل مثيل AspectClass المناسب. مشابهة لـ RuleClass.
  3. AspectParameters هي طريقة تتيح لك ترجمة طريقة عرض معيّنة يتم نشرها في الرسم البياني للاعتمادية. إنه حاليًا سلسلة إلى خريطة السلسلة. ومن الأمثلة الجيدة على أسباب استخدام ذاكرة التخزين المؤقت للبروتوكولات: إذا كانت إحدى اللغات تحتوي على واجهات برمجة تطبيقات متعددة، يجب نشر المعلومات المتعلقة بواجهة برمجة التطبيقات التي ينبغي إنشاء المخزن المؤقت للبروتوكول منها على الرسم البياني للاعتمادية.
  4. تمثّل الخاصية Aspect جميع البيانات اللازمة لحساب جانب يوزع الرسم البياني للاعتمادية. وتتألف من فئة العرض، وتعريفها ومعلماتها.
  5. RuleAspect هي الدالة التي تحدِّد الجوانب التي يجب نشر قاعدة معيّنة فيها. إنها دالة Rule -> Aspect.

من بين الإضافات غير المتوقّعة إلى حد ما أنّ الجوانب يمكن إرفاقها بجوانب أخرى. على سبيل المثال، من المحتمل أن يكون هناك جانب يجمع ممر الصف الدراسي لـ Java IDE. من المحتمل أن يرغب في معرفة جميع ملفات .jar على مسار الصف الدراسي، ولكن بعضها عبارة عن مخازن البروتوكولات المؤقتة. في هذه الحالة، يجب إرفاق جانب IDE إلى زوج (قاعدة proto_library + جانب Java Proto).

تم رصد تعقيدات الجوانب في الصف الدراسي AspectCollection.

المنصات وسلاسل الأدوات

يتوافق Bazel مع الإصدارات المتعددة المنصات، أي الإصدارات التي يمكن أن تتضمّن العديد من البُنى التي يتم فيها تنفيذ إجراءات الإصدار وبُنى متعدّدة لإنشاء الرمز. يُشار إلى هذه التصاميم باسم المنصات في قناة Bazel parlance (المستندات الكاملة هنا).

يتم وصف المنصّة من خلال ربط قيمة المفتاح من إعدادات الإعدادات المحدودة (مثل مفهوم "البنية &quot، و"وحدة المعالجة المركزية"؛ والقيم المحدودة (مثل وحدة معالجة مركزية معيّنة مثل x86_64). ولدينا "quot;القواميس" لمعرفة القيم والقيود الأكثر استخدامًا في مستودع @platforms.

يكمن مفهوم toolchain في حقيقة أنّه بناءً على الأنظمة الأساسية التي يعمل عليها الإصدار والمنصّات المستهدَفة، قد تحتاج إلى استخدام برامج تحويل مختلفة، على سبيل المثال، قد تعمل سلسلة أدوات C++ معيّنة على نظام تشغيل محدّد ويمكنها استهداف بعض أنظمة التشغيل الأخرى. يجب أن يحدّد Bazel العارض C++ الذي يُستخدم بناءً على تنفيذ التنفيذ والنظام الأساسي المستهدف (مستندات سلاسل الأدوات هنا).

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

  1. قاعدة toolchain() التي توضّح مجموعة التنفيذ والاستهداف تتوافق مع سلسلة أدوات وتخبرها بنوع (مثل C++ أو Java) الخاصة بسلسلة الأدوات (وهي القاعدة التي يتم تمثيلها من خلال القاعدة toolchain_type())
  2. قاعدة بلغة محددة تصف سلسلة الأدوات الفعلية (مثل cc_toolchain())

يتم إجراء ذلك بهذه الطريقة لأننا نحتاج إلى معرفة القيود لكل سلسلة مفاتيح من أجل تنفيذ دقة سلسلة الأدوات وقواعد اللغة *_toolchain() وتحتوي على معلومات أكثر من ذلك، لذا يستغرق تحميل المزيد من الوقت.

يتم تحديد منصات التنفيذ بإحدى الطرق التالية:

  1. في ملف WORKSPACE باستخدام الدالة register_execution_platforms()
  2. في سطر الأوامر باستخدام خيار سطر الأوامر "--tratra_execution_platforms"

يتم احتساب مجموعة الأنظمة الأساسية للتنفيذ المتاحة في RegisteredExecutionPlatformsFunction .

يتم تحديد النظام الأساسي المستهدف للهدف الذي تم ضبطه من خلال PlatformOptions.computeTargetPlatform() . هذه هي قائمة من المنصات لأننا نريد في نهاية المطاف إتاحة منصّات متعددة مستهدَفة، ولكن لم ننفّذها بعد.

يتم تحديد مجموعة سلاسل الأدوات التي سيتم استخدامها للاستهداف الذي تم ضبطه من خلال ToolchainResolutionFunction. وهي وظيفة مما يلي:

  • مجموعة سلاسل الأدوات المسجّلة (في ملف WORKSPACE والضبط)
  • منصات التنفيذ والهدف المطلوبة (في عملية الضبط)
  • مجموعة أنواع سلسلة الأدوات المطلوبة في الهدف الذي تم ضبطه (في UnloadedToolchainContextKey))
  • مجموعة قيود النظام الأساسي للتنفيذ للهدف الذي تم ضبطه (السمة exec_compatible_with) والضبط (--experimental_add_exec_constraints_to_targets)، في UnloadedToolchainContextKey

وتكون النتيجة UnloadedToolchainContext، والتي هي في الأساس خريطة من نوع سلسلة الأدوات (التي يتم تمثيلها كمثيل ToolchainTypeInfo) إلى تصنيف سلسلة الأدوات المحددة. يُطلق على هذه العملية اسم &"quot;unload" لأنّه لا يحتوي على سلاسل الأدوات نفسها، بل يعرض تصنيفاتها فقط.

وبعد ذلك، يتم تحميل سلاسل الأدوات باستخدام ResolvedToolchainContext.load() واستخدامها من خلال تنفيذ الاستهداف المحدّد الذي طلبها.

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

القيود

في بعض الأحيان، يريد المستخدم ضبط استهداف ليتوافق مع عدد قليل من المنصّات. لدى "بازل" آليات متعدّدة (للأسف) لتحقيق هذه الغاية:

  • قيود خاصة بالقاعدة
  • environment_group() / environment()
  • قيود النظام الأساسي

غالبًا ما تُستخدَم قيود خاصة بالقواعد ضمن قواعد Google للغة Java، فهي في طريقها للوصول إليها وهي غير متاحة في Bazel، ولكن رمز المصدر قد يتضمّن مراجع إليها. يُطلق على السمة التي تحكم ذلك اسم constraints= .

Environment_group() وEnvironment()

وهذه القواعد هي آلية قديمة ولا يتم استخدامها على نطاق واسع.

يمكن أن تُعلِم جميع قواعد الإصدار عن "البيئة المحيطة" والتي يمكن إنشاؤها من أجلها، حيث تكون ""environment&quot" مثالاً على قاعدة environment().

هناك طرق مختلفة يمكن تحديد البيئات المتوافقة للقاعدة:

  1. استخدام السمة restricted_to= هذا هو النموذج الأكثر مباشرةً من المواصفات، حيث يحدّد المجموعة الدقيقة للبيئات المتوافقة مع القاعدة لهذه المجموعة.
  2. استخدام السمة compatible_with= ويشير هذا إلى البيئات التي تتوافق مع القاعدة بالإضافة إلى البيئات المتوافقة مع نظام التشغيل ""العادي&quot: المتاحة تلقائيًا.
  3. من خلال السمتَين "على مستوى الحزمة" default_restricted_to= وdefault_compatible_with=
  4. من خلال المواصفات التلقائية في environment_group() قاعدة تنتمي كل بيئة إلى مجموعة من التطبيقات المشابهة ذات الصلة بموضوعها (مثل "البنية المركزية لـ "وحدة المعالجة المركزية" (CPU) "&&;;;JDK والإصدارات&;; أو "أنظمة تشغيل الجوّال"). يتضمن تعريف مجموعة البيئة أيًا من هذه البيئات يجب أن تكون متوافقة مع "default;default&quot؛ إذا لم يتم تحديدها خلافًا لذلك من خلال السمات restricted_to= / environment(). وتكتسب القاعدة التي لا تضمّ هذه السمات كل الإعدادات التلقائية.
  5. من خلال السمة التلقائية لفئة القاعدة. ويؤدي ذلك إلى إلغاء الإعدادات التلقائية العامة لجميع مثيلات فئة القاعدة المحددة. ويمكن استخدام ذلك مثلاً لجعل جميع قواعد *_test قابلة للاختبار بدون أن تشير كل نسخة إلى هذه الإمكانية صراحةً.

يتم تنفيذ environment() كقاعدة عادية في حين أن environment_group() هي فئة فرعية من Target وليس Rule (EnvironmentGroup) ووظيفة متاحة تلقائيًا من Starlark (StarlarkLibrary.environmentGroup()) ما يؤدي في النهاية إلى إنشاء هدف يحمل الاسم نفسه. والهدف من ذلك هو تجنب الاعتمادية الدورية التي قد تنشأ لأن كل بيئة تحتاج إلى الإعلان عن مجموعة البيئة التي تنتمي إليها كل مجموعة بيئية يجب أن تعلن عن بيئاتها التلقائية.

يمكن حصر الإصدار في بيئة معيّنة باستخدام خيار سطر الأوامر --target_environment.

يتوفّر التحقّق من القيود في RuleContextConstraintSemantics وTopLevelConstraintSemantics.

قيود النظام الأساسي

إنّ الطريقة الحالية "الرسمية"للتعبير عن المنصّات المتوافقة مع الاستهداف هي استخدام القيود نفسها المستخدَمة لوصف سلاسل الأدوات والمنصّات. قيد المراجعة في طلب السحب #10945.

مستوى العرض

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

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

ويتم تنفيذ ذلك في الأماكن التالية:

  • تعرض واجهة RuleVisibility بيان إذن الوصول. ويمكن أن تكون هذه البيانات إمّا ثابتة (تكون علنية بالكامل أو خاصة بالكامل) أو قائمة من التصنيفات.
  • يمكن أن تشير التصنيفات إلى مجموعات الحِزم (قائمة الطُرق المحدّدة مسبقًا) أو إلى الحِزم مباشرةً (//pkg:__pkg__) أو الأنواع الفرعية من الحِزم (//pkg:__subpackages__). ويختلف هذا عن بنية سطر الأوامر التي تستخدم //pkg:* أو //pkg/....
  • تم تنفيذ مجموعات الحِزم كهدف خاص بها (PackageGroup) هدف مستهدف تم ضبطه (PackageGroupConfiguredTarget). يمكننا على الأرجح استبدالها بقواعد بسيطة إذا أردنا ذلك. يتم تنفيذ المنطق بمساعدة: PackageSpecification، التي تتوافق مع نمط واحد مثل //pkg/...، وPackageGroupContents، والذي يتوافق مع سمة package_group's packages واحدة، وPackageSpecificationProvider، التي يتم تجميعها من خلال package_group وincludes العابرة لها.
  • يتم إجراء الإحالة الناجحة من قوائم تصنيفات مستوى الرؤية إلى الاعتماديات في DependencyResolver.visitTargetVisibility وبعض الأماكن الأخرى المتنوعة.
  • تم التحقق الفعلي في CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

المجموعات المتداخلة

في كثير من الأحيان، يجمّع الهدف الذي تم إعداده مجموعة من الملفات من المهام التابعة له، ويضيف خاصّة به، ويتم لفّ المجموعة المجمّعة في مقدّم معلومات انتقالية بحيث يمكن للأهداف التي تم ضبطها تعتمد عليها تنفيذ الإجراء نفسه. أمثلة:

  • ملفات عنوان C++ المستخدمة لإصدار معيّن
  • ملفات الكائن التي تمثّل الإغلاق غير المباشر لسمة cc_library
  • مجموعة ملفات Jjar التي يجب أن تكون على مسار الفئة لقاعدة Java لتجميعها أو تشغيلها
  • مجموعة ملفات Python في الإغلاق العام لقاعدة Python

إذا استخدمنا هذه الطريقة البسيطة باستخدام List أو Set مثلاً، سنبدأ باستخدام الذاكرة التربيعية: إذا كانت هناك سلسلة من القواعد وإذا أضفت كل قاعدة ملفًا واحدًا، سيكون لدينا أكثر من +2 أو أكثر من أعضاء المجموعة.

للتغلب على هذه المشكلة، ابتكرنا مفهوم NestedSet. وهي عبارة عن بنية بيانات تتألف من حالات NestedSet الأخرى وبعض الأعضاء الآخرين، وتشكّل بذلك رسمًا بيانيًا دائريًا موجزًا لمجموعات. ستكون غير قابلة للتغيير، ويمكن تكرار أعضاءهم. ونحدّد ترتيب تكرار متعدد (NestedSet.Order): الطلب المسبق، والطلب المتأخر، والطوبولوجي (العقدة تأتي دائمًا بعد الأسلاف) و "don't ولا يهمّها، ولكن يجب أن تكون نفسها في كل وقت&quot.

وتُسمّى بنية البيانات نفسها depset في تطبيق Starlark.

العناصر والإجراءات

ويتكون الإصدار الفعلي من مجموعة من الأوامر التي يجب تنفيذها لإنتاج الناتج الذي يريده المستخدم. ويتم تمثيل الطلبات على أنها أمثلة للفئة Action ويتم تمثيل الملفات كأمثلة للفئة Artifact. ويتم تنظيمها على شكل رسم بياني دائري ثنائي الاتجاه موجّه يُعرف باسم "الرسم البياني الإرشادي"؛

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

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

ليس هناك سبب أساسي وراء عدم تمكن عناصر المصدر من إنشاء عناصر متفرعة أو عناصر رمزية لم يتم حلها، ولكن لم ننفّذه بعد (وعلى الرغم من ذلك، علينا أن نشير إلى دليل ملفات المصدر في ملف BUILD هو من بين المشاكل القليلة المعروفة وغير الصحيحة في Bazel، فنحن نوفّر طريقة تنفيذ لهذا النوع من الأعمال التي تم تفعيلها من خلال سمة JVM).

نوع بارز من Artifact هو الوسيط. وتتم الإشارة إليها من خلال مثيلات Artifact التي تمثل مخرجات MiddlemanAction. ويتم استخدامها في بعض الحالات الخاصة:

  • يُستخدم التجميع كتجميع لتجميع العناصر معًا. ويعود السبب في ذلك إلى أنه إذا كان هناك الكثير من الإجراءات التي تستخدم المجموعة الكبيرة نفسها من الإدخالات، ليس لدينا حواف تعتمد على N*M، بل سنطبق الإجراء N+M فقط (سيتم استبدالها بمجموعات متداخلة).
  • يضمن وسيط الاعتمادية تنفيذ إجراء قبل آخر. تُستخدَم غالبًا للربط، لكن أيضًا للتجميع C++ (راجع CcCompilationContext.createMiddleman()للتوضيح)
  • يتم استخدام وسيطات إدارة الملفات لضمان توفّر شجرة الملفات التي يتم تشغيلها حتى لا يحتاج الاعتماد على شجرة الملفات إلى الاعتماد على بيان النتيجة وكل عنصر واحد يشير إليه شجرة الملفات.

يمكن فهم الإجراءات على أفضل وجه بصفتها أمرًا يحتاج إلى التشغيل، والبيئة التي يحتاج إليها ومجموعة النتائج الناتجة عنها. العناصر التالية هي المكوّنات الأساسية لوصف الإجراء:

  • سطر الأوامر الذي يجب تشغيله
  • عناصر الإدخال التي يحتاجها
  • متغيرات البيئة التي يجب تحديدها
  • التعليقات التوضيحية التي تصف البيئة (مثل النظام الأساسي) التي يجب تشغيلها فيها \

هناك أيضًا بعض الحالات الخاصة الأخرى، مثل كتابة ملف يعرف المحتوى الخاص به باسم Bazel. هذه فئة فرعية من AbstractAction. معظم الإجراءات هي SpawnAction أو StarlarkAction (وهي نفسها يجب ألا تكون فئات منفصلة)، على الرغم من أن Java وC++ لها أنواع إجراءات خاصة (JavaCompileAction وCppCompileAction وCppLinkAction).

وأخيرًا نرغب في نقل كل شيء إلى SpawnAction، وJavaCompileAction هو متقارب جدًا، ولكن C++ هي حالة خاصة إلى حد ما بسبب تحليل ملف .d ويتضمن فحصًا.

غالبًا ما يكون الرسم البياني للحركة "embed;quot"داخل الرسم البياني لـ Skyframe: من الناحية النظرية، يتم تمثيل تنفيذ الإجراء كاستدعاء ActionExecutionFunction. يتم وصف الربط من حافة تبعية الرسم البياني للإجراء إلى حافة تبعية إطار Skysky في ActionExecutionFunction.getInputDeps() وArtifact.key() وإجراء بعض التحسينات للحفاظ على انخفاض حواف Skyframe:

  • لا تحتوي العناصر المشتقة على SkyValue الخاصة بها. بدلاً من ذلك، يتم استخدام Artifact.getGeneratingActionKey() لمعرفة المفتاح للإجراء الذي يتم إنشاؤه.
  • تحتوي المجموعات المتداخلة على مفتاح Skyframe خاص بها.

الإجراءات المشتركة

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

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

إذا كان هناك إجراءان يؤديان إلى إنشاء ملف الإخراج نفسه، يجب أن يكونا متطابقَين تمامًا: يحتويان على المُدخلات نفسها، ويتم عرض المُخرجات نفسها وتشغيل سطر الأوامر نفسه. تم تنفيذ علاقة المعادلة هذه في Actions.canBeShared() ويتم التحقق منها بين مرحلتَي التحليل والتنفيذ من خلال الاطّلاع على كل إجراء. وقد تم تنفيذ ذلك في SkyframeActionExecutor.findAndStoreArtifactConflicts()، وهو أحد الأماكن القليلة في Bazel التي تتطلّب عرضًا للعمارة

مرحلة التنفيذ

هذا عندما يبدأ Bazel بتنفيذ إجراءات الإصدار، مثل الأوامر التي تنتج نتائج.

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

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

  • يغيّر سطر الأوامر عند نقل الحزمة من مسار مسار الحزمة إلى إدخال آخر (كان هذا الإجراء شائعًا).
  • ينتج عن ذلك أوامر أوامر مختلفة إذا تم تشغيل إجراء عن بُعد مما إذا كان يتم تشغيله محليًا
  • يتطلب تحويل سطر أوامر خاصًا بالأداة قيد الاستخدام (فكِّر في الفرق بين مسارات Java وC++ بما في ذلك المسارات)
  • يؤدي تغيير سطر الأوامر لأحد الإجراءات إلى إبطال إدخال ذاكرة التخزين المؤقت للإجراء
  • تم إيقاف --package_path ببطء وبوتيرة ثابتة

بعد ذلك، يبدأ "بازل" باجتياز الرسم البياني للإجراءات (الرسم البياني الثنائي والموجّه الذي يتألف من إجراءات وقطع الإدخال والإخراج) وإجراءات التشغيل. يمثّل تنفيذ كل إجراء مثالاً على SkyValueفئة ActionExecutionValue.

بما أنّ تنفيذ أحد الإجراءات باهظ التكلفة، نوفّر بضع طبقات من ذاكرة التخزين المؤقت يمكن الوصول إليها خلف Skyframe:

  • يحتوي ActionExecutionFunction.stateMap على بيانات تجعل إعادة تشغيل Skyframe منخفضة بنسبة ActionExecutionFunction
  • تحتوي ذاكرة التخزين المؤقت للإجراءات المحلية على بيانات حول حالة نظام الملفات.
  • تحتوي أنظمة التنفيذ عن بُعد عادةً على ذاكرة التخزين المؤقت الخاصة بها.

ذاكرة التخزين المؤقت للإجراءات المحلية

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

ويتم فحص ذاكرة التخزين المؤقت هذه بحثًا عن النتائج باستخدام الطريقة ActionCacheChecker.getTokenIfNeedToExecute() .

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

  1. مجموعة ملفات الإدخال والإخراج ومجموع الاختبار الخاص بها
  2. وهو مفتاح البحث ";&;; في معظم الأحيان"هو سطر الأوامر الذي تم تنفيذه، ولكن بشكل عام، يمثّل كل العناصر التي لا يتم تسجيلها باستخدام المجموع الاختباري لملفات الإدخال (مثل FileWriteAction، وهو المجموع الاختباري للبيانات التي تمت كتابتها)

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

التعرّف على الإدخال وإدخاله

تكون بعض الإجراءات أكثر تعقيدًا من استخدام مجموعة من المدخلات. يتم إجراء التغييرات على مجموعة المدخلات من الإجراء في شكلين:

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

ويتم تنفيذ هذه الإجراءات باستخدام طُرق اتخاذ إجراء:

  1. تم طلب Action.discoverInputs(). من المفترَض أن يتم عرض مجموعة متداخلة من العناصر التي يتبيّن أنها مطلوبة. ويجب أن تكون هذه العناصر مصدرًا لأنه لا توجد حواف اعتمادية في الرسم البياني للإجراء التي لا يكون لها مكافئ في الرسم البياني المستهدف الذي تم ضبطه.
  2. يتم تنفيذ الإجراء من خلال الاتصال بـ Action.execute().
  3. في نهاية Action.execute()، يمكن اتّخاذ الإجراء المناسب: Action.updateInputs() لإبلاغ "بازل" بأنّ جميع مصادر الإدخال غير مطلوبة. قد يؤدي ذلك إلى إنشاء إصدارات متزايدة غير صحيحة إذا تم الإبلاغ عن إدخال غير مستخدم.

عندما تعرض ذاكرة التخزين المؤقت للإجراء نتيجة على نسخة جديدة من الإجراء (مثل تلك التي تم إنشاؤها بعد إعادة تشغيل الخادم)، تطلب Bazel updateInputs() نفسها بحيث تعكس مجموعة الإدخالات نتيجة اكتشاف الإدخال وإجراء تقليم من قبل.

يمكن للإجراءات Starlark الاستفادة من المنشأة من أجل الإعلان عن بعض الإدخالات على أنها غير مستخدَمة باستخدام وسيطة unused_inputs_list= الخاصة بـ ctx.actions.run().

طرق متعددة لتنفيذ الإجراءات: الاستراتيجيات/سياقات العمل

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

في ما يلي مراحل دورة حياة الإجراء:

  1. عند بدء مرحلة التنفيذ، يتم سؤال BlazeModule مثيلات عن سياقات الإجراءات التي تتّبعها. وهذا يحدث في أداة إنشاء ExecutionTool. يتم تحديد أنواع سياق الإجراءات من خلال نسخة افتراضية من ClassJava تشير إلى واجهة فرعية لـ ActionContext وعليك تحديد سياق الإجراء.
  2. يتم اختيار سياق الإجراء المناسب من السياقات المتاحة وتتم إعادة توجيهه إلى ActionExecutionContext وBlazeExecutor .
  3. تتطلب الإجراءات سياقات باستخدام ActionExecutionContext.getContext() و BlazeExecutor.getStrategy() (يجب أن تكون هناك طريقة واحدة فقط لتنفيذ ذلك...)

يمكن للاستراتيجيات استدعاء استراتيجيات أخرى لأداء وظائفها، ويتم استخدام ذلك مثلاً في الاستراتيجية الديناميكية التي تبدأ الإجراءات محليًا وعن بُعد، ثم تستخدم أيًا منها تنتهي أولاً.

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

وفي حال تغيير الأداة، يجب إعادة تشغيل عملية العامل. ويتم تحديد ما إذا كان من الممكن إعادة استخدام العامل من خلال حساب المجموع الاختباري للأداة المستخدَمة باستخدام WorkerFilesHash. يعتمد ذلك على معرفة إدخالات الإجراء التي تمثّل جزءًا من الأداة وتلك التي تمثّل الإدخالات. ويتم تحديد ذلك من خلال منشئ الإجراء: Spawn.getToolFiles() ويتم احتساب الملفات التي يتم تشغيلها من Spawn كجزء من الأداة.

مزيد من المعلومات حول الاستراتيجيات (أو سياقات الإجراءات):

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

مدير الموارد المحلية

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

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

يتوفّر وصف أكثر تفصيلاً لإدارة الموارد المحلية هنا.

بنية دليل الإخراج

يتطلّب كل إجراء مكانًا منفصلاً في دليل الإخراج حيث يضع النتائج. يكون عادةً موضع العناصر المشتقة ما يلي:

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

كيف يتم تحديد اسم الدليل المرتبط بإعداد معيّن؟ هناك نوعان من السمات غير المرغوب فيها المتعارضة:

  1. إذا كان هناك إعدادان يمكن أن يحدثا في الإصدار نفسه، يجب أن يكون لديهما أدلة مختلفة حتى يمكن لكل منهما الحصول على إصدار خاص به من الإجراء نفسه، وبخلاف ذلك، في حال عدم تطابق الإعدادين معًا مثل سطر الأوامر في إجراء ما إنتاج ملف الإخراج نفسه، لا يعرف Bazel أي إجراء يمكن اختياره (a"action;action")
  2. إذا كان هناك إعدادان يمثلان "و"; تقريبًا&quot: يجب أن يكون لديهما الاسم نفسه، بحيث يمكن إعادة استخدام الإجراءات التي تم تنفيذها في إحداهما على أخرى في حال تطابق سطري الأوامر: على سبيل المثال، يجب ألا تؤدي التغييرات في خيارات سطر الأوامر إلى برنامج التجميع Java إلى إعادة تنفيذ إجراءات التجميع ++C.

حتى الآن، لا نبتكر طريقة تعتمد على مبدأ حلّ هذه المشكلة، والتي تشبه إلى حدّ كبير مشكلة قطع الإعدادات. يمكنك الاطّلاع على مزيد من المناقشات حول الخيارات هنا. ومن أهمّ المشاكل التي تواجه المشكلة هي قواعد Starlark (التي لا يكون فيها مؤلفوها عادةً على دراية كافية بـ Bazel) وجوانبها التي تضيف بُعدًا آخر إلى المساحة التي قد تؤدي إلى إنتاج ملف "الاقتباس" و" الأسعار نفسه"

المنهج الحالي هو أن شريحة المسار للضبط هي <CPU>-<compilation mode> مع إضافة لاحقات مختلفة حتى لا تؤدي عمليات النقل في التنفيذ التي تم تنفيذها في Java إلى تعارض في الإجراءات. وبالإضافة إلى ذلك، تتم إضافة المجموع الاختباري لمجموعة الانتقالات الخاصة بإعداد Starlark حتى يتمكّن المستخدمون من عدم إحداث تعارضات في الإجراءات. بعيدة جدًا عن المثالية. يتم تنفيذ الإجراء في OutputDirectories.buildMnemonic() ويعتمد على كل جزء ضبط يضيف جزءًا خاصًا به إلى اسم دليل الناتج.

الفحوصات

يقدّم Bazel دعمًا وافرًا لإجراء الاختبارات. وهو يدعم ما يلي:

  • إجراء الاختبارات عن بُعد (في حال توفر خلفية عن بُعد للتنفيذ)
  • إجراء الاختبارات عدة مرات في آن واحد (لبيانات استقرار أو جمع التوقيت)
  • تقسيم الاختبارات (تقسيم حالات الاختبار في الوقت نفسه إلى عمليات متعددة للسرعة)
  • إعادة إجراء الاختبارات غير المستقرة
  • تجميع الاختبارات في مجموعات اختبار

تكون الاختبارات عبارة عن استهدافات مهيأة بشكل منتظم تحتوي على TestProvider، وتوضّح كيفية إجراء الاختبار:

  • العناصر التي ينتج عنها الاختبار. هذه ملف &"cache;cache حالة" التي تحتوي على رسالة TestResultData متسلسلة
  • عدد مرات إجراء الاختبار
  • عدد الأجزاء التي يجب تقسيم الاختبار إليها
  • بعض المعلمات حول كيفية إجراء الاختبار (مثل انتهاء مهلة الاختبار)

تحديد الاختبارات التي سيتم تشغيلها

ويُعد تحديد الاختبارات التي يتم تنفيذها عملية معقدة.

أولاً، أثناء تحليل النمط المستهدف، يتم توسيع مجموعات الاختبار بشكل متكرر. تم تنفيذ التوسيع في TestsForTargetPatternFunction. ومن الأمور المفاجئة إلى حد ما أنه إذا أعلنت حزمة اختبار عن عدم إجراء أي اختبارات، فإنها تشير إلى كل اختبار في حزمتها. يتم تنفيذ ذلك في Package.beforeBuild() من خلال إضافة سمة ضمنية تُسمى $implicit_tests لاختبار قواعد المجموعة.

بعد ذلك، تتم فلترة الاختبارات حسب الحجم والعلامات والمهلة واللغة وفقًا لخيارات سطر الأوامر. يتم تنفيذ ذلك في TestFilter ويتم استدعاءه من TargetPatternPhaseFunction.determineTests() أثناء التحليل المستهدف ويتم وضع النتيجة في TargetPatternPhaseValue.getTestsToRunLabels(). يرجع السبب في عدم قدرة سمات القاعدة التي يمكن فلترتها إلى حدوث ذلك قبل مرحلة التحليل، وبالتالي لا تتوفّر عملية الضبط.

تتم بعد ذلك معالجة هذه المشكلة بشكلٍ أكبر في BuildView.createResult(): تتم فلترة الأهداف التي تعذّر تحليلها ويتم تقسيم الاختبارات إلى اختبارات حصرية وغير حصرية. بعد ذلك، يتم وضعه في AnalysisResult، وهذا هو الطريقة التي يحدد بها ExecutionTool الاختبارات التي يجب إجراؤها.

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

الاختبارات الجارية

الطريقة التي يتم بها إجراء الاختبارات هي من خلال طلب عناصر حالة ذاكرة التخزين المؤقت. بعد ذلك، يتم تنفيذ علامة TestRunnerAction، والتي تعمل في نهاية الأمر على طلب TestActionContext الذي يختاره سطر الأوامر --test_strategy الذي يُجري الاختبار بالطريقة المطلوبة.

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

بالإضافة إلى ملف حالة ذاكرة التخزين المؤقت، ترسل كل عملية اختبار عددًا من الملفات الأخرى. ويتم وضعها في الدليل الإرشادي "test;test;quot، وهو الدليل الفرعي الذي يحمل الاسم testlogs لدليل الناتج للضبط المستهدف:

  • test.xml، وهو ملف XML بنمط JUnit يفصّل حالات الاختبار الفردية في جزء الاختبار
  • test.log، تكون مخرجات وحدة التحكم في الاختبار غير مفصولة لأغراض مثل stdout وstderr.
  • test.outputs، الدليل &quot،غير المُعلَن عن المخرجات&;; يتم استخدامه من خلال الاختبارات التي تريد إخراج الملفات بالإضافة إلى ما تطبعه الوحدة الطرفية.

هناك أمران يمكن أن يحدث أثناء تنفيذ الاختبار ولا يمكن تنفيذهما أثناء إنشاء أهداف منتظمة: تنفيذ الاختبار الحصري وبث النتائج.

يجب إجراء بعض الاختبارات في وضع حصري، على سبيل المثال لا يمكن مزامنتها مع اختبارات أخرى. ويمكن تقدير ذلك من خلال إضافة السمة tags=["exclusive"] إلى قاعدة الاختبار أو تشغيل الاختبار باستخدام السمة --test_strategy=exclusive. يتم تنفيذ كل اختبار حصري من خلال استدعاء منفصل لـ Skyframe تطلب تنفيذ الاختبار بعد الإنشاء "primary". تمّ تنفيذ هذا الإجراء في SkyframeExecutor.runExclusiveTest().

وعلى عكس الإجراءات العادية التي يتم التخلص من ناتجها عند انتهاء الإجراء، يمكن للمستخدم طلب بث نتائج الاختبارات ليتم بثّها إبلاغه بمدى تقدّم اختبار طويل الأمد. ويتم تحديد ذلك من خلال خيار سطر الأوامر --test_output=streamed، ويشير إلى تنفيذ اختبار حصري، بحيث لا يتم تبادل مخرجات الاختبارات المختلفة.

يتم تنفيذ ذلك في الفئة StreamedTestOutput التي تحمل اسمًا مناسبًا ويعمل من خلال إحداث تغييرات في ملف test.log الخاص بالاختبار المعنيّ وتفريغ وحدات بايت جديدة في الوحدة الطرفية حيث قواعد البازل.

تتوفّر نتائج الاختبارات التي تم إجراؤها على متن الحافلة الخاصة بالفعالية من خلال الاطّلاع على أحداث مختلفة (مثل TestAttempt أو TestResult أوTestingCompleteEvent). يتم الاحتفاظ بهذه الاختبارات في بروتوكول إنشاء الحدث، ويتم نقلها إلى وحدة التحكم بحلول AggregatingTestListener.

مجموعة التغطية

يتم الإبلاغ عن التغطية من خلال الاختبارات بتنسيق LCOV في الملفات.bazel-testlogs/$PACKAGE/$TARGET/coverage.dat .

لجمع التغطية، يتم تضمين كل تنفيذ تجريبي في نص برمجي يُسمى collect_coverage.sh .

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

يتم تنفيذ تفاعل collect_coverage.sh بواسطة استراتيجيات الاختبار ويتطلب ذلك أن يكون collect_coverage.sh على مدخلات الاختبار. ويتم تنفيذ ذلك من خلال السمة الضمنية :coverage_support التي يتم حلها إلى قيمة علامة الضبط --coverage_support (راجِع TestConfiguration.TestOptions.coverageSupport)

وتقدّم بعض اللغات قياسًا في وضع عدم الاتصال، ما يعني أنّه يتم إدراج أدوات التغطية في وقت التجميع (مثل C++ ) بينما يتم وضع أدوات أخرى على الإنترنت في قياس الأداء.

وهناك مفهوم أساسي آخر وهو التغطية الأساسية. يشير هذا المصطلح إلى تغطية مكتبة أو برنامج ثنائي أو اختبار ما إذا لم يتم تشغيل أي رمز فيها. تكمن المشكلة التي تحلّها في أنّك إذا كنت تريد حساب تغطية الاختبار لبرنامج ثنائي، لن يكفي دمج تغطية جميع الاختبارات، لأنه قد يكون هناك رمز في البرنامج الثنائي غير مرتبط بأي اختبار. وبالتالي، نحن ننشئ ملف تغطية لكل برنامج ثنائي يتضمن الملفات التي نجمعها فقط بدون أسطر مغطّاة. ملف التغطية الأساسي لأحد الأهداف هو bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . يتم إنشاؤه أيضًا للبرامج الثنائية والمكتبات، بالإضافة إلى الاختبارات إذا اجتازت علامة --nobuild_tests_only إلى Bazel.

التغطية الأساسية غير مفعّلة حاليًا.

نتتبّع مجموعتَين من ملفات جمع التغطية لكل قاعدة، هما: مجموعة الملفات التي تم قياسها ومجموعة ملفات البيانات الوصفية للأدوات.

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

مجموعة ملفات البيانات الوصفية للأدوات هي مجموعة من الملفات الإضافية التي يحتاج إليها الاختبار لإنشاء ملفات LCOV التي يتطلبها Bazel منها. عمليًا، يتألف هذا الملف من ملفات خاصة بوقت التشغيل، على سبيل المثال، gcc emits .gcno files أثناء التجميع. وتتم إضافتها إلى مجموعة إدخالات الإجراءات التجريبية في حال تفعيل وضع التغطية.

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

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

وننشئ أيضًا "تقرير التغطية" "يدمج التغطية التي تم جمعها لكل اختبار في استدعاء Bazel. تتم معالجة هذا الأمر من قِبل CoverageReportActionFactory ويتم استدعاءه من BuildView.createResult() . ويمكنهم الوصول إلى الأدوات التي يحتاجونها من خلال الاطّلاع على سمة :coverage_report_generator من الاختبار الأول الذي تم تنفيذه.

محرّك طلب البحث

لدى "بازل" لغة صغيرة تم استخدامها لتقديم أسئلة مختلفة حول الرسوم البيانية المختلفة. يتم توفير أنواع طلبات البحث التالية:

  • يتم استخدام bazel query للتحقيق في الرسم البياني المستهدف.
  • يتم استخدام bazel cquery للتحقيق في الرسم البياني المستهدف الذي تم ضبطه.
  • يتم استخدام bazel aquery للتحقيق في الرسم البياني للإجراء.

ويتم تنفيذ كل واحدة من هذه الفئات الفرعية من خلال الفئة الفرعية AbstractBlazeQueryEnvironment. يمكن تنفيذ وظائف إضافية لطلب البحث من خلال التقسيم الفرعي QueryFunction . للسماح ببث نتائج طلب البحث، بدلاً من جمعها إلى بعض بنية البيانات، يتم تمرير query2.engine.Callback إلى QueryFunction، وهو ما يستدعي النتائج التي يريد عرضها.

يمكن أن تظهر نتيجة طلب البحث بطرق مختلفة، مثل التصنيفات والتصنيفات وفئات القواعد وXML وProtobuf وما إلى ذلك. ويتم تنفيذها كفئات فرعية من OutputFormatter.

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

نظام الوحدات

يمكنك تمديد البازل من خلال إضافة وحدات إليه. يجب أن تكون كل وحدة فرعية من النوع الفرعي BlazeModule (الاسم هو من بقايا سجل Bazel عندما كان يُطلق عليه اسم Blaze)، ويحصل على معلومات عن أحداث مختلفة أثناء تنفيذ الأمر.

وتُستخدَم عادةً لتنفيذ أجزاء مختلفة من وظيفة "non-core"شبكة الوظائف التي يحتاج إليها بعض إصدارات Bazel فقط (مثل التي نستخدمها في Google):

  • واجهات لأنظمة التنفيذ عن بُعد
  • طلبات جديدة

مجموعة نقاط الإضافات التي تقدّمها BlazeModule هي خطر إلى حد ما. لا يجب استخدامها كمثال لمبادئ التصميم الجيد.

حافلة للفعاليات

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

  • تم تحديد قائمة أهداف الإصدار المراد إنشاؤها (TargetParsingCompleteEvent)
  • وقد تم تحديد الإعدادات ذات المستوى الأعلى (BuildConfigurationEvent)
  • تم إنشاء هدف بنجاح أو لا (TargetCompleteEvent)
  • تم إجراء اختبار (TestAttempt، TestSummary)

ويتم تمثيل بعض هذه الفعاليات خارج Bazel في بروتوكول إنشاء الفعاليات (وهي BuildEvents). لا يسمح هذا الأمر بـ BlazeModule فقط، بل أيضًا خارج إطار Bazel لملاحظة الإصدار. ويمكن الوصول إليها إما كملف يحتوي على رسائل بروتوكول أو يمكن لـ Bazel الاتصال بخادم (يُطلق عليه "خدمة إنشاء الإصدارات") لبث الأحداث.

يتم تنفيذ ذلك في حزمتَي Java build.lib.buildeventservice وbuild.lib.buildeventstream.

مستودعات خارجية

تم تصميم Bazel في الأصل لاستخدامه في أحادي المصدر (شجرة واحدة تحتوي على كل ما يحتاج إلى تشييده)، إلا أن Bazel يعيش في عالم لا يكون فيه صحيحًا على الإطلاق. "المستودعات الخارجية&quot: مجرّد استخدام لعالمي العالم: فهي تمثّل الرمز اللازم للبناء ولكنه ليس في شجرة المصدر الرئيسية.

ملف WORKSPACE

يتم تحديد مجموعة المستودعات الخارجية من خلال تحليل ملف WORKSPACE. على سبيل المثال، إقرار يشبه ما يلي:

    local_repository(name="foo", path="/foo/bar")

تتوفَّر النتائج في المستودع المُسمّى @foo. في حال تعقيد هذا الإجراء، يمكن تحديد قواعد مستودع جديدة في ملفات Starlark، والتي يمكن استخدامها بعد ذلك لتحميل رمز Starlark الجديد الذي يمكن استخدامه لتحديد قواعد المستودع الجديدة وما إلى ذلك.

لمعالجة هذه الحالة، يتم تقسيم تحليل ملف WORKSPACE (في WorkspaceFileFunction) إلى أجزاء وفقًا للتوضيحات الواردة في load(). تتم الإشارة إلى فهرس المجموعة عن طريق WorkspaceFileKey.getIndex() واحتساب WorkspaceFileFunction حتى فهرس X يعني أنه يمكن تقييمه حتى عبارة X X load().

جارٍ استرجاع المستودعات

قبل أن يكون رمز المستودع متوفّرًا لتطبيق Bazel، يجب جلبه. يؤدي هذا إلى إنشاء Bazel دليل ضمن $OUTPUT_BASE/external/<repository name>.

يتم استرجاع المستودع في الخطوات التالية:

  1. يدرك PackageLookupFunction أنه يحتاج إلى مستودع وينشئ RepositoryName بدلاً من SkyKey، ما يستدعي RepositoryLoaderFunction.
  2. يعيد RepositoryLoaderFunction توجيه الطلب إلى RepositoryDelegatorFunction لأسباب غير واضحة (يذكر الرمز أنّه لتجنّب إعادة تنزيل العناصر في حال إعادة تشغيل Skyframe، ولكن ذلك ليس سببًا صعبًا جدًا).
  3. يتعرّف RepositoryDelegatorFunction على قاعدة المستودع التي طُلب منها استرجاعها من خلال تكرارها في أجزاء من ملف WORKSPACE إلى أن يتم العثور على المستودع المطلوب.
  4. يتم العثور على السمة RepositoryFunction المناسبة التي تنفّذ عملية استرجاع المستودع، وهي: تنفيذ تنفيذ Starlark للمستودع أو خريطة تم ترميزها بشكلٍ ثابت للمستودعات التي تم تنفيذها في Java.

وهناك العديد من طبقات التخزين المؤقت بما أن جلب المستودع يمكن أن يكون باهظًا للغاية:

  1. هناك ذاكرة تخزين مؤقت للملفات التي تم تنزيلها والتي يتم استخدامها من خلال المجموع الاختباري الخاص بها RepositoryCache. يتطلب هذا أن يكون المجموع الاختباري متوفرًا في ملف Workspace، ولكن هذا الأمر جيد على "الزخرفة". يتشارك هذا النظام كل مثيل خادم Bazel على محطة العمل نفسها، بغض النظر عن مساحة العمل أو قاعدة الإخراج التي يعمل بها.
  2. تتم كتابة ملف &"محدّد الموقع&quot في كل مستودع ضمن $OUTPUT_BASE/external يحتوي على مجموع اختباري للقاعدة التي تم استخدامها لجلبها. في حال إعادة تشغيل خادم Bazel بدون تغيُّر المجموع الاختباري، لن يتم استرجاعه. تم تنفيذ ذلك في RepositoryDelegatorFunction.DigestWriter .
  3. يحدّد خيار سطر الأوامر --distdir ذاكرة تخزين مؤقت أخرى تُستخدم للبحث عن عناصر ليتم تنزيلها. ويُعدّ هذا مفيدًا في إعدادات المؤسسة حيث يجب ألا يجلب Bazel عناصر عشوائية من الإنترنت. ويتم تنفيذ ذلك من خلال DownloadManager .

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

الأدلة المُدارة

في بعض الأحيان، تحتاج المستودعات الخارجية إلى تعديل الملفات ضمن جذر مساحة العمل (مثل مدير حِزم يحتوي على الحِزم التي تم تنزيلها في دليل فرعي للشجرة المصدر). ويتناقض هذا مع افتراض Bazel إلى أنّه لا يتم تعديل الملفات المصدر إلا من قِبل المستخدم وليس وحده، كما يسمح للحزمة بالرجوع إلى كل دليل ضمن جذر مساحة العمل. يُجري Bazel أمرَين لتنفيذ هذا النوع من المستودعات الخارجية:

  1. يسمح للمستخدم بتحديد أدلة فرعية لمساحة العمل على Bazel غير مسموح له بالوصول إليها. تم إدراج الملفات في ملف باسم .bazelignore وتم تنفيذ الوظيفة في BlacklistedPackagePrefixesFunction.
  2. يتم ترميز عملية الربط من الدليل الفرعي لمساحة العمل إلى المستودع الخارجي الذي يتم التعامل معه من خلال ManagedDirectoriesKnowledge والتعامل مع المراجع FileStateValue بالطريقة نفسها المتّبعة في المستودع الخارجي العادي.

تعيينات المستودع

وقد يحدث أن هناك مستودعات متعددة تريد أن تعتمد على المستودع نفسه، ولكن في إصدارات مختلفة (هذا مثال على مشكلة تبعية الماس والماسورة;). على سبيل المثال، إذا كان هناك برنامجان ثنائيان في مستودعات منفصلة في الإصدار يريدان الاعتماد على Gvava، من المفترض أن يشيرا معًا إلى Gwava بتصنيفات تبدأ في @guava// ويتوقع أن يعنيا إصدارات مختلفة منه.

لذلك، يسمح Bazel بإعادة ربط تصنيفات المستودع الخارجي حتى تتمكّن السلسلة @guava// من الإشارة إلى مستودع Gava واحد (مثل @guava1//) في مستودع أحد البرامج الثنائية ومستودع آخر لجوافا (مثل @guava2//) ومستودع آخر.

بدلاً من ذلك، يمكن استخدام هذا أيضًا للانضمام إلى الماس. إذا كان المستودع يعتمد على @guava1//، بينما يعتمد مستودع آخر على @guava2//، تتيح عملية ربط المستودع إعادة ربط كلا المستودعَين لاستخدام مستودع @guava// أساسي.

يتم تحديد التعيين في ملف WORKSPACE باعتباره سمة repo_mapping لتعريفات المستودع الفردية. بعد ذلك، تظهر في Skyframe كعضو في WorkspaceFileValue، حيث يتم توصيلها بما يلي:

  • Package.Builder.repositoryMapping المُستخدَم لتحويل سمات القواعد التي تحمل قيمة التصنيف في الحِزمة من خلال RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping التي تُستخدم في مرحلة التحليل (لحل مشاكل مثل $(location) التي لا يتم تحليلها في مرحلة التحميل)
  • BzlLoadFunction لحلّ التصنيفات في بياناتload()

وحدات بت JNI

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

يكون رمز C++ موجودًا ضمن src/الرئيسية/الأصلية وصفوف Java بالطرق الأصلية:

  • NativePosixFiles وNativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperations وWindowsFileProcesses
  • com.google.devtools.build.lib.platform

مخرجات وحدة التحكم

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

بعد استدعاء استدعاء إجراء عن بُعد (RPC) مباشرةً من العميل، يتم إنشاء حالتين (RpcOutputStream) لنسختين (بالنسبة إلى stdout وstderr) تعيد توجيه البيانات المطبوعة إلى العميل. ويتم لفّها بعد ذلك في إقران OutErr (زوج (stdout, stderr)). كل ما يجب طباعته على وحدة التحكّم يتم من خلال مصادر البيانات هذه. بعد ذلك، يتم تسليم ساحات المشاركات هذه إلى BlazeCommandDispatcher.execExclusively().

وتتم طباعة النتائج تلقائيًا باستخدام تسلسلات إلغاء ANSI. عندما لا تكون هذه العناصر مرغوبة (--color=no)، تتم إزالتها بواسطة AnsiStrippingOutputStream. بالإضافة إلى ذلك، تتم إعادة توجيه System.out وSystem.err إلى ساحات المشاركات هذه. تهدف هذه العملية إلى طباعة معلومات تصحيح الأخطاء باستخدام System.err.println() وتبقى في النهاية مخرجات الوحدة الطرفية من العميل (التي تختلف عن الخادم). يجب الانتباه إلى أنّه إذا أدت عملية إلى إخراج ثنائي (مثل bazel query --output=proto)، لن يتم تنفيذ أي عملية نقل بيانات.

يتم التعبير عن الرسائل القصيرة (الأخطاء والتحذيرات وما شابه) من خلال واجهة EventHandler. وتجدر الإشارة إلى أنّ هذه المعلومات تختلف عن ما ينشره المستخدم على EventBus (الأمر مربك). تتضمّن كل Event EventKind (خطأ، وتحذير، ومعلومات، وبعض الأخطاء الأخرى) وقد تتضمّن Location (المكان الوارد في رمز المصدر الذي تسبب في وقوع الحدث).

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

وتسمح بعض EventHandler أيضًا بنشر الأحداث التي تجد طريقها في نهاية المطاف إلى الحافلة التي تنظّم الفعالية (العادية Event_لا__تظهر هناك). إليك عمليات تنفيذ ExtendedEventHandler والاستخدام الرئيسي لها هو إعادة تشغيل أحداث EventBus المخزّنة مؤقتًا. تعمل جميع أحداث EventBus هذه على تنفيذ Postable، ولكن ليس كل ما يتم نشره على EventBus يُنفّذ هذه الواجهة بالضرورة، بل يجب فقط تنفيذ تلك الواجهة المخزَّنة مؤقتًا في ExtendedEventHandler (سيكون ذلك مفيدًا ومعظم المهام، ولا يتم تنفيذها).

تنبعث الطاقة الناتجة عن المحطة الطرفية في الغالب من خلال UiEventHandler، ما يؤدي إلى مسؤولية جميع عمليات التنسيق المميزة التي يتم تحقيقها في الوقت الحالي والتقدّم في التقارير على Bazel. فِيهْ مُنَبِّهِينْ:

  • حافلة للفعاليات
  • تم بث ساحة الأحداث عبر المراسل الصحفي

إنّ الاتصال المباشر لآلة تنفيذ الأوامر (على سبيل المثال، أي بقية من Bazel) مخصّص لبث RPC إلى العميل هو Reporter.getOutErr()، ما يسمح بالوصول المباشر إلى مصادر البث هذه. ولا يتم استخدامها إلا عندما يحتاج الأمر إلى تفريغ كميات كبيرة من البيانات الثنائية المحتملة (مثل bazel query).

بازيلينغ

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

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

بدأ محلّل المحتوى وتوقّف تشغيله في BlazeRuntime.initProfiler() وBlazeRuntime.afterCommand() على التوالي، ويحاول البقاء على قيد الحياة لأطول فترة ممكنة كي نتمكّن من إنشاء محتوى مميّز. لإضافة شيء إلى الملف الشخصي، اتصل بـ Profiler.instance().profile(). وتعرِض Closeable، حيث يمثّل الإغلاق نهاية المهمة. وتُستخدم على أفضل نحو مع كشوفات تجربة المراجع.

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

تجربة Bazel

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

يتوفّر نوعان من اختبارات الدمج:

  1. تم تنفيذ الكلمات الرئيسية باستخدام إطار عمل Bash مُتقن للغاية ضمن src/test/shell.
  2. ويتم تنفيذ الإجراءات في Java. يتم تنفيذ هذه الفئات باعتبارها فئات فرعية من BuildIntegrationTestCase

BuildIntegrationTestCase هو إطار العمل المُفضَّل لاختبار التكامل، فهو مجهّز بشكلٍ جيد لمعظم سيناريوهات الاختبار. ولأنّه إطار عمل Java، يوفّر إمكانية تصحيح الأخطاء والدمج السلس مع العديد من أدوات التطوير الشائعة. هناك أمثلة متعدّدة على BuildIntegrationTestCase صف في مستودع Bazel.

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