Skip to content

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 for dotnet publish (default: /tmp/worker-out).
  • --config PATH — path to config.json (default: ./config.json); used to find last_published.version for the version-change report.
  • --library-version V — override the semver base from <worker_root>/versions.env (the script reads LIBRARY_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). Rejects osx-* / 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 — default docker.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) and zip -X to 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>, then csproj.stem).
  • read_library_version(worker_root) — reads LIBRARY_VERSION= from <worker_root>/versions.env; returns "0.0.0" if absent.

See also