معمارية تطبيقات المصادر المفتوحة › مشروع Yocto 50 / 51 السابق التالي

مشروع Yocto

بنية تطبيقات المصادر المفتوحة (المجلد 2)Yocto Project

بنية تطبيقات المصادر المفتوحة (المجلد 2) Yocto Project

Elizabeth Flanagan

إذا استمتعت بهذه الكتب، فقد تستمتع أيضًا بـالتصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.

مشروع Yocto (Yocto Project) هو مشروع مفتوح المصدر يوفّر نقطة انطلاق مشتركة لمطوّري أنظمة لينكس المدمجة (embedded Linux)، كي ينشئوا توزيعات (distributions) مُخصَّصة للمنتجات المدمجة في بيئة محايدة تجاه العتاد (hardware-agnostic). وبدعم من مؤسسة لينكس (Linux Foundation)، فإن مشروع Yocto هو أكثر من مجرد نظام بناء (build system). فهو يوفّر أدوات وعمليات وقوالب وأساليب يستطيع بها المطوّرون إنشاء منتجات سوق الأنظمة المدمجة ونشرها بسرعة. ومن مكوّناته الأساسية نظام البناء Poky. ولأن Poky نظام كبير ومعقّد، فسنركّز في هذا الفصل على أحد مكوّناته الأساسية، وهو BitBake. إن BitBake أداة بناء (build tool) مستوحاة من Gentoo-Portage، يستخدمها كلٌّ من مجتمعي Yocto وOpenEmbedded للاستفادة من بيانات الوصف (metadata) في إنشاء صور لينكس من المصدر.

في عام 2001، كشفت شركة Sharp عن الجهاز المحمول SL-5000 المسمّى Zaurus، الذي كان يشغّل توزيع لينكس مدمجًا مسمّى Lineo. وقليلًا بعد إطلاق Zaurus، أسّس Chris Larson مشروع OpenZaurus، وهو توزيع لينكس بديل لـ SharpROM، مبني على نظام بناء اسمه buildroot. ومع تأسيس المشروع، بدأ الناس في المساهمة بمزيدٍ كبير من حزم البرمجيات، وكذلك بأهدافٍ لأجهزة أخرى، ولم يمض وقت طويل قبل أن يبدأ نظام بناء OpenZaurus في إظهار هشاشته. وفي يناير 2003، بدأ المجتمع يناقش نظام بناء جديد يضمّ نموذج الاستخدام المجتمعي لنظام بناء عام لتوزيعات لينكس المدمجة، وهو ما سيصبح في النهاية OpenEmbedded. وانطلق Chris Larson وMichael Lauer وHolger Schurig في العمل على OpenEmbedded عبر نقل مئات حزم OpenZaurus إلى نظام البناء الجديد.

ينبع مشروع Yocto من هذا العمل. وفي قلب المشروع يوجد نظام البناء Poky، الذي أنشأه Richard Purdie. فقد بدأ كفرع مستقر (stabilized branch) من OpenEmbedded يستخدم مجموعة جوهرية من آلاف وصفات (recipes) OpenEmbedded، عبر مجموعة محدودة من البنى (architectures). ومع مرور الوقت، اندمج ببطء ليصبح أكثر من مجرد نظام بناء مدمج، بل منصّة كاملة لتطوير البرمجيات، مع إضافة خاصة (plugin) لـ Eclipse، وبديلٍ عن fakeroot، وصورًا قائمة على QEMU. وفي أواخر نوفمبر 2010، أعلنت مؤسسة لينكس أن هذا العمل بأكمله سيستمر تحت اسم مشروع Yocto بوصفه مشروعًا مدعومًا من مؤسسة لينكس. وعندئذٍ اتُّفق على أن ينسّق مشروع Yocto وOpenEmbedded عملهما حول مجموعة أساسية من بيانات وصف الحزم اسمها OE-Core، وأن يجمعا أفضل ما في Poky وOpenEmbedded مع زيادة الاعتماد على تقنيات الطبقات (layering) لإضافة المكوّنات الأخرى.

23.1. مقدمة إلى نظام البناء Poky

نظام البناء Poky هو قلب مشروع Yocto. وفي الإعداد الافتراضي لـ Poky، يمكنه توفير نواة صورة (image footprint) مبدئية تمتد من صورة دنيا يمكن الوصول إليها عبر الصدفة (shell) وصولًا إلى صورة متوافقة مع Linux Standard Base (LSB) ذات واجهة مستخدم مرجعية قائمة على GNOME Mobile and Embedded (GMAE) تسمّى Sato. ومن هذه الأنواع الأساسية من الصور، يمكن إضافة طبقات من بيانات الوصف (metadata layers) لتوسيع الوظائف؛ إذ يمكن للطبقة توفير حزمة برمجية إضافية لنوع صورة معيّن، أو إضافة حزمة دعم لوحة (board support package, BSP) لعتادٍ إضافي، أو حتى أن تمثّل نوع صورة جديد. وسنستخدم إصدار 1.1 من Poky، المسمّى «edison»، لنُظهر كيف يستخدم BitBake هذه الوصفات وملفات الإعداد لتوليد صورة مدمجة.

على مستوى عالٍ جدًا، تبدأ عملية البناء بإعداد بيئة الصدفة (shell environment) لجلسة البناء. ويُنجَز ذلك عبر تحميل ملف oe-init-build-env الموجود في جذر شجرة مصدر Poky. وهذا يُعِدّ بيئة الصدفة، وينشئ مجموعة أوّلية قابلة للتخصيص من ملفات الإعداد، ويغلّف بيئة تشغيل BitBake بسكربت صدفة تستخدمه Poky لتحديد ما إذا كانت الحد الأدنى من متطلبات النظام قد تحقّقت.

على سبيل المثال، أحد ما سيبحث عنه هو وجود Pseudo، وهو بديلٌ عن fakeroot تبرّع به لمشروع Yocto نظام Wind River. وفي هذه المرحلة، ينبغي أن يكون bitbake core-image-minimal، على سبيل المثال، قادرًا على إنشاء بيئة تصريف متقاطع (cross-compilation environment) عاملة بكامل طاقتها، ثم إنشاء صورة لينكس اعتمادًا على تعريف الصورة الخاص بـ core-image-minimal من المصدر، كما هو معرّف في طبقة بيانات الوصف الخاصة بمشروع Yocto.

الشكل 23.1: نظرة عامة على مستوى عالٍ لتنفيذ مهام Poky

أثناء إنشاء صورتنا، سيقوم BitBake بتحليل إعداداته، وإدراج أي طبقات أو أصناف (classes) أو مهام أو وصفات إضافية مُعرَّفة، ثم يبدأ بإنشاء سلسلة تبعيات موزونة (weighted dependency chain). وتوفّر هذه العملية خريطةً مرتّبة وموزونة لأولويات المهام. ثم يستخدم BitBake هذه الخريطة لتحديد الحزم التي يجب بناؤها وبأي ترتيب، بحيث تُلبّى تبعيات التصريف بكفاءة قصوى. وتُوزن المهام التي تحتاجها أكبرُ عدد من المهام الأخرى وزنًا أعلى، وبالتالي تُنفَّذ في وقتٍ أسبق خلال عملية البناء. ثم يُنشأ طابور تنفيذ المهام (task execution queue) الخاص ببنائنا. كما يخزّن BitBake ملخّصات بيانات الوصف المُحلَّلة، وإذا تبيّن له في التشغيلات اللاحقة أن بيانات الوصف قد تغيّرت، فإنه يستطيع إعادة تحليل ما تغيّر فقط. إن مجدول BitBake (scheduler) ومحلّله (parser) من أكثر التصاميم المعمارية إثارة للاهتمام في BitBake، وستُناقَش لاحقًا بعض القرارات المحيطة بهما وبتنفيذهما على يد المساهمين في BitBake.

ثم يمرّ BitBake عبر طابور المهام الموزون الخاص به، مُنشئًا خيوطًا (حتى العدد المعرّف في BB_NUMBER_THREADS داخل conf/local.conf) تبدأ بتنفيذ تلك المهام بالترتيب المحدد مسبقًا. وقد تُعدَّل المهام المنفَّذة أثناء بناء حزمة ما، أو يُضاف في بدايتها أو في نهايتها شيء ما، عبر وصفتها. ويبدأ الترتيب الأساسي الافتراضي لمهام تنفيذ الحزمة بجلب مصدر الحزمة وفكّ حزمته، ثم بإعداد المصدر المفكوك وتصريفه تصريفًا متقاطعًا. ثم تُقسَّم الشيفرة المصرَّفة إلى حزم، وتُجرى حسابات مختلفة على ناتج التصريف، مثل إنشاء معلومات حزم التصحيح (debug package information). ثم تُغلَّف الحزم الناتجة في صيغة حزم مدعومة؛ منها RPM وipk وdeb، ثم يستخدم BitBake هذه الحزم لبناء نظام الملفات الجذري (root file system).

مفاهيم نظام البناء Poky

من أقوى خصائص نظام البناء Poky أن كل جوانب البناء يتحكم فيها بيانات الوصف (metadata). ويمكن تجميع بيانات الوصف إجمالًا في ملفَي إعداد أو في وصفات حزم. والوصفة (recipe) مجموعة من بيانات وصف غير قابلة للتنفيذ، يستخدمها BitBake لضبط المتغيرات أو لتعريف مهام إضافية وقت البناء. وتحتوي الوصفة على حقول مثل وصف الوصفة وإصدارها ورخصة الحزمة ومستودع المصدر الأصلي (upstream). وقد تشير أيضًا إلى أن عملية البناء تستخدم autotools أو make أو distutils أو أي عملية بناء أخرى، وعندئذٍ يمكن تعريف الوظائف الأساسية عبر الأصناف (classes) التي ترثها من تعريفات الأصناف الموجودة في ./meta/classes ضمن طبقة OE-Core. ويمكن أيضًا تعريف مهام إضافية، وكذلك متطلبات سابقة للمهام (task prerequisites). كما يدعم BitBake كلَّي _prepend و_append بوصفهما وسيلةً لتوسيع وظائف المهمة عبر حقن الشيفرة، المُشار إليها باستخدام اللاحقة prepend أو append، في بداية المهمة أو في نهايتها.

