وقتی برنامهنویسها تغییراتی را روی پروژه خود اعمال میکنند، آن را در قالب یک نسخه جدید منتشر مینمایند و برای اینکه بین نسخههای مختلف تفکیک قائل شوند، برای هر کدام شمارهای در نظر میگیرند. برای تعیین اینکه شماره نسخهی پروژه چه باید باشد، استانداردهای مختلفی تعریف شدهاست. اکثر استانداردهای نسخهبندی نرم افزار در دنیا، از یک ساختار دنبالهای پیروی میکنند. شماره نسخهها معمولاً دنبالهای از چند عدد و یا احیاناً همراه با چند کاراکترند که هر کدام بیانگر مفهومی هستند، و هر چه این اعداد و این کاراکترها بیشتر باشند و جلوتر بروند، بدین معنیست که نسخه یاد شده جدیدتر است. البته ممکن است شرکتها و سازمانهای مختلف برای محصولات خود، یک استاندارد داخلی تعریف کنند و به آن پایبند باشند، مثلاً شرکت apple برای نسخهبندی محصولاتش یک سری اصول دارد، شرکت microsoft برای ویندوزهایش یک سری اصول، زبان برنامهنویسی python یک سری و ... .
اما میتوانیم بگوییم مهمترین، معروفترین و پراستفادهترین این استانداردها semantic versioning، یا به اختصار SemVer میباشد. ویژگیهای SemVer را شخصی به نام Tom Preston-Werner نوشته که اتفاقاً یکی از بنیانگذاران گیت هاب نیز هست.
در این مقاله به توضیح مفصل استاندارد SemVer خواهیم پرداخت، با ما همراه باشید...
جا دارد اشاره کنیم در زمان نگارش این مقاله، نسخهی این استاندارد 2.0.0 است و مطالب بر اساس مستندات این شماره نسخه تنظیم شدهاند.
در ساختار نسخهبندی SemVer، شماره نسخهی هر پروژه، با چند عدد مشخص میشود که جایگاه هر کدام از این اعداد، یک مفهوم خاص دارد. شمای کلی نسخه پروژهها به این شکل است:
دقت داشته باشیم که حضور سه مورد اول (major، minor و patch) در بیان شماره نسخه یک برنامه الزامیست و دو مورد بعدی دلخواه میباشند. معنی هر کدام نیز:
· major: مشخص کننده نسخه اصلی پروژه است. هر بار که برنامهنویس، پروژه را بطور کامل و کلی تغییر میدهد، آن را یک شماره بالا میبرد. یعنی هر موقع نسخهای از پروژه آماده شد که تغییرات بزرگ و اساسی در آن انجام گرفته، این عدد تغییر میکند. وقتی نسخه major تغییر میکند، مشکل backward compatibility بوجود میآید. این عبارت را می توان اینگونه ترجمه کرد: «سازگاری با نسخه های قبلی». این ترکیب را در حوزه مهندسی برق و کامپیوتر زیاد می شنوید. اگر نسخهی جدید یک محصول، با نسخههای قبلیاش سازگاری نداشته باشد، conflict داشته باشد و در کل ویژگیهایشان یا طرز استفاده آنها با یکدیگر همخوانی نداشته باشند، اصطلاحاً گفته میشود آن نسخه از محصول مشکل backward compatibility دارد. یا اگر این مشکل بین نسخههای قدیمی و جدیدش وجود نداشته باشد، میگویند آن نسخهی جدید backward compatible است.
· minor: مشخص کنندهی نسخه فرعی پروژه است. یعنی تغییرات کوچک. مثلاً اگر یک تابع یا یک ابزار یا ... را به نسخه اصلی پروژه اضافه کنیم، یک شماره به عدد مربوط به نسخه minor اضافه میشود. نسخه فرعی میتواند قابلیت جدیدی به پکیج اضافه کند، اما نباید تغییری ایجاد کند که مشکل backward compatibility پیش بیاید. در مورد مفهوم backward compatibility و جایگاه آن در استانداردگذاری پروژه ها، در ادامه توضیحات بیشتری داده ایم.
· patch: این عدد مربوط به patch ها و bugfix هاست. توسعه دهندههای پروژه وقتی یک باگ را که مربوط به نسخه فرعیست برطرف میکنند، عدد patch را یک شماره بالا میبرند و دوباره آن را منتشر میکنند. در نسخه patch معمولاً قابلیتی به پروژه اضافه نمیشود و صرفاً همان رفع مشکل و احیاناً بهبود کارآیی کد اتفاق میافتد. طبیعیست از آنجایی که رخ دادن باگ خیلی محتمل است و توسعهدهندهها مدام در حال bugfix هستند، این عدد به سرعت پیش میرود. واژه patch به معنی وصله، وصله کردن و تعمیر کردن است. همانطور که میبینید، نام آن نیز بیانگر کارکردش است.
گفتیم که نسخهبندی بر اساس semantic versioning حداقل باید سه مورد بالا را داشته باشد. و اما دو مورد دیگر که دلخواه هستند:
· pre-release label: میشود با اضافه کردن یک علامت خط تیره (hyphen، یا همان dash معروف) در ادامهی عدد مربوط به patch، این برچسب را اضافه کرد. مثلاً
1.0.0-alpha، 2.0.0-beta.3
این برچسب به ما میگوید که نسخه پایدار(stable) نیست. و نمیتواند همهی معیارهایی را که قرار است نسخهی متناظرش داشته باشد بدون اشکال محقق کند، و نیاز به تکمیل دارد. منظورمان از نسخه متناظرِ یک شماره نسخهی برچسبدار، همان نسخه است ولی بدون برچسب. قالب این برچسب از یک سری کاراکتر تشکیل شده که با نقطه (.) از هم جدا شدهاند. به هرکدام از قسمتهای برچسب pre-release که بین نقطهها قرار دارند یک علامت (identifier) میگویند. همانطور که از اسم pre-release مشخص است، کاربرد مهم این نحوه برچسب گذاری قبل از انتشار یک نسخه اصلی، آن است که نسخه مذکور توسط یک گروهِ تست(test group) به خوبی تست شود و از آن بازخورد گرفته شود. ما معمولاً در این قسمت، سه برچسب alpha، beta و rc را زیاد میبینیم. غالباً برچسب alpha به این معنیست که قابلیتهای نسخهی جدید هنوز کامل نشدهاند. beta یعنی قابلیتهای جدید کامل شده ولی نیاز دارند که اصلاحاتی(refactor هایی) روی کد صورت بگیرد، و rc که مخفف (release candidate است) بدین معنیست که به احتمال قوی کد مربوط به همین برچسب منتشر میشود، مگر اینکه در لحظات پایانی bug ای اتفاق بیفتد. در برخی متن ها pre-release label را با عبارت pre-release tag نیز بیان می کنند.
·metadata یا build metadata: میتوان با اضافه کردن یک علامت بعلاوه (+) بعد از عدد patch (یا اگر برچسب pre-release وجود داشت بعد از آن)، برچسب metadata اضافه کرد. این برچسب نوعی کامنت است که ما روی شماره نسخه میزنیم. مثلاً
1.0.0-alpha+001، 1.0.0+20130313144700 ، 1.0.0-beta+exp.sha.5114f85
اینکه عبارت مربوط به برچسب pre-release و همینطور metadata چه کاراکترهایی میتواند داشته باشد و از چه قوانینی پیروی میکند، بصورت مفصل در وبسایت رسمی semver.org توضیح داده شده.
تقدم و تأخر نسخهها (مبحث precedence)
برای تشخیص اینکه چه شماره نسخهای از نسخهی دیگر جدیدتر است (بحث precedence)، اعداد مربوط به نسخهها را از چپ به راست (اول عدد نسخه اصلی، بعد عدد نسخه فرعی، بعد patch و بعد برچسب pre-release) بررسی میکنیم، هر جا که عددی بزرگتر بود، آن شماره نسخه جدیدتر است. نسخه جدیدتر یعنی نسخه ای که precedence بیشتری دارد یا دیرتر منتشر شده. نحوه مقایسه نسخه ها در این استاندارد، درست مثل مقایسه اعداد در مبناهای مختلف است. مثلاً:
1.0.0 < 2.0.0 < 2.1.0 < 2.1.1
نکته آنکه وقتی اعداد مربوط به نسخه اصلی، فرعی و patch دو نسخه با هم برابر بودند، نسخهای که برچسب دارد قدیمیتر محسوب میشود. مثلاً:
1.0.0-alpha < 1.0.0
حال اگر در مقایسه دو شماره نسخه، هر سه عدد نسخه اصلی، فرعی و patch یکی بودند و هر دوی آنها نیز برچسب pre-release داشتند، برچسبها با این قوانین مقایسه میشوند:
1. از چپ به راست علامتها (عبارتهای بین نقطهها) را با هم مقایسه میکنیم، آن علامتهایی که از جنس عدد هستند بصورت عددی مقایسه میشوند و هر کدام که بیشتر بود نسخهی جدیدتر به حساب میآید. در کل هم علامتهای عددی نسبت به علامتهای کاراکتری قدیمیتر محسوب میشوند (یعنی مثلاً عدد 3 از رشته beta قدیمیتر است). و آن علامتهایی که کاراکتری هستند، از چپ به راست هر کدام که در جدول ASCII کد پایینتری داشته باشند، قدیمیترند.
2. زمانی که علامتهای ابتدایی یکی باشند، شماره نسخهای که تعداد علامتهای بیشتری دارد جدیدتر محسوب میشود.
در مثال زیر میتوانیم قوانین بالا را بهتر مشاهده کنیم:
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0
مشکل backward compatibility
در استاندارد SemVer، تنها زمانی که نسخه major یک پکیج ارتقا پیدا میکند، ممکن است مشکل backward compatibility پیش بیاید. به عبارت دیگر موقع ارتقاء نسخه minor یا patch، نباید چنین مشکلی داشته باشیم. فرض کنیم ما داریم از پکیجی در پروژهمان استفاده میکنیم. با گذشت زمان و وقتی نسخه اصلی این پکیج ارتقاء مییابد، نیاز است در پروژه کدی را که مربوط به این پکیج بوده بازبینی و ویرایش کنیم و با نسخه جدید پکیج سازگارش کنیم. البته به شرط اینکه نسخهبندی پکیج اصولی و استاندارد صورت گرفته باشد. این را هم باید در نظر بگیریم که گاهی ما بعنوان استفادهکننده یا کاربر یک پکیج، ممکن است تنها بخشی از قابلیتهای آن پکیج را استفاده کنیم، حال اگر بعد از ارتقا نسخه اصلی، علیرغم اینکه تغییرات اساسی و breaking change رخ داده، در آن قسمت از کد پکیج که ما از آن استفاده کردهایم تغییری رخ نداده باشد، دیگر نیازی نیست کدمان را تغییر بدهیم.
تیم توسعه پکیجها معمولاً یک دستورالعمل برای ارتقا آنها در اختیار قرار میدهند. مثلاً در داکیومنت خود ذکر میکنند که در ارتقا پروژه از نسخهای به نسخهی دیگر، تابع A به پروژه اضافه شد، قابلیت B بهبود پیدا کرد و ویژگی C حذف شد. و اگر نیاز باشد به ازای این تغییرات، برنامهنویس (کاربر) قسمتهایی از کدش را تغییر بدهد، یا از سینتکس جدیدی برای استفاده از قابلیتهای قبلی استفاده کند، در همان دستورالعمل بدان اشاره میکنند. یا وقتی که یک قابلیت حذف میشود، اشاره میکنند که اگر قبلاً کسی از آن قابلیت استفاده میکرده، از این موضوع مطلع شود. برنامه نویسان با این نسخهبندیها زیاد درگیر هستند. یک توسعهدهنده دائماً با ارتقاء پکیجهای پروژهاش سر و کار دارد، مشکل هماهنگی بین آنها را حل میکند، از قابلیتهای جدید آنها در کدش استفاده میکند و ... . به همین دلیل، خوب است که همه توسعهدهندهها با یک روال مرسوم آشنایی داشته باشند:
وقتی در پروژهمان نسخهی یک پکیج را ارتقا میدهیم، باید سعی کنیم حتماً به داکیومنت یا وبسایت رسمی آن پکیج سر بزنیم و مستندات مربوط به ارتقاءش را مطالعه کنیم. برای این کار، دنبال کلیدواژههایی مثل change log، migration، breaking change و امثال اینها میگردیم.
بخش change log در داکیومنتها، معمولاً لیست نسخهها را همراه با لیست تغییرات اعمال شده در آن نسخهها برایمان میآورد.
بخش migration هم معمولاً به ما میگوید که ضمن تغییر پکیج از یک نسخه قدیمی به نسخهی جدید، لازم است چه تغییراتی را در کدمان اعمال کنیم.
و بخش breaking change هم میگوید کدام یک از تغییرات اعمال شده در پکیج، برنامهنویس را ملزم به تغییر کد قبلیِ مربوط به آن قسمت میکند.
نکاتی در مورد استاندارد SemVer
· پروژه، محصول، پکیج، یا هر کدی که از استاندارد SemVer برای نسخهبندیاش استفاده میکند، باید یک public api در اختیار کاربرانش قرار دهد. این public api در واقع همان راه استفاده کاربران از محصول است، مثلاً اینکه چه توابعی باید فراخوانی شوند تا از آن پکیج استفاده کنیم و ... . این public api باید دقیق و جامع باشد. منظور از public api همان مفهوم همیشگی api است که همه برنامهنویسها در ذهن دارند. یعنی راه و روش استفاده کاربران از قابلیتهای یک کد.
· وقتی یک نسخه از یک پکیج منتشر(release) شد، دیگر نباید محتوای آن نسخه تغییر کند.
· برای شروع توسعه یک پروژه، و وقتی هنوز هیچ انتشار رسمی از آن صورت نگرفته، عدد نسخه اصلی باید 0 باشد. مثلاً وقتی شماره نسخه پکیجی 0.X.Y است، یعنی هنوز در مرحله توسعه اولیه میباشد و public api آن پایدار نیست. برنامهنویسها وقتی شروع به توسعه پروژه میکنند، کار را با شماره نسخه 0.1.0 آغاز میکنند. بعداً وقتی پروژهشان برای اولین بار آماده ی فاز production شد، یا به حالت پایدار رسید، میتوانند شماره نسخهاش را 1.0.0 بگذارند و آن را منتشر کنند.
· بدیهیست که وقتی عدد یک جایگاه اضافه میشود، اعداد جایگاههای پایینتر 0 میشوند. مثلاً اگر نسخه فعلی یک پکیج 1.4.7 باشد و ما بخواهیم یک عدد به عددِ نسخه major آن اضافه کنیم، یعنی نسخه اصلیاش 2 شود، شماره کلی نسخه مان اینگونه خواهد شد: 2.0.0، یا اگر نسخه فعلی پکیجمان 2.5.1 باشد و بخواهیم نسخه فرعی را یک شماره زیاد کنیم، عدد نسخهمان اینگونه خواهد بود: 2.6.0
برای درک بهتر این موضوع، خوب است سری به این آدرس بزنیم:
https://jubianchi.github.io/semver-check/#/
· اگر خواستیم به یک بخش(مثلاً یک قابلیت یا یک functionality) از public api تگ deprecated بزنیم، باید عدد نسخه minor را اضافه کنیم.( deprecated یعنی منسوخ شده. در دنیای برنامهنویسی وقتی یک تکنولوژی یا یک قابلیت دیگر پشتیبانی نمیشود، میگویند deprecated است.) بهتر و اصولیتر آن است که وقتی میخواهیم یک کارکرد پروژهمان را حذف کنیم، قبل از حذف کردن، ابتدا حداقل طی یک مرتبه آپدیت نسخه minor، در داکیومنت اعلام کنیم که کارکرد مذکور در حال deprecate شدن است، یعنی در یک مرتبه ارتقاء نسخه فرعی برچسب deprecated را در داکیومنت به آن بزنیم، ولی آن را حذف نکنیم، و در ارتقاء نسخه اصلی بعد به کلی حذفش کنیم. به این ترتیب به کاربر فرصت اطلاع یافتن میدهیم تا بتواند با یک شیب ملایم و با ریسک کمتر آن قسمت را از کدش حذف کند.
این هم یک لیست خلاصه از بازه های semver: