> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# أدوات البناء Build Tools وماذا يحدث عندما تبني مشروعك
- URL: https://arabroot.io/articles/ادوات-البناء-build-tools-من-make-الى-maven-و-gradle-و-dotnet/
- Published: 2026-10-07T15:30:00.000Z
- Updated: 2026-10-09T13:02:30.000Z
- Description: ما الذي يحدث بين ضغطة البناء والملف الناتج، ولماذا ينجح البناء على جهازك ويفشل على سيرفر CI، ومتى يعطيك نفس النتيجة مرتين.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, DevOps وCI/CD, الاستضافة الذاتية

كل مطور يضغط زر البناء Build في محرره عشرات المرات في اليوم، وفي خط CI يكتب سطراً مثل `mvn package` أو `dotnet build` وينتظر النتيجة الخضراء، وغالباً لا يسأل نفسه ماذا حدث بين الضغطة والملف الناتج حتى يأتي يوم يفشل فيه البناء على سيرفر CI وينجح على جهازه، أو يأخذ البناء عشر دقائق بعد أن كان دقيقة، أو يطلب منك المدقق أن تثبت أن الملف المنشور في الإنتاج بني فعلاً من الكود الموجود في المستودع، وعندها تكتشف أن «البناء» عملية لها خطوات ومدخلات ومخرجات، وأن فهمها يوفر عليك ساعات من التخمين.

وفي [مقال أنماط البرمجة وأنظمة الأنواع](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) تكلمنا عن الفرق بين اللغات المترجمة Compiled واللغات المفسرة Interpreted، وفي هذا المقال سوف ننتقل خطوة إلى الأمام ونرى ما الذي تفعله أدوات البناء Build Tools فوق المترجم Compiler. وسوف نناقش في هذا المقال ما يلي:

- ماذا تعني كلمة البناء: الترجمة Compile والربط Link والاختبار Test والتحزيم Package والناتج Artifact.
- Make كأصل الفكرة، ولماذا ما زالت قاعدته تعمل في كل أداة حديثة.
- Maven ودورة حياته وملف pom.xml، ثم Gradle، ثم dotnet وMSBuild، ثم npm scripts والمجمعات Bundlers باختصار، ثم البناء المدمج في Go وCargo.
- حل الاعتماديات Dependency Resolution وملفات القفل Lock Files باختصار.
- البناء التدريجي Incremental Build وذاكرة البناء Build Cache.
- البناء القابل للتكرار Reproducible Build: ما تعريفه، وما الذي يكسره.
- كيف يعمل البناء داخل خط CI.
- تجربة عملية نبني فيها نفس البرنامج الصغير بـ Maven وGradle وdotnet داخل Containers، مع المخرجات والأوقات الحقيقية، ثم Make وGo باختصار.

## ماذا يحدث عندما تضغط زر البناء؟

البناء Build هو تحويل الكود المصدري Source Code الذي يكتبه الإنسان إلى ملف أو مجموعة ملفات يمكن تشغيلها أو نشرها، ويسمى هذا الملف الناتج Artifact، مثل ملف `jar` في Java، أو ملف `dll` في .NET، أو ملف تنفيذي Executable في Go وC، أو مجلد `dist` فيه ملفات JavaScript مضغوطة لموقع ويب. والصورة التالية تبين الخطوات التي تمر بها أغلب عمليات البناء مهما كانت اللغة:

![ست بطاقات مرقمة من اليمين إلى اليسار: Source فيها ملف Vat.java وpom.xml، ثم Resolve وفيها سحابة Maven Central لتنزيل الاعتماديات، ثم Compile وفيها ترس javac وcsc وgcc، ثم Test وفيها علامة صح JUnit وxUnit، ثم Package وفيها صندوق vat-1.0.0.jar، ثم Publish وفيها سيرفر registry. قوس أزرق تحت الخطوات من 2 إلى 5 مكتوب عليه أنها ما يفعله أمر واحد مثل mvn package، وقوس برتقالي تحت النشر بأنه خطوة منفصلة في CI](https://arabroot.io/content/images/2026/10/build-tools-01-build-steps.webp)

خطوات البناء في أغلب اللغات، والأسماء تختلف من أداة إلى أخرى

وفيما يلي معنى كل خطوة باختصار:

- **حل الاعتماديات Dependency Resolution**: قراءة قائمة المكتبات التي يحتاجها المشروع من ملف الوصف، ثم تنزيلها مع المكتبات التي تحتاجها هي بدورها Transitive Dependencies إلى ذاكرة محلية على الجهاز.
- **الترجمة Compile**: تحويل الكود إلى bytecode كما في Java و.NET، أو إلى كود آلة Machine Code كما في C وGo وRust، وفي هذه الخطوة تظهر أخطاء الأنواع والأسماء.
- **الربط Link**: في اللغات التي تنتج كود آلة، يجمع الرابط Linker ملفات الكائنات Object Files والمكتبات في ملف تنفيذي واحد، أما في Java و.NET فالربط يحدث غالباً وقت التشغيل.
- **الاختبار Test**: تشغيل اختبارات الوحدة Unit Tests، وإذا فشل اختبار واحد يتوقف البناء ولا يخرج أي ملف، وهذا مقصود.
- **التحزيم Package**: وضع الناتج في شكل قابل للنقل، مثل `jar` أو حزمة NuGet أو Docker Image.
- **النشر Publish**: رفع الناتج إلى مستودع Repository أو Registry حتى تستخدمه مشاريع أخرى أو تنشره إلى السيرفر، وهذه الخطوة تكون عادةً في خط CI وليس على جهاز المطور.

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

## Make: من أين بدأت الفكرة؟

في ورقة [Make: A Program for Maintaining Computer Programs](https://plan9.io/7thEdMan/vol2/make?ref=arabroot.io) التي نشرها Stuart Feldman من Bell Laboratories ضمن توثيق الإصدار السابع من Unix بتاريخ 15 أغسطس 1978، يصف المشكلة التي جاءت منها الأداة بوضوح: في أي مشروع برمجي من السهل أن تفقد متابعة الملفات التي تحتاج إلى إعادة ترجمة بعد تعديل جزء من الكود، فإذا نسيت ترجمة ملف تغير سوف تحصل على برنامج لا يعمل وخطأ يصعب تتبعه، وإذا أعدت ترجمة كل شيء احتياطاً فهذا هدر كبير. وقد حصل Feldman على [جائزة ACM Software System Award عام 2003](https://awards.acm.org/award%5Fwinners/feldman%5F1240498?ref=arabroot.io) عن Make، ونص الجائزة يقول إنه لا يكاد يوجد نظام برمجي كبير في العالم لم يمر عبر نسخة من Make أو أداة خرجت منها.

والفكرة بسيطة: تكتب في ملف `Makefile` قواعد Rules، وكل قاعدة فيها هدف Target والملفات التي يعتمد عليها Prerequisites والأوامر Recipe التي تنتجه، ثم يقرر Make بنفسه ما يعيد بناءه، والقاعدة كما يكتبها [دليل GNU Make](https://www.gnu.org/software/make/manual/make.html?ref=arabroot.io) أن الهدف قديم Out of Date إذا لم يكن موجوداً أو كان أقدم من أي ملف يعتمد عليه، بمقارنة وقت آخر تعديل Modification Time. والصورة التالية تبين ذلك على المثال الذي سوف نشغله في التجربة العملية:

![مخطط اعتماديات: الملف التنفيذي vat في الأعلى يعتمد على main.o وvat.o، وmain.o يعتمد على main.c وvat.h، وvat.o يعتمد على vat.c وvat.h. الملف vat.c عليه وقت 10:07 touched باللون الأحمر، والمسار الأحمر من vat.c إلى vat.o إلى vat يعاد بناؤه، وmain.o مكتوب عليه up to date. وفي اليمين القاعدة بالعربية وقاعدتان من Makefile](https://arabroot.io/content/images/2026/10/build-tools-02-make-graph.webp)

Make يبني مخطط الاعتماديات ويعيد بناء ما تغير تحته فقط

لاحظ أن Make لا يعرف شيئاً عن لغة C، فهو يعرف الملفات وأوقاتها والأوامر فقط، ولذلك ما زال يستخدم حتى اليوم لأغراض لا علاقة لها بالترجمة، مثل ملف `Makefile` في المجلد الرئيسي للمشروع فيه أوامر مختصرة مثل `make test` و`make deploy`. ولكن هناك مشكلتان رئيسيتان في Make عندما يكبر المشروع: الأولى أنه لا يعرف شيئاً عن المكتبات الخارجية ومن أين تنزل ونسخها، والثانية أن المقارنة بالوقت تخدعه أحياناً، فإذا نسخت الملفات أو استرجعتها من Git بأوقات جديدة يظن أن كل شيء تغير، وإذا تغير خيار في الأمر نفسه مثل `CFLAGS` لا يعرف أن الناتج يجب أن يتغير. ومن هنا جاءت أدوات تعرف اللغة والمكتبات، وأشهرها في عالم Java أداة [Apache Ant](https://ant.apache.org/?ref=arabroot.io) ثم Maven ثم Gradle.

## Maven: اصطلاح بدل الإعداد

Ant كان يشبه Make بصيغة XML: أنت تكتب كل خطوة بنفسك، وكل مشروع له شكل مختلف، أما Maven فقد عكس الفكرة: بدل أن تكتب الخطوات تصف المشروع، وتلتزم بشكل ثابت للمجلدات (`src/main/java` للكود و`src/test/java` للاختبارات)، وMaven يعرف الخطوات وحده. ووصف المشروع يكون في ملف [pom.xml](https://maven.apache.org/guides/introduction/introduction-to-the-pom.html?ref=arabroot.io) واختصاره Project Object Model، وفيه إحداثيات المشروع Coordinates (`groupId` و`artifactId` و`version`) والاعتماديات Dependencies والإضافات Plugins.

### دورة حياة البناء Build Lifecycle

بحسب [توثيق Maven لدورة حياة البناء](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html?ref=arabroot.io) هناك ثلاث دورات مدمجة: `default` للبناء والنشر، و`clean` للتنظيف، و`site` لتوليد موقع توثيق المشروع، والدورة الأساسية `default` مكونة من مراحل Phases بالترتيب: `validate` ثم `compile` ثم `test` ثم `package` ثم `verify` ثم `install` ثم `deploy`، والقاعدة المهمة أنك عندما تطلب مرحلة ينفذ Maven كل المراحل التي قبلها بالترتيب، والصورة التالية تبين ذلك:

![سبع مراحل من اليمين إلى اليسار: validate وcompile وtest وpackage باللون البرتقالي، ثم verify وinstall وdeploy باللون الرمادي، وفوق البرتقالية قوس مكتوب عليه mvn package. ملف pom.xml يغذي المرحلة الأولى، وinstall يكتب في المستودع المحلي ~/.m2/repository، وdeploy يرفع إلى Maven Central أو Nexus، وسهم download من السحابة إلى المستودع المحلي](https://arabroot.io/content/images/2026/10/build-tools-03-maven-lifecycle.webp)

عندما تكتب mvn package ينفذ Maven المراحل من validate إلى package

وفي الإعداد أعلاه لاحظ التالي:

- المرحلة Phase اسم فقط، والعمل الفعلي يقوم به هدف Goal من إضافة Plugin مربوط بها، فمرحلة `compile` يقوم بها الهدف `compiler:compile` من الإضافة `maven-compiler-plugin`، وسوف ترى هذه الأسماء في مخرج التجربة العملية.
- `install` لا يعني تثبيت البرنامج على السيرفر، وإنما نسخ الناتج إلى المستودع المحلي Local Repository في `~/.m2/repository` حتى تستخدمه مشاريع أخرى على نفس الجهاز.
- `deploy` يرفع الناتج إلى مستودع بعيد Remote Repository مثل Nexus أو Artifactory أو Maven Central، وليس إلى سيرفر الإنتاج.

### ما الجديد في Maven 3.10؟

صدر Maven 3.10.0 في 27 سبتمبر 2026، وبحسب [ملاحظات الإصدار](https://maven.apache.org/docs/3.10.0/release-notes.html?ref=arabroot.io) فإن أهم ما يعنينا هنا ثلاثة أمور: الأول أن البناء القابل للتكرار Reproducible Builds أصبح مفعلاً افتراضياً لأن ملف Super POM يعطي قيمة افتراضية للخاصية `project.build.outputTimestamp`، والثاني أن Super POM لم يعد يحتوي على إدارة نسخ الإضافات Plugin Management، فالمطلوب أن يحدد كل مشروع نسخ إضافاته بنفسه، والثالث أن التحقق من ملف POM أصبح أشد، فأشياء مثل تكرار نفس الاعتمادية مرتين كانت تعطي تحذيراً في 3.9 وأصبحت توقف البناء. وسوف نرى أثر الأمر الأول في التجربة العملية.

## Gradle: البناء ككود

Gradle يأخذ من Maven شكل المجلدات ومستودعات المكتبات، لكنه يكتب ملف البناء كبرنامج بلغة Kotlin أو Groovy بدل XML، ويبني على مفهوم المهام Tasks، وكل مهمة لها مدخلات Inputs ومخرجات Outputs معرفة. وبحسب [توثيق البناء التدريجي في Gradle 9.8.0](https://docs.gradle.org/current/userguide/incremental%5Fbuild.html?ref=arabroot.io) فإن Gradle يأخذ قبل تنفيذ المهمة بصمة Fingerprint لمدخلاتها فيها مسارات الملفات وهاش Hash لمحتوى كل ملف، وبعد التنفيذ يأخذ بصمة للمخرجات، وفي المرة التالية إذا كانت البصمات نفسها يعتبر المهمة محدثة `UP-TO-DATE` ويتجاوزها. والفرق المهم عن Make أن المقارنة بالمحتوى وليس بالوقت، وسوف نرى في التجربة أن إضافة تعليق Comment في ملف اختبار تعيد ترجمة الملف ولا تعيد تشغيل الاختبارات، لأن الناتج المترجم لم يتغير.

وGradle يحتاج إلى نسخة محددة منه، لذلك الطريقة التي يوصي بها [توثيق Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle%5Fwrapper.html?ref=arabroot.io) هي تشغيل البناء عبر سكربت `gradlew` الموجود في المستودع، والذي ينزل نسخة Gradle المحددة في المشروع قبل التشغيل، وبهذا يبني كل المطورين وخط CI بنفس النسخة. وفي التجربة العملية استخدمنا Docker Image رسمية فيها Gradle 9.8.0 مباشرة لأن نسخة الأداة ثابتة في وسم ال Image نفسه.

## dotnet وMSBuild

في عالم .NET الأمر `dotnet build` هو الواجهة، والذي يعمل تحتها هو MSBuild، وبحسب [توثيق dotnet build](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-build?ref=arabroot.io) فإن الأمر يبني المشروع واعتمادياته إلى ملفات فيها كود IL بامتداد `dll`، ومعها ملف `pdb` للتصحيح Debugging، وملف `deps.json` فيه قائمة الاعتماديات، وملف `runtimeconfig.json` فيه نسخة ال Runtime المطلوبة، ويشغل قبلها `dotnet restore` تلقائياً لتنزيل حزم NuGet، وتشغيل `dotnet build` يعادل `dotnet msbuild -restore`. ووصف المشروع في ملف `csproj` بصيغة XML، والاعتماديات فيه عناصر `PackageReference` كما يشرح [توثيق NuGet](https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files?ref=arabroot.io).

أما البناء التدريجي في MSBuild فيعمل بطريقة قريبة من Make، فبحسب [توثيق Incremental Builds](https://learn.microsoft.com/en-us/visualstudio/msbuild/incremental-builds?ref=arabroot.io) لكل هدف Target مدخلات Inputs ومخرجات Outputs، ويقارن MSBuild وقت كل ملف مدخل بوقت الملف الناتج المقابل له، فإذا كانت كل المخرجات أحدث من مدخلاتها يتجاوز الهدف.

⚠️

إذا كانت مشاريع الاختبار عندك تستخدم Microsoft.Testing.Platform مثل xUnit v3 وانتقلت إلى .NET 10 SDK، فقد يفشل `dotnet test` برسالة تقول إن الاختبار عبر هدف VSTest لم يعد مدعوماً، والحل كما في [توثيق dotnet test](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-dotnet-test?ref=arabroot.io) إضافة قسم `test` إلى ملف `global.json` فيه `"runner": "Microsoft.Testing.Platform"`، وسوف ترى هذا الملف في التجربة العملية.

## npm scripts والمجمعات Bundlers باختصار

في مشاريع JavaScript لا توجد ترجمة بالمعنى الذي رأيناه في Java، لكن توجد خطوة بناء في أغلب المشاريع الحديثة، لأن الكود يكتب بـ TypeScript أو JSX أو بمئات الملفات الصغيرة، ويحتاج إلى تحويل وتجميع في ملفات قليلة يفهمها المتصفح. والأداة الأولى هنا هي خاصية `scripts` في ملف `package.json`، والتي يشرحها [توثيق npm scripts](https://docs.npmjs.com/cli/v11/using-npm/scripts?ref=arabroot.io)، فتكتب فيها أوامر مثل `"build": "vite build"` وتشغلها بـ `npm run build`، وهي في الحقيقة أقرب إلى Makefile فيه أوامر مختصرة منها إلى أداة بناء تعرف الاعتماديات بين الملفات. والعمل الفعلي تقوم به المجمعات Bundlers مثل [Vite](https://vite.dev/guide/?ref=arabroot.io) و[esbuild](https://esbuild.github.io/?ref=arabroot.io)، والتي تتبع ملفات `import` من نقطة البداية وتخرج مجلداً مثل `dist` فيه الملفات النهائية.

وفي خط CI استخدم `npm ci` بدل `npm install`، والسبب كما في [توثيق npm ci](https://docs.npmjs.com/cli/v11/commands/npm-ci?ref=arabroot.io) أنه يشترط وجود ملف `package-lock.json`، ويتوقف بخطأ إذا كان لا يطابق `package.json` بدل أن يعدله، ويحذف مجلد `node_modules` قبل التثبيت، ولا يكتب أبداً في `package.json` أو ملف القفل.

## Go وCargo: البناء جزء من اللغة

اللغات الأحدث لم تترك البناء لأداة خارجية، ففي Go الأمر `go build` يعرف الحزم والاعتماديات من ملف `go.mod`، ويحفظ نتائج الترجمة في ذاكرة البناء Build Cache، وبحسب [توثيق الأمر go](https://pkg.go.dev/cmd/go?ref=arabroot.io) فإن مكانها الافتراضي مجلد اسمه `go-build` داخل مجلد ال Cache للمستخدم ويمكن تغييره بالمتغير `GOCACHE`. ومن جهة التكرار فإن Russ Cox كتب في مقال [Perfectly Reproducible, Verified Go Toolchains](https://go.dev/blog/rebuild?ref=arabroot.io) في أغسطس 2023 أن أدوات Go نفسها أصبحت منذ الإصدار 1.21 قابلة للتكرار تماماً، أي أن المدخل الوحيد المؤثر في بنائها هو الكود المصدري.

وفي Rust تقوم Cargo بالدورين معاً: مدير الحزم وأداة البناء، فالأمر [cargo build](https://doc.rust-lang.org/cargo/commands/cargo-build.html?ref=arabroot.io) ينزل الاعتماديات ويبنيها ويبني المشروع، والنسخ الدقيقة تحفظ في ملف `Cargo.lock` الذي يقول عنه [دليل Cargo](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html?ref=arabroot.io) إنه يحتوي معلومات دقيقة عن الاعتماديات وتحدثه Cargo ولا يعدل يدوياً، وإن كان لديك شك فضعه في نظام إدارة النسخ مثل Git.

## حل الاعتماديات وملفات القفل باختصار

عندما تكتب في `pom.xml` أنك تحتاج `junit-jupiter` بالإصدار 6.1.3، فأنت لا تطلب مكتبة واحدة، وإنما شجرة كاملة، وسوف ترى في التجربة العملية أن هذه المكتبة الواحدة جلبت معها ثماني مكتبات أخرى، وأداة البناء هي التي تقرر أي نسخة تأخذ إذا طلبت مكتبتان نسختين مختلفتين من نفس الاعتمادية، ولكل أداة قاعدتها، ففي Maven مثلاً يشرح [توثيق آلية الاعتماديات](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html?ref=arabroot.io) قاعدة «الأقرب يفوز» Nearest Definition.

وملف القفل Lock File يسجل النسخ الدقيقة التي اختيرت، حتى يحصل كل مطور وكل بناء في CI على نفس الشجرة، ومثاله `package-lock.json` في npm و`Cargo.lock` في Rust و`go.sum` في Go، وفي Gradle يمكن تفعيل [قفل الاعتماديات Dependency Locking](https://docs.gradle.org/current/userguide/dependency%5Flocking.html?ref=arabroot.io). أما أمان هذه الشجرة، أي من أين تأتي الحزم وكيف تكتشف الحزم الخبيثة والثغرات المعروفة فيها، فهو موضوع مستقل شرحناه في [مقال أمن سلسلة التوريد البرمجية](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) و[مقال فحص الاعتماديات SCA](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF%D9%8A%D8%A7%D8%AA-sca-%D9%85%D8%B9-osv-scanner-%D9%88-dependency-track/)، ولن نكرره هنا.

## البناء التدريجي Incremental Build وذاكرة البناء Build Cache

البناء الكامل في كل مرة هو الحل البسيط، وهو صحيح دائماً لكنه بطيء، لذلك تحاول كل أداة أن تعرف ما الذي تغير وتبني ذلك فقط، وهذا هو البناء التدريجي Incremental Build. وهناك طريقتان للمعرفة: المقارنة بوقت التعديل كما في Make وMSBuild، وهي سريعة لكنها تخطئ إذا تغيرت الأوقات دون تغير المحتوى، والمقارنة بهاش المحتوى كما في Gradle وGo، وهي أدق. والصورة التالية تبين كيف يقرر Gradle مصير كل مهمة:

![في اليمين لوحة Task inputs فيها ملفات *.java وclasspath وcompiler flags وJDK، يخرج منها هاش fingerprint. الهاش يدخل معيناً مكتوب عليه compare، ويخرج منه ثلاثة أسهم: أخضر إلى UP-TO-DATE إذا كان نفس الهاش في آخر تشغيل، وأزرق إلى FROM-CACHE إذا كان في ال Cache، وبرتقالي إلى EXECUTED لهاش جديد ثم تخزن المخرجات. وفي اليسار قاعدة بيانات Build cache محلي أو مشترك يستخدمه developer وCI runner](https://arabroot.io/content/images/2026/10/build-tools-04-incremental-cache.webp)

البصمة تقرر: لا عمل، أو نسخ من ال Cache، أو تنفيذ ثم تخزين

وذاكرة البناء Build Cache تذهب خطوة أبعد من البناء التدريجي، فالبناء التدريجي يعرف آخر تشغيل في نفس المجلد فقط، أما ال Build Cache فيحفظ مخرجات كل مهمة مع بصمة مدخلاتها، فإذا ظهرت نفس البصمة مرة أخرى في أي وقت أو في أي مجلد أو حتى على جهاز آخر عبر Cache مشترك Remote Cache تنسخ المخرجات بدل التنفيذ، وهذا ما يعرفه [توثيق Gradle Build Cache](https://docs.gradle.org/current/userguide/build%5Fcache.html?ref=arabroot.io) بأنه آلية تحفظ مخرجات البناء محلياً أو عن بعد وتعيد استخدامها عندما تكون المدخلات لم تتغير. وفي التجربة العملية سوف ترى `FROM-CACHE` بعد `gradle clean` مباشرة.

وقد يتساءل البعض: هل يمكن أن يعطيني ال Cache ناتجاً خاطئاً؟ نعم، إذا كانت المهمة تعتمد على شيء لم يعلن كمدخل، مثل متغير بيئة Environment Variable تقرؤه أداة خارجية، أو ملف خارج المشروع، وعندها تتطابق البصمة ويختلف الناتج الحقيقي، لذلك إذا كتبت مهام بناء خاصة بك فعرف كل مدخلاتها، وإذا شككت في ناتج غريب فابن مرة بدون Cache لتتأكد، مثل `dotnet build --no-incremental` أو `gradle build --rerun-tasks`.

## البناء القابل للتكرار Reproducible Build

التعريف الذي يعتمده مشروع Reproducible Builds في [صفحة التعريف](https://reproducible-builds.org/docs/definition/?ref=arabroot.io) هو أن البناء قابل للتكرار إذا استطاع أي طرف، بنفس الكود المصدري وبيئة البناء وتعليمات البناء، أن ينتج نسخاً مطابقة بايت ببايت Bit-by-bit من كل الملفات الناتجة المحددة. والفائدة الأمنية مباشرة: إذا بنى طرفان مستقلان نفس الكود وحصلا على نفس الهاش، فأنت تعرف أن الملف المنشور لم يحقن فيه شيء أثناء البناء، وهذه نقطة يعود إليها [مقال أمن سلسلة التوريد](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) عند الكلام عن اختراق نظام البناء نفسه.

![في اليمين لوحة نفس المدخلات الثلاثة: source code وenvironment وinstructions، تخرج منها أسهم إلى builder A وbuilder B، وكل واحد ينتج صندوقاً أخضر عليه sha256 b8ccfef595ab، وبينهما علامة صح ونص نسخة مطابقة بايت ببايت. وفي الأسفل لوحة ما الذي يتسرب إلى الناتج فيغير الهاش: timestamps وbuild path وtool versions وfile order](https://arabroot.io/content/images/2026/10/build-tools-05-reproducible.webp)

نفس المدخلات تعطي نفس الهاش، وأي تسرب من البيئة يكسر ذلك

والصعوبة أن البيئة تتسرب إلى الناتج بطرق لا تنتبه لها، وأشهرها أربعة:

- **الوقت Timestamps**: ملفات `jar` و`zip` تحفظ وقت كل ملف داخلها، فإذا بنيت مرتين في دقيقتين مختلفتين يختلف الهاش، والحل المتعارف عليه وقت ثابت مثل المتغير [SOURCE\_DATE\_EPOCH](https://reproducible-builds.org/docs/source-date-epoch/?ref=arabroot.io)، وفي Maven الخاصية `project.build.outputTimestamp` التي يشرحها [دليل Maven للبناء القابل للتكرار](https://maven.apache.org/guides/mini/guide-reproducible-builds.html?ref=arabroot.io).
- **المسار Build Path**: بعض المترجمات تكتب المسار الكامل للمجلد داخل الناتج، وتوثيق خيار [Deterministic في مترجم C#](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/code-generation?ref=arabroot.io) يذكر مسار المجلد الحالي ضمن المدخلات التي تؤثر على الناتج، وسوف نرى ذلك في التجربة العملية، وفي Go الخيار `-trimpath` يحذف مسارات الملفات من الناتج.
- **نسخ الأدوات Tool Versions**: نسخة مختلفة من المترجم أو من إضافة Plugin قد تنتج bytecode مختلفاً، لذلك ثبت نسخة ال Docker Image ونسخ الإضافات.
- **ترتيب الملفات File Order**: ترتيب الملفات في الأرشيف قد يتبع ترتيب نظام الملفات الذي يختلف من جهاز لآخر، وبحسب [توثيق Gradle للتعامل مع الملفات](https://docs.gradle.org/current/userguide/working%5Fwith%5Ffiles.html?ref=arabroot.io) فإن مهام الأرشيف مثل Jar وZip تنتج منذ Gradle 9.0.0 أرشيفات قابلة للتكرار افتراضياً بترتيب ثابت للملفات وأوقات ثابتة وصلاحيات ثابتة.

والخبر الجيد أن الأدوات الكبيرة جعلت ذلك افتراضياً تقريباً في 2026: Gradle 9 للأرشيفات، وMaven 3.10 بقيمة افتراضية للوقت، و.NET بخاصية `Deterministic` المفعلة افتراضياً كما يذكر توثيق المترجم أعلاه، ويبقى عليك أن تثبت البيئة نفسها، وأسهل طريقة لذلك أن تبني داخل Container بنسخة Image محددة وفي مسار ثابت.

## البناء داخل خط CI

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

![تدفق مرقم من اليمين إلى اليسار: 1 لابتوب git push، 2 سيرفر Gitea يطلق ال Workflow، 3 لوحة RUNNER فيها Container maven:3.10.0 بنسخة أدوات ثابتة وقاعدة بيانات ~/.m2 cache وعلامة صح mvn verify، 4 صندوق artifact or Image مع رقم الإصدار والهاش يرفع إلى registry، 5 خط أخضر ينقل نفس الملف إلى staging ثم production دون إعادة بناء](https://arabroot.io/content/images/2026/10/build-tools-06-ci-build.webp)

ابن مرة واحدة في CI بأدوات ثابتة، ثم انقل نفس الناتج إلى كل البيئات

وفيما يلي القواعد التي تجعل البناء في CI سريعاً وموثوقاً:

1. **ابن داخل Container بنسخة محددة** مثل `maven:3.10.0-eclipse-temurin-25`، ولا تستخدم وسماً متحركاً مثل `latest`، والسبب أن الوسم المتحرك يغير نسخة الأداة دون أن يتغير الكود، فيتغير الناتج أو يفشل البناء فجأة.
2. **احفظ ذاكرة الاعتماديات بين التشغيلات**: مجلد `~/.m2` في Maven، و`~/.gradle` في Gradle، و`~/.nuget/packages` في .NET، إما عبر آلية ال Cache في منصة CI، أو عبر Volume في ال Runner الخاص بك، أو عبر [Cache Mounts في Docker BuildKit](https://docs.docker.com/build/cache/optimize/?ref=arabroot.io) إذا كان البناء داخل Dockerfile.
3. **شغل الاختبارات كجزء من البناء**، ولا تنشر ناتجاً تجاوز الاختبارات بخيار مثل `-DskipTests`.
4. **ابن مرة واحدة وانقل الناتج**، فلا تعيد البناء لكل بيئة، والسبب أن البناء الثاني قد يختلف عن الأول، وقد شرحنا هذه الفكرة في [مقال البيئات وGitOps](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%8A%D8%A6%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D8%A7%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA-%D9%88-gitops-%D9%84%D9%84%D9%81%D8%B1%D9%82/).
5. **إذا كان الناتج Docker Image** فاستخدم [البناء متعدد المراحل Multi-stage Build](https://docs.docker.com/build/building/multi-stage/?ref=arabroot.io): مرحلة فيها SDK كامل تبني، ومرحلة أخيرة صغيرة فيها ال Runtime والناتج فقط.

وإذا كنت تشغل CI على سيرفرك فقد شرحنا تجهيز Runner وأول Workflow يبني Image ويرفعها في [دليل Gitea Actions](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/)، وأين تضع فحوص الأمان بجانب البناء في [مقال DevSecOps للمبتدئين](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/).

## تجربة عملية: نفس البرنامج بثلاث أدوات بناء

الآن سوف نبني برنامجاً صغيراً واحداً بـ Maven ثم Gradle ثم dotnet، والبرنامج يحسب السعر بعد ضريبة القيمة المضافة 15% بالهللات (حتى نتجنب أخطاء الأرقام العشرية)، ومعه اختباران، وكل بناء يعمل داخل Container بنسخة محددة من الأداة، والذاكرة المحلية للمكتبات في Volume منفصل حتى نرى الفرق بين البناء الأول والبناء التالي. والأوقات التي سوف تراها حقيقية من جهاز واحد، لكنها توضيحية فقط ولا تصلح للمقارنة بين الأدوات، لأن كل أداة تنزل مكتبات مختلفة وتشغل JVM أو Runtime مختلفاً.

📌

في التجربة نشغل Container ينتظر بالأمر `sleep infinity`، ثم ننسخ الملفات إليه بـ `docker cp` وننفذ البناء بـ `docker exec`، وبهذا تعمل نفس الأوامر على Linux وWindows وmacOS دون مشكلات المسارات المشتركة، وفي مشروعك الحقيقي يكفي `docker run --rm -v "$PWD":/src …`.

### Maven: البناء الأول والثاني

المثال التالي يبين ملف `pom.xml`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.arabroot</groupId>
  <artifactId>vat</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.release>25</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
```

والكود في الملف `src/main/java/io/arabroot/Vat.java`:

```java
package io.arabroot;

public final class Vat {
    public static final int RATE_PERCENT = 15;

    // amounts in halalas (1 riyal = 100 halalas) to avoid floating point errors
    public static long total(long net) {
        if (net < 0) throw new IllegalArgumentException("net must be >= 0");
        return net + Math.round(net * RATE_PERCENT / 100.0);
    }

    public static void main(String[] args) {
        long net = Long.parseLong(args.length > 0 ? args[0] : "10000");
        System.out.println("net=" + net + " total=" + total(net));
    }
}
```

والاختبار في الملف `src/test/java/io/arabroot/VatTest.java`:

```java
package io.arabroot;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class VatTest {
    @Test
    void addsFifteenPercent() {
        assertEquals(11500, Vat.total(10000));
    }

    @Test
    void rejectsNegative() {
        assertThrows(IllegalArgumentException.class, () -> Vat.total(-1));
    }
}
```

الآن سوف نقوم بتشغيل ال Container ونسخ المشروع إليه، ثم البناء الأول والذاكرة المحلية فارغة:

```bash
docker run -d --name lab-build-maven -v lab-build-m2:/root/.m2 -w /src maven:3.10.0-eclipse-temurin-25 sleep infinity
docker cp . lab-build-maven:/src
docker exec lab-build-maven mvn -v
docker exec lab-build-maven bash -c 'time mvn -B package'
```

والمخرج سوف يكون كما يلي، مختصراً: أبقينا سطرين من أسطر التنزيل الكثيرة (182 ملفاً) وحذفنا الأسطر الفارغة:

```text
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
Maven home: /usr/share/maven
Java version: 25.0.4.1, vendor: Eclipse Adoptium, runtime: /opt/java/openjdk
Default locale: en_US, platform encoding: UTF-8, time zone: Etc/UTC
OS name: "linux", version: "5.15.167.4-microsoft-standard-wsl2", arch: "amd64", family: "unix"

[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-resources-plugin/3.5.0/maven-resources-plugin-3.5.0.pom (7.7 kB at 24 kB/s)
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/junit/jupiter/junit-jupiter-api/6.1.3/junit-jupiter-api-6.1.3.jar (313 kB at 498 kB/s)
[INFO] --- resources:3.5.0:resources (default-resources) @ vat ---
[INFO] skip non existing resourceDirectory /src/src/main/resources
[INFO] --- compiler:3.16.0:compile (default-compile) @ vat ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 1 source file with javac [debug release 25] to target/classes
[INFO] --- resources:3.5.0:testResources (default-testResources) @ vat ---
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ vat ---
[INFO] Compiling 1 source file with javac [debug release 25] to target/test-classes
[INFO] --- surefire:3.6.0:test (default-test) @ vat ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] Running io.arabroot.VatTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.282 s -- in io.arabroot.VatTest
[INFO] --- jar:3.5.1:jar (default-jar) @ vat ---
[INFO] Building jar: /src/target/vat-1.0.0.jar
[INFO] BUILD SUCCESS
[INFO] Total time:  01:22 min

real	1m31.769s
```

في المخرج أعلاه لاحظ التالي:

- كل سطر يبدأ بـ `---` هو هدف Goal من إضافة مربوط بمرحلة: `resources` ثم `compiler:compile` ثم `surefire:test` ثم `jar:jar`، وهذا هو ترتيب دورة الحياة الذي رأيناه في الصورة.
- نسخ الإضافات مثل `compiler:3.16.0` و`surefire:3.6.0` جاءت من الإعداد الافتراضي لأننا لم نحددها، وفي مشروع حقيقي حددها في قسم `pluginManagement`، والسبب أن Maven 3.10 حذف إدارة نسخ الإضافات من Super POM كما ذكرنا، وتحديدها يجعل البناء لا يتغير مع نسخة Maven.
- أغلب الدقيقة والنصف ذهبت في تنزيل 182 ملفاً هي الإضافات والمكتبات، والترجمة نفسها أجزاء من الثانية.

وشجرة الاعتماديات التي جاءت من سطر واحد في `pom.xml` تراها بالأمر التالي:

```bash
docker exec lab-build-maven mvn -B -o dependency:tree
```

```text
[INFO] io.arabroot:vat:jar:1.0.0
[INFO] \- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO]    +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
[INFO]    |  +- org.opentest4j:opentest4j:jar:1.3.0:test
[INFO]    |  +- org.junit.platform:junit-platform-commons:jar:6.1.3:test
[INFO]    |  +- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO]    |  \- org.jspecify:jspecify:jar:1.0.0:test
[INFO]    +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test
[INFO]    \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
[INFO]       \- org.junit.platform:junit-platform-engine:jar:6.1.3:test
```

الآن البناء الثاني بعد `clean` والذاكرة المحلية ممتلئة، ثم بناء ثالث دون أي تغيير، ثم تشغيل الناتج:

```bash
docker exec lab-build-maven bash -c 'du -sh /root/.m2/repository; time mvn -B -q clean package'
docker exec lab-build-maven bash -c 'time mvn -B package | grep -E "compiler|Nothing to compile|Total time"'
docker exec lab-build-maven java -cp target/vat-1.0.0.jar io.arabroot.Vat 10000
```

```text
25M	/root/.m2/repository

real	0m29.310s

[INFO] --- compiler:3.16.0:compile (default-compile) @ vat ---
[INFO] Nothing to compile - all classes are up to date.
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ vat ---
[INFO] Nothing to compile - all classes are up to date.
[INFO] Total time:  13.348 s

real	0m20.534s

net=10000 total=11500
```

لاحظ أن البناء نزل من دقيقة ونصف إلى نصف دقيقة لأن 25 ميجابايت من المكتبات أصبحت في ال Volume، وأن البناء دون تغيير لم يترجم شيئاً (`Nothing to compile`) لكنه أخذ عشرين ثانية، والسبب أن Maven يعيد تشغيل الاختبارات وتحزيم ال jar في كل مرة، فالبناء التدريجي في Maven محدود بالترجمة، وهذا من أهم الفروق عن Gradle كما سيأتي.

### نكسر البناء: اختبار يفشل

الآن سوف نغير نسبة الضريبة في الكود إلى 14% عن قصد، ونرى ماذا يحدث للبناء:

```bash
docker exec lab-build-maven bash -c 'sed -i "s/RATE_PERCENT = 15/RATE_PERCENT = 14/" src/main/java/io/arabroot/Vat.java; mvn -B -o clean package 2>&1 | grep -E "ERROR|FAIL|Tests run|BUILD|expected" | head -12; ls target/*.jar'
```

```text
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.867 s <<< FAILURE! -- in io.arabroot.VatTest
[ERROR] io.arabroot.VatTest.addsFifteenPercent -- Time elapsed: 0.293 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <11500> but was: <11400>
[ERROR] Failures:
[ERROR]   VatTest.addsFifteenPercent:9 expected: <11500> but was: <11400>
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.6.0:test (default-test) on project vat: There are test failures.
[ERROR]
[ERROR] See /src/target/surefire-reports for the individual test results.
[ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
[ERROR] -> [Help 1]
ls: cannot access 'target/*.jar': No such file or directory
```

وهذا بالضبط ما تريده من أداة البناء: فشل الاختبار أوقف دورة الحياة عند مرحلة `test`، فلم تصل إلى `package` ولم يخرج أي ملف `jar` يمكن أن ينشر بالخطأ، وسطر `expected: <11500> but was: <11400>` يخبرك بالقيمة المتوقعة والقيمة الفعلية ورقم السطر في الاختبار. بعدها أعدنا القيمة إلى 15.

### هل بناء Maven قابل للتكرار؟

الآن سوف نبني مرتين بينهما ثلاث ثوان ونقارن الهاش، دون أن نضيف أي إعداد خاص في `pom.xml`:

```bash
docker exec lab-build-maven bash -c 'mvn -B -q -o clean package -DskipTests && sha256sum target/vat-1.0.0.jar && sleep 3 && mvn -B -q -o clean package -DskipTests && sha256sum target/vat-1.0.0.jar'
docker exec lab-build-maven jar tvf target/vat-1.0.0.jar
docker exec lab-build-maven bash -c 'mvn -B -q help:effective-pom -Doutput=/tmp/ep.xml >/dev/null 2>&1; grep -n outputTimestamp /tmp/ep.xml'
```

```text
67b79ece197d767cebe19cbaff91934f654e21ac5c53742e1be123bda80f66e7  target/vat-1.0.0.jar
67b79ece197d767cebe19cbaff91934f654e21ac5c53742e1be123bda80f66e7  target/vat-1.0.0.jar
     0 Fri Feb 01 00:00:00 UTC 1980 META-INF/
    99 Fri Feb 01 00:00:00 UTC 1980 META-INF/MANIFEST.MF
     0 Fri Feb 01 00:00:00 UTC 1980 io/
     0 Fri Feb 01 00:00:00 UTC 1980 io/arabroot/
     0 Fri Feb 01 00:00:00 UTC 1980 META-INF/maven/
     0 Fri Feb 01 00:00:00 UTC 1980 META-INF/maven/io.arabroot/
     0 Fri Feb 01 00:00:00 UTC 1980 META-INF/maven/io.arabroot/vat/
  1456 Fri Feb 01 00:00:00 UTC 1980 io/arabroot/Vat.class
   806 Fri Feb 01 00:00:00 UTC 1980 META-INF/maven/io.arabroot/vat/pom.xml
    49 Fri Feb 01 00:00:00 UTC 1980 META-INF/maven/io.arabroot/vat/pom.properties
20:    <project.build.outputTimestamp>1980-02-01T00:00:00Z</project.build.outputTimestamp>
```

الهاش متطابق، وكل الملفات داخل ال jar بتاريخ 1 فبراير 1980 بدل وقت البناء، والسبب هو القيمة الافتراضية التي أضافها Maven 3.10 للخاصية `project.build.outputTimestamp` كما ترى في السطر الأخير. وفي مشروعك الحقيقي ضع قيمة لهذه الخاصية في `pom.xml` بنفسك (عادةً تاريخ آخر commit أو تاريخ الإصدار) بدل الاعتماد على القيمة الافتراضية، وتحقق بالأمر `mvn clean verify artifact:compare` كما يشرح دليل Maven للبناء القابل للتكرار.

### Gradle: UP-TO-DATE وFROM-CACHE

نفس مجلد `src` بدون تغيير، ومعه ملفان بدل `pom.xml`، الأول `settings.gradle.kts`:

```kotlin
rootProject.name = "vat"
```

والثاني `build.gradle.kts`:

```kotlin
plugins {
    java
}

group = "io.arabroot"
version = "1.0.0"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
}
```

الآن البناء الأول:

```bash
docker run -d --name lab-build-gradle -v lab-build-gradle-home:/home/gradle/.gradle -w /src gradle:9.8.0-jdk25 sleep infinity
docker cp . lab-build-gradle:/src
docker exec lab-build-gradle bash -c 'time gradle build --console=plain'
```

```text
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :assemble
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :check
> Task :build

BUILD SUCCESSFUL in 1m 41s
4 actionable tasks: 4 executed

real	1m42.067s
```

ثم ثلاث تجارب متتالية: بناء دون تغيير، ثم إضافة تعليق Comment فقط في ملف الاختبار، ثم `clean` مع ذاكرة البناء `--build-cache` مرتين:

```bash
docker exec lab-build-gradle bash -c 'gradle build --console=plain
echo "// a comment only" >> src/test/java/io/arabroot/VatTest.java; gradle build --console=plain
gradle clean build --build-cache --console=plain -q; sha256sum build/libs/vat-1.0.0.jar
gradle clean build --build-cache --console=plain; sha256sum build/libs/vat-1.0.0.jar'
```

```text
> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar UP-TO-DATE
> Task :assemble UP-TO-DATE
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test UP-TO-DATE
> Task :check UP-TO-DATE
> Task :build UP-TO-DATE

BUILD SUCCESSFUL in 3s
4 actionable tasks: 4 up-to-date

> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar UP-TO-DATE
> Task :assemble UP-TO-DATE
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test UP-TO-DATE
> Task :check UP-TO-DATE
> Task :build UP-TO-DATE

BUILD SUCCESSFUL in 3s
4 actionable tasks: 1 executed, 3 up-to-date

b8ccfef595abf5d48bd6c95543b74cedf6ddb80816dc4e95dcfd815f5bba5531  build/libs/vat-1.0.0.jar

> Task :clean
> Task :compileJava FROM-CACHE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar
> Task :assemble
> Task :compileTestJava FROM-CACHE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test FROM-CACHE
> Task :check UP-TO-DATE
> Task :build

BUILD SUCCESSFUL in 3s
5 actionable tasks: 2 executed, 3 from cache
b8ccfef595abf5d48bd6c95543b74cedf6ddb80816dc4e95dcfd815f5bba5531  build/libs/vat-1.0.0.jar
```

في المخرج أعلاه لاحظ التالي:

- البناء دون تغيير أخذ 3 ثوان وكل المهام `UP-TO-DATE` بما فيها `test`، على عكس Maven الذي أعاد تشغيل الاختبارات في كل مرة.
- بعد إضافة التعليق أعاد Gradle ترجمة ملف الاختبار فقط (`compileTestJava`)، لكن مهمة `test` بقيت `UP-TO-DATE`، والسبب أن التعليق لا يظهر في ال bytecode، فالمخرجات المترجمة التي هي مدخلات مهمة الاختبار لم يتغير هاشها، وهذا هو الفرق بين المقارنة بالمحتوى والمقارنة بالوقت.
- بعد `clean` جاءت نتائج الترجمة والاختبار من ال Build Cache (`FROM-CACHE`) فلم تنفذ الاختبارات من جديد، وأعيد تحزيم ال jar فقط.
- الهاش `b8ccfef5…` هو نفسه في المرتين، وهو أيضاً نفس الهاش الذي حصلنا عليه في Container سابق حذفناه وبدأنا من جديد، أي أن ال jar قابل للتكرار بين بيئتين نظيفتين دون أي إعداد إضافي، وهذا أثر الأرشيفات القابلة للتكرار افتراضياً في Gradle 9.

### dotnet: نفس البرنامج بلغة C#

المشروع هنا مكون من مشروعين: البرنامج ومشروع الاختبارات، والمثال التالي يبين الملف `Vat/Vat.csproj`:

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <Version>1.0.0</Version>
  </PropertyGroup>
</Project>
```

والكود في الملف `Vat/Vat.cs`:

```csharp
namespace ArabRoot;

public static class Vat
{
    public const int RatePercent = 15;

    // amounts in halalas (1 riyal = 100 halalas)
    public static long Total(long net)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(net);
        return net + (long)Math.Round(net * RatePercent / 100.0, MidpointRounding.AwayFromZero);
    }

    public static void Main(string[] args)
    {
        long net = args.Length > 0 ? long.Parse(args[0]) : 10000;
        Console.WriteLine($"net={net} total={Total(net)}");
    }
}
```

ومشروع الاختبار `Vat.Tests/Vat.Tests.csproj`:

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <IsPackable>false</IsPackable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="xunit.v3" Version="4.0.1" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="../Vat/Vat.csproj" />
  </ItemGroup>
</Project>
```

والاختبار في الملف `Vat.Tests/VatTests.cs`:

```csharp
using Xunit;

namespace ArabRoot.Tests;

public class VatTests
{
    [Fact]
    public void AddsFifteenPercent() => Assert.Equal(11500, Vat.Total(10000));

    [Fact]
    public void RejectsNegative() =>
        Assert.Throws<ArgumentOutOfRangeException>(() => Vat.Total(-1));
}
```

وملف `global.json` في المجلد الرئيسي حتى يعمل `dotnet test` بوضع Microsoft.Testing.Platform:

```json
{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}
```

الآن سوف نقوم بتشغيل ال Container وإنشاء ملف ال Solution بصيغة `slnx` ثم البناء الأول:

```bash
docker run -d --name lab-build-dotnet -v lab-build-nuget:/root/.nuget/packages -w /src -e DOTNET_CLI_TELEMETRY_OPTOUT=1 -e DOTNET_NOLOGO=1 mcr.microsoft.com/dotnet/sdk:10.0.401 sleep infinity
docker cp . lab-build-dotnet:/src
docker exec lab-build-dotnet bash -c 'dotnet new sln -n Vat --format slnx && dotnet sln add Vat/Vat.csproj Vat.Tests/Vat.Tests.csproj; time dotnet build -c Release --tl:off'
```

وآخر أسطر المخرج:

```text
  Determining projects to restore...
  Restored /src/Vat/Vat.csproj (in 292 ms).
  Restored /src/Vat.Tests/Vat.Tests.csproj (in 30.21 sec).
  Vat -> /src/Vat/bin/Release/net10.0/Vat.dll
  Vat.Tests -> /src/Vat.Tests/bin/Release/net10.0/Vat.Tests.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:43.55

real	0m44.549s
```

ثم الاختبارات، ثم بناء دون تغيير، ثم الملفات الناتجة وحجم ذاكرة الحزم:

```bash
docker exec lab-build-dotnet bash -c 'dotnet test -c Release --no-build'
docker exec lab-build-dotnet bash -c 'time dotnet build -c Release --tl:off 2>&1 | tail -5'
docker exec lab-build-dotnet bash -c 'dotnet Vat/bin/Release/net10.0/Vat.dll 10000; ls Vat/bin/Release/net10.0/; du -sh /root/.nuget/packages; ls /root/.nuget/packages | wc -l'
```

```text
Running tests from /src/Vat.Tests/bin/Release/net10.0/Vat.Tests.dll (net10.0|x64)
/src/Vat.Tests/bin/Release/net10.0/Vat.Tests.dll (net10.0|x64) passed (1s 388ms)

Test run summary: Passed!
  total: 2
  failed: 0
  succeeded: 2
  skipped: 0
  duration: 2s 344ms

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:05.86

real	0m6.602s

net=10000 total=11500
Vat
Vat.deps.json
Vat.dll
Vat.pdb
Vat.runtimeconfig.json
39M	/root/.nuget/packages
17
```

لاحظ أن الملفات الناتجة هي نفسها التي ذكرها توثيق `dotnet build`: ملف `dll` وملف `pdb` وملف `deps.json` وملف `runtimeconfig.json`، ومعها ملف تنفيذي صغير `Vat` يشغل ال dll، وأن الحزمة الواحدة `xunit.v3` جلبت معها 17 حزمة بحجم 39 ميجابايت، وأن الاستعادة Restore أخذت ثلاثين ثانية من أصل 44.

### نكسر التكرار: نفس الكود في مجلد آخر

خاصية `Deterministic` مفعلة افتراضياً في .NET، فهل يكفي ذلك؟ الآن سوف نبني نفس المشروع مرتين في `/src`، ثم مرة في نسخة منه في `/build2`، ونطبع الهاش ومسار ملف ال pdb المكتوب داخل ال dll:

```bash
docker exec lab-build-dotnet bash -c 'cp -r /src /build2; for d in /src /src /build2; do cd $d; dotnet build Vat/Vat.csproj -c Release --no-incremental -v q --tl:off >/dev/null; echo "$d: $(sha256sum Vat/bin/Release/net10.0/Vat.dll | cut -c1-16)  $(grep -a -o "[/a-zA-Z0-9_.]*Vat\.pdb" Vat/bin/Release/net10.0/Vat.dll)"; done'
```

```text
/src: 234b072f795185e1  /src/Vat/obj/Release/net10.0/Vat.pdb
/src: 234b072f795185e1  /src/Vat/obj/Release/net10.0/Vat.pdb
/build2: dc7fa4f236a99272  /build2/Vat/obj/Release/net10.0/Vat.pdb
```

في نفس المجلد الهاش متطابق، وفي مجلد آخر تغير الهاش مع أن الكود والأداة نفسها، والسبب ظاهر في العمود الثاني: المسار الكامل لملف ال pdb مكتوب داخل ال dll، وهذا ما يقصده توثيق المترجم عندما يضع مسار المجلد الحالي ضمن مدخلات الناتج. والحل العملي أن تبني دائماً في مسار ثابت داخل Container (مثل `/src` في كل بناء)، أو أن تستخدم خيارات تعيين المسارات في المترجم إذا كان لا بد من مسارات مختلفة.

### Make وGo باختصار

أخيراً المثال الصغير بلغة C الذي رأيته في صورة Make، وملف `Makefile` كما يلي (الأسطر التي تحت كل قاعدة تبدأ بحرف Tab وليس بمسافات):

```makefile
CC = gcc
CFLAGS = -O2 -Wall

vat: main.o vat.o
	$(CC) -o vat main.o vat.o

main.o: main.c vat.h
	$(CC) $(CFLAGS) -c main.c

vat.o: vat.c vat.h
	$(CC) $(CFLAGS) -c vat.c

clean:
	rm -f vat *.o

.PHONY: clean
```

والملفات الثلاثة `vat.h` و`vat.c` و`main.c`:

```c
/* vat.h */
long vat_total(long net);

/* vat.c */
#include "vat.h"

long vat_total(long net) { return net + (net * 15 + 50) / 100; }

/* main.c */
#include <stdio.h>
#include "vat.h"

int main(void) { printf("net=10000 total=%ld\n", vat_total(10000)); return 0; }
```

نشغلها في Image الرسمية لـ Go لأنها تحتوي على `gcc` و`make`:

```bash
docker run -d --name lab-build-make -w /src golang:1.27.1-bookworm sleep infinity
docker cp . lab-build-make:/src
docker exec lab-build-make bash -c 'make; ./vat; make; sleep 1; touch vat.c; make; touch vat.h; make'
```

```text
gcc -O2 -Wall -c main.c
gcc -O2 -Wall -c vat.c
gcc -o vat main.o vat.o
net=10000 total=11500
make: 'vat' is up to date.
gcc -O2 -Wall -c vat.c
gcc -o vat main.o vat.o
gcc -O2 -Wall -c main.c
gcc -O2 -Wall -c vat.c
gcc -o vat main.o vat.o
```

البناء الأول ترجم الملفين وربطهما، والثاني لم يفعل شيئاً، وبعد `touch vat.c` أعاد ترجمة `vat.c` فقط ثم الربط، وبعد `touch vat.h` أعاد ترجمة الملفين لأن كليهما يعتمد عليه، وهذا كله دون أن يتغير حرف واحد في المحتوى، لأن `touch` يغير الوقت فقط وMake يقرأ الوقت.

وفي Go نفس البرنامج في ملفين، `go.mod`:

```text
module example.com/vat

go 1.27
```

و`main.go`:

```go
package main

import "fmt"

func total(net int64) int64 { return net + (net*15+50)/100 }

func main() { fmt.Printf("net=10000 total=%d\n", total(10000)) }
```

والتجربة: بناء بذاكرة بناء فارغة، ثم بناء ثان، ثم البناء في مجلدين مختلفين مع `-trimpath` وبدونه:

```bash
docker exec lab-build-make bash -c 'cp -r /go-src /tmp/other; go clean -cache; cd /go-src; time go build -o vat .; rm vat; time go build -o vat .; ./vat
for d in /go-src /tmp/other; do cd $d; go build -o vat . && echo "$d default:   $(sha256sum vat | cut -c1-16)"; go build -trimpath -o vat . && echo "$d -trimpath: $(sha256sum vat | cut -c1-16)"; done'
```

```text
real	0m25.343s

real	0m0.528s
net=10000 total=11500
/go-src default:   9accaa7a1cd41e6b
/go-src -trimpath: e7d2f2e2972dae34
/tmp/other default:   229d1e91e93fcb6f
/tmp/other -trimpath: e7d2f2e2972dae34
```

البناء الأول أخذ 25 ثانية لأن `go clean -cache` حذف حتى نتائج ترجمة المكتبة القياسية Standard Library، والثاني نصف ثانية من ال Build Cache، وفي المجلدين اختلف الهاش بدون `-trimpath` وتطابق معه، وهي نفس مشكلة المسار التي رأيناها في .NET.

### النتائج في جدول واحد

| الأداة              | البناء الأول            | بعد امتلاء ال Cache | دون أي تغيير                  | تكرار الهاش                     |
| ------------------- | ----------------------- | ------------------- | ----------------------------- | ------------------------------- |
| Maven 3.10.0        | 1m31s مع 182 تنزيلاً    | 29s (مع clean)      | 20s، الاختبارات تعمل مرة أخرى | متطابق افتراضياً بوقت 1980      |
| Gradle 9.8.0        | 1m42s                   | 3s مع FROM-CACHE    | 3s، كل المهام UP-TO-DATE      | متطابق حتى بين Containers جديدة |
| dotnet SDK 10.0.401 | 44s منها 30s للاستعادة  | لم نقس              | 6.6s                          | متطابق في نفس المسار فقط        |
| Go 1.27.1           | 25s مع المكتبة القياسية | 0.5s                | 0.5s                          | متطابق مع \-trimpath            |

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

### التنظيف

```bash
docker rm -f lab-build-maven lab-build-gradle lab-build-dotnet lab-build-make
docker volume rm lab-build-m2 lab-build-gradle-home lab-build-nuget
docker image rm maven:3.10.0-eclipse-temurin-25 gradle:9.8.0-jdk25 mcr.microsoft.com/dotnet/sdk:10.0.401
```

واحذف ال Images فقط إذا لم تكن تستخدمها في مشاريع أخرى.

## أي أداة تستخدم؟

في أغلب الحالات لا تختار أداة البناء، وإنما تأتي مع اللغة والفريق: Go وRust معهما أداتهما، و.NET معه dotnet وMSBuild، وJavaScript معه npm ومجمع مثل Vite. والاختيار الحقيقي في Java بين Maven وGradle، وفيما يلي نقاط القوة ونقاط الضعف لكل منهما:

| الأداة | نقاط القوة                                                                             | نقاط الضعف                                                                 |
| ------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Maven  | شكل ثابت لكل المشاريع، أي مطور Java يفهم pom.xml فوراً، دعم واسع في كل أدوات CI والفحص | XML طويل، البناء التدريجي محدود بالترجمة، والمنطق الخاص يحتاج إضافة Plugin |
| Gradle | بناء تدريجي بالمحتوى، Build Cache محلي ومشترك، مرونة كاملة لأن الملف كود               | المرونة نفسها تجعل كل مشروع مختلفاً، ومنحنى تعلم أعلى                      |

والقاعدة: للمشاريع الصغيرة والمتوسطة وللفرق التي تريد شيئاً لا يحتاج تفكيراً Maven يكفي، وللمشاريع الكبيرة متعددة الوحدات Multi-module حيث يصبح وقت البناء مشكلة يومية فإن Gradle مع ال Build Cache يوفر وقتاً حقيقياً، وإذا كبر المشروع أكثر من ذلك ستسمع عن أدوات مثل [Bazel](https://bazel.build/?ref=arabroot.io) المصممة للمستودعات الكبيرة جداً مع Cache مشترك.

## أخطاء شائعة

- **«يعمل على جهازي»**: نسخة JDK أو SDK على جهاز المطور تختلف عن CI، والحل نسخة محددة في Image البناء، وGradle Wrapper أو ملف `global.json` لتثبيت نسخة SDK في .NET.
- **وسم `latest` في Image البناء**: يتغير دون أن تعرف، فيتغير الناتج أو يفشل البناء في يوم لم يتغير فيه الكود.
- **تجاوز الاختبارات للسرعة**: `-DskipTests` في خط النشر يعني أن الاختبار الذي رأيناه يفشل كان سيمر إلى الإنتاج.
- **عدم حفظ ذاكرة الاعتماديات في CI**: كل بناء ينزل مئات الملفات، ويصبح البناء معطلاً كلما تعطل المستودع الخارجي.
- **إعادة البناء لكل بيئة**: ناتج staging ليس هو ناتج الإنتاج، فما اختبرته ليس ما نشرته.
- **ترك نسخ الإضافات Plugins دون تحديد** في Maven: تتغير مع نسخة Maven نفسها.
- **الثقة بال Cache دائماً**: مهمة خاصة لم تعرف كل مدخلاتها تعطي ناتجاً قديماً، وعند الشك ابن دون Cache.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- البناء تحويل الكود إلى ناتج Artifact عبر خطوات ثابتة تقريباً: حل الاعتماديات ثم الترجمة ثم الربط ثم الاختبار ثم التحزيم، والنشر خطوة منفصلة في CI.
- Make الذي وصفه Feldman عام 1978 وضع القاعدة التي ما زالت تعمل: يعاد بناء الهدف إذا كان أقدم من أي ملف يعتمد عليه.
- Maven يصف المشروع في `pom.xml` ويشغل دورة حياة ثابتة كل مرحلة فيها تشغل ما قبلها، وGradle يقارن هاش المدخلات فيتجاوز ما لم يتغير وينسخ من ال Build Cache، وdotnet يعمل فوق MSBuild الذي يقارن أوقات الملفات.
- البناء القابل للتكرار يعني نفس الهاش لكل من يبني نفس الكود بنفس البيئة والتعليمات، وأعداؤه الوقت والمسار ونسخ الأدوات وترتيب الملفات، وMaven 3.10 وGradle 9 و.NET تعالج أغلبها افتراضياً.
- في CI ابن داخل Container بنسخة محددة، واحفظ ذاكرة الاعتماديات، وشغل الاختبارات، وابن مرة واحدة وانقل نفس الناتج إلى كل البيئات.

وأقرب المقالات إلى هذا الموضوع على الموقع:

[![](https://arabroot.io/content/images/2026/10/gitea-actions-cover-cover-2.webp)دليلGitea Actions: تشغيل CI/CD وبناء Docker Images على سيرفركاجعل سيرفرك يختبر الكود ويبني Docker Images تلقائياً مع كل push، دون أن يخرج كودك أو أسرارك إلى خدمة CI خارجية.](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/)[![](https://arabroot.io/content/images/2026/10/devsecops-cover-cover.webp)دليلDevSecOps للمبتدئين: الأمن داخل خط CI/CD ومتى يعمل كل فحصما الذي يجده كل فحص أمني في خط CI/CD وما الذي يفوته، وكيف ترتب نتائجه حتى تصل إلى قرار بدل قائمة طويلة.](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/)[![](https://arabroot.io/content/images/2026/10/supply-chain-cover-cover.webp)دليلأمن سلسلة التوريد البرمجية Software Supply Chain للمبتدئينكل ما بين كود غيرك وسيرفرك قد يحمل كود المهاجم، وهنا تعرف التهديدات الحقيقية بحوادث موثقة والدفاع المناسب لكل حلقة.](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/sca-cover-cover.webp)دليلSCA: كيف تجد الثغرات المعروفة في مكتبات مشروعككيف تجد أداة فحص الاعتماديات SCA المكتبات المصابة في مشروعك، ولماذا تختلف نتائج الأدوات، وكيف تبقي مكتباتك محدثة.](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF%D9%8A%D8%A7%D8%AA-sca-%D9%85%D8%B9-osv-scanner-%D9%88-dependency-track/)[![](https://arabroot.io/content/images/2026/10/environments-gitops-cover-cover.webp)دليلمن الفرع إلى الإنتاج: البيئات وGitOps للفرقكيف يختبر فريقك كل ميزة وحدها ويعرضها على العميل دون أن تصل ميزة غير جاهزة إلى الإنتاج.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%8A%D8%A6%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D8%A7%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA-%D9%88-gitops-%D9%84%D9%84%D9%81%D8%B1%D9%82/)[![](https://arabroot.io/content/images/2026/10/paradigms-types-cover-cover.webp)دليلأنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟نحل مسألة واحدة بعدة أساليب برمجة، ونرى هل تكتشف اللغة أخطاءك قبل التشغيل أم بعده، وماذا يعني ذلك عند اختيار لغة لمشروعك.](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة العملية على Docker Engine 29.8.0 مع Maven 3.10.0 وGradle 9.8.0 على Temurin JDK 25.0.4.1، و.NET SDK 10.0.401، وGo 1.27.1 وGNU Make 4.3 وgcc 12.2.0.