يمكن تقسيم ملفات الإعداد إلى نوعين: منها ما يُعِدّ BitBake وعموم جلسة البناء، ومنها ما يُعِدّ الطبقات المختلفة التي تستخدمها Poky لإنشاء إعدادات مختلفة لصورة هدف (target). والطبقة (layer) هي أي تجميع من بيانات الوصف يوفّر نوعًا ما من الوظائف الإضافية. وهذه قد تكون حزم دعم لوحات (BSP) لأجهزة جديدة، أو أنواع صور إضافية، أو برمجيات إضافية خارج الطبقات الأساسية. وفي الواقع، فإن بيانات الوصف الأساسية لمشروع Yocto، وهي meta-yocto، هي نفسها طبقة تُطبَّق فوق طبقة بيانات الوصف الخاصة بـ OE-Core، وهي meta، وتضيف برمجيات وأنواع صور إضافية إلى طبقة OE-Core.

ومثال على كيفية استخدام الطبقات هو إنشاء جهاز NAS للوحة Intel n660 (Crownbay)، باستخدام x32، وهي واجهة تطبيقات ثنائية (ABI) أصلية جديدة بقياس 32 بت، مع طبقة برمجية مخصّصة تضيف واجهة مستخدم.

بالنظر إلى المهمة التي بين أيدينا، يمكننا تفصيل هذه الوظيفة إلى طبقات. وعند أدنى مستوى سنستخدم طبقة BSP خاصة بـ Crownbay تتيح وظائف عتاد خاصة بـ Crownbay، مثل تعريفات عتاد الفيديو (video drivers). ولأننا نريد x32، سنستخدم طبقة meta-x32 التجريبية. وستُضاف وظيفة NAS فوق ذلك بإضافة طبقة NAS المثال الخاصة بمشروع Yocto، وهي meta-baryon. وأخيرًا، سنستخدم طبقة خيالية اسمها meta-myproject، لتوفّر البرمجيات والإعدادات اللازمة لإنشاء واجهة مستخدم رسومية لإعداد الجهاز.

أثناء إعداد بيئة BitBake، تُولَّد بعض ملفات الإعداد الأولية عبر تحميل oe-build-init-env. وتتيح لنا ملفات الإعداد هذه قدرًا كبيرًا من التحكم في كيفية ما يُولّده Poky وفيماذا يُولّده. وأول ملفات الإعداد هذه هو bblayers.conf. وهذا هو الملف الذي سنستخدمه لإضافة طبقات إضافية من أجل بناء مشروعنا المثال.

إليك مثالًا على ملف bblayers.conf:

# LAYER_CONF_VERSION is increased each time build/conf/bblayers.conf
# changes incompatibly
LCONF_VERSION = "4"
BBFILES ?= ""
BBLAYERS = " \
/home/eflanagan/poky/meta \
/home/eflanagan/poky/meta-yocto \
/home/eflanagan/poky/meta-intel/crownbay \
/home/eflanagan/poky/meta-x32 \
/home/eflanagan/poky/meta-baryon\
/home/eflanagan/poky/meta-myproject \
"

