تحزيم حزم بايثون
معمارية تطبيقات المصادر المفتوحة (المجلد الأول)حزمة بايثون
معمارية تطبيقات المصادر المفتوحة (المجلد الأول) حزمة بايثون
Tarek Ziadé
إذا استمتعتَ بهذه الكتب، فقد تستمتع أيضًا بـ Software Design by Example in Python و Software Design by Example in JavaScript.
14.1. المقدمة
هناك مذهبان فكريان فيما يتعلّق بتثبيت التطبيقات. فالمذهب الأول، وهو الشائع في Windows و Mac OS X، يقول إنَّ التطبيقات ينبغي أن تكون مكتفيئةً بذاتها، وألاَّ يعتمد تثبيتها على أيِّ شيء آخر. تُبسِّط هذه الفلسفة إدارةَ التطبيقات: فكلُّ تطبيقٍ جهازٌ «مستقلٌّ» قائمٌ بذاته، ولا ينبغي لتثبيته وإزالته أن يزعج بقيةَ نظام التشغيل. وإذا احتاج التطبيق إلى مكتبةٍ غير شائعة، فإنَّ تلك المكتبة تُضمَّن في توزيع التطبيق.
أما المذهب الثاني، وهو القاعدةُ في الأنظمة المعتمدة على Linux، فيتعامل مع البرمجيات بوصفها مجموعةً من وحداتٍ صغيرة مكتفيئةٍ بذاتها تُسمَّى حزمًا (packages). فتُحزَّم المكتبات داخل الحزم، وقد تعتمد حزمةُ مكتبةٍ ما على حزمٍ أخرى. وقد يقتضي تثبيتُ تطبيقٍ واحدٍ العثورَ على إصداراتٍ بعينها من عشرات المكتبات الأخرى وتثبيتها. وتُجلَب هذه التبعيات عادةً من مستودعٍ مركزيٍّ يحتوي على آلاف الحزم. وهذه الفلسفة هي سببُ استخدام توزيعات Linux لأنظمة إدارة حزمٍ معقّدةٍ مثل dpkg و RPM لتتبّع التبعيات ومنع تثبيت تطبيقين يستخدمان إصداراتٍ غيرَ متوافقة من المكتبة نفسها.
لكلِّ نهجٍ مزاياه وعيوبه. فوجودُ نظامٍ عالي الترابطية (highly modular) يُمكن فيه تحديثُ كلِّ جزءٍ أو استبدالُه يجعلَ الإدارةَ أسهل، لأنَّ كلَّ مكتبةٍ موجودةٌ في مكانٍ واحد، وتستفيدُ منها جميعُ التطبيقات التي تستخدمها عند تحديثها. فمثلًا، سيصل إصلاحٌ أمنيٌّ في مكتبةٍ ما إلى جميع التطبيقات التي تستخدمها دفعةً واحدة، في حين لو شحنتُ تطبيقٌ بمكتبته الخاصة لكان نشرُ ذلك الإصلاح الأمني أعقدَ بكثير، ولا سيما إذا استخدمت تطبيقاتٌ مختلفةٌ إصداراتٍ مختلفة من المكتبة.
غير أنّ بعض المطوّرين يرون في تلك الترابطية عيبًا، لأنَّهم لا يسيطرون على تطبيقاتهم وتبعياتهم. فإيصالُ جهازٍ برمجياتٍ مستقلٍّ (standalone software appliance) أسهلُ لهم، لأنَّهم يتأكّدون بذلك من أنَّ بيئة التطبيق مستقرّةٍ ولا تخضع لـ«جحيم التبعيات» (dependency hell) أثناء ترقية النظام.
كما تجعل التطبيقاتُ المكتفيئةُ بذاتها حياةَ المطوّر أسهلَ حين تحتاج إلى دعم عدّة أنظمة تشغيل. فبعض المشاريع تذهب بعيدًا لصدور تطبيقاتٍ محمولة (portable) تُلغي أيَّ تفاعلٍ مع النظام المُضيفِ عبر العمل داخل دليلٍ مكتتفٍ بذاته، حتى بالنسبة إلى ملفات السجلّات (log files).
كان نظامُ تغليف بايثون (Python packaging) مقصودًا أن يجعل الفلسفةَ الثانية—تعدُّدَ التبعيات في كلِّ عملية تثبيت—ملائمةً للمطوّر والمستخدم وباقاتِ الأدوات وكلِّ الأطراف بأقصى ما يمكن. وللأسف، كان فيه (وهو فيه الآن) كمٌّ من العيوب التي سبّبت أو سمحت بكلِّ أنواع المشكلات: مخطّطاتِ إصداراتٍ غير بديهية، وسوءٍ في معالجة ملفات البيانات، وصعوبةٍ في إعادة التغليف، وغيرها. وقبل ثلاث سنوات، قرّرتُ أنا ومجموعةٌ من بايثونيين آخرين إعادةَ اختراعه لمعالجة هذه المشكلات. ونسمّي أنفسنا «زمالة التغليف» (Fellowship of the Packaging)، ويصف هذا الفصل المشكلاتِ التي كنّا نحاول إصلاحها، وكيف يبدو حلُّنا.
المصطلحات
في بايثون، الحزمة (package) هي دليلٌ يحتوي على ملفات بايثون. وتُسمَّى ملفاتُ بايثون وحدات (modules). ويجعل هذا التعريف استعمالَ كلمة «package» غامضًا إلى حدٍّ ما، إذ تستخدمها أنظمةٌ كثيرةٌ أيضًا للإشارة إلى إصدارٍ (release) من مشروعٍ ما.
ويصير مطوّرو بايثون أنفسهم غامضين في هذا الأمر أحيانًا. ومن طرائق للتخلّص من هذا الغموض استعمالُ عبارة «حزم بايثون» (Python packages) حين نتحدّث عن دليلٍ يحوي وحدات بايثون. أمّا مصطلح «الإصدار» (release) فيُستخدم لتعريف إصدارٍ واحدٍ من مشروع، ومصطلح «التوزيع» (distribution) فيعرّف توزيعَ إصدارٍ ما، مصدريًّا كان أم ثنائيًّا، كملفِّ tarball أو zip.
14.2. عبءُ مطوِّر بايثون
يرغب معظمُ مبرمكي بايثون في أن تكون برامجُهم قابلةً للاستخدام في أيِّ بيئة. وغالبًا ما يودّون كذلك استخدامَ مزيجٍ من مكتبات بايثون القياسية ومكتباتٍ تعتمد على النظام. لكنَّ ما لم تحزم تطبيقَك على حدةٍ لكلِّ نظام تغليفٍ موجود، فأنت محكومٌ بأن تقدّم إصداراتٍ خاصّةً ببايثون—أي إصداراتٍ مصمَّمةً للتثبيت داخل تثبيتِ بايثون بغضِّ النظر عن نظام التشغيل الأساسي—ثمَّ تأمل أن:
- يستطيع باقون الحزم (packagers) في كلِّ نظامٍ مستهدِفٍ إعادةَ تغليف عملك،
- تُعاد تغليفُ التبعيات التي لديك في كلِّ نظامٍ مستهدِفٍ بدوره،
- وتُوصَف تبعياتُ النظام وصفًا واضحًا.
أحيانًا، يكون هذا ببساطةٍ مستحيلًا. فمثلًا، يستخدم Plone (وهو نظامُ إدارةِ محتوى كاملٌ مدعومٌ ببايثون) مئاتِ المكتباتِ الصغيرةِ المكتسبةِ الصِّرفِ من بايثون، والتي لا تتوفّر دائمًا بوصفها حزمًا في كلِّ أنظمة التغليف الموجودة. وهذا يعني أنّ Plone يجب أن يشحن كلَّ ما يحتاجه في تطبيقٍ محمول. ولتحقيق ذلك، يستخدم zc.buildout، الذي يجمع كلَّ تبعياته وينشئ تطبيقًا محمولًا يعمل على أيِّ نظامٍ داخل دليلٍ واحد. وهو فعليًّا إصدارٌ ثنائي، إذ يُصرَّف في موضعه أيُّ جزءٍ من شيفرة C.
وهذا مكسبٌ كبيرٌ للمطوّرين: فكلُّ ما عليهم فعله هو وصفُ تبعياتهم باستخدام معايير بايثون الموصوفة أدناه، واستعمالُ zc.buildout لإصدار تطبيقهم. لكنَّه كما نُوقِش سابقًا، يُنشئ هذا النوعُ من الإصدارات حصنًا داخل النظام، وهو ما سيكرهه معظمُ مديري الأنظمة في Linux. أمّا مديرو Windows فلن يمانعوه، لكنَّ من يديرون CentOS أو Debian سوف يمانعونه، لأنَّ تلك الأنظمة تقوم إدارتها على افتراض أنَّ كلَّ ملفٍّ في النظام مسجَّلٌ ومصنَّفٌ ومعروفٌ لأدوات الإدارة.
سيودُّ هؤلاء المديرون إعادةَ تغليف تطبيقك وفق معاييرهم الخاصة. والسؤال الذي علينا الإجابةُ عنه هو: «هل يمكن أن يكون لبايثون نظامُ تغليفٍ يُترجَم آليًّا إلى أنظمة التغليف الأخرى؟» إن كان ذلك ممكنًا، لأمكن تثبيتُ تطبيقٍ واحدٍ أو مكتبةٍ واحدةٍ على أيِّ نظامٍ دون عملٍ تغليفٍ إضافي. وهنا لا تعني كلمة «آليًّا» بالضرورة أنَّ العملَ يجب أن يُنجَز كليًّا بواسطة سكربت؛ فباقون حزم RPM أو dpkg سيقولون لك إنَّ ذلك مستحيل—إلَّمَّا يحتاجون دائمًا إلى إضافة بعض التفاصيل في المشاريع التي يعيدون تغليفها. وسيقولون لك أيضًا إنَّهم كثيرًا ما يواجهون صعوبةً في إعادة تغليف قطعةِ شيفرةٍ ما لأنَّ مطوّريها لم يكونوا على علمٍ ببضع قواعد تغليفٍ أساسية.
إليك مثالًا واحدًا لما يمكنك فعله لإغضاب باقوني الحزم أثناء استخدام نظام تغليف بايثون الحالي: أن تُصدر مكتبةً باسم «MathUtils» واسمُ إصدارٍ هو «Fumanchu». فقد وجد عالمُ الرياضياتِ اللامعُ الذي كتب المكتبةَ طريفًا أن يستخدم أسماءَ قططه لأصدارات مشروعه. لكن كيف يمكن لباقون الحزم أن يعرف أنَّ «Fumanchu» هو اسمُ قطته الثانية، وأنَّ الأولى كانت تُسمَّى «Phil»، بحيث تأتي إصدارةُ «Fumanchu» بعد إصدارة «Phil»؟
قد يبدو هذا متطرِّفًا، لكنَّه يمكن أن يحدث بالأدوات والمعايير القائمة اليوم. والأسوأُ أنَّ أدواتٍ مثل easy_install أو pip تستخدم سجلَّها الخاصَّ غيرَ المعياريّ لتتبّع الملفات المُثبَّتة، وستُرتِّب إصدارات «Fumanchu» و «Phil» ترتيبًا أبجديًّا.
والمشكلةُ الأخرى هي كيفيةُ التعامل مع ملفات البيانات. فمثلًا، ماذا لو كان تطبيقُك يستخدم قاعدةَ بيانات SQLite؟ إن وضعتَها داخل دليل الحزمة الخاصة بك، فقد يفشل تطبيقُك لأنَّ النظام يمنعُك من الكتابة في ذلك الجزء من الشجرة. وكذلك فإنَّ هذا يخرق الافتراضات التي تتبناها أنظمة Linux بشأن مكان بيانات التطبيقات لأغراض النسخ الاحتياطي (/var).
وفي العالم الحقيقي، يحتاج مديرو الأنظمة إلى القدرة على وضع ملفاتك حيثما يريدون دون كسر تطبيقك، وأنت بدورك تحتاج إلى إخبارهم بماهيّة تلك الملفات. لنُعِد صياغةَ السؤال إذن: هل يمكن أن يكون في بايثون نظامُ تغليفٍ يوفّر كلَّ المعلومات اللازمة لإعادة تغليف تطبيقٍ ما بأيِّ نظام تغليفٍ من الأنظمة الخارجية القائمة، دون الحاجة إلى قراءة الشيفرة، ويرضي الجميع في الوقت نفسه؟
14.3. البنيةُ المعماريةُ الحالية للتغليف
حزمةُ Distutils التي تأتي مع مكتبة بايثون القياسية مليئةٌ بالمشكلات الموصوفة أعلاه. ولأنَّها المعيار، فإنَّ الناس إمَّا يتعاملون معها وعيوبها، وإمَّا يستخدمون أدواتٍ أكثرَ تقدّمًا مثل Setuptools التي تضيف ميزاتٍ فوقها، أو Distribute وهي شعبةٌ (fork) من Setuptools. وهناك أيضًا Pip، وهو مُثبِّتٌ أكثرَ تقدّمًا يعتمد على Setuptools.
غير أنَّ هذه الأدواتَ الأحدثَ تشترك جميعها في الأساس Distutils وترث مشكلاته. محاوَلاتٌ كثيرةٌ جرت لإصلاح Distutils في مكانه، لكنَّ الشيفرةَ مُعتمَدةٌ من قِبل أدواتٍ أخرى إلى عمقٍ يجعل أيَّ تغييرٍ عليها، حتى في تفاصيلها الداخلية، تراجعًا محتمَلًا في منظومة تغليف بايثون بأكملها.
قرّرنا تبعًا لذلك تجميدَ Distutils والبدءَ في تطوير Distutils2 انطلاقًا من قاعدة الشيفرة نفسها، من غير أن نُكثرَ من الحديث عن التوافق الخلفي. ولفهم ما الذي تغيَّر ولماذا، لنُنظر عن قربٍ أكبر إلى Distutils.
14.3.1. أساسياتُ Distutils وعيوبُ التصميم
يحتوي Distutils على أوامر، كلُّ واحدٍ منها صنفٌ له طريقةُ run يمكن استدعاؤها مع بعض الخيارات. ويوفِّر Distutils كذلك صنفَ Distribution الذي يحوي قيمًا عامةً يمكن لكلِّ أمرٍ الاطّلاعُ عليها.
ولاستعمال Distutils، يضيف المطوّر وحدةَ بايثونٍ واحدةً إلى المشروع، تُسمَّى عادةً setup.py. وتحتوي هذه الوحدة على استدعاءٍ لنقطة دخول Distutils الرئيسة، وهي دالةُ setup. ويمكن لهذه الدالة أن تأخذ خياراتَ كثيرةً تُحفظ في كائنِ Distribution وتستخدمها الأوامر. وفيما يلي مثالٌ يعرّف بعضَ الخيارات القياسية، مثل اسم المشروع وإصداره، وقائمةَ الوحدات التي يحتويها:
from distutils.core import setup
setup(name='MyProject', version='1.0', py_modules=['mycode.py'])
يمكن بعد ذلك استعمالُ هذه الوحدة لتشغيل أوامر Distutils مثل sdist، الذي ينشئ توزيعَ مصدرٍ في أرشيفٍ ويضعه في دليل dist:
$ python setup.py sdist
وباستخدام السكربت نفسه، يمكنك تثبيتَ المشروع باستعمال الأمر install:
$ python setup.py install
ويوفِّر Distutils أوامرَ أخرى، منها:
uploadلرفعِ توزيعٍ إلى مستودعٍ على الشبكة.registerلتسجيل البيانات الوصفية لمشروعٍ في مستودعٍ على الشبكة من غير رفعِ توزيعٍ بالضرورة،bdistلإنشاءِ توزيعٍ ثنائي، وbdist_msiلإنشاءِ ملف.msiلويندوز.
كما يتيح لك الخياراتُ الأخرى في سطر الأوامر الاطّلاعَ على معلوماتٍ عن المشروع.
$ python setup.py --name
MyProject
وعليه، فإنَّ تثبيتَ مشروعٍ ما أو الحصولَ على معلوماتٍ عنه يتمّ دائمًا باستدعاء Distutils من خلال هذا الملف. فمثلًا، لمعرفة اسم المشروع:

