Skip to content

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-dll and another under application/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-dllDotNetDllEngine (managed path)
  • application/x-native-dllNativeDllEngine (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-rids sidecar is consumed
  • Engines — the DotNetDll vs NativeDll contrast in detail