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.
Licensed feature
Runtime package building is part of a licensed ALbuild tier and calls Assert-ALbuildLicensed on entry. See Licensing & tiers.
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
and 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
-FastLaneMinMajorsplits 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.
-MaxVersionsPerSlicebounds 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.
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.
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.
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: plan the outstanding work across all products, grouped by platform version, in lanes and slices.Invoke-BcRuntimeFactory: run a slice through a worker pool, checkpointing each version.Build-BcRuntimePackages: the batched engine for a single product.Get-BcRuntimePackageBuildPlan: compute the incremental plan.New-BcRuntimePackage: generate a single runtime package.New-BcIndirectNuGetPackage: the platform→runtime indirect package.Publish-BcRuntimePackage: publish to Blob + feed.


