# إضافات البناء

يضم Elide مُصرّفًا، ومحلِّلًا لتبعيات Maven، ومُجمِّعًا للحاويات، ومشغّلًا للاختبارات. لكنه لا يضم `protoc` ولا `shellcheck` ولا ما سيحتاجه مشروعك لاحقًا. **إضافة البناء** هي طريقتك لتعريف البناء بأداة كهذه: وحدة JavaScript أو TypeScript يقيّمها Elide أثناء ضبط البناء.

تصرّح الإضافة بمهام. وتصبح كل مهمة منها مهمة حقيقية في المخطط: يمكنك تسميتها هدفًا للبناء، وجعل مهمة أخرى تعتمد عليها، ووضعها في مجموعة، ورؤيتها في `elide build --inspect`.

## التصريح بإضافة

أدرجها في ملف البيان، نسبةً إلى جذر المشروع:

```pkl
amends "elide:project.pkl"

name = "demo"

extensions {
  "tools/protoc.ts"
}
```

يُصرَّح بالإضافات ولا تُكتشف أبدًا، لذا فإن تشغيل إحداها دائمًا شيء طلبه مشروعك. والمسار غير الموجود يُفشل البناء بدلًا من ألا يُسجَّل شيء بصمت.

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

يسمّي التصريح أحد شيئين، ويحدد Elide أيهما من امتداد الملف وحده: وحدة ES، أي `.ts` أو `.mts` أو `.js` أو `.mjs` أو `.jsx` أو `.tsx`، أو أرشيف حزمة `.elx` (راجع [تحزيم إضافة](#packaging-an-extension)). لا يُقبل `.cjs`: فالعقد تصدير افتراضي لا يستطيع سكربت CommonJS التصريح به، ويُقرأ ملف `.js` دائمًا وحدةً أيًّا كان ما يقوله أقرب `package.json`. وأي شيء آخر يُفشل البناء بدلًا من تخمينه.

ولا يلزم أن يكون المُدخل مسارًا أصلًا: إذ يمكنه تسمية حزمة تُنزَّل من عنوان URL أو من npm أو من Maven. راجع [جلب إضافة](#fetching-an-extension).

## كتابة إضافة

```ts
export default ({ task }: ElideBuild) => {
  task(
    "protoc",
    {
      description: "Generate protobuf sources",
      group: "compile",
      inputs: ["proto/**/*.proto"],
      outputs: ["src/main/generated"],
    },
    (ctx) => {
      const result = ctx.exec("protoc", [
        `--kotlin_out=${ctx.layout.projectDir}/src/main/generated`,
        "proto/api.proto",
      ]);
      if (!result.ok) ctx.fail(result.stderr);
    },
  );
};
```

ثم:

```bash
elide build --inspect   # protoc  Generate protobuf sources
elide build protoc      # run just that task
elide build compile     # run it with everything else in its group
```

تُنشر الأنواع في [`@elide-dev/types`](https://www.npmjs.com/package/@elide-dev/types) باسم `ElideBuild` و `ElideBuildTaskContext` و `ElideBuildTaskOptions`.

يعمل التصدير الافتراضي لوحدتك أثناء ضبط البناء، ويمكن أن يكون `async`. ويعمل إجراء كل مهمة عند تشغيل المهمة، ويمكن أن يكون `async` أيضًا؛ وينتظر البناء في الحالتين.

## تحزيم إضافة

الإضافة التي تتجاوز ملفًا واحدًا تُوزَّع أرشيفًا: ملف zip فيه ملف بيان باسم `extension.json` في جذره، ويحمل الامتداد `.elx`. والاسم ليس زخرفة: فامتداد الملف في التصريح هو ما يخبر Elide بأن أمامه حزمة، و `.elx` هو الامتداد الوحيد الذي يفعل ذلك، لذا يُحمَّل `tools/protoc.elx` ولا تُحمَّل البايتات نفسها باسم `tools/protoc.zip`.

```
protoc-tasks/
├── extension.json
├── tasks.ts
└── lib/
    └── flags.ts
```

```json
{
  "name": "protoc-tasks",
  "version": "1.0.0",
  "description": "Generates sources with protoc",
  "license": "Apache-2.0",
  "repository": "https://github.com/example/protoc-tasks",
  "homepage": "https://example.dev/protoc-tasks",
  "authors": ["Example Inc."],
  "type": "source",
  "entrypoint": "tasks.ts"
}
```

يتطلب ملف بيان الأرشيف `name` و `type`. وقيمة `type` هي `source`، النوع الوحيد الموجود، ويجب أن تسمّي حزمة `source` أيضًا `entrypoint`، وهو مسار نسبةً إلى جذر الأرشيف؛ ويُرفض ما يشير إلى خارج الأرشيف. وكل ما عدا ذلك بيانات وصفية يعيدها Elide إليك، وتُتجاهل المفاتيح التي لا يعرفها، لذا يمكن للحزمة أن تحمل مفاتيح خاصة بها.

### بناء الحزمة

صرّح بالحزمة مُخرَجًا، وسيُنتجها `elide build`:

```pkl
amends "elide:project.pkl"

import "elide:Extensions.pkl" as Extensions

name = "protoc-tasks"
version = "1.0.0"
description = "Generates sources with protoc"

artifacts {
  ["tasks"] = new Extensions.ExtensionArchive {
    entrypoint = "tasks.ts"
    srcs { "lib/**/*.ts" }
    license = "Apache-2.0"
    repository = "https://github.com/example/protoc-tasks"
    authors { "Example Inc." }
  }
}
```

يسجّل ذلك مهمة باسم `package-extension-tasks` يشغّلها `elide build` العادي، وتكتب `.dev/artifacts/tasks.elx`. ويُكتب `extension.json` أعلاه نيابةً عنك: تأخذ `name` و `version` و `description` قيم المشروع افتراضيًا، وتكون قيمة `type` دائمًا `source`.

`entrypoint` هي الوحدة التي يصرّح تصديرها الافتراضي بمهامك. و `srcs` هي الوحدات التي تستوردها، مسارات أو أنماط glob نسبةً إلى جذر المشروع؛ وتُحزَّم نقطة الدخول نفسها دائمًا. تحتفظ الملفات بمساراتها في مشروعك، لذا تُحلّ الاستيرادات النسبية داخل الأرشيف تمامًا كما في نسختك المحلية. ويجب أن يقع كل ملف محزَّم داخل المشروع بعد حلّ روابطه: فالرابط الرمزي الذي يشير إلى خارج المشروع يُفشل البناء بدلًا من توزيع ما يشير إليه.

ويمكنك بالطبع بناء ملف zip بنفسك، من المجلد الذي يحوي ملف البيان، كي يقع `extension.json` في الجذر:

```bash
cd protoc-tasks && zip -r ../tools/protoc.elx .
```

### حزمة مصدرية

يعني `"type": "source"` أن الأرشيف يحمل شيفرتك المصدرية، وأن امتداد ملف نقطة الدخول يحدد طريقة تحميلها، تمامًا كما في ملف تصرّح به مباشرة. تُقيَّم نقطة الدخول `.ts` أو `.tsx` أو `.mts` أو `.js` أو `.jsx` أو `.mjs` وحدةً ضيفة، ولأن Elide يفك الأرشيف قبل تقييمه، تُحلّ الاستيرادات النسبية داخل الحزمة (`import { flags } from "./lib/flags.ts"`).

لا يُجمَّع شيء ولا يُصرَّف في الطريق: فالحزمة توزّع المصادر التي كتبتها، وتكتب الإضافة نفسها سواء انتقلت ملفًا واحدًا أو أرشيفًا: التصدير الافتراضي نفسه، و ⁦`task(...)`⁩ نفسها، و `ctx` نفسه.

### التصريح بحزمة

مثل إضافة محلية، أي مسار مع إعدادات إن كانت تقبلها، أو شيئًا يُجلب:

```pkl
extensions {
  "tools/shellcheck.elx"
  new {
    from = "tools/protoc.elx"
    config { greeting = "hei" }
  }
}
```

يفك Elide الأرشيف ضمن مجلد البناء، في `.dev/extensions/packages/<archive>-<hash>/contents/`، ويفكه من جديد كلما تغيّر الأرشيف. ويُرفض أي مُدخل قد يُكتب خارج ذلك المجلد بدلًا من فكّه.

### ما يعرضه `--inspect`

يطبع `elide build --inspect` قسم `Extensions:` يغطي كل ما صرّح به ملف البيان. ولكل إضافة يعرض التصريح الذي كتبته، واسم الحزمة وإصدارها، ووصفها، ونوع الإضافة، أي `JavaScript/TypeScript module` أو `packaged extension`، وترخيصها، ومستودعها، وأسماء المهام التي صرّحت بها.

```
Extensions:
  tools/protoc.elx   protoc-tasks 1.0.0
    Generates sources with protoc
    packaged extension · Apache-2.0 · https://github.com/example/protoc-tasks · Example Inc.
    tasks: protoc-generate, protoc-clean
```

توزيع الأرشيف متروك لك: أرفقه بإصدار، أو قدّمه من عنوان URL، أو انشره حزمة npm بما تستخدمه بالفعل. وأيًّا كانت طريقة انتقاله، يستهلكه المشروع بالطريقة نفسها؛ راجع [جلب إضافة](#fetching-an-extension).

## جلب إضافة

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

```pkl
extensions {
  "url:https://downloads.example.com/protoc-tasks-1.4.0.elx"
  "npm:@acme/protoc-tasks@1.4.0"
  "maven:com.acme:protoc-tasks:elx:1.4.0"
  new {
    from = "npm:@acme/codegen@2.0.1"
    config { proto = "src/proto" }
  }
}
```

لا يُحجز إلا `url:` و `npm:` و `maven:`. وكل ما عداها مسار، لذا يبقى `C:\tools\ext.ts` يعني ما يقوله، ولا يمكن لمخطط يضيفه Elide لاحقًا أن يغيّر طريقة قراءة ملف بيان يُبنى بالفعل.

بعد التنزيل لا يختلف شيء. يُحمَّل `.elx` المجلوب حزمةً و `.ts` المجلوب وحدةً ضيفة: فالحاسم هو ما حُلّ إليه، لا طريقة وصوله. ويجب أن يكون `url:` عنوان URL مطلقًا بـ `https`؛ ويُرفض `http` غير المشفّر إلا إلى مضيف loopback، وهو موجود كي تتمكن من الاختبار على شيء محلي. تُتبع عمليات إعادة التوجيه حتى خمس قفزات، وتخضع كل قفزة للقاعدة نفسها، لذا لا يمكن لإعادة توجيه أن تنقل جلبًا من loopback إلى جهاز آخر دون تشفير.

### الإصدارات مثبّتة

يسمّي الإصدار البعيد إصدارًا واحدًا بالضبط. يرفض مخطط ملف البيان `npm:tasks` و `maven:com.acme:tasks` لأنهما لا يسمّيان أي إصدار، ويرفض `npm:tasks@^1.2` و `npm:tasks@latest` و `maven:com.acme:tasks:1.2.3-SNAPSHOT` لأنها تسمّي أكثر من إصدار.

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

### مكان تخزينها مؤقتًا

تُحفظ المُخرَجات المجلوبة في `.dev/dependencies/extensions/remote/`، بجانب مستودع Maven المحلول، وتشاركها مساحة العمل كلها بالطريقة نفسها. ولأن التصريح مثبّت، لا يتقادم المخزَّن أبدًا: لا يصل إلى الشبكة إلا البناء الأول، ويعمل `elide build --offline` من بعده. والتصريح الذي لم يُجلب ولا يمكن جلبه يقول ذلك:

```
Build extension 'npm:@acme/protoc-tasks@1.4.0' has not been downloaded, and --offline forbids fetching it
Run the build once without --offline to populate the cache.
```

يُتحقق من أي ملخّص ينشره المصدر: `dist.integrity` أو `dist.shasum` في npm، وملف `.sha1` المرافق في Maven. والمستودع الذي يقدّم مُخرَجًا دون ملخّص مقبول؛ أما الذي يقدّم الاثنين ويناقض نفسه فيُفشل البناء.

### تثبيت البايتات

يحدد تثبيت الإصدار أي إصدار يعمل؛ ويحدد تثبيت `integrity` أي بايتات. اكتبه بصيغة Subresource Integrity، أي `sha256-<base64>` أو `sha512-<base64>`، بجانب المصدر الذي ينتمي إليه:

```pkl
extensions {
  new {
    from = "url:https://downloads.example.com/protoc-tasks-1.4.0.elx"
    integrity = "sha512-Ln7jRoUMYMK7TBZ0bNyPmrHDCgAsQ3bIt5nZeUuzpuGqBbw1zYWTPdA2tRbClC2CoRP3UExpHnCw+5kZLgP0lQ=="
  }
}
```

لا يُوثق بـ `url:` إلا عبر TLS ومضيفه، لذا فالتثبيت هو ما يجعله قابلًا لإعادة الإنتاج. يُتحقق منه قبل تخزين التنزيل مؤقتًا، ومرة أخرى عند كل إصابة في الذاكرة المؤقتة، وتعمل حزمة npm من شجرة مستخرَجة من البايتات المتحقَّق منها نفسها، لذا لا يستطيع الخادم ولا الذاكرة المؤقتة المشتركة لمساحة العمل تغيير ما يعمل من وراء ظهرك. ويُعاد تنزيل الملف المخزَّن الذي لم يعد يطابق التثبيت ويُستبدل؛ ومع `--offline` يفشل البناء بدلًا من ذلك مسمّيًا الملف المخزَّن. ويُضاف التثبيت إلى ما ينشره السجل بدلًا من أن يحل محله: إذ يظل ملخّص npm أو Maven قيد التحقق.

ولا يقبله إلا المصدر المجلوب. ويُرفض تثبيت مسار داخل مشروعك، فبايتاته هي أصلًا ما تحويه شجرتك.

التنزيل محدود في الحجم والوقت: تفشل الاستجابة التي تتجاوز 64 MiB، وكذلك المضيف الذي يستجيب ثم يتوقف، بدلًا من تعليق عملية الضبط.

### حزم npm

يُخزَّن ملف tarball مؤقتًا مرة واحدة لمساحة العمل، ويُستخرج إلى مجلد البناء في مشروعك، ضمن `.dev/extensions/npm/<name>/<version>/contents/`، ويُحمَّل من نقطة دخوله: تلك التي يسمّيها `extension.json` إن كانت الحزمة تتضمنه، وإلا فـ `main` في `package.json`، وإلا فـ `index.js`. والحزمة التي تحمل `extension.json` حزمة بالمعنى الكامل، وتُقرأ تمامًا كما يُقرأ `.elx`: فإن كانت غير صالحة، أو تسمّي `type` لا يعرفه هذا الإصدار من Elide، أفشلت البناء بدلًا من الرجوع إلى `main`.

يمر الحلّ بثلاث خطوات بالترتيب: `dependencies.npm.registry` إذا ضبطه ملف البيان، ثم `$ELIDE_PKGST_NPM_ENDPOINT`، وهو المتغير نفسه الذي يقرؤه محلِّل npm الخاص بـ Elide، وإن غاب الاثنان فالخادم الوسيط `npm.pkg.st` التابع لـ Elide، أو السجل العام `registry.npmjs.org` عند تمرير `--use-pkgst=false`.

```pkl
dependencies {
  npm {
    registry = "https://npm.internal.example.com"
  }
}
```

### مُخرَجات Maven

تُجلب الإحداثية من المستودعات التي تصرّح بها بالفعل، أي `dependencies.maven.repositories`، إضافة إلى الخادم الوسيط الخاص بـ Elide و Maven Central ما لم يكن `enableDefaultRepositories` معطّلًا، باستخدام بيانات الاعتماد التي تحملها تلك المستودعات. لا تُرسل بيانات الاعتماد إلا إلى مخطط المستودع نفسه ومضيفه ومنفذه؛ وتُتبع إعادة التوجيه إلى أي مكان آخر من دونها. ويقع `[:packaging[:classifier]]` المعتاد بين المُخرَج والإصدار، والإحداثية التي لا تسمّي تحزيمًا تأخذ `elx`: يجلب `maven:com.acme:tasks:1.4.0` الملف `tasks-1.4.0.elx`، تمامًا مثل `maven:com.acme:tasks:elx:1.4.0`. ويُرفض التحزيم الذي لا يستطيع Elide تحميله (`maven:com.acme:tasks:zip:1.4.0`) كما يُرفض أي تصريح لا يمكن تحميله.

تُجرَّب المستودعات بالترتيب. ويُتجاوز المستودع الذي يستجيب بخطأ، أو باستجابة كبيرة جدًا أو متوقفة، أو بإعادة توجيه مرفوضة، إلى التالي؛ أما ملف `.sha1` المنشور الذي لا يطابق المُخرَج فيُفشل البناء.

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

## ضبط إضافة

يُصرَّح بالإضافة التي تقبل إعدادات في كتلة بدلًا من مسار مجرد، وتحمل الإعدادات معها. لا يفسّرها ⁦Elide:⁩

```pkl
extensions {
  "tools/shellcheck.ts"
  new {
    from = "tools/protoc.ts"
    config {
      version = "25.1"
      plugins = new Listing { "grpc" }
    }
  }
}
```

تصل تلك الكتلة إلى تلك الإضافة، وإليها وحدها، باسم `config`، أثناء تصريحها بالمهام وأثناء تشغيلها:

```ts
export default ({ task, config }: ElideBuild) => {
  task("protoc", { description: `protoc ${config.version}` }, (ctx) => {
    const { version, plugins } = ctx.config;
  });
};
```

الإضافة المصرَّح بها بمسار مجرد تقرأ `config` فارغًا، لذا يكفي التحقق من أن الإعداد `undefined` لتطبيق قيمة افتراضية.

الإضافة التي تريد إعدادات مُنمَّطة وموثَّقة توزّع وحدة Pkl خاصة بها، ينسخها مشروعك ويستوردها ويملؤها. يستورد ملف البيان الملفات المحلية، ومكتبة Pkl القياسية، والمخطط الخاص بـ Elide، ولا يستورد أبدًا `package://` أو أي URI شبكي آخر، لذا تقيم الوحدة في مشروعك:

```pkl
import "tools/Protoc.pkl"

extensions {
  new {
    from = "tools/protoc.ts"
    config = new Protoc.Settings { version = "25.1" }
  }
}
```

تتمايز المُدخلات بالمسار أيًّا كانت طريقة كتابتها، لذا لا يمكن التصريح بالإضافة نفسها، أو ضبطها، مرتين.

اكتب القائمة داخل الكتلة بصيغة `Listing` (أو `List("grpc")`). أما العناصر المضمَّنة، مثل `plugins { "grpc" }`، فستصل إلى إضافتك مفهرسة بالموضع لا قائمةً، لذا يرفضها ملف البيان ويخبرك بالصيغة الصحيحة.

الإعدادات بيانات. يمكن أن تكون القيمة سلسلة نصية، أو قيمة منطقية، أو رقمًا، أو `Duration`، أو `DataSize` (تصل إلى إضافتك عددًا من البايتات)، أو `Regex`، أو كتلة أو `Listing` أو `List` أو `Set` أو `Map` أو `Pair` منها. أما `Bytes` أو الصنف أو الاسم البديل للنوع أو الدالة فخطأ في ملف البيان يذكر موضعه، مثل `config.flags.blob`.

## خيارات المهام

| الخيار | المعنى |
| --- | --- |
| `description` | يعرضه `elide build --inspect`. |
| `dependsOn` | أسماء المهام التي يجب أن تكتمل أولًا؛ ويمكن إعطاء اسم واحد دون مصفوفة. |
| `before` | المهام التي يجب أن تسبقها هذه المهمة: اسم مهمة، أو `deps`/`compile`/`test` لكل مهمة في تلك المجموعة. |
| `group` | `deps` أو `compile` أو `test` للانضمام إلى مجموعة مدمجة؛ وأي اسم آخر ينشئ مجموعة خاصة بك. |
| `default` | ما إذا كان `elide build` المجرد يشغّل هذه المهمة. القيمة الافتراضية `true`. |
| `inputs` | مسارات أو أنماط glob نسبةً إلى المشروع، يحدد محتواها ما إذا كانت المهمة ستُعاد. |
| `outputs` | مسارات أو أنماط glob نسبةً إلى المشروع تكتبها المهمة. |
| `cache` | ما إذا كان يمكن لذاكرة البناء المؤقتة أرشفة مخرجات هذه المهمة. القيمة الافتراضية `true`. |
| `options` | خيارات سطر الأوامر التي تقبلها هذه المهمة (انظر أدناه). |

`inputs` و `outputs` عقد واحد: صرّح بكليهما فيتخطى Elide المهمة إذا لم يتغير شيء (راجع [مهام البناء](/docs/toolchain/build-tasks))؛ وصرّح بأحدهما أو بلا شيء فتعمل في كل بناء. وتُحسب إضافتك مُدخلًا أيضًا، أي الملف نفسه وكل وحدة يستوردها بمسار نسبي، لذا فإن تعديل نقطة الدخول أو أحد مساعديها يعيد تشغيل المهام التي تصرّح بها.

والمهمة التي صرّحت بكليهما تُخزَّن مؤقتًا أيضًا مثل مهام Elide نفسه: تُؤرشف مخرجاتها عند نجاحها، وتُستعاد في أي بناء لاحق تحمل مُدخلاته البصمة نفسها، بما في ذلك نسخة محلية جديدة. اضبط `cache: false` للإبقاء على فحص التحديث وتخطي الأرشيف، وهو الخيار الصحيح عندما تعتمد مخرجاتك على شيء لا تسمّيه `inputs`.

تُحسب بصمة التصريح من وضع كل ملف يسمّيه ومحتواه، ويقرأ إجراؤك الملفات نفسها والبصمات نفسها عبر `ctx.inputs` و `ctx.outputs`؛ راجع [الملفات التي يسمّيها التصريح](#the-files-a-declaration-names).

## الخيارات التي تقبلها مهمتك

ينبغي للمهمة التي تقرأ خيارًا أن تصرّح بذلك. تُطبع الخيارات المصرَّح بها عبر `elide build --inspect` و `elide build <task> --help`، ويُتحقق منها قبل تشغيل أي شيء، لذا يُبلَّغ عن الخطأ الإملائي بدلًا من إسقاطه بصمت:

```ts
task(
  "publish",
  {
    options: [
      { name: "channel", description: "Channel to publish to", arity: "required" },
      { name: "tag", description: "Extra tag to push", arity: "required", repeatable: true },
      { name: "sign", description: "Sign the release", negatable: true, default: "true" },
    ],
  },
  (ctx) => {
    ctx.option("channel"); // first value, or the declared default
    ctx.values("tag"); // every value, in command-line order
    ctx.flag("sign"); // `--no-sign` reads false; absent reads the default
  },
);
```

| الحقل | المعنى |
| --- | --- |
| `name` | الخيار، دون شرطات وبصيغة ⁦kebab-case:⁩ `"dry-run"` هو `--dry-run`. |
| `description` | يعرضه `--inspect` و `--help`. |
| `arity` | `"none"` خيار دون قيمة، و `"required"` يتطلب `--flag=value`، و `"optional"` يقبل الاثنين. القيمة الافتراضية `"none"`. |
| `repeatable` | ما إذا كان يمكن تمرير الخيار أكثر من مرة. القيمة الافتراضية `false`. |
| `negatable` | ما إذا كان `--no-<name>` مقبولًا، ويُقرأ `"false"`. القيمة الافتراضية `false`. |
| `default` | القيمة التي تعود إليها القراءة عند عدم تمرير الخيار. |
| `keyed` | ما إذا كانت القيمة الممرَّرة للخيار تحدد ما تنتجه المهمة. القيمة الافتراضية `true`. |

يطبع `--inspect` و `--help` التصريح كما يجب كتابته، لذا تُقرأ الخيارات الثلاثة أعلاه `--channel=<value>` و `--tag=<value> (repeatable)` و `--sign (or --no-sign) [default: true]`.

الخيار الذي لا يصرّح به شيء يُفشل البناء، مع أقرب اسم مصرَّح به إن وُجد؛ وكذلك القيمة الممنوحة لخيار دون قيمة، والخيار المطلوب المتروك دون قيمة، وتكرار خيار يقبل قيمة واحدة. أما خيارات Elide نفسه، مثل `--verbose` و `--project` و `--offline` وغيرها، فمقبولة دائمًا.

تقرأ `ctx.option` و `ctx.flag` و `ctx.values` عبر التصريح، وهذا ما يجعل `--no-sign` والقيمة الافتراضية قابلين للقراءة أصلًا: فلا يقول سطر الأوامر أيًّا منهما بنفسه. والاسم الذي لم تصرّح به مهمتك يُقرأ من سطر الأوامر كما حُلِّل.

الخيار المصرَّح به جزء مما يحدد إعادة تشغيل مهمتك: إذ تُسجَّل القيم التي حُلّ إليها بجانب `inputs`، لذا لا يُعدّ البناء الذي يعمل مع `--channel=stable` محدَّثًا أبدًا، ولا يتلقى ما خزّنه `--channel=canary`، مقارنةً ببناء عمل مع القيمة الأخرى. اضبط `keyed: false` على الخيار الذي يغيّر مقدار ما تطبعه المهمة لا ما تكتبه، وإلا فإن كل بناء يكتبه بشكل مختلف سيخطئ الذاكرة المؤقتة دون داعٍ. والخيار الذي يقرؤه إجراؤك دون التصريح به لا يحدد شيئًا على الإطلاق.

## التشغيل قبل مهمة أخرى

يسمّي `dependsOn` ما يجب تشغيله أولًا. ويسمّي `before` ما يجب أن ينتظر، بما في ذلك مهام لم تصرّح بها، وهذا ما تحتاجه مهمة توليد الشيفرة:

```ts
task(
  "generate-sources",
  { before: "compile", default: false, outputs: ["src/main/generated"] },
  (ctx) => { ... },
);
```

تمثّل `compile` و `deps` و `test` كل مهمة في تلك المجموعة، وهذا ما يجب أن تعنيه عبارة «قبل التصريف»: فالمفاتيح الحقيقية لكل لغة ولكل مجموعة مصادر (`compile-kotlin-main`، `compile-java-test`). وأي اسم آخر هو اسم مهمة واحدة، والاسم الذي لم يسجّله شيء يُفشل البناء بدلًا من ألا يعمل شيء بصمت. وفي مساحة العمل، تختار الإضافة التي يصرّح بها عضو مهام ذلك العضو فقط، بينما تختار الإضافة التي يصرّح بها ملف البيان الجذري المجموعة عبر كل الأعضاء.

ويجرّ رابط كهذا مهمتك أيضًا إلى أي بناء يشغّل المهمة التي تسبقها: إذ يشغّل `elide build compile` في المثال أعلاه `generate-sources` أولًا، رغم أنها اختارت الخروج من `default`.

## البناء التزايدي

تُبلَّغ المهمة الجارية بما وجده البناء متقادمًا، ويمكنها الاحتفاظ بقيم لتشغيلها التالي:

```ts
task("codegen", { inputs: ["proto", "schema"], outputs: ["gen"] }, (ctx) => {
  for (const input of ctx.dirty.inputs) ctx.log(`changed: ${input}`);
  if (ctx.dirty.outputs.length) ctx.log("an output is missing or was edited");
  if (ctx.dirty.extension) ctx.log("this extension or one of its imports changed");

  ctx.log(`last built version ${ctx.state.get("version") ?? "(never)"}`);
  ctx.state.set("version", ctx.manifest.version);
});
```

لا تسمّي `ctx.dirty` إلا التصريحات التي تغيّرت؛ وفي التشغيل الأول يُدرج كل ما صرّحت به. وتحفظ `ctx.state` السلاسل النصية والأرقام والقيم المنطقية و `null` والمصفوفات والكائنات البسيطة، مفهرسة بالاسم، وتُرجع `get` القيمة `null` لمفتاح لم يُخزَّن فيه شيء. ليس للدالة والرمز و `undefined` صيغة تخزين، والقيمة التي تحتوي نفسها تُرفض بدلًا من تتبّعها. والتشغيل الفاشل يتخلص من حالة المهمة كلها، بما في ذلك ما سجّلته التشغيلات السابقة.

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

## الملفات التي يسمّيها التصريح

`ctx.inputs` و `ctx.outputs` هما التصريحات نفسها، محلولةً ومختومةً: المسح نفسه والملخّص نفسه اللذان استخدمهما البناء ليقرر ما إذا كانت مهمتك محدَّثة:

```ts
task("codegen", { inputs: ["proto"], outputs: ["gen"] }, (ctx) => {
  for (const file of ctx.inputs.files()) {
    ctx.log(`${file.path} ${file.size}b ${file.stamp}`);
  }

  const before = ctx.state.get("proto") ?? {};
  const after = Object.fromEntries(ctx.inputs.files().map((f) => [f.path, f.stamp]));
  for (const path of Object.keys(after)) {
    if (after[path] !== before[path]) ctx.exec("protoc", ["--kotlin_out=gen", path]);
  }
  ctx.state.set("proto", after);

  ctx.log(`produced ${ctx.outputs.stamp()}`);
});
```

تُدرج `files()` كل ملف عادي تسمّيه التصريحات، نسبةً إلى المشروع ومفصولًا بـ `/`، مرتبًا ودون تكرار. ويحمل كل مُدخل `path` و `absolute` و `size` و `modified` و `stamp`، وهو SHA-256 لوضع ذلك الملف ومحتواه. و `stamp()` هو الملخّص للمجموعة كلها.

تقبل كلتاهما أنماطًا خاصة بها لا يلزم أن تكون مما صرّحت به: إذ تضيّق `ctx.inputs.files("proto/api/**/*.proto")` القائمة، و `ctx.inputs.stamp("proto")` هي بالضبط البصمة التي سجّلها البناء لذلك التصريح. وتُحلّ كلتاهما عند استدعائهما، لذا فإن قراءة `ctx.outputs` بعد كتابة مخرجاتك تصف ما أنتجته للتو.

تعتمد الأختام على المحتوى: فالتعديل الذي لا يغيّر حجم الملف ووقت تعديله يغيّرها مع ذلك، ويعيد تشغيل مهمتك. وتسمّي `ctx.dirty` التصريح كاملًا؛ أما الختم لكل ملف المحفوظ في `ctx.state`، كما في المثال، فهو طريقة المهمة لمعرفة أي ملفاتها تغيّر.

## تصريحات يجب أن يحسبها البناء

بعض ما يحدد ما إذا كانت المهمة محدَّثة لا يمكن كتابته في التصريح: شجرة مولَّدة، أو classpath حلّته مهمة أخرى، أو إصدار الأداة التي توشك على تشغيلها. امنح المهمة خطّاف `prepare` وصرّح به هناك:

```ts
task(
  "package",
  {
    outputs: ["dist"],
    prepare: (ctx) => {
      ctx.inputs.add("gen/**/*.kt");
      ctx.inputs.value("classpath", ctx.state.of("resolve")?.get("classpath"));
      ctx.outputs.add(`dist/${ctx.manifest.version}.tgz`);
    },
  },
  (ctx) => {
    for (const changed of ctx.dirty.values) ctx.log(`${changed} changed`);
  },
);
```

ما يضيفه الخطّاف تصريح كأي تصريح آخر: يُختم النمط، وتسمّيه `ctx.dirty.inputs`، ويدخل في مفتاح الذاكرة المؤقتة للمهمة تمامًا كما يدخل النمط المصرَّح به. وتصرّح `ctx.inputs.value(name, value)` بمُدخل ليس ملفًا، أي أي شيء يستطيع البناء حسابه ولا يستطيع فحصه بـ stat، وتغييره يعيد تشغيل المهمة ويغيّر مفتاح أرشيفها؛ وتسمّي `ctx.dirty.values` ما تغيّر منها. والمهمة التي يُستكمل تصريحها بهذه الطريقة تُتتبَّع وتُخزَّن مؤقتًا وتُتخطى بوصفها محدَّثة كأي مهمة أخرى.

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

يمكن لإجرائك إضافة **مخرجات** لا يعرفها إلا عند كتابتها، باستخدام ⁦`ctx.outputs.add(...)`⁩ نفسها؛ فتُختم وتُؤرشف مع البقية عند عودته. ولا يمكنه إضافة مُدخلات: فبحلول ذلك الوقت يكون البناء قد قرر تشغيله، لذا فإن `ctx.inputs.add` المتأخرة خطأ لا تصريح لا يفعل شيئًا بصمت.

## تمرير قيمة إلى مهمة أخرى

تُقيَّم وحدة إضافتك مرة واحدة لكل بناء وتتشاركها كل المهام التي تصرّح بها، لذا فإن متغير الوحدة الذي تعيّنه مهمة غالبًا ما يبقى للمهمة التالية، لكن لا شيء يضمن ذلك، والتقييم الجديد يبدأ من لا شيء. عامل نطاق الوحدة ذاكرةً مؤقتة، لا قناةً. وما تحفظه المهمة في `ctx.state` هو الطريقة المدعومة لتمرير قيمة:

```ts
task("stage", (ctx) => {
  ctx.state.set("staged", `${ctx.layout.buildDir}/staged`);
});

task("publish", { dependsOn: "stage" }, (ctx) => {
  const stage = ctx.state.of("stage");
  if (stage === null) return ctx.skip("nothing has been staged");
  ctx.exec("rsync", [String(stage.get("staged")), "example:/srv"]);
});
```

تُرجع `ctx.state.of(name)` عرضًا لما لدى تلك المهمة، أي `get` و `has` و `keys`، و `null` إذا لم تنشر شيئًا على الإطلاق. والمهمة التي تخطّاها البناء لكونها محدَّثة، أو استعادها من الذاكرة المؤقتة، تجيب بما احتفظت به في آخر تشغيل لها: فكونها محدَّثة تقرير عما أنتجته، وما نشرته جزء من ذلك. وتعني `null` أنه لم يكن هناك ما يُقرأ: فالاسم ليس مهمة صرّحت بها إضافة، أو لم تكن في هذا البناء، أو فشلت، أو لم تعمل بعد؛ بينما العرض الذي لا مفاتيح فيه مهمة لم تحتفظ بشيء. اجعل مهمتك تعتمد على المهمة التي تقرؤها؛ فمن دون رابط تصبح «لم تعمل بعد» حالة تسابق.

تنتقل القيمة بالبنية التي كانت عليها، لا نصًا، وتنطبق على ما يمكن نقله القواعد نفسها التي تنطبق على `ctx.state`. وقراءة اسمك تُرجع ما تحتفظ به الآن.

## ما تتلقاه مهمتك

يحمل الكائن الممرَّر إلى إجرائك الأشياء نفسها التي يقرؤها البناء:

- `project`: الاسم والإصدار والوصف ومجلد الجذر ومساحة العمل التي ينتمي إليها.
- `manifest`: ملف `elide.pkl` المحلول، بيانات JavaScript عادية. وهو بالضبط ما يكتبه `elide project export --format=json`، لذا تعمل ⁦`manifest.dependencies.maven.packages.map(...)`⁩ كما تتوقع.
- `config`: الإعدادات التي صرّح بها ملف البيان لهذه الإضافة (انظر أعلاه)، وتكون فارغة إن لم يصرّح بشيء.
- `layout`: `projectDir` و `buildDir` و `artifacts` و `staging` وما شابهها، مسارات مطلقة.
- `option(name)` / `flag(name)` / `values(name)`: الخيارات التي شُغّل بها هذا الاستدعاء، مقروءة عبر تصريحك (انظر أعلاه)؛ و `options` هو التحليل الخام لكل ما لم تصرّح به.
- `task`: تصريحك أنت، كما سجّله البناء، بما في ذلك الخيارات التي يصرّح بها.
- `dirty` / `state`: ما تغيّر منذ آخر تشغيل لمهمتك، وما احتفظت به، وما سجّلته المهام الأخرى في هذا البناء (انظر أعلاه).

## الإبلاغ عن النتيجة

| في إجرائك | ما يسجّله البناء |
| --- | --- |
| العودة، أو إرجاع `true` | نجاح |
| `ctx.fail(reason)` | فشل، مع ذلك السبب |
| إرجاع `false` | فشل، دون سبب |
| `ctx.skip(reason)` | تخطٍّ، مع ذلك السبب |
| رمي استثناء أو رفض | فشل، مع رسالته |
| `ctx.exit(code, reason)` | فشل مع الخروج بـ `code` |

تكتب `ctx.log` و `ctx.warn` و `ctx.error` أسطرًا منسوبة إلى مهمتك. وهي رسائل بناء، لذا تذهب إلى الخطأ القياسي مع كل رسائل البناء الأخرى؛ ولا يحمل الإخراج القياسي إلا ما يكتبه برنامج شغّلته.

### الخروج بحالة محددة

تُفشل `ctx.exit(code, …message)` المهمة وتطلب من البناء الخروج بـ `code`. أما `0` فيعني النجاح، وهذا ما يجعلها قرار النجاح أو الفشل كله لمهمة تغلّف برنامجًا:

```ts
ctx.exit(ctx.exec("shellcheck", ["scripts/build.sh"]).exitCode);
```

استخدمها عندما تحمل الحالة معنى قد تمحوه القيمة المنطقية. فسقّاطة الصحة التي تخرج بـ 2 لأن المحلّل انهار، و 1 لأنه وجد مخالفة جديدة، لا تفيد إلا إذا استطاع المستدعي التمييز بينهما؛ وكذلك التشخيص الذي يخرج بـ 2 لـ «غير مدعوم على هذا المضيف» و 1 لـ «وُجد شيء».

لا يصل الرمز إلى العملية إلا إذا كانت مهمتك هي التي أوقفت البناء؛ فإن فشلت مهمة أخرى أولًا فهي الغالبة. والرمز الواقع خارج `0`-`255` يُلفّ كما تبلّغ عنه الصدفة، ولا يُلفّ إلى صفر أبدًا.

## تشغيل أدوات خارجية

```ts
const result = ctx.exec("shellcheck", ["scripts/build.sh"], {
  cwd: "tools",            // relative paths resolve against the project root
  env: { CI: "1" },        // added on top of the inherited environment
  quiet: false,            // set true to keep the program's output out of the build log
  timeout: 30_000,         // milliseconds; the child is killed and the task fails
});

result.ok;        // exited zero
result.exitCode;  // 0, 3, …
result.stdout;    // captured up to 1 MiB, even when not quiet; the rest is streamed only
result.stderr;    // under the same cap
```

تنتظر `exec` حتى يخرج البرنامج، وتبث مخرجاته إلى عرض البناء فور وصولها، منسوبة إلى مهمتك. لا تتدخل أي صدفة: تصل الوسائط إلى البرنامج كما كتبتها تمامًا، لذا لا يمكن إعادة تفسير أي شيء في اسم ملف أو خيار بوصفه صياغة صدفة. ويُبحث عن اسم الأمر المجرد في `PATH` الذي ستعمل به العملية الفرعية، أي الذي يضبطه `env` إن ضبطه، ويُحلّ الاسم الذي يحتوي فاصلًا نسبةً إلى `cwd`. وإلغاء البناء ينهي البرنامج وكل ما شغّله. ويحدّ `timeout` البرنامج الذي سمّيته؛ وبعد خروجه تُقرأ المخرجات التي لا تزال تصل لثانية إضافية ثم تُسقط، لذا لا تؤخر عمليةُ خلفيةٍ تركها ممسكة بمخرجاته مهمتَك، ولا يُنتظر تلك العملية ولا تُنهى.

وكل ما يملكه JavaScript الضيف متاح أيضًا، مثل `node:fs` و `node:child_process` و `fetch`، لذا استخدم `exec` عندما تريد نسبة المخرجات إلى مهمتك، و `node:child_process` عندما تحتاج إلى شيء لا تغطيه. وتأتي مع ذلك قاعدة واحدة: العملية الفرعية التي تمنحها طرفية البناء نفسها (`stdio: "inherit"`) تكتب حيث يرسم عرض التقدم، لذا سلّمها الطرفية عبر `ctx.foreground`. أما العملية الفرعية التي تمرّر مخرجاتها وتطبعها بنفسك فلا تحتاج إلى شيء: إذ تمر مخرجات `console` الخاصة بك عبر العرض كأي مخرجات أخرى.

### منح برنامج الطرفية

```ts
const code = await ctx.foreground(
  () =>
    new Promise((resolve) => {
      const child = spawn(bin, args, { stdio: "inherit" });
      child.on("close", resolve);
    }),
);
```

يتوقف عرض التقدم عن الرسم طوال مدة عمل الدالة بالضبط، ويعود أيًّا كانت طريقة انتهائها، لذا لا تمحو مخرجات البرنامج وصفوف العرض بعضها بعضًا أبدًا. وتحتفظ الدالة `async` بالطرفية حتى يستقر وعدها، وتُرجع ⁦`await ctx.foreground(...)`⁩ ما أرجعته الدالة وتعيد رمي ما رمته.

غلّف التسليم، لا المهمة كلها: فكل ما تفعله مهمتك قبل تشغيل العملية، مثل حلّ المسارات وقراءة الملفات واستدعاء `ctx.tools`، يجب أن يظل يعرض التقدم. ولا تستخدمها إلا عندما يكتب شيء إلى واصفات البناء نفسها، أي عملية فرعية بـ stdio موروث أو أي شيء تفاعلي مثل REPL أو مصحّح أخطاء. فالمخرجات المكتوبة بـ `ctx.log` أو `ctx.exec` أو `ctx.tools` أو `console` الخاصة بمهمتك مرتّبة أصلًا مع العرض.

## استخدام سلسلة الأدوات المدمجة

يحمل Elide بالفعل مُصرّف Kotlin و `javac` و `jar` و Native Image و ktfmt و google-java-format. والمهمة التي تحتاج إلى أحدها لا يلزمها تشغيل Elide آخر للوصول إليه:

```ts
// Before: a new process per compile, diagnostics as text on a stream.
const compiled = ctx.exec("elide", ["kotlinc", "-d", ".dev/classes", "-cp", cp, "src/main/kotlin"]);
if (!compiled.ok) return ctx.fail(compiled.stderr);
ctx.exec("elide", ["jar", "--create", "--file", ".dev/lib/app.jar", "-C", ".dev/classes", "."]);

// After: the same tools, in-process, with structured results.
const result = ctx.tools.kotlinc({
  sources: ["src/main/kotlin"],                 // files, directories or globs, project-relative
  out: ".dev/classes",                          // a directory, or a path ending in `.jar`
  classpath: ctx.tools.classpath(),             // the resolved compile classpath, in order
  friendPaths: [".dev/classes-core"],           // whose `internal` declarations this compile may see
  args: ["-Xjvm-default=all"],                  // any further kotlinc flags, verbatim
});
if (!result.ok) {
  const first = result.diagnostics.find((d) => d.severity === "error");
  return ctx.fail(`${first?.file}:${first?.line}: ${first?.message}`);
}

ctx.tools.jar({
  out: ".dev/lib/app.jar",
  entries: [{ from: ".dev/classes" }, { from: "LICENSE", into: "META-INF" }],
  mainClass: "com.acme.MainKt",
  manifest: { "Implementation-Version": ctx.project.version ?? "dev" },
});
```

ينتظر كل استدعاء حتى تعود الأداة، ويُرجع `{ ok, exitCode, tool, diagnostics }`. تُعرض تشخيصات المُصرّف وأداة التنسيق في مخرجات البناء تمامًا كما يعرضها التصريف المدمج، وتعود في `diagnostics` بصيغة `{ severity, message, file, line, column }`؛ أما `jar` و `nativeImage` فتنتجان نصًا يُبث إلى سجل البناء منسوبًا إلى مهمتك (مرّر `quiet: true` لإخفائه)، وتتركان `diagnostics` فارغة. المسارات نسبية إلى المشروع ما لم تكن مطلقة. يمكنك كتابة المخرجات في أي مكان، لكن المخرجات الواقعة ضمن مجلد المشروع وحدها يمكن تخزينها مؤقتًا واستعادتها، لذا أبقها هناك عندما تصرّح بها المهمة.

- `kotlinc({ sources, out, classpath?, friendPaths?, args?, incremental?, plugins? })`: مُصرّف Kotlin المدمج. تُطبَّق إضافات المُصرّف التي يصرّح بها ملف البيان أو يفعّلها (`kotlin.features`، `kotlin.plugins`، `mavenDependencies().kotlinPlugins`) كما في التصريف المدمج، وتنضم بيئات التشغيل التي توفرها تلك الميزات (بيئة Metro مثلًا) إلى classpath معها: فالإضافة دون بيئة تشغيلها تُفشل المُصرّف بدلًا من ألا تفعل شيئًا. ويشغّل `plugins: false` المُصرّف المجرد، وهو ما تحتاجه المصادر التي تقتصر على التعليقات التوضيحية. ولا يُضاف أي `-jvm-target` ما لم تمرّره في `args`.
- `javac({ sources, out, classpath?, processorPath?, args? })`: مُصرّف Java المدمج؛ ويفعّل `processorPath` معالجة التعليقات التوضيحية.
- `jar({ out, entries, exclude?, date?, manifest?, mainClass?, quiet? })`: كل مُدخل مجلد (محتواه)، أو ملف، أو أرشيف آخر تُؤخذ مُدخلاته (`archive: true`)، ويقع في جذر الأرشيف أو تحت `into`. يُكتب `Manifest-Version` و `Created-By` دائمًا؛ ويصبح `mainClass` هو `Main-Class`.
- `nativeImage({ args, cwd?, quiet? })`: برنامج تشغيل Native Image المدمج؛ تُربط المسارات النسبية في `args` بـ `cwd`، وقيمته الافتراضية جذر المشروع. ويعمل الباني بوصفه JVM فرعية مثبّتة على JDK الخاص بـ Elide.
- `format({ tool, files, check?, args? })`: أدوات التنسيق المدمجة: `tool` هي `"ktfmt"` لـ Kotlin أو `"javaformat"` لـ google-java-format. و `files` ملفات أو مجلدات أو أنماط glob، مقصورة على ما تقبله أداة التنسيق (`.kt`/`.kts` أو `.java`)؛ وإن لم يطابق شيء ينجح الاستدعاء ولا يفعل شيئًا. تُعاد كتابة الملفات في مكانها، أو مع `check: true` يُبلَّغ عنها فقط، ويفشل الاستدعاء إذا كان أي ملف سيتغير. وفي الحالتين تُدرج `changed` المسارات المطلقة للملفات التي لم تكن منسّقة، ويصل كل منها تشخيصًا: تحذيرًا مع `check`، ومعلومة عند إعادة الكتابة، وخطأً للملف الذي تعذّرت قراءته أو تحليله. وتمرّر `args` خيارات إضافية إلى أداة التنسيق، مثل `["--google-style"]`.
- `classpath({ sourceSet?, usage? })`: classpath التبعيات المحلول، مسارات مطلقة بالترتيب. و `usage` هي `"compile"` (الافتراضي؛ ما تراه مهام التصريف)، أو `"runtime"`، أو `"test"` (classpath التشغيل لمجموعة الاختبار)، أو `"processor"`، أو `"modules"`. وتُرجع ما حلّه هذا البناء، لذا ينبغي للمهمة التي تقرؤها أن تستخدم `dependsOn: "maven-dependencies"`.

`ctx.tools.version` هو إصدار أشكال الخيارات والنتائج هذه، وهو `1` اليوم. ولا يتغير إلا مع تغيير غير متوافق؛ فالخيار الاختياري الجديد أو عضو النتيجة الجديد لا يغيّره. والإضافة التي يجب أن تعمل مع أكثر من إصدار من Elide تتفرّع بحسبه.

لا تعمل المُصرّفات داخل العملية نفسها إلا عبر سلسلة الأدوات المدمجة. وملف البيان الذي يختار `kotlin.toolchain` مخصّصًا أو مُدارًا، أو `jvm.java.compiler.mode` الخارجي، يتلقى من `ctx.tools.kotlinc` / `ctx.tools.javac` خطأً يسمّي المهمة والأداة؛ أزل الاختيار، أو شغّل سلسلة الأدوات تلك برنامجًا عبر `ctx.exec`. وينطبق الأمر نفسه على توزيعة بُنيت دون سلسلة أدوات JVM.

### إعادة تحزيم أرشيف

تقرأ `jar` أرشيفًا موجودًا مصدرًا، لذا فإن تقليص تبعية وختمها بشكل قابل لإعادة الإنتاج استدعاء واحد داخل العملية بدلًا من سلسلة `unzip` / `rm -rf` / `jar`:

```ts
ctx.tools.jar({
  out: ".dev/lib/compiler-slim.jar",
  entries: [{ from: "libs/compiler.jar", archive: true }, { from: ".dev/patched" }],
  exclude: ["com/intellij/**", "META-INF/*.SF"],
  date: "2024-01-01T00:00:00Z",
});
```

لا يُستنتج `archive: true` أبدًا من امتداد الملف: فمن دونه يُضاف ملف `.jar` الذي يسمّيه `from` ملفًا واحدًا. ويُسقط `META-INF/MANIFEST.MF` الخاص بالأرشيف المصدر، إذ تحمل النتيجة ملف البيان الذي يصفه `manifest` و `mainClass` لا ملف المصدر أبدًا، ويعيد `into` تأصيل كل ما عدا ذلك. والعضو الذي سيقع مساره بعد `into` خارج الأرشيف (`../x`، أو `/x` مطلق) يُفشل المهمة بدلًا من كتابته، وكذلك `into` الذي يخرج منه.

تُطابق أنماط `exclude` المسار الذي سيأخذه المُدخل *داخل الأرشيف*، بعد أن يعيد `into` تأصيله. وهي مسارات لا أسماء حزم، لذا فشجرة الحزمة هي `"kotlin/**"` ونوع الملف هو `"META-INF/*.SF"`. والنمط في الخيارات ينطبق على كل مُدخل أيًّا كان مصدره؛ والنمط على مُدخل ينطبق على ذلك المُدخل وحده، إضافة إلى الأنماط العامة.

يختم `date` كل مُدخل، بما فيه ملف البيان، بتلك اللحظة، وتكتب الأداة ملفات الجذر بترتيب مرتّب، لذا فإن إعادة تحزيم مُدخلات غير متغيرة تعطي نتيجة متطابقة بايتًا ببايت، وهو ما يحتاجه ختم AOT أو ختم التوزيعة. ومن دونه لا تكون أوقات المُدخلات قابلة لإعادة الإنتاج: فالمُدخل المُمرَّر دون تغيير يحتفظ بأوقات تعديل ملفاته، بينما يحمل عضو الأرشيف، والمُدخل ذو `into`، والمُدخل الذي مسّه استبعاد أو تصادم، وقت الاستدعاء.

تُحزَّم المُدخلات بالترتيب الذي تُدرجها به، ويمكن لاثنين منها المطالبة بمسار الأرشيف نفسه: فيفوز الأخير، ويحذّر البناء مسمّيًا المسار والمصدر الذي أُخذ منه. لا يُدمج شيء: فمُخرَج `Jar` المصرَّح به في ملف البيان يوحّد ملفات `META-INF/services` من جذوره، أما `ctx.tools.jar` فلا، لذا فإن المهمة التي تحتاج إلى ذلك التوحيد تكتب الملف المدموج بنفسها وتُدرجه أخيرًا.