الشكل 14.1: الإعداد
وبذلك يكون setup.py هو الطريقة التي يتفاعل بها الجميع مع المشروع، سواء لبنائه أو تغليفه أو نشره أو تثبيته. يصف المطوّر محتوى مشروعه عبر الخيارات التي يمرّرها إلى دالة، ويستعمل ذلك الملف في كلِّ مهامِّ التغليف التي يقوم بها. كما تستخدمه أدواتُ التثبيت لتثبيت المشروع على النظام الهدف.
$ python setup.py --name
Building lxml version 2.2.
NOTE: Trying to build without Cython, pre-generated 'src/lxml/lxml.etree.c'
needs to be available.
Using build configuration of libxslt 1.1.26
Building against libxml2/libxslt in the following directory: /usr/lib/lxml
كونُ الوحدةِ الوحيدةِ في بايثون مستعمَلةً لتغليف المشروع وإصداره وتثبيته في آنٍ واحدٍ، يُعدُّ من أبرز عيوب Distutils. فمثلًا، إن أردت الحصولَ على name من مشروع lxml، فسينفّذ setup.py أشياءَ كثيرةً جدًّا غيرَ إعادةِ سلسلةٍ نصيةٍ بسيطةٍ كما هو متوقَّع:
قد لا ينجح الأمرُ حتى في بعض المشاريع، لأنَّ المطوّرين يفترضون أنَّ setup.py لا يُستعمل إلّا للتثبيت، وأنَّ ميزات Distutils الأخرى لا يستعملونَها إلّا أثناء التطوير. والأدوارُ المتعدّدةُ لسكربت setup.py تسبِّب ارتباكًا شديدًا.
14.3.2. البياناتُ الوصفية و PyPI
حين يبني Distutils توزيعًا، ينشئ ملفَّ Metadata يوافق المعيارَ الموصوف في PEP 314[1]. ويحتوي نسخةً ساكنةً من جميع البيانات الوصفية المعتادة، مثل اسم المشروع أو إصدار التوزيع. وحقولُ البيانات الوصفية الرئيسة هي:
Name: اسمُ المشروع.Version: إصدارُ التوزيع.Summary: وصفٌ في سطرٍ واحد.Description: وصفٌ مفصَّل.Home-Page: عنوانُ URL الخاص بالمشروع.Author: اسمُ المؤلِّف.Classifiers: مصنِّفاتُ المشروع. وتوفِّر بايثون قائمةَ مصنِّفاتٍ للرخصة، ودرجةِ نضج التوزيع (beta، alpha، final)، وغيرها.RequiresوProvidesوObsoletes: تُستخدَم لتعريف التبعيات مع الوحدات.
هذه الحقولُ قابلةٌ في معظمها بسهولةٍ لمطابقةِ نظائرها في أنظمة التغليف الأخرى.
وفهرسُ حزم بايثون (Python Package Index) أي PyPI[2]، وهو مستودعٌ مركزيٌّ للحزم مثل CPAN، يستطيع تسجيلَ المشاريع ونشرَ الإصدارات عبر أوامر register و upload في Distutils. يبني الأمرُ register ملفَّ Metadata ويرسله إلى PyPI، مما يتيح للناس والأدوات—مثل أدوات التثبيت—تصفّحَها عبر صفحات الويب أو عبر خدمات الويب.

