Build Script
The three packaging modes (managed, native, both) are produced by three scripts that share a common layer of helpers in @common/scripts/. This page is the reference for those scripts.
Scripts at a glance
| Script | Mode | Output |
|---|---|---|
WebSocketManagerController/scripts/build_managed.py |
managed | lib/<tfm>/*.dll nupkg |
WebSocketManagerController/scripts/build_aot.py |
native | runtimes/<rid>/native/lib*.so nupkg |
WebSocketManagerController/scripts/build_both.sh |
both | single nupkg with both layouts |
The shared helpers (repack_nupkg, rewrite_nuspec_version, sha256_file, read_package_id_from_csproj, read_library_version) live in @common/scripts/build_managed.py and are imported by the AOT and both-orchestrator scripts. The two-package model: @common/scripts/build_managed.py is the managed build (with shared helpers); @common/scripts/build_aot.py is the AOT build (imports the helpers); the worker-local build_both.sh is a thin shell orchestrator.
build_managed.py
python3 scripts/build_managed.py [--csproj PATH] [--out DIR] [--config PATH] [--library-version V]
--csproj PATH(required) — path to the worker .csproj.--out DIR— output directory fordotnet publish(default:/tmp/worker-out).--config PATH— path toconfig.json(default:./config.json); used to findlast_published.versionfor the version-change report.--library-version V— override the semver base from<worker_root>/versions.env(the script readsLIBRARY_VERSION=by default).
Flow:
1. dotnet publish <csproj> -c Release -o <out> — the csproj's PackageWorker target stages lib/<tfm>/*.dll + a .nuspec into <out>/<csproj-stem>.nupkg.stage/, then zips the nupkg deterministically (mtime=1980-01-01 00:00, sorted entries, zip -X).
2. SHA-256 the nupkg, derive <LIBRARY_VERSION>-sha.<short>.
3. Re-pack with the hash-derived version (extract → rewrite <version> in the nuspec → re-zip deterministically).
4. Write build_version.txt next to the nupkg.
5. Compare against config.json's last_published.version and report whether a publish is needed.
build_aot.py
python3 scripts/build_aot.py [--managed-csproj PATH] [--rids RID1,RID2]
[--native-csproj PATH] [--out DIR]
[--config PATH] [--library-version V]
[--docker-image IMG]
--managed-csproj PATH(required) — used to auto-discover the AOT csproj as<dir>.Native/<aot>.csproj. The managed csproj's directory anchors the search.--rids— comma-separated RIDs (default:linux-arm64,linux-x64). Rejectsosx-*/win-*(the cross-build image doesn't ship those toolchains).--native-csproj PATH— override the auto-discovered AOT csproj.--out DIR— output dir for the per-RID AOT publish (default:/tmp/worker-aot-out).--config PATH,--library-version V— same as the managed build.--docker-image IMG— defaultdocker.haenerconsulting.com/library/dotnet-crossbuild:10.0. Set to''to skip Docker (host must have clang + cross-binutils).
Flow per RID:
1. docker run --rm --platform linux/{amd64,arm64} -v <src>:/src -v <nuget-cache>:/root/.nuget/packages -e VIRTUFIN_PACKAGES_USER -e VIRTUFIN_PACKAGES_TOKEN <image> bash -c "dotnet publish <csproj> -c Release -r <RID> -p:PublishAot=true -p:StripSymbols=false -o /<out>/<RID>"
2. The AOT compiler emits <AssemblyName>.so (or <AssemblyNameNative>.so if the <AssemblyName> differs; the script renames the file to lib<AssemblyName>.so per NuGet convention).
3. stage_native_libs copies each .so into a single runtimes/<rid>/native/lib*.so layout.
4. write_supported_rids writes the .supported-rids sidecar at the nupkg root (one RID per line, in build order).
5. Emit a stub .nuspec (the AOT path doesn't produce one from an MSBuild target — we synthesize it).
6. Re-pack + SHA-256 + content-addressed version, same as the managed build.
The Docker invocation mounts the worker's src/ directory at /src and the per-RID publish output at /<out>. The AOT csproj's <Compile Include="..\<managed>\Logic.cs"/> resolves correctly inside the container.
build_both.sh
bash scripts/build_both.sh [--rids RID1,RID2] [--out DIR] [--no-clean] [--skip-managed] [--skip-aot]
--rids— comma-separated RIDs (default:linux-arm64,linux-x64).--out DIR— output dir for the combined nupkg (default:<repo>/build/<PackageId>.nupkg.both/).--no-clean— don't delete the output dir before building.--skip-managed/--skip-aot— skip one of the sub-builds (useful for incremental rebuilds when only one of the two csprojs has changed).
Flow:
1. Subprocess build_managed.py (or skip).
2. Subprocess build_aot.py (or skip).
3. Stage the managed nupkg's lib/<tfm>/*.dll into a single staging dir.
4. Stage the AOT nupkg's runtimes/<rid>/native/lib*.so + .supported-rids into the same staging dir.
5. Emit a combined .nuspec (<PackageId> matches both paths; <virtufinLibrary> matches the assembly basename).
6. Deterministic mtimes (find … -exec touch -t 198001010000) so the SHA-256 is a pure function of file contents.
7. Re-pack + SHA-256 + content-addressed version.
8. Verify that the managed and AOT sub-builds' versions match (catches the case where the worker source has diverged between the two targets).
The shared helpers in @common/scripts/build_managed.py
These are imported by the AOT and both-orchestrator scripts. Don't import them from worker code; the python3 scripts/... wrappers are the only intended entry point.
sha256_file(path)— SHA-256 of a file, 64KB chunks.rewrite_nuspec_version(stage_dir, package_id, new_version)— in-place regex replace of<version>...</version>in<package_id>.nuspec.repack_nupkg(stage_dir, nupkg_path)— zip the stage dir with FIXED entry mtimes (1980-01-01 00:00) andzip -Xto strip Unix extras. The output is byte-stable across rebuilds of unchanged source.extract_nupkg(nupkg_path, stage_dir)— unzip for re-pack (used by the version-rewrite step).read_package_id_from_csproj(csproj)— reads<PackageId>(falls back to<AssemblyName>, thencsproj.stem).read_library_version(worker_root)— readsLIBRARY_VERSION=from<worker_root>/versions.env; returns"0.0.0"if absent.
See also
- Packaging Modes — when to pick which mode
- Runtime Arch Check — how the
.supported-ridssidecar is consumed - Engines — the DotNetDll vs NativeDll contrast in detail