Packaging Modes
A worker nupkg can ship in three packaging modes: managed, native, or both. Each is produced by a different build script. The choice depends on whether you need one engine, both, or want to defer the engine choice to deploy time.
Mode 1: managed only
python3 scripts/build_managed.py
Output: <PackageId>.nupkg containing lib/<tfm>/*.dll (the worker assembly + transitive managed deps). The default PackageWorker MSBuild target in the managed csproj stages the output, computes SHA-256, and writes build_version.txt. Use this when:
- You only target the
DotNetDllEngine(managed, in-process). - The worker is in active development (the managed path is easier to debug).
- The worker has un-AOT-portable dependencies.
The nupkg can be registered with application/x-dotnet-dll only.
Mode 2: native only
python3 scripts/build_aot.py
Output: <PackageId>.nupkg containing runtimes/<rid>/native/lib*.so (per-arch native libraries). The AOT csproj sets <PublishAot>true</PublishAot>, <RuntimeIdentifiers>linux-x64;linux-arm64</RuntimeIdentifiers>, and per-RID <ObjCopyName> (the cross-binutils binaries from the cross-build image). The build script invokes dotnet publish -r <RID> --publish-aot inside the docker.haenerconsulting.com/library/dotnet-crossbuild:10.0 image (which provides clang + cross-binutils — the host machine rarely has them natively). Each RID's output is renamed to lib<AssemblyName>.so per NuGet convention, and a .supported-rids sidecar manifest is written at the nupkg root.
Use this when:
- You only target the
NativeDllEngine(NativeAOT, in-process). - The worker is performance-critical.
- The worker is fully trusted (no managed dependencies that aren't AOT-portable).
The nupkg can be registered with application/x-native-dll only.
Mode 3: both (single nupkg, both layouts)
bash scripts/build_both.sh
Output: a single <PackageId>.nupkg containing both lib/<tfm>/*.dll AND runtimes/<rid>/native/lib*.so. The script runs the managed build and the AOT build, then stages both into a single staging dir, writes a combined .nuspec, and produces a single nupkg with one content-addressed version.
Use this when:
- You want one deployable artifact that supports either engine.
- You want to A/B test managed vs AOT in production (register one worker under
application/x-dotnet-dlland another underapplication/x-native-dll, point both at the same nupkg URL). - You want a staged rollout (deploy AOT, drain managed, eventually switch to AOT-only — but keep the option to roll back to managed without changing the artifact).
The combined nupkg can be registered under either or both MIME types.
The script-level details
All three scripts live in WebSocketManagerController/scripts/ and are thin wrappers around the shared logic in @common/scripts/:
build_managed.py→@common/scripts/build_managed.py --csproj <path>build_aot.py→@common/scripts/build_aot.py --managed-csproj <path> --native-csproj <path>build_both.sh→ subprocesses both, then combines
The shared @common/scripts/build_managed.py exports helpers (repack_nupkg, rewrite_nuspec_version, sha256_file, read_package_id_from_csproj, read_library_version) that the AOT script and the both-orchestrator import. The two-package model: @common/scripts/build_managed.py is the managed build; @common/scripts/build_aot.py is the AOT build; the worker-local build_both.sh is the orchestrator.
What the nupkg contains
Mode 1 (managed):
<PackageId>.nupkg/
├── <PackageId>.nuspec # id, version, virtufinAbiVersion, etc.
└── lib/
└── net10.0/
├── WebSocketManagerController.dll
├── Virtufin.Worker.DevKit.dll
├── Virtufin.Api.Client.dll
├── CloudNative.CloudEvents.dll
├── Grpc.*.dll
└── ...
Mode 2 (native):
<PackageId>.nupkg/
├── <PackageId>.nuspec
├── .supported-rids # one RID per line
└── runtimes/
├── linux-x64/
│ └── native/
│ └── libWebSocketManagerController.so
└── linux-arm64/
└── native/
└── libWebSocketManagerController.so
Mode 3 (both):
<PackageId>.nupkg/
├── <PackageId>.nuspec
├── .supported-rids
├── lib/
│ └── net10.0/... # same as mode 1
└── runtimes/
└── <rid>/native/... # same as mode 2
The <PackageId> is Virtufin.Worker.WebSocketManagerController in all three modes (per the rename plan, the project directory carries the variant suffix but the NuGet id does not — the id is the wire contract). The <virtufinLibrary> is WebSocketManagerController in all three modes (matches the assembly basename; the AOT .so is renamed from WebSocketManagerController.so to libWebSocketManagerController.so at build time).
The MIME-type contract
The nupkg's engine is selected by the mime_type field passed to CreateWorker:
application/x-dotnet-dll→DotNetDllEngine(managed path)application/x-native-dll→NativeDllEngine(AOT path)
If the nupkg was built in mode 1 and you try to register it as application/x-native-dll, the WM's NativeDllEngine will fail to find the .so and the load will error. If the nupkg was built in mode 2 and you try to register it as application/x-dotnet-dll, the WM's DotNetDllEngine will fail to find the .dll.
If the nupkg was built in mode 3 (both), you can register workers under either MIME type. The single nupkg contains both layouts. The WM picks the right one based on the engine the MIME type selects.
Content-addressed versioning
Every nupkg produced by any of the three scripts is named <PackageId>.<LIBRARY_VERSION>-sha.<8hex>.nupkg, where <8hex> is the first 8 chars of the nupkg's SHA-256. Re-publishes of unchanged source are no-ops (the SHA is stable because the build script uses deterministic mtimes — 1980-01-01 00:00 — and sorted zip entries). The build_version.txt at the nupkg root is the canonical version string that binance.py (and other consumers) read to construct the download URL.
See also
- Build Script — the build_aot.py + build_managed.py + build_both.sh reference
- Runtime Arch Check — how the
.supported-ridssidecar is consumed - Engines — the DotNetDll vs NativeDll contrast in detail