الشكل 14.2: مستودع PyPI
يمكنك تصفّحَ المشاريع حسب Classifiers، والحصولَ على اسم المؤلِّف وعنوان المشروع. وفي الوقت نفسه، يمكن استعمالُ Requires لتعريف تبعياتٍ على وحدات بايثون. ويُستخدَم الخيارُ requires لإضافة عنصر بياناتٍ وصفيةٍ من نوع Requires إلى المشروع:
from distutils.core import setup
setup(name='foo', version='1.0', requires=['ldap'])
وتعريفُ التبعيةِ على وحدة ldap أمرٌ تقريريٌّ محض: فلا أدواتَ ولا مُثبِّتاتَ تضمن وجودَ تلك الوحدة. كان هذا كافيًا لو عرّفت بايثون المتطلّباتِ على مستوى الوحدات عبر كلمة مفتاحية require كما يفعل Perl. عندئذٍ لا يكون الأمرُ سوى أن تتصفَّح أدواتُ التثبيتِ التبعياتِ في PyPI وتثبِّتَها؛ وهذا في جوهره ما يفعله CPAN. لكنَّ ذلك مستحيلٌ في بايثون، إذ يمكن لوحدةٍ اسمُها ldap أن توجد في أيِّ مشروعِ بايثون. ولأنَّ Distutils يسمح للناس بإصدار مشاريعَ قد تحتوي عدّةَ حزمَ ووحدات، فإنَّ حقل البيانات الوصفية هذا عديمُ الفائدة تمامًا.
ومن عيوب ملفات Metadata الأخرى أنَّها تُنشَأ بواسطة سكربتِ بايثون، فهي خاصّةٌ بالمنصّة التي تُنفَّذ فيها. فمثلًا، قد يعرِّف مشروعٌ يوفِّر ميزاتٍ خاصّةً بويندوز ملفَّ setup.py الخاصَّ به على النحو الآتي:
from distutils.core import setup
setup(name='foo', version='1.0', requires=['win32com'])
لكنَّ هذا يفترض أنَّ المشروع لا يعمل إلّا تحت ويندوز، حتى إن كان يوفِّر ميزاتٍ محمولة. ومن طرائق حلِّ ذلك جعلُ الخيار requires خاصًّا بويندوز:
from distutils.core import setup
import sys
if sys.platform == 'win32':
setup(name='foo', version='1.0', requires=['win32com'])
else:
setup(name='foo', version='1.0')
وهذا يفاقم المشكلةَ فعليًّا. تذكَّر أنَّ السكربت يُستخدَم لبناء أرشيفاتِ المصدر التي تُنشر بعد ذلك للعالم عبر PyPI. وهذا يعني أنَّ ملفَّ Metadata الساكنَ المُرسَل إلى PyPI يعتمد على المنصّة التي صُرِّف عليها. بمعنىً آخر، لا سبيلَ إلى الإشارة في الحقل بياناتٍ وصفية إلى أنَّه خاصٌّ بمنصّة بعينها.
14.3.3. بنيةُ PyPI

الشكل 14.3: سير عمل PyPI
وكما ذُكر سابقًا، فإنَّ PyPI فهرسٌ مركزيٌّ لمشاريع بايثون، يستطيع الناسُ تصفّحَ المشاريع القائمة حسب الفئة أو تسجيلَ أعمالهم الخاصة. ويمكن رفع التوزيعات المصدرية أو الثنائية وإضافتُها إلى مشروعٍ قائم، ثمَّ تنزيلُها للتثبيت أو للدراسة. كما يقدِّم PyPI خدماتِ ويبٍ يمكن أن تستعملَها أدواتٌ مثل أدوات التثبيت.
تسجيل المشاريع ورفع التوزيعات
يتم تسجيلُ مشروعٍ في PyPI باستعمال الأمر register في Distutils. وهو يبني طلبَ POST يحتوي بيانات المشروع الوصفية، أيًّا كان إصدارُه. ويتطلَّب الطلبُ ترويسةَ Authorization، لأنَّ PyPI يستعمل المصادقةَ الأساسية (Basic Authentication) للتأكّد من أنَّ كلَّ مشروعٍ مسجَّلٍ مرتبطٌ بمستخدمٍ سجَّل مع PyPI أوّلًا. وتُحفَظ بياناتُ الاعتماد في إعدادات Distutils المحلية أو تُكتب في نافذة الاستجابة كلَّما استُدعي أمرُ register. وفيما يلي مثالٌ على استعماله:
$ python setup.py register
running register
Registering MPTools to http://pypi.python.org/pypi
Server response (200): OK
ويحصل كلُّ مشروعٍ مسجَّلٍ على صفحة ويبٍ تحتوي نسخةً HTML من البيانات الوصفية، ويستطيع باقون الحزم رفعَ التوزيعات إلى PyPI باستعمال upload:
$ python setup.py sdist upload
running sdist
…
running upload
Submitting dist/mopytools-0.1.tar.gz to http://pypi.python.org/pypi
Server response (200): OK
ومن الممكن أيضًا توجيهُ المستخدمين إلى موضعٍ آخر عبر حقل البيانات الوصفية Download-URL بدلًا من رفع الملفات مباشرةً إلى PyPI.
الاستعلامُ عن PyPI
إلى جانب صفحات HTML التي ينشرها PyPI لمستخدمي الويب، يوفِّر خدمتين يمكن للأدوات استعمالهما لتصفّح المحتوى: بروتوكول Simple Index وواجهات XML-RPC.
يبدأ بروتوكولُ Simple Index من http://pypi.python.org/simple/، وهي صفحةُ HTML بسيطةٌ تحتوي روابطَ نسبيةً إلى كلِّ مشروعٍ مسجَّل:
<head><title>Simple Index</title></head><body>
⋮ ⋮ ⋮
<a href='MontyLingua/'>MontyLingua</a><br/>
<a href='mootiro_web/'>mootiro_web</a><br/>
<a href='Mopidy/'>Mopidy</a><br/>
<a href='mopowg/'>mopowg</a><br/>
<a href='MOPPY/'>MOPPY</a><br/>
<a href='MPTools/'>MPTools</a><br/>
<a href='morbid/'>morbid</a><br/>
<a href='Morelia/'>Morelia</a><br/>
<a href='morse/'>morse</a><br/>
⋮ ⋮ ⋮
</body>
فمثلًا، يملك مشروع MPTools رابطَ MPTools/، ما يعني أنَّ المشروع موجودٌ في الفهرس. والموقعُ الذي يشير إليه يحتوي قائمةَ كلِّ الروابط المتعلقة بالمشروع:
- روابطُ كلِّ توزيعٍ مخزَّنٍ في PyPI
- روابطُ كلِّ عنوان URL محدَّدٍ في
Metadata، لكلِّ إصدارٍ من المشروع المسجَّل - روابطُ كلِّ Download-URL محدَّدٍ في
Metadata، كذلك لكلِّ إصدار.
تحتوي صفحةُ MPTools على:
<head><title>Links for MPTools</title></head>
<body><h1>Links for MPTools</h1>
<a href="../../packages/source/M/MPTools/MPTools-0.1.tar.gz">MPTools-0.1.tar.gz</a><br/>
<a href="http://bitbucket.org/tarek/mopytools" rel="homepage">0.1 home_page</a><br/>
</body>
ويمكن لأدواتٍ مثل أدوات التثبيت التي تريد العثورَ على توزيعاتِ مشروعٍ ما أن تبحث عنه في صفحة الفهرس، أو أن تتحقّق ببساطة ممّا إذا كان http://pypi.python.org/simple/PROJECT_NAME/ موجودًا.
وللبروتوكول هذا قيدان رئيسان. أولًا، إنَّ PyPI خادمٌ واحدٌ في الوقت الحالي، ومع أنَّ الناس يملكون عادةً نسخًا محليةً من محتواه، فقد واجهنا عدّةَ مرّاتٍ توقُّفٍ في السنتين الأخيرتين شلَّت مطوّرين يعملون باستمرار مع أدوات تثبيتٍ تتصفَّح PyPI لجلب كلِّ التبعيات التي يحتاجها المشروع عند بنائه. فمثلًا، ينتج بناءُ تطبيقِ Plone مئاتِ الاستعلاماتِ في PyPI لجلب جميع القطع المطلوبة، ومن ثمَّ قد يعمل PyPI كنقطة فشلٍ وحيدة.
ثانيًا، حين لا تكون التوزيعات مخزَّنةً في PyPI ويُتوفَّر رابطُ Download-URL في صفحة Simple Index، على أدوات التثبيت أن تتبع ذلك الرابط وترجو أن يكون الموضعُ متاحًا وأن يحتوي فعلًا على التوزيع. وهذه التحويلات غير المباشرة تُضعِف أيَّ عمليّةٍ تستند إلى Simple Index.
هدفُ بروتوكول Simple Index هو إعطاءِ أدوات التثبيت قائمةَ روابطٍ تستطيع استعمالَها لتثبيت مشروع. أما بياناتُ المشروع الوصفية فلا تُنشر هناك؛ بل توجد طرائقُ XML-RPC للحصول على معلوماتٍ إضافية عن المشاريع المسجَّلة:
>>> import xmlrpclib
>>> import pprint
>>> client = xmlrpclib.ServerProxy('http://pypi.python.org/pypi')
>>> client.package_releases('MPTools')
['0.1']
>>> pprint.pprint(client.release_urls('MPTools', '0.1'))
[{'comment_text': &rquot;,
'downloads': 28,
'filename': 'MPTools-0.1.tar.gz',
'has_sig': False,
'md5_digest': '6b06752d62c4bffe1fb65cd5c9b7111a',
'packagetype': 'sdist',
'python_version': 'source',
'size': 3684,
'upload_time': <DateTime '20110204T09:37:12' at f4da28>,
'url': 'http://pypi.python.org/packages/source/M/MPTools/MPTools-0.1.tar.gz'}]
>>> pprint.pprint(client.release_data('MPTools', '0.1'))
{'author': 'Tarek Ziade',
'author_email': 'tarek@mozilla.com',
'classifiers': [],
'description': 'UNKNOWN',
'download_url': 'UNKNOWN',
'home_page': 'http://bitbucket.org/tarek/mopytools',
'keywords': None,
'license': 'UNKNOWN',
'maintainer': None,
'maintainer_email': None,
'name': 'MPTools',
'package_url': 'http://pypi.python.org/pypi/MPTools',
'platform': 'UNKNOWN',
'release_url': 'http://pypi.python.org/pypi/MPTools/0.1',
'requires_python': None,
'stable_version': None,
'summary': 'Set of tools to build Mozilla Services apps',
'version': '0.1'}
ومشكلةُ هذا النهج أنَّ بعض البيانات التي تنشرها واجهات XML-RPC كان يمكن تخزينُها كملفاتٍ ساكنةٍ ونشرُها في صفحة Simple Index لتبسيط العمل على أدوات العميل. ومن شأن ذلك أيضًا تجنُّبَ العمل الإضافي الذي يضطر إليه PyPI لمعالجة تلك الاستعلامات. من المقبولُ تمامًا أن تُنشر بياناتٌ غيرُ ساكنةٍ، مثل عدد مرات تنزيل كلِّ توزيعٍ، في خدمةِ ويبٍ متخصِّصة، لكنَّ من غير المعقول أن يُضطر المرء إلى استعمال خدمتين مختلفتين للحصول على جميع البيانات الساكنة عن مشروع.
14.3.4. بنيةُ تثبيتِ بايثون
إن ثبَّتَ مشروعَ بايثون باستعمال python setup.py install، ستنسخُ Distutils—وهو مُضمَّنٌ في المكتبة القياسية— الملفاتَ إلى نظامك.
- ستستقرُّ حِزَمُ بايثون (packages) والوحدات في دليل بايثون الذي يُحمَّل عند بدء المفسِّر (interpreter): في أحدثِ إصدارات Ubuntu تنتهي في
/usr/local/lib/python2.6/dist-packages/، وفي Fedora في/usr/local/lib/python2.6/sites-packages/. - يمكن أن تستقرَّ ملفاتُ البيانات (data files) المعرَّفةُ في المشروع في أيِّ موضعٍ على النظام.
- وسكربتُ التنفيذ (executable script) يستقرُّ في دليل
binعلى النظام. وبحسب المنصّة، قد يكون ذلك/usr/local/binأو دليلbinخاصًّا بتثبيت بايثون.
منذ بايثون 2.5، تُنسخُ ملفُّ البيانات الوصفية بجوار الوحدات والحزم باسم project-version.egg-info. فمثلًا، قد يملك مشروعُ virtualenv ملفَّ virtualenv-1.4.9.egg-info. ويمكنُ اعتبارُ ملفات البيانات الوصفية هذه قاعدةَ بياناتٍ للمشاريع المُثبَّتة، إذ من الممكن تكرارُها لبناء قائمةِ مشاريعٍ مع إصداراتها. غير أنَّ مُثبِّت Distutils لا يسجّل قائمةَ الملفات التي يثبِّتها على النظام. وهذا أمرٌ يُحزن، لأنَّ الأمر install يملك خيارًا اسمه --record يمكن استعمالُه لتسجيل جميع الملفات المُثبَّتة في ملفٍّ نصّي. غير أنَّ هذا الخيار غيرُ مستخدَمٍ افتراضيًا، ولا يكاد توثيقُ Distutils يذكره.
14.3.5. Setuptools و Pip وما شابههما
وكما ذُكر في المقدّمة، حاولت بعضُ المشاريع إصلاحَ بعض مشكلات Distutils، بدرجاتِ نجاحٍ متفاوتة.
مسألةُ التبعيات
سمحَ PyPI للمطوّرين بنشر مشاريعَ بايثون يمكن أن تتضمَّن عدّةَ وحداتٍ منظَّمةً في حِزَم بايثون. لكنَّه في الوقت نفسه سمح للمشاريع بتعريف تبعياتٍ على مستوى الوحدات عبر Require. الفكرتان معقولتان، لكنَّ جمعُهما معًا ليس كذلك.
والشيءُ الصحيحُ كان هو وجودُ تبعياتٍ على مستوى المشروع، وهو بالضبط ما أضافته Setuptools كميزةٍ فوق Distutils. كما وفَّر سكربتًا اسمه easy_install لجلب التبعيات وتثبيتها آليًّا بالبحث عنها في PyPI. عمليًّا، لم تُستخدَم التبعيةُ على مستوى الوحدات حقًّا قطُّ، وانتقل الناسُ إلى امتدادات Setuptools. ولأنَّ هذه الميزات أُضيفت في خياراتٍ خاصّةٍ بـ Setuptools تتجاهلها Distutils أو PyPI، فإنَّ Setuptools أنشأ معيارَه الخاصَّ عمليًّا وصار اختراقًا فوق تصميمٍ سيّئ.
يحتاج easy_install تبعًا لذلك إلى تنزيل أرشيف المشروع وتشغيل سكربت setup.py الخاص به مجدَّدًا للحصول على البيانات الوصفية التي يحتاجها، وعليه أن يفعل ذلك مع كلِّ تبعية. وتُبنى شبكةُ التبعيات قطعةً قطعةً بعد كلِّ تنزيل.
حتى لو قُبلت البياناتُ الوصفيةُ الجديدةُ في PyPI وأصبح تصفُّحُها ممكنًا على الإنترنت، فإنَّ easy_install سيظلُّ بحاجةٍ إلى تنزيل جميع الأرشيفات، لأنَّ البيانات الوصفيةَ المنشورةَ في PyPI، كما قيل سابقًا، خاصّةٌ بالمنصّة التي رُفعت إليها، وقد تختلف عن المنصّة الهدف. لكنَّ هذه القدرةَ على تثبيت المشروع وتبعياته كانت كافيةً في 90% من الحالات وكانت ميزةً عظيمة. لذلك أصبح Setuptools مستخدَمًا على نطاقٍ واسع، وإن كان لا يزال يعاني مشكلاتٍ أخرى:
- إذا أخفق تثبيتُ التبعية، فلا يوجد تراجعٌ (rollback) وقد ينتهي النظام في حالةٍ معطوبة.
- تُبنى شبكةُ التبعيات أثناء التثبيت على الذَّب، فإذا صُدِم تعارضُ في التبعيات قد ينتهي النظام في حالةٍ معطوبةٍ أيضًا.
مسألةُ إزالةِ التثبيت
لم يوفِّر Setuptools أداةَ إزالةِ تثبيت، رغم أنَّ بياناته الوصفيةَ الخاصّةَ كان يمكن أن تتضمَّن ملفًّا يسرد الملفات المُثبَّتة. أمَّا Pip فقد وسَّع بيانات Setuptools الوصفيةَ لتسجيل الملفات المُثبَّتة، ولذلك يستطيع إزالةَ التثبيت. لكنَّ هذه مجموعةٌ أخرى من البيانات الوصفية الخاصّة، ما يعني أنَّ تثبيتَ بايثون واحدًا قد يحتوي حتى أربعةِ أنواعٍ مختلفة من البيانات الوصفية لكلِّ مشروعٍ مُثبَّت:
- بيانات
Distutilsالوصفيةُ من نوعegg-info، وهي ملفُّ بياناتٍ وصفيةٍ واحد. - بيانات
Setuptoolsالوصفيةُ من نوعegg-info، وهي دليلٌ يحوي البيانات الوصفيةَ وخياراتٍ إضافيّةً خاصّةً بـSetuptools. - بيانات
Pipالوصفيةُ من نوعegg-info، وهي نسخةٌ موسَّعةٌ من السابقة. - ما ينشئه نظامُ التغليف المُضيف.
14.3.6. وماذا عن ملفات البيانات؟
في Distutils، يمكن تثبيتُ ملفات البيانات في أيِّ موضعٍ على النظام. إن عرّفتَ بعضَ ملفات بيانات الحزمة في سكربت setup.py على هذا النحو:
setup(…,
packages=['mypkg'],
package_dir={'mypkg': 'src/mypkg'},
package_data={'mypkg': ['data/*.dat']},
)
عندئذٍ ستُدرَج جميعُ الملفات ذات الامتداد .dat في مشروع mypkg ضمن التوزيع، وتُثبَّت في النهاية إلى جانب وحدات بايثون داخل تثبيت بايثون.
أمَّا ملفاتُ البيانات التي يلزم تثبيتُها خارج توزيع بايثون، فهناك خيارٌ آخر يخزّن الملفاتَ في الأرشيف لكنه يضعها في مواضعَ محدَّدة:
setup(…,
data_files=[('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
('config', ['cfg/data.cfg']),
('/etc/init.d', ['init-script'])]
)
وهذا خبرٌ سيّئٌ جدًّا لباقوني حزم أنظمة التشغيل، لأسبابٍ عِدّة:
- ملفاتُ البيانات ليست جزءًا من البيانات الوصفية، لذا يحتاج باقو الحزم إلى قراءة
setup.pyوأحيانًا إلى الغوص في شيفرة المشروع. - لا ينبغي للمطوّر أن يكون هو من يقرّر أين ينبغي أن تستقرَّ ملفاتُ البيانات على النظام الهدف.
- لا توجد فئاتٌ لهذه ملفات البيانات: فالصورُ وصفحاتُ
manوكلُّ ما عداها تُعامَل بالطريقة نفسها.
وباقو الحزم الذي يحتاج إلى إعادة تغليف مشروعٍ يحوي ملفًّا من هذا النوع لا يملك خيارًا سوى ترقيع ملف setup.py كي يعمل كما هو متوقَّع على منصّته. ولكي يفعل ذلك، عليه مراجعةُ الشيفرة وتغييرُ كلِّ سطرٍ يستعمل تلك الملفات، لأنَّ المطوّر قد افترض موضعَها. ولم يحسّن Setuptools ولا Pip من ذلك.
14.4. معاييرُ محسَّنة
انتهينا إذن إلى بيئة تغليفٍ مختلطةٍ ومرتبكة، حيث يقود كلَّ شيءٍ وحدةُ بايثونٍ واحدة، والبياناتُ الوصفيةُ ناقصة، ولا سبيلَ إلى وصف كلِّ ما يحتويه مشروع. وهذا ما نفعله لتحسين الأمور.
14.4.1. البياناتُ الوصفية
الخطوةُ الأولى هي تصحيحُ معيار Metadata لدينا. يعرِّف PEP 345 إصدارًا جديدًا يتضمَّن:
- طريقةً أكثر عقلانيةً لتعريف الإصدارات،
- وتبعيّاتٍ على مستوى المشروع،
- وطريقةً ساكنةً لتعريف القيم الخاصّة بالمنصّات.
الإصدار
أحدُ أهداف معيار البيانات الوصفية هو التأكّد من أنَّ جميع الأدوات التي تعمل على مشاريع بايثون قادرةٌ على تصنيفِها بالطريقة نفسها. أمَّا بخصوص الإصدارات، فيعني ذلك أنَّ كلَّ أداةٍ ينبغي أن تعرف أنَّ «1.1» تأتي بعد «1.0». لكن إذا كان للمشاريع مخطّطاتُ إصداراتٍ خاصّة، فسيصبح ذلك أصعبَ بكثير.
الطريقةُ الوحيدة لضمان اتّساق الإصدارات هي نشرُ معيارٍ تلتزم به المشاريع. والمخطّطُ الذي اخترناه هو مخطّطٌ كلاسيكيٌّ قائمٌ على التسلسل. وكما هو معرَّف في PEP 386، فإنَّ صيغته هي:
N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]
حيث:
- N عددٌ صحيح. ويمكنك استعمالُ عددِ قيم N كما تشاء وفصلُها بنقاط، ما دام هناك قيمتان على الأقل (MAJOR.MINOR).
- a و b و c و rc هي محدِّدات ألفا (alpha) وبيتا (beta) ومرشَّحِ إصدار (release candidate). ويتبعها عددٌ صحيح. ولمرشَّحِ الإصدار محدِّدان لأننا أردنا أن يبقى المخطّطُ متوافقًا مع بايثون التي تستعمل rc، لكننا نجد c أبسط.
- dev متبوعًا برقمٍ هو محدِّدُ إصدارٍ تطويري (dev marker).
- وpost متبوعًا برقمٍ هو محدِّدُ ما بعد الإصدار (post-release marker).
وحسب عمليةِ إصدار المشروع، يمكن استخدامُ محدِّدات dev أو post لكلِّ الإصدارات الوسيطة بين إصدارين نهائيين. ومعظمُ العمليات تستخدم محدِّدات dev.
واتّفق هذا المخطّط مع PEP 386 الذي يعرّف ترتيبًا صارمًا:
- alpha < beta < rc < final
- dev < non-dev < post، حيث يمكن أن يكون non-dev إصدار alpha أو beta أو rc أو final
وهذا مثالٌ كامل على الترتيب:
1.0a1 < 1.0a2.dev456 < 1.0a2 < 1.0a2.1.dev456
< 1.0a2.1 < 1.0b1.dev456 < 1.0b2 < 1.0b2.post345
< 1.0c1.dev456 < 1.0c1 < 1.0.dev456 < 1.0
< 1.0.post456.dev34 < 1.0.post456
هدفُ هذا المخطّط هو تيسيرُ الأمر على أنظمة التغليف الأخرى لتحويل إصدارات مشاريع بايثون إلى مخطّطاتها الخاصة. ويرفض PyPI الآن أيَّ مشروعٍ يرفع بياناتٍ وصفيةً متوافقةً مع PEP 345 بأرقامِ إصداراتٍ لا تتبع PEP 386.
التبعيات
يعرِّف PEP 345 ثلاثةَ حقولٍ جديدة تحلُّ محلَّ حقول PEP 314 وهي Requires و Provides و Obsoletes. وهذه الحقولُ هي Requires-Dist و Provides-Dist و Obsoletes-Dist، ويمكن استخدامها عدّةَ مرّاتٍ في البيانات الوصفية.
أمَّا Requires-Dist، فإنَّ كلَّ مُدخَلٍ فيه يحتوي سلسلةَ نصّيةً تسمّي مشروع Distutils آخرَ يحتاجه هذا التوزيع. وصيغةُ سلسلة المتطلّب مطابقةٌ تمامًا لصيغةِ اسمِ مشروع Distutils (مثلًا كما يوجد في الحقل Name)، يتبعها اختياريًّا تصريحُ إصدارٍ بين قوسين. وينبغي أنَّ تطابق أسماءُ مشاريع Distutils هذه الأسماءَ الموجودة في PyPI، ويجب أنَّ تتبع تصريحاتُ الإصدارات القواعدَ الموصوفة في PEP 386. وهذه بعض الأمثلة:
Requires-Dist: pkginfo
Requires-Dist: PasteDeploy
Requires-Dist: zope.interface (>3.5.0)
ويُستخدَم Provides-Dist لتعريف أسماءٍ إضافيةٍ يحتويها المشروع. وهو مفيدٌ حين يريد مشروعٌ أن يندمج مع مشروعٍ آخر. فمثلًا يستطيع مشروع ZODB أن يشمل مشروع transaction ويصرِّح بما يلي:
Provides-Dist: transaction
ويُفيد Obsoletes-Dist في وسم مشروعٍ آخر بأنَّه إصدارٌ قديم:
Obsoletes-Dist: OldName
محدِّداتُ البيئة
محدِّدُ البيئة (environment marker) هو وسمٌ يمكن إضافتُه في نهايةِ حقلٍ ما بعد فاصلةِ منقوطة، ليضيف شرطًا يتعلَّق ببيئة التنفيذ. وهذه بعض الأمثلة:
Requires-Dist: pywin32 (>1.0); sys.platform == 'win32'
Obsoletes-Dist: pywin31; sys.platform == 'win32'
Requires-Dist: foo (1,!=1.3); platform.machine == 'i386'
Requires-Dist: bar; python_version == '2.4' or python_version == '2.5'
Requires-External: libxslt; 'linux' in sys.platform
تُحتفظ لغةُ محدِّدات البيئة الدقيقة (micro-language) عمدًا بسيطةً بما يكفي ليفهمها مبرمجو بايثون غيرُ المتمرّسين: فهي تقارنُ السلاسلَ النصّيةَ بمشغِّلي == و in (ونقيضيهما)، وتسمحُ بالدمج المنطقيّ المعتاد. والحقولُ في PEP 345 التي يمكن أن تستخدم هذا المحدِّد هي:
Requires-PythonRequires-ExternalRequires-DistProvides-DistObsoletes-DistClassifier
14.4.2. ما المُثبَّتُ فعلًا؟
إنَّ وجودَ صيغةِ تثبيتٍ واحدةٍ مشتركةٍ بين جميع أدوات بايثون هو أمرٌ لازمٌ من أجل التوافقية (interoperability). فإذا أردنا للمُثبِّت A أن يكتشف أنَّ المُثبِّت B ثبَّت مشروع Foo من قبل، فيلزم أن يتشاركا ويحدِّثا قاعدةَ بياناتٍ واحدةً للمشاريع المُثبَّتة.
بالطبع، يُفترض بالمستخدمين أن يستخدموا مُثبِّتًا واحدًا في نظامهم، لكن قد يريدون الانتقالَ إلى مُثبِّتٍ أحدثَ له ميزاتٌ خاصة. فمثلًا، يأتي Mac OS X مع Setuptools، فيحصل المستخدمون تلقائيًّا على سكربت easy_install. وإذا أرادوا الانتقالَ إلى أداةٍ أحدث، فيلزم أن تكون متوافقةً مع الإصدارات السابقة (backward compatible).
ومن المشكلات الأخرى عند استعمال مُثبِّتِ بايثون على منصّةٍ لها نظامُ تغليفٍ مثل RPM، انعدامُ وسيلةٍ لإعلام النظام بأنَّ مشروعًا ما يجري تثبيته. والأسوأُ أنَّه، فحتى لو استطاع مُثبِّتُ بايثون أن يُخطرَ نظامَ التغليف المركزي، لَزِمنا أن تتوفّر لدينا خريطةٌ (mapping) بين بيانات بايثون الوصفية وبيانات النظام الوصفية. فاسمُ المشروع مثلًا قد يكون مختلفًا لدى كلٍّ منهما. وقد يحدث ذلك لأسبابَ عِدّة. أشيعُها تعارضُ الأسماء: فمشروعٌ آخر خارج عالم بايثون يستخدم الاسمَ نفسه لحزمة RPM. وسببٌ آخر أنَّ الاسمَ المستخدَم يتضمَّن بادئةَ python وهي تكسر عُرفَ المنصّة. فمثلًا، إن سمّيتَ مشروعك foo-python، فهناك احتمالٌ كبيرٌ أن تُسمَّى حزمةُ Fedora RPM بـ python-foo.
ومن طرائق تفادي هذه المشكلة تركُ تثبيتِ بايثون العامّ كما هو، تحت يد نظام التغليف المركزي، والعملُ في بيئةٍ معزولة. تتيحُ ذلك أدواتٌ مثل Virtualenv.
وعلى أيِّ حال، نحن بحاجةٍ فعلًا إلى صيغةِ تثبيتٍ واحدةٍ في بايثون، لأنَّ التوافقيةَ مسألةٌ تُشغل أنظمةَ التغليف الأخرى أيضًا حين تثبّت مشاريعَ بايثون خاصةً بها. فبمجرد أن يُسجِّل نظامُ تغليفٍ من أنظمة الغير مشروعًا جديدَ التثبيت في قاعدة بياناته الخاصة على النظام، يلزمه أن يولِّد بياناتِ بايثون الوصفيةَ الصحيحةَ لتثبيت بايثون نفسه، بحيث تظهر المشاريعُ بوصفها مُثبَّتةً أمام مُثبِّتي بايثون أو أيّ واجهات برمجيةٍ تستعلم عن تثبيت بايثون.
ويمكن معالجةُ مسألة خريطة البيانات الوصفية في تلك الحالة: فبما أنَّ حزمة RPM تعرف مشاريعَ بايثون التي تلُفّها، تستطيع توليدَ بياناتِ بايثون الوصفيةِ الملائمةِ على مستوى بايثون. فمثلًا، هي تعرف أنَّ python26-webob تُسمَّى WebOb في منظومة PyPI.
والعودةُ إلى معيارنا: يعرِّف PEP 376 معيارًا للحزم المُثبَّتة، صيغتُه شديدةُ الشبه بما تستخدمه Setuptools و Pip. وهذه البنيةُ مجلدٌ ذو امتداد dist-info يحتوي على:
METADATA: البياناتُ الوصفية، كما هو موصوف في PEP 345 و PEP 314 و PEP 241.RECORD: قائمةُ الملفات المُثبَّتةٍ في صيغةٍ شبيهة بـ csv.INSTALLER: اسمُ الأداة المستخدَمة لتثبيت المشروع.REQUESTED: وجودُ هذا الملفِ يدلُّ على أنَّ تثبيت المشروع قد طُلب صراحةً (أي لم يُثبَّت كتبعية).
ومتى فهمت جميعُ الأدوات القائمةُ هذه الصيغةَ، استطعنا أن ندير المشاريع في بايثون من غير اعتمادٍ على مُثبِّتٍ بعينه وعلى ميزاته. كما أنَّ PEP 376 يعرّف البياناتَ الوصفيةَ على أنها مجلد، فإنَّ إضافةَ ملفاتٍ جديدةٍ لتوسيعها تصبح سهلة. وفي الواقع، يمكن إضافةُ ملفِّ بياناتٍ وصفيةٍ جديدٍ اسمه RESOURCES، الموصوفٍ في القسم التالي، في المستقبل القريب من غير تعديل PEP 376. وفي النِّهاية، إن ثبتَ أنَّ هذا الملف الجديد مفيدٌ لجميع الأدوات، فسيُضاف إلى الـ PEP.
14.4.3. بنيةُ ملفات البيانات
كما وُصفت سابقًا، نحتاج إلى تركِ باقون الحزم يقرّرون أين يضعون ملفات البيانات أثناء التثبيت دون كسر شيفرة المطوّر. وفي الوقت نفسه، يجب أن يستطيع المطوّر العملَ مع ملفات البيانات دون أن يقلقَ بشأن مواضعها. وحلُّنا هو الحلُّ المعتاد: التوجيه غير المباشر (indirection).
استخدامُ ملفات البيانات
لنفترض أن تطبيقَ MPTools يحتاج إلى العمل مع ملف إعدادات. سيضع المطوّرُ ذلك الملفَ داخل حزمة بايثون ويستخدم __file__ للوصول إليه:
import os
here = os.path.dirname(__file__)
cfg = open(os.path.join(here, 'config', 'mopy.cfg'))
وهذا يعني أنَّ ملفات الإعدادات تُثبَّت كأنها شيفرة، وأنَّ على المطوّر أن يضعها بجوار شيفرته: في هذا المثال، في دليلٍ فرعيٍّ اسمه config.
وتستخدم البنيةُ الجديدةُ لملفات البيانات التي صمَّمناها شجرةَ المشروع بوصفها جذرَ كلِّ الملفات، وتسمحُ بالوصول إلى أيِّ ملفٍّ في الشجرة، سواء أكان في حزمة بايثون أو في دليلٍ عادي. وقد مكَّن ذلك المطوّرين من إنشاء دليلٍ مخصَّصٍّ لملفات البيانات والوصول إليها باستخدام pkgutil.open:
import os
import pkgutil
# Open the file located in config/mopy.cfg in the MPTools project
cfg = pkgutil.open('MPTools', 'config/mopy.cfg')
يبحث pkgutil.open عن بيانات المشروع الوصفية ويتحقَّق ممّا إذا كانت تحتوي ملف RESOURCES. وهذه خريطةٌ بسيطةٌ تربط الملفاتَ بمواضعها التي قد يحتويها النظام:
config/mopy.cfg {confdir}/{distribution.name}
وهنا يشير المتغيِّر {confdir} إلى دليل إعدادات النظام، ويتضمَّن {distribution.name} اسمَ مشروع بايثون كما يوجد في البيانات الوصفية.