ملف طبقات BitBake، bblayers، يعرّف المتغير BBLAYERS الذي يستخدمه BitBake للبحث عن طبقات BitBake. ولفهم هذا الأمر فهمًا كاملًا، ينبغي أيضًا أن ننظر في كيفية بناء طبقاتنا فعليًا. وباستخدام meta-baryon (git://git.yoctoproject.org/meta-baryon) طبقةً مثالًا، نريد فحص ملف إعداد الطبقة. وهذا الملف، conf/layer.conf، هو ما يحلّله BitBake بعد تحليله الأولي لـ bblayers.conf. ومن هنا يضيف وصفات وأصنافًا وإعدادات إضافية إلى البناء.

الشكل 23.2: مثال على تنقيح BitBake بالطبقات (layering)

إليك layer.conf الخاص بـ meta-baryon:

# Layer configuration for meta-baryon layer
# Copyright 2011 Intel Corporation
# We have a conf directory, prepend to BBPATH to prefer our versions
BBPATH := "${LAYERDIR}:${BBPATH}"

# We have recipes-* directories, add to BBFILES
BBFILES := "${BBFILES} ${LAYERDIR}/recipes-*/*/*.bb ${LAYERDIR}/recipes-*/*/*.bbappend"

BBFILE_COLLECTIONS += "meta-baryon"
BBFILE_PATTERN_meta-baryon := "^${LAYERDIR}/"
BBFILE_PRIORITY_meta-baryon = "7"

تساعد جميع ملفات إعداد BitBake على توليد مخزن بيانات BitBake (datastore) الذي يُستخدم أثناء إنشاء طابور تنفيذ المهام. وفي بداية البناء، تُشغَّل صنف BBCooker الخاص بـ BitBake. ويدير الـ cooker تنفيذ مهام البناء عبر خبز الوصفات. ومن أول ما يفعله الـ cooker هو محاولة تحميل بيانات الإعداد وتحليلها. وتذكّر، مع ذلك، أن BitBake يبحث عن نوعين من بيانات الإعداد. ولإخبار نظام البناء بالمكان الذي ينبغي أن يجد فيه بيانات الإعداد هذه (ومن ثمّ أين يجد بيانات وصف الوصفات)، تُستدعى طريقة parseConfigurationFiles الخاصة بالـ cooker. وباستثناءات قليلة، فإن أول ملف إعداد يبحث فيه الـ cooker هو bblayers.conf. وبعد تحليل هذا الملف، يحلّل BitBake بعد ذلك ملف layer.conf الخاص بكل طبقة.

وبعد تحليل ملفات إعداد الطبقات، تحلّل parseConfigurationFiles بدورها الملف bitbake.conf، الذي غرضه الأساسي هو ضبط متغيرات وقت البناء العامة، مثل تسمية بنية المجلدات لمجلدات rootfs المختلفة وLDFLAGS الأوّلي المستخدَم أثناء التصريف. ولن يلمس معظم المستخدمين النهائيين هذا الملف، لأن كل ما يحتاج إلى تغييره هنا سيكون في سياق وصفة، لا على مستوى البناء كله، أو يمكن تجاوزه في ملف إعداد مثل local.conf.

أثناء تحليل هذا الملف، يدرج BitBake أيضًا ملفات الإعداد الكامنة لكل طبقة في BBLAYERS، ويضيف المتغيرات الموجودة في تلك الملفات إلى مخزن بياناته.

إليك جزءًا من bitbake.conf يُظهر ملفات الإعداد المُدرَجة:

include conf/site.conf
include conf/auto.conf
include conf/local.conf
include conf/build/${BUILD_SYS}.conf
include conf/target/${TARGET_SYS}.conf
include conf/machine/${MACHINE}.conf

مثال وصفة BitBake لـ grep

DESCRIPTION = "GNU grep utility"
HOMEPAGE = "http://savannah.gnu.org/projects/grep/"
BUGTRACKER = "http://savannah.gnu.org/bugs/?group=grep"

SECTION = "console/utils"

LICENSE = "GPLv3"
LIC_FILES_CHKSUM = "file://COPYING;md5=8006d9c814277c1bfc4ca22af94b59ee"

PR = "r0"

SRC_URI = "${GNU_MIRROR}/grep/grep-${PV}.tar.gz"
SRC_URI[md5sum] = "03e3451a38b0d615cb113cbeaf252dc0"
SRC_URI[sha256sum]="e9118eac72ecc71191725a7566361ab7643edfd3364869a47b78dc934a357970"

inherit autotools gettext

EXTRA_OECONF = "--disable-perl-regexp"

do_configure_prepend
() {
rm
-f ${S}/m4/init.m4
}

do_install () {
  autotools_do_install
  install -d ${D}${base_bindir}
  mv ${D}${bindir}/grep ${D}${base_bindir}/grep.${PN}
  mv ${D}${bindir}/egrep ${D}${base_bindir}/egrep.${PN}
  mv ${D}${bindir}/fgrep ${D}${base_bindir}/fgrep.${PN}
}

pkg_postinst_${PN}() {
  update-alternatives --install ${base_bindir}/grep grep grep.${PN} 100
  update-alternatives --install ${base_bindir}/egrep egrep egrep.${PN} 100
  update-alternatives --install ${base_bindir}/fgrep fgrep fgrep.${PN} 100
}

pkg_prerm_${PN}() {
  update-alternatives --remove grep grep.${PN}
  update-alternatives --remove egrep egrep.${PN}
  update-alternatives --remove fgrep fgrep.${PN}
}

23.2. بنية BitBake

قبل أن نتعمّق في بعض جوانب التصميم المعماري الحالي لـ BitBake، من المفيد أن نفهم كيف كان BitBake يعمل في السابق. ولأن نقدّر بقدر ما قطعه BitBake من أشواط، سننظر في الإصدار الأول، BitBake 1.0. وفي ذلك الإصدار الأول من BitBake، كانت سلسلة تبعيات البناء تُحدَّد استنادًا إلى تبعيات الوصفات. وإذا فشل أمرٌ ما أثناء بناء صورة، كان BitBake ينتقل إلى المهمة التالية ويحاول بناءها مرة أخرى لاحقًا. وهذا يعني، بطبيعة الحال، أن عمليات البناء كانت تستغرق وقتًا طويلًا جدًا. ومن الأمور التي كان BitBake يفعلها أيضًا أنه يحتفظ بكل متغير استخدمته الوصفة في قاموس واحد ضخم. ونظرًا إلى عدد الوصفات وإلى عدد المتغيرات والمهام اللازمة لإتمام عملية بناء، كان BitBake 1.0 جشعًا في استهلاك الذاكرة. وفي زمن كانت الذاكرة فيه باهظة والأنظمة تملك ذاكرة أقل بكثير، كانت عمليات البناء أمورًا مؤلمة. ولم يكن من غير المألوف أن ينفد نظام من الذاكرة (وهو يكتب إلى ذاكرة التبادل (swap)) بينما يجهد نفسه في بناء طويل الأمد. وفي صورته الأولى، وإن كان يؤدّي وظيفته (أحيانًا)، إلا أنه كان يؤدّيها ببطء أثناء استهلاكه لكم هائل من الموارد. والأسوأ من ذلك، وبما أن BitBake 1.0 لم يكن لديه مفهوم ذاكرة تخزين مؤقت دائمة للبيانات (data persistence cache) أو حالة مشتركة (shared state)، فلم يكن لديه القدرة كذلك على تنفيذ عمليات بناء تدريجية (incremental builds). وإذا فشل بناء، كان على المرء أن يعيده من الصفر.

ويكشف الفارق السريع (diff) بين إصدار BitBake الحالي المستخدم في Poky «edison» 1.13.3 وإصدار 1.0 تنفيذ بنية العميل/الخادم (client-server) في BitBake، وذاكرة التخزين المؤقت الدائمة للبيانات، ومخزن بياناته، وتحسين الكتابة عند النسخ (copy-on-write) لمخزن بياناته، وتنفيذ الحالة المشتركة، وتحسينات جذرية في كيفية تحديده لسلاسل تبعيات المهام والحزم. وقد جعل هذا التطوّر BitBake أكثر موثوقية وكفاءة وديناميكية. وقُدر كبير من هذه الوظيفة نشأ من الحاجة إلى عمليات بناء أسرع وأكثر موثوقية، تستخدم موارد أقل. وثلاثة تحسينات في BitBake سنفحصها هي: تنفيذ بنية عميل/خادم، والتحسينات المتعلقة بتخزين بيانات BitBake، والعمل الذي أُنجز على كيفية تحديد BitBake لسلسلة تبعيات البناء والمهام.

الاتصال بين العمليات في BitBake (IPC)

بما أننا نعرف الآن الكثير عن كيفية استخدام نظام البناء Poky للإعدادات والوصفات والطبقات لإنشاء صور مدمجة، فنحن مستعدون للنظر تحت غطاء BitBake وفحص كيفية تجميع كل ذلك معًا. وبالبدء من الملف التنفيذي الأساسي لـ BitBake، bitbake/bin/bake، يمكننا أن نبدأ في رؤية العملية التي يتبعها BitBake وهو يبدأ بإعداد البنية التحتية اللازمة لبدء البناء. وأول ما يستحق الاهتمام هو الاتصال بين العمليات (Interprocess Communications, IPC) في BitBake. وفي البداية، لم يكن لدى BitBake أي مفهوم للعميل/الخادم. وقد أُدرجت هذه الوظيفة في تصميم BitBake على مدى فترة من الزمن، للسماح لـ BitBake بتشغيل عمليات متعددة أثناء البناء، إذ كان أحادي الخيط (single-threaded) في بادئ الأمر، ولتوفير تجارب مستخدم مختلفة.

الشكل 23.3: نظرة عامة على الاتصال بين العمليات في BitBake

تبدأ جميع عمليات بناء Poky بتشغيل نسخة واجهة مستخدم. وتوفّر واجهة المستخدم آليةً لتسجيل مخرجات البناء وحالة البناء وتقدّمه، وكذلك لاستقبال الأحداث من مهام البناء عبر وحدة أحداث BitBake. وواجهة المستخدم الافتراضية المستخدَمة هي knotty، وهي واجهة سطر الأوامر في BitBake. وتُسمّى knotty، أي «(لا) tty»، لأنها تتعامل مع كل من ttys وnon-ttys، وهي إحدى بضع واجهات مدعومة. ومن بين واجهات المستخدم الإضافية هذه واجهة Hob. وHob هي الواجهة الرسومية لـ BitBake، أي نوعٌ من «قائد BitBake». وإلى جانب الوظائف المعتادة التي تراها في واجهة knotty، تضيف Hob (وكتبها Joshua Lock) القدرةَ على تعديل ملفات الإعداد، وإضافة طبقات وحزم إضافية، وتخصيص البناء تخصيصًا كاملًا.

وتتمتع واجهات مستخدم BitBake بالقدرة على إرسال الأوامر إلى الوحدة التالية التي يُشغّلها الملف التنفيذي لـ BitBake، وهي خادم BitBake. وكما في واجهة المستخدم، يدعم BitBake أيضًا أنواعًا مختلفة متعددة من الخوادم، مثل XMLRPC. والخادم الافتراضي الذي يستخدمه معظم المستخدمين عند تنفيذ BitBake من واجهة knotty هو خادم العمليات (process server) في BitBake. وبعد تشغيل الخادم، يُشغّل الملف التنفيذي لـ BitBake الـ cooker.

الـ cooker جزءٌ أساسي من BitBake، وهو الموضع الذي تُستدعى منه معظم الأشياء اللافتة للنظر والتي تحدث أثناء بناء Poky. فالـ cooker هو ما يدير تحليل بيانات الوصف، ويبادر بتوليد شجرتي التبعيات والمهام، ويدير البناء. ومن وظائف بنية خادم BitBake السماح بطرق متعددة لتمرير واجهة أوامر الأوامر (command API)، بشكل غير مباشر، إلى واجهة المستخدم. ووحدة command هي العامل في BitBake؛ فهي تشغّل أوامر البناء وتستدعي أحداثًا تُمرَّر إلى واجهة المستخدم عبر معالج أحداث BitBake. وبعد تشغيل الـ cooker من الملف التنفيذي لـ BitBake، يهيّئ مخزن بيانات BitBake ثم يبدأ في تحليل جميع ملفات إعداد Poky. ثم ينشئ كائن runqueue، ويطلق البناء.

تخزين بيانات BitBake عبر النسخ عند الكتابة (Copy-on-Write) باستخدام DataSmart

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

كان هذا أحد أكبر نقاط الألم (pain points) في BitBake المبكر. وكانت هناك مشكلتان رئيسيتان يلزم حلهما للمساعدة على زيادة الأداء: إحداهما الحساب المسبق لسلسلة تبعيات البناء (precomputation)، والأخرى تقليل حجم البيانات المخزَّنة في الذاكرة. وأكثير من البيانات المخزَّنة لوصفة لا تتغيّر من وصفة إلى أخرى؛ فمثلًا، مع TMPDIR وBB_NUMBER_THREADS وغيرهما من متغيرات BitBake العامة، كان من غير الكفء الاحتفاظ في الذاكرة بنسخة من بيئة البيانات كاملة لكل وصفة. وكان الحلّ هو قاموس Tom Ansell القائم على النسخ عند الكتابة (copy-on-write)، والذي «يستغل الأصناف (classes) ليكون لطيفًا وسريعًا». إن وحدة COW في BitBake هي اختراق جريء بشكل استثنائي وذكي في آن. وتشغيل python BitBake/lib/bb/COW.py وفحص الوحدة سيعطيك فكرةً عن كيفية عمل هذا التنفيذ القائم على النسخ عند الكتابة، وعن كيفية استخدام BitBake له لتخزين البيانات بكفاءة

وتخزّن وحدة DataSmart، التي تستخدم قاموس COW، بيانات الإعداد الأولي لـ Poky، وبيانات ملفات .conf وملفات .bbclass، في كائن بيانات على هيئة dict. ويمكن لكل واحد من هذه الكائنات أن يحتوي كائن بيانات آخر لا يمثّل سوى فرق (diff) البيانات. فإذا غيّرت وصفة أمرًا ما من إعداد البيانات الأوّلي، فإن BitBake لا ينسخ الإعداد بأكمله ليحلّده، بل يخزّن فرق كائن بيانات الأصل في الطبقة التالية في مكدس COW. وعندما تتم محاولة الوصول إلى متغير، تستخدم وحدة البيانات DataSmart للنظر في أعلى المكدس. وإذا لم يُعثر على المتغير، تستدعي وحدة البيانات حينئذٍ الانتقال إلى مستوى أدنى من المكدس حتى تجده أو ترمي خطأً.

ومن الأمور الأخرى اللافتة في وحدة DataSmart أيضًا موضوع توسيع المتغيرات (variable expansion). ولأن متغيرات BitBake يمكن أن تحتوي على شيفرة Python قابلة للتنفيذ، فإن أحد ما يلزم فعله هو تمرير المتغير عبر bb.codeparser الخاص بـ BitBake، للتأكد من أنه Python صالح وأنه لا يحتوي على مراجع دائرية. ومثال على متغير يحتوي على شيفرة Python هو هذا المثال المأخوذ من ./meta/conf/distro/include/tclibc-eglibc.inc:

LIBCEXTENSION = "${@['', '-gnu'][(d.getVar('ABIEXTENSION', True) or '') != '']}"

يُدرَج هذا المتغير من أحد ملفات إعداد OE-Core، وهو ./meta/conf/distro/include/defaultsetup.conf، ويُستخدم لتوفير مجموعة من الخيارات الافتراضية عبر إعدادات التوزيعات (distro) المختلفة التي قد نرغب في وضعها فوق Poky أو OpenEmbedded. وهذا الملف يستورد بعض المتغيرات الخاصة بـ eglibc التي تُضبط بحسب قيمة متغير BitBake آخر، وهو ABIEXTENSION. وأثناء إنشاء مخزن البيانات، يلزم تحليل شيفرة Python الموجودة داخل هذا المتغير والتحقق منها، للتأكد من أن المهام التي تستخدم هذا المتغير لن تفشل.

مجدول BitBake (Scheduler)

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

ولأنه لم تُنشأ أي سلسلة تبعيات محسوبة مسبقًا، كان ترتيب تنفيذ المهام يُحدَّد أثناء عملية البناء. وقد حدّ هذا من BitBake إلى أن يكون أحادي الخيط. ولإعطاء فكرة عن مدى ألام عمليات بناء BitBake أحادية الخيط، فإن أصغر صورة، «core-image-minimal»، على جهاز مطوّر نموذجي في عام 2011 (Intel Core i7 مع 16 غيغابايت من ذاكرة DDR3) تستغرق نحو ثلاث أو أربع ساعات لبناء سلسلة أدوات تصريف متقاطع كاملة، واستخدامها لإنتاج حزم تُستخدم بدورها لإنشاء صورة.وللمقارنة، استغرق بناء على الجهاز نفسه مع ضبط BB_NUMBER_THREADS على 14 وPARALLEL_MAKE على «-j 12» نحو 30 إلى 40 دقيقة.ومن المتخيَّل، فكان تشغيل BitBake أحادي الخيط، دون ترتيب محسوب مسبقًا لتنفيذ المهام، على عتادٍ أبطأ وأقلّ ذاكرة، مع إهدار جزء كبير من الذاكرة في نسخٍ مكرّرة من مخزن البيانات بأكمله، يستغرق وقتًا أطول بكثير.

التبعيات

عندما نتحدث عن تبعيات البناء، علينا التمييز بين الأنواع المختلفة. فتبعية البناء، أو DEPENDS، هي ما نتطلبه كمتطلبٍ سابق كي يتمكّن Poky من بناء الحزمة المطلوبة، أما تبعية وقت التشغيل، RDEPENDS، فتتطلب أن تحتوي الصورة التي ستُثبَّت فيها الحزمة على الحزمة المذكورة في RDEPENDS. خذ، على سبيل المثال، الحزمة task-core-boot. فإذا نظرنا إلى وصفت لها في

meta/recipes-core/tasks/task-core-boot.bb

فسنرى متغيّرَين من متغيرات BitBake مضبوطين: RDEPENDS وDEPENDS. ويستخدم BitBake هذين الحقلين أثناء إنشاء سلسلة تبعياته.

إليك جزءًا من task-core-boot.bb يُظهر DEPENDS وRDEPENDS:

DEPENDS = "virtual/kernel"
 ...

RDEPENDS_task-core-boot = "\
base-files \
base-passwd \
busybox \
initscripts \
...

الحزم ليست الشيء الوحيد في BitBake الذي له تبعيات. فالمهام أيضًا لها تبعياتها الخاصة. وداخل نطاق طابور التنفيذ (runqueue) الخاص بـ BitBake، نُميّز أربعة أنواع: معتمدة داخليًا، ومعتمدة على DEPENDS، ومعتمدة على RDEPENDS، ومعتمدة على مهام أخرى (inter-task).

تُعرَّف المهام المعتمدة داخليًا داخل الوصفة، وتضيف مهمةً قبل و/أو بعد مهمة أخرى. فمثلًا، في وصفة، يمكننا إضافة مهمة اسمها do_deploy بإضافة السطر addtask deploy before do_build after do_compile. وسيضيف هذا تبعيةً لتشغيل المهمة do_deploy قبل بدء do_build، لكن بعد اكتمال do_compile. أما المهام المعتمدة على DEPENDS وRDEPENDS فهي مهام تُنفَّذ بعد مهمةٍ محدَّدة. فمثلًا، إذا أردنا تشغيل do_deploy الخاص بحزمة بعد do_install الخاص بـ DEPENDS أو بـ RDEPENDS الخاص بها، فإن وصفتنا ستتضمن do_deploy[deptask] = 'do_install' أو do_deploy[rdeptask] = 'do_install'. أما تبعيات المهام الأخرى (inter-task)، فإذا أردنا أن تعتمد مهمةٌ ما على مهمة في حزمة أخرى، فإننا نضيف، باستخدام المثال أعلاه الخاص بـ do_deploy، ما يلي: do_deploy[depends] = ":do_install".

طابور التنفيذ (RunQueue)

نظرًا إلى أن بناء الصورة قد يضمّ مئات الوصفات، لكل منها حزمٌ ومهام متعددة، ولكل منها تبعيتها، فإن BitBake مطالب الآن بترتيب كل ذلك في صيغة يمكنه استخدامها كترتيب تنفيذ. وبعد أن يحصل الـ cooker على القائمة الكاملة بالحزم التي يلزم بناؤها من تهيئة كائن bb.data، سيبدأ بإنشاء خريطة مهام موزونة من هذه البيانات، لإنتاج قائمة مرتّبة بالمهام التي يحتاج إلى تشغيلها، وتُسمّى runqueue. وبعد إنشاء runqueue، يستطيع BitBake بدء تنفيذه حسب ترتيب الأولوية، مع إسناد كل جزءٍ منه إلى خيطٍ مختلف.

داخل وحدة provider، يبحث BitBake أولًا عمّا إذا كان هناك PREFERRED_PROVIDER لحزمةٍ أو صورة ما. ولأن أكثر من وصفة يمكن أن توفّر حزمة ما، ولأن المهام معرَّفة داخل الوصفات، يحتاج BitBake إلى تحديد أي مزوّد (provider) للحزمة سيستخدمه. وهو يرتّب كل مزوّدي الحزمة، موزنًا كل مزوّد وفق معايير مختلفة. فمثلًا، الإصدارات المفضَّلة من البرمجيات تحصل على أولوية أعلى من غيرها. غير أن BitBake يأخذ في الحسبان أيضًا إصدار الحزمة، وكذلك تبعيات الحزم الأخرى. وبعد أن يكون قد اختار الوصفة التي سيشتق منها حزمته، يتكرّر BitBake عبر DEPENDS وRDEPENDS لتلك الوصفة، ثم ينتقل إلى حساب مزوّدي تلك الحزم. وستُنتج هذه السلسلة المتفاعلة قائمةً بالحزم اللازمة لتوليد الصورة، وكذلك قائمةً بمزوّدين لتلك الحزم.

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

إنشاء runqueue معقّد بعض الشيء. فأولًا، يمرّ BitBake عبر قائمة أسماء المهام داخل كائن TaskData، لتحديد تبعيات المهام. وبينما يمرّ عبر TaskData، يبدأ ببناء خريطة مهام موزونة. وعندما يكتمل ذلك، إذا لم يجد أي تبعيات دائرية أو مهام غير قابلة للبناء أو أي مشكلات من هذا النوع، فإنه يرتّب خريطة المهام بحسب الوزن ويعيد كائن runqueue كاملًا إلى الـ cooker. وسيبدأ الـ cooker في محاولة تنفيذ runqueue، مهمةً مهمةً. وبحسب حجم الصورة وموارد الحوسبة، قد يستغرق Poky من نصف ساعة إلى ساعات لتوليد سلسلة أدوات تصريف متقاطع، وتغذية حزم (package feed)، وصورة لينكس المدمجة المحدَّدة. ومن الجدير بالذكر أن المسار بأكمله، منذ تنفيذ bitbake من سطر الأوامر وحتى ما قبل تنفيذ طابور تنفيذ المهام، استغرق أقل من بضع ثوانٍ.

23.3. الخاتمة

في مناقشاتي مع أعضاء المجتمع وفي ملاحظاتي الشخصية، حددت عددًا من المجالات التي كان يمكن، ربما، أن تكون قد نُفِّذت بصورة مختلفة، وكذلك عددًا من الدروس القيّمة. ومن المهم الإشارة إلى أن «تحليل ما بعد الوقائع» (arm chair quarterbacking) — أي تحليل نقدي لجهدٍ تطويرٍ دام عقدًا من الزمان بقصد نقده — ليست انتقاصًا لمن أمضوا وقتهم وجهدهم في مجموعة برمجيات استثنائية تمامًا. وكطوّرين، فإن أصعب أجزاء عملنا هو التنبؤ بما سنحتاجه بعد سنوات، وكيف يمكننا وضع إطار عمل يمكّننا من إنجاز ذلك العمل الآن. وقليلون يمكنهم تحقيق ذلك دون بعض العثرات.

الدرس الأول هو أن تتأكد من وضع وثيقة معايير مكتوبة ومُتَّفق عليها، يفهمها المجتمع جيدًا. وينبغي أن تُصمَّم هذه الوثيقة لأقصى قدرٍ من المرونة والنمو.

أحد المواضع التي اصطدمت فيها بهذه المشكلة شخصيًا هو عملي في صنف إنشاء بيان تراخيص OE-Core، وخاصة تجاربي مع متغير LICENSE. ولما لم يكن هناك معيار موثَّق بوضوح لمحتوى ما ينبغي أن يحتوي عليه LICENSE، فقد أظهرت مراجعةُ الوصفات العديدة المتاحة تنوّعاتٍ كثيرة. فقد احتوت سلاسل LICENSE المختلفة على كل شيء، من قيم قابلة للتحليل بواسطة شجرة الصياغة المجرّدة في Python (abstract syntax tree) إلى قيمٍ لا يُرجى منها استخلاص بيانات ذات معنى. وكان هناك عُرفٌ مستخدَم على نطاق واسع داخل المجتمع؛ غير أن ذلك العرف كان له تنوّعات كثيرة، بعضها أقلّ صحةً من بعضه الآخر. ولم تكن هذه مشكلة المطوّر الذي كتب الوصفة؛ بل كانت إخفاقًا من المجتمع في تعريف معيار.

ولما كان العمل السابق على متغير LICENSE، خارج التحقق من وجوده، لا يكاد يُذكر، فلم يكن هناك اهتمام خاص بمعايير هذا المتغير. ولكان من الممكن تفادي الكثير من المتاعب لو ضُبط معيار متفق عليه على مستوى المشروع في وقتٍ مبكر.

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

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

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

أحد المواضح الواضحة التي كان يمكن أن يفيد فيها ذلك في مشروع Yocto هو تحديد احتياجات المستخدمين النهائيين ذوي الأنظمة منخفضة الذاكرة. فلو أُوليَ مخزن بيانات BitBake قدرٌ أكبر من التفكير في وقتٍ أبكر، ربما كان بإمكاننا التنبؤ بالمشكلات المرتبطة باستهلاك مخزن البيانات ذاكرةً مفرطة، والتعامل معها مبكرًا.

والدرس هنا هو أنه، وإن كان من المستحيل تقريبًا تحديد كل نقطة ألم سيصطدم بها مشروعك خلال عمره، فإن قضاء وقت في تخطيطٍ مبدئي جادّ سيساعد في تقليل الجهد اللاحق. وBitBake وOE-Core ومشروع Yocto محظوظون جميعًا في هذا الصدد، إذ أُنجز قدرٌ لا بأس به من التخطيط المعماري مبكرًا. وقد مكّننا ذلك من إجراء تغييرات معمارية كبرى دون ألمٍ ومعاناةٍ كبيرين.

23.4. شكر وتقدير

أولًا، أشكر Chris Larson وMichael Lauer وHolger Schurig، والكثير جدًا من الأشخاص الذين ساهموا في BitBake وOpenEmbedded وOE-Core ومشروع Yocto على مدى السنوات الماضية. كما يتوجّه الشكر إلى Richard Purdie لما سمح لي بأن أستنصحه، في الجانبين التاريخي والتقني لـ OE، ولتشجيعه وإرشاده الدائمين، ولا سيما في بعض «السحر الأسود» الخاص بـ BitBake.