import { Callout } from "zudoku/ui/Callout";

# Runtime packages

[`Build-BcRuntimePackages`](../powershell-module/runtime-packages#build-bcruntimepackages) produces signed Business Central **runtime packages** for on-premise distribution, one per supported platform version, and publishes them (Blob + NuGet feed) together with an "indirect" package that maps each platform version to its runtime package.

<Callout type="caution" title="Licensed feature">
Runtime package building is part of a licensed ALbuild tier and calls `Assert-ALbuildLicensed` on entry. See [Licensing &amp; tiers](licensing).
</Callout>

---

## Why it must be optimised

A runtime package is generated **server-side** by a running Business Central service tier, and
Microsoft only guarantees it on the exact platform version that produced it. So the
`product × platform version` matrix is **irreducible**: you genuinely have to produce every cell.

What *is* reducible is the number of containers you start to do it. Measured on one real
catalogue run:

| | Measured |
| --- | --- |
| Provisioning one container | **238–564 s**, depending on the BC major |
| Value-adding work per product, once the container is up | a near-constant **~80 s** |

In other words, **the overwhelming majority of the wall-clock time is container provisioning**,
not building packages. Any optimisation that does not reduce container starts is noise.

---

## Invert the loop: the runtime factory

The naive arrangement, and the one a per-product pipeline forces on you, iterates
*platform versions per product*. That pays the provisioning cost **once per product per version**.
Inverting it to iterate *products per platform version* pays it **once per version, full stop**.

For a 12-product catalogue over ~192 platform versions that is roughly **2,300 container starts
against 192**.

Inverting the loop also removes a failure mode instead of working around it. Several products
depend on other products in the same catalogue. Per-product pipelines had to fetch those
dependencies' runtime packages from blob storage and **skip** the platform version when they were
not there yet. Built in one container in dependency order, the dependency is simply present: it
was built moments earlier, in the same container.

This is what [`Get-BcRuntimeWorkSet`](../powershell-module/runtime-packages#get-bcruntimeworkset)
and [`Invoke-BcRuntimeFactory`](../powershell-module/runtime-packages#invoke-bcruntimefactory) do.

### Planning: work sets, lanes and slices

`Get-BcRuntimeWorkSet` turns "which `(product, app version, platform version)` runtime packages are
still missing?" into an ordered, sliceable plan. It is a **pure function**, with no feed, blob or
artifact calls, so the planning policy is unit-testable and you can inspect an entire run before a
single container starts.

- **Lanes.** A product release invalidates the whole matrix at once, and a full rebuild takes hours
  however you schedule it. Customers pull the newest majors first, so `-FastLaneMinMajor` splits the
  plan: those versions are built and published first and the long tail follows behind, without
  holding the release up.
- **Slices.** Each slice is the unit of work for one agent job. `-MaxVersionsPerSlice` bounds how
  long a single job runs, which matters because a job holds one of the organisation's few parallel
  slots for its whole lifetime, and because a job that dies then loses only its in-flight version.

### Execution: a worker pool inside one job

`Invoke-BcRuntimeFactory` runs a slice through a pool of worker threads, each owning **one
container for one platform version**.

The pool lives *inside* a job on purpose. Throughput is capped by the organisation's handful of
parallel self-hosted jobs, not by the build host, which was measured with ~72 GB of RAM still free
while four containers ran. Adding pipeline stages cannot get past that cap; adding workers inside a
job does, and costs nothing extra.

<Callout type="tip" title="Size the pool by disk, not by RAM">
Each container costs roughly **2 GB of disk**. On a well-specified build host it is the disk, not
the memory, that caps how many workers you can run in parallel.
</Callout>

### Checkpointing: never lose a finished version

One real run built 186 runtime packages and shipped 130 of them: the upload ran once, after the
whole loop, so cancelling the run threw away ten hours of finished work.

`-OnVersionComplete` now runs after **each** platform version, so a cancelled or crashed run loses
at most the version still in flight. The callback runs on the **dispatcher** thread deliberately:
uploading to blob storage and pushing to a feed needs an Azure PowerShell context, and those
context objects are not safe to share across runspaces. Workers produce files; the dispatcher
ships them.

### Per-app visibility

Azure DevOps renders a stage as a single icon, and giving every app its own stage would mean a
container per `(app, platform version)`, exactly the arrangement the factory exists to remove. The
per-app result is therefore carried in artefacts a single job can publish:

| Output | What you get |
| --- | --- |
| The log | One collapsible `##[group]` per platform version, one line per app. |
| `-JUnitPath` | One test case per app per version. Feed it to `PublishTestResults@2` for a green/red/skipped entry per app in the **Tests** tab. |
| `-SummaryPath` | The app × version matrix for `##vso[task.uploadsummary]`. |

Worker output is captured per version and replayed by the dispatcher when that version finishes, so
the log reads as ordered blocks instead of interleaved lines from parallel workers.

---

## Still incremental

Independent of the factory, `(app, version)` combinations already present in Blob or the feed are
skipped. See
[`Get-BcRuntimePackageBuildPlan`](../powershell-module/runtime-packages#get-bcruntimepackagebuildplan).
Cached BC artifacts and the image cache mean a repeated version does not re-download the artifact
either.

---

## What is preserved

The engine preserves the full runtime-package behaviour:

- `ONPREM` / `BC{n}` preprocessor-symbol injection and version-feature application.
- **Signing** of both the app and the runtime package.
- The `runtime-{version}` NuGet id scheme with platform/application version ranges pinned to the build.
- The **indirect package** that maps each platform version to its runtime package.
- Blob retention and downloads publishing.

---

## Related cmdlets

- [`Get-BcRuntimeWorkSet`](../powershell-module/runtime-packages#get-bcruntimeworkset): plan the outstanding work across all products, grouped by platform version, in lanes and slices.
- [`Invoke-BcRuntimeFactory`](../powershell-module/runtime-packages#invoke-bcruntimefactory): run a slice through a worker pool, checkpointing each version.
- [`Build-BcRuntimePackages`](../powershell-module/runtime-packages#build-bcruntimepackages): the batched engine for a single product.
- [`Get-BcRuntimePackageBuildPlan`](../powershell-module/runtime-packages#get-bcruntimepackagebuildplan): compute the incremental plan.
- [`New-BcRuntimePackage`](../powershell-module/runtime-packages#new-bcruntimepackage): generate a single runtime package.
- [`New-BcIndirectNuGetPackage`](../powershell-module/runtime-packages#new-bcindirectnugetpackage): the platform→runtime indirect package.
- [`Publish-BcRuntimePackage`](../powershell-module/runtime-packages#publish-bcruntimepackage): publish to Blob + feed.