الشكل 14.4: العثور على ملف
ما دام ملفُّ البيانات الوصفية RESOURCES هذا يُنشأ وقت التثبيت، فإنَّ الواجهةَ البرمجيةَ ستجد للمطوّر موضعَ mopy.cfg. وبما أنَّ config/mopy.cfg هو المسارُ النسبيُّ بالنسبة إلى شجرة المشروع، فهذا يعني أننا نستطيع أيضًا أن نوفِّر وضعَ تطويرٍ تُولَّد فيه بياناتُ المشروع الوصفيةُ في موضعها وتُضاف إلى مسارات البحث في pkgutil.
تصريحُ ملفات البيانات
عمليًّا، يستطيع المشروعُ تحديدُ مواضع ملفاتِ البياناتِ التي ينبغي أن تصل إليها بتعريفِ مُوجِّه (mapper) في ملفِّ setup.cfg الخاصِّ به. والمُوجِّهُ قائمةٌ من ثنائيات (نمط بأسلوب glob، هدف). ويشير كلُّ نمطٍ إلى أحد عدّة ملفاتٍ في شجرة المشروع، بينما الهدفُ هو مسارُ تثبيتٍ قد يحتوي متغيّراتٍ بين أقواس مربّعة. فمثلًا، قد يبدو ملفُ setup.cfg الخاصُّ بـ MPTools كما يلي:
[files]
resources =
config/mopy.cfg {confdir}/{application.name}/
images/*.jpg {datadir}/{application.name}/
وستوفِّر وحدةُ sysconfig وتوثِّق قائمةً محدَّدةً من المتغيّرات التي يمكن استخدامها، مع قيمٍ افتراضيةٍ لكل منصّة. فمثلًا، {confdir} هو /etc على Linux. ويستطيع المُثبِّتاتُ إذن استخدامَ هذا المُوجِّهِ مقترنًا بـ sysconfig وقتَ التثبيت ليعرفوا أين ينبغي وضعُ الملفات. وفي النِّهاية، سيولِّدون ملفَّ RESOURCES المذكورَ سابقًا ضمن البيانات الوصفية المُثبَّتة، ليتمكّن pkgutil من العثور على الملفات مجدَّدًا.

الشكل 14.5: المُثبِّت
14.4.4. تحسيناتُ PyPI
ذكرتُ سابقًا أنَّ PyPI كان نقطةَ فشلٍ وحيدةً فعليًّا. يعالج PEP 380 هذه المشكلةَ بتعريفِ بروتوكولِ نسخٍ متطابق (mirroring) كي يتمكّن المستخدمون من الرجوعِ إلى خوادمَ بديلةٍ حين يتعطَّل PyPI. والهدفُ هو تمكين أعضاء المجتمع من تشغيل مرايا (mirrors) حول العالم.

الشكل 14.6: النسخ المتطابق
تُتاح قائمةُ المرايا في صورةِ قائمةٍ من أسماءِ المضيفات على الشكل X.pypi.python.org، حيث يقع X في التسلسل a,b,c,…,aa,ab,…. إنَّ a.pypi.python.org هو الخادمُ الرئيس (master)، وتبدأ المرايا من b. ويشير سجلُّ CNAME باسم last.pypi.python.org إلى اسمِ المضيف الأخير، فيستطيع العملاءُ الذين يستخدمون PyPI الحصولَ على قائمةِ المرايا بالنظر في سجلّ CNAME.
فمثلًا، يخبرنا هذا الاستدعاء بأنَّ آخر مرآةٍ هي h.pypi.python.org، ما يعني أنَّ لدى PyPI حاليًّا ستَّ مرايا (من b إلى h):
>>> import socket
>>> socket.gethostbyname_ex('last.pypi.python.org')[0]
'h.pypi.python.org'
ويُتيح هذا البروتوكولُ، على الورق، للعملاءِ تحويلَ الطلباتِ إلى أقرب مرآةٍ عبر تحديد المرايا مكانيًّا حسب عناوين IP الخاصة بها، وكذلك الرجوعَ إلى المرآة التالية إذا تعطَّلت مرآةٌ أو الخادمُ الرئيس. وبروتوكولُ النسخ المتطابق نفسه أعقدُ من مجرّد rsync، لأننا أردنا الإبقاءَ على إحصاءاتِ التنزيلات دقيقةً وتوفيرَ أمنٍ أدنى.
المزامنة
يجب أن تُقلِّص المرايا كمَّ البياناتِ المنقولةَ بين الخادم المركزي والمرآة. ولتحقيق ذلك، يجب أن تستخدم استدعاءَ changelog عبر XML-RPC في PyPI، ولا تجلب إلّا الحزمَ التي تغيَّرت منذ المرة السابقة. ولكلِّ حزمة P، يجب أن تنسخ المستندَين /simple/P/ و /serversig/P.
وإذا حُذفت حزمةٌ على الخادم المركزي، فيجب أن تحذفَ الحزمةَ وكلَّ الملفات المرتبطة بها. وللكشف عن تعديل ملفات الحزمة، يمكنها تخزينُ ETag للملف مؤقتًا، وطلبُ تخطّي المجلوبِ باستخدام ترويسة If-None-Match. ومتى انتهت المزامنة، تُغيِّر المرآةُ last-modified الخاصَّ بها إلى التاريخ الحالي.
انتشارُ الإحصاءات
حين تُنزِّل إصدارًا من أيِّ المرايا، يضمن البروتوكولُ أنَّ تُرسَل عمليةُ التنزيل إلى خادم PyPI الرئيس، ثمَّ إلى المرايا الأخرى. ويضمن هذا أنَّ يحصل الأشخاصُ أو الأدواتُ التي تتصفَّح PyPI لمعرفة كم مرّةٍ نُزِّل الإصدارُ على قيمةٍ مجمَّعةٍ عبر جميع المرايا.
تُجمَّع الإحصاءاتُ في ملفات CSV يوميةٍ وأسبوعيةٍ داخل دليل stats في PyPI المركزي نفسه. ويحتاج كلُّ مرآةٍ أن توفِّر دليلَ local-stats يحوي إحصاءاتها الخاصة. ويوفِّر كلُّ ملفٍّ عددَ التنزيلات لكلِّ أرشيفٍ، مُجمَّعًا حسب وكيلِ الاستخدام. ويزور الخادمُ المركزيُّ المرايا يوميًّا لجمع تلك الإحصاءات، ثمَّ يعيدُ دمجَها في دليل stats العامّ، لذا يجب أن تُبقي كلُّ مرآةٌ local-stats محدَّثةً مرّةً على الأقلِّ في اليوم.
مصداقيةُ المرايا
في أيِّ نظامِ نسخٍ متطابقٍ موزَّع، قد يريد العملاءُ التحقّقَ من صحّةِ النسخِ المُطابِقة. وتشمل بعضُ التهديداتِ الممكنة:
- قد يُخترق الفهرسُ المركزي،
- وقد تُعبَّث بالمرايا،
- أو هجومُ رجلٍ في المنتصف (man-in-the-middle) بين الفهرس المركزي والمستخدم النهائي، أو بين مرآةٍ والمستخدم النهائي.
وللكشف عن الهجوم الأول، يحتاج مؤلِّفو الحزم إلى توقيعِ حزمهم بمفاتيح PGP، ليتمكّن المستخدمونَ من التحقّق من أنَّ الحزمةَ جاءت من المؤلِّف الذي يثقون به. أمَّا بروتوكولُ النسخ المتطابق نفسه فيعالج التهديدَ الثاني فقط، مع أنَّ هناك محاولةٌ ما للكشف عن هجمات رجل في المنتصف.
ويوفِّر الفهرسُ المركزيُّ مفتاحَ DSA على العنوان /serverkey، بصيغة PEM كما يولِّدها openssl dsa -pubout[3]. ولا يجوز نسخُ هذا العنوان، ويجب أن تجلبَ العملاءُ serverkey الرسميَّ من PyPI مباشرةً، أو تستخدموا النسخةَ التي جاءت مع برمجية عميل PyPI. وينبغي للمرايا مع ذلك أن تُنزِّل المفتاحَ كي تتمكّن من كشف تدويرِ المفاتيح (key rollover).
ولكلِّ حزمةٍ، تُوفَّر نسخةٌ موقَّعةٌ (mirrored signature) على العنوان /serversig/package. وهذه هي توقيعُ DSA الخاصُّ بالعنوان المقابل /simple/package، بصيغة DER، باستخدام SHA-1 مع DSA[4].
ويحتاج العملاءُ الذين يستخدمون مرآةً إلى تنفيذ الخطوات التالية للتحقّق من حزمة:
- نزِّل صفحةَ
/simple، واحسب تجزئةَ SHA-1 الخاصة بها. - احسب توقيعَ DSA لتلك التجزئة.
- نزِّل
/serversigالمقابلَ، وقارِنه بايتًا ببايت مع القيمة المحسوبة في الخطوة 2. - احسب تجزئاتِ MD5 لكلِّ الملفات التي تُنزِّلها من المرآة، وتحقَّق منها (مقابل صفحة
/simple).
ولا حاجة إلى التحقّق عند التنزيل من الفهرس المركزي، ولا ينبغي للعملاء أن يفعلوا ذلك لتقليل الحمل الحسابي.
مرّةً في السنة تقريبًا، سيُستبدَل المفتاحُ بمفتاحٍ جديد. وستضطر المرايا إلى إعادة جلب جميع صفحاتِ /serversig. ويحتاج العملاءُ الذين يستخدمون المرايا إلى العثور على نسخةٍ موثوقةٍ من مفتاح الخادم الجديد. ومن طرائق الحصول على واحدةٍ تنزيلُه من https://pypi.python.org/serverkey. وللكشف عن هجمات رجل في المنتصف، يحتاج العملاءُ إلى التحقّق من شهادة خادم SSL، التي ستوقُّعها جهةُ CACert.
14.5. تفاصيلُ التنفيذ
يجري تنفيذُ معظمِ التحسيناتِ الموصوفة في القسم السابق في Distutils2. ولم يعد ملفُّ setup.py مستخدَمًا، ويُوصَف المشروعُ بالكامل في setup.cfg، وهو ملفٌّ ساكنٌ شبيهٌ بـ .ini. وبذلك نُسهِّل على باقوني الحزم تغييرَ سلوكِ تثبيتِ المشروع دون التعامل مع شيفرة بايثون. وهذا مثالٌ على ملفٍّ من هذا النوع:
[metadata]
name = MPTools
version = 0.1
author = Tarek Ziade
author-email = tarek@mozilla.com
summary = Set of tools to build Mozilla Services apps
description-file = README
home-page = http://bitbucket.org/tarek/pypi2rpm
project-url: Repository, http://hg.mozilla.org/services/server-devtools
classifier = Development Status :: 3 - Alpha
License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)
[files]
packages =
mopytools
mopytools.tests
extra_files =
setup.py
README
build.py
_build.py
resources =
etc/mopytools.cfg {confdir}/mopytools
ويستخدم Distutils2 ملفَّ الإعدادِ هذا من أجل:
- توليد ملفات بياناتٍ وصفيةٍ بصيغة
META-1.2يمكن استخدامها في إجراءاتَ متنوّعة، مثل التسجيل في PyPI. - تشغيل أيِّ أمرِ من أوامر إدارة الحزم، مثل
sdist. - تثبيت مشروعٍ قائمٍ على
Distutils2.
كما ينفِّذ Distutils2 وحدةَ VERSION عبر وحدته version.
سيجد تنفيذُ INSTALL-DB طريقَه إلى المكتبة القياسية في بايثون 3.3، وستكون في وحدة pkgutil. وفي هذه الفترة، تتوفَّر نسخةٌ من هذه الوحدة في Distutils2 للاستعمال الفوري. وستتيح لنا الواجهاتُ البرمجيةُ الموفَّرةُ تصفّحَ التثبيت ومعرفةَ ما ثُبِّت بالضبط.
وتُعدُّ هذه الواجهاتُ البرمجيةُ الأساسَ لبعض مزايا Distutils2 الرائعة:
- المُثبِّت ومُزيلُ التثبيت
- عرضُ شبكة التبعيات للمشاريع المُثبَّتة
14.6. الدروسُ المستفادة
14.6.1. الأمرُ كلُّه متعلِّقٌ بـ PEPs
إنَّ تغييرَ معماريةٍ واسعةٍ ومعقّدةٍ مثل تغليف بايثون يحتاج إلى أن يُنجَز بعنايةٍ عبر تغيير المعايير ضمن عملية PEP. وتغييرُ PEP أو إضافتُه يستغرق، من تجربتي، نحو سنة.
كان أحدُ الأخطاء التي ارتكبَها المجتمعُ في الطريق أنَّه قدَّم أدواتٍ حلَّت بعضَ المشكلاتِ بتوسيع البيانات الوصفية وطريقةِ تثبيتِ تطبيقات بايثون، من غير أن يحاول تغييرَ PEPs المتأثِّرة.
بعبارةٍ أخرى، كانت التطبيقاتُ تُثبَّت بطرقَ مختلفةٍ حسب الأداة التي تستخدمها، أي المكتبةُ القياسية Distutils أو Setuptools. حُلَّت المشكلاتُ لجزءٍ من المجتمع الذي يستخدم هذه الأدوات الجديدة، لكنَّ ذلك أضاف مشكلاتٍ أكثرَ لبقية العالم. فقد واجه باقون حزم أنظمة التشغيل، على سبيل المثال، عدةَ معاييرَ بايثون: المعيارَ الرسميَّ الموثَّق، والمعيارَ الفعليَّ الذي فرضته Setuptools.
لكنَّه في أثناء ذلك، تتيح Setuptools في آنٍ واحدٍ تجربةَ بعض الابتكارات على نطاقٍ واقعيٍّ (المجتمعُ بأسره)، بسرعةٍ بالغة، وكانت الملاحظاتُ التي رُدَّت لا تُقدَّرُ بثمنٍ. واستطعنا كتابةَ PEPs جديدةٍ بمزيدٍ من الثقة بشأن ما نجح وما لم ينجح، وربما كان من المستحيل فعل ذلك بطريقةٍ أخرى. فالأمرُ كلُّه هو التعرُّفُ على اللحظةِ التي تُطلق شرارة تغييرٍ في الـ PEP.
14.6.2. الحزمةُ التي تدخلُ المكتبةَ القياسيةَ إحدى قدميها في القبر
أنا أُعِيد صياغةَ عبارة Guido van Rossum في عنوان هذا القسم، لكنَّ ذلك جانبٌ واحدٌ من فلسفةِ «البطاريات مُضمَّنة» (batteries-included) في بايثون، وهو جانبٌ أثّر كثيرًا في جهودنا.
Distutils جزءٌ من المكتبة القياسية، وسيصبح Distutils2 كذلك قريبًا. الحزمةُ الموجودةُ في المكتبة القياسية صعبةُ التطوّر جدًا. وهناك بالطبع عملياتُ إهمالٍ (deprecation) يمكنكَ خلالها إسقاطَ واجهةٍ برمجيةٍ أو تغييرَها بعد إصدارين ثانويَّين من بايثون. لكنَّه بمجرَّد نشر واجهةٍ برمجيةٍ فإنَّها ستبقى هناك سنواتٍ طويلة.
لذلك، فإنَّ أيَّ تغييرٍ تُجريه في حزمةٍ ضمن المكتبة القياسية لا يُعدُّ إصلاحًا لعِللٍ، يمثّل اضطرابًا محتمَلًا في المنظومة البيئية. فحين تُجري تغييراتٍ مهمّة، عليك أن تنشئ حزمةً جديدة.
وقد تعلّمتُ ذلك بالطريقة الصعبة مع Distutils، إذ اضطررتُ في النهاية إلى التراجع عن كلِّ التغييرات التي أجريتها عليه لأكثرَ من سنة وإنشاء Distutils2. وفي المستقبل، إذا تغيَّرت معاييرُنا مجدَّدًا
14.6.3. التوافقُ الخلفي
تغييرُ طريقة عمل التغليف في بايثون عمليةٌ طويلةٌ جدًّا: فمنظومةُ بايثون البيئيةُ تحتوي على مشاريعَ كثيرةٍ جدًّا قائمةٍ على أدوات تغليفٍ أقدم، لذا سيُثار قدرٌ كبيرٌ من المقاومة للتغيير (وستستمر). (وقد استغرقَ بلوغُ توافقٍ في بعض الموضوعات التي ناقشناها في هذا الفصل عدةَ سنوات، بدلًا من بضعة أشهرٍ كما توقَّعتُ في الأصل.) ومثل بايثون 3، ستمرّ سنواتٌ قبل أن تنتقل كلُّ المشاريع إلى المعيار الجديد.
ولذلك يجب أن يكون كلُّ ما نفعله متوافقًا خلفيًّا مع جميع الأدوات والتثبيتاتِ والمعايير السابقة، وهو ما يجعل تنفيذَ Distutils2 مسألةً شاقّةً (wicked problem).
فمثلًا، إذا اعتمد مشروعٌ يستعمل المعاييرَ الجديدة على مشروعٍ آخر لا يستعملها بعد، فلا يمكننا إيقافَ عمليةِ التثبيتِ بإخبار المستخدم النهائي بأنَّ التبعيةَ بصيغةٍ غير معروفة!
فمثلًا، يحتوي تنفيذُ INSTALL-DB على شيفرةِ توافقٍ تتيح تصفّحَ المشاريعِ المُثبَّتةِ بواسطة Distutils الأصلية أو Pip أو Distribute أو Setuptools. كما يستطيع Distutils2 تثبيتَ المشاريعِ التي أُنشئت بواسطة Distutils الأصلية بتحويل بياناتها الوصفية على الذَّب.
14.7. المراجعُ والمساهمات
أُخذت بعضُ الأقسام في هذه الورقة مباشرةً من مستندات PEP المختلفة التي كتبناها لأجل التغليف. ويمكنك العثورُ على المستندات الأصلية في http://python.org:
- PEP 241: البياناتُ الوصفية لحزم برمجيات بايثون 1.0:
http://python.org/peps/pep-0214.html - PEP 314: البياناتُ الوصفية لحزم برمجيات بايثون 1.1:
http://python.org/peps/pep-0314.html - PEP 345: البياناتُ الوصفية لحزم برمجيات بايثون 1.2:
http://python.org/peps/pep-0345.html - PEP 376: قاعدةُ بياناتِ توزيعات بايثون المُثبَّتة:
http://python.org/peps/pep-0376.html - PEP 381: بنيةُ البنية التحتيةِ للنسخ المتطابق في PyPI:
http://python.org/peps/pep-0381.html - PEP 386: تغييرُ وحدةِ مقارنة الإصدارات في Distutils:
http://python.org/peps/pep-0386.html
وأودُّ أن أشكرَ جميعَ الأشخاص الذين يعملون على التغليف؛ ستجد أسماءَهم في كلِّ PEPٍ ذكرتُه. وأودُّ كذلك أن أشكرَ خاصةً جميعَ أعضاء «زمالة التغليف» (Fellowship of the Packaging). كما أشكر Alexis Metaireau و Toshio Kuratomi و Holger Krekel و Stefane Fermigier على ملاحظاتهم على هذا الفصل.
والمشاريعُ التي نُوقشت في هذا الفصل هي:
Distutils:http://docs.python.org/distutilsDistutils2:http://packages.python.org/Distutils2Distribute:http://packages.python.org/distributeSetuptools:http://pypi.python.org/pypi/setuptoolsPip:http://pypi.python.org/pypi/pipVirtualenv:http://pypi.python.org/pypi/virtualenv
الحواشي
- مقترحاتُ تحسين بايثون (Python Enhancement Proposals) أو PEPs التي نشيرُ إليها مُلخَّصةٌ في نهاية هذا الفصل
- كان سابقًا يُعرف باسم CheeseShop.
- أي RFC 3280 SubjectPublicKeyInfo، مع الخوارزمية 1.3.14.3.2.12.
- أي بصيغة RFC 3279 Dsa-Sig-Value، التي تنشئها الخوارزمية 1.2.840.10040.4.3.