diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 825bb43..4fd4047 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -109,6 +109,18 @@ jobs:
shell: pwsh
run: .\build\pack.ps1
+ - name: Smoke-test the packed package (Windows)
+ shell: pwsh
+ run: .\build\test-package.ps1
+
+ - name: Upload win-x64 Native Artifacts
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: native-win-x64
+ path: artifacts/native/win-x64/*
+ if-no-files-found: warn
+
- name: Upload NuGet Packages
uses: actions/upload-artifact@v4
if: always()
@@ -124,3 +136,85 @@ jobs:
name: symbol-packages
path: artifacts/nuget/*.snupkg
if-no-files-found: warn
+
+ # ---------------------------------------------------------------------------
+ # Linux: prove the *cross-platform* package (packed on Windows above) actually
+ # works on Linux. Downloads the finished 'nuget-packages' artifact and consumes
+ # it as a real NuGet package via build/test-package.sh: restore -> the bundled
+ # Linux idlc runs code generation -> DllImport resolves libddsc.so -> pub/sub
+ # round-trip, then the ddsmonitor tool is installed and served. No native build.
+ # ---------------------------------------------------------------------------
+ linux-verify:
+ runs-on: ubuntu-latest
+ needs: [ build ]
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # Required for Nerdbank.GitVersioning (applies to the smoke-test project)
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
+
+ - name: Download cross-platform packages
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-packages
+ path: artifacts/nuget/
+
+ - name: Smoke-test the cross-platform package (Linux)
+ shell: bash
+ run: build/test-package.sh
+
+ # ---------------------------------------------------------------------------
+ # Publish to NuGet.org — ONLY on a version tag (vX.Y.Z), and ONLY after the
+ # cross-platform package has been verified on both Windows (build) and Linux
+ # (linux-verify). Pushes the packages produced by the build job (both
+ # CycloneDDS.NET and CycloneDDS.NET.DdsMonitor) plus their symbol packages.
+ #
+ # Requires a repository secret named NUGET_API_KEY:
+ # Repo -> Settings -> Secrets and variables -> Actions -> New repository secret.
+ # To release: create a GitHub Release with a tag matching the NBGV version of
+ # the target commit (e.g. v0.3.2 — check with `nbgv get-version` first).
+ # ---------------------------------------------------------------------------
+ publish:
+ runs-on: ubuntu-latest
+ needs: [ build, linux-verify ]
+ if: startsWith(github.ref, 'refs/tags/v')
+
+ steps:
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Download packages
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-packages
+ path: artifacts/nuget/
+
+ - name: Download symbol packages
+ uses: actions/download-artifact@v4
+ with:
+ name: symbol-packages
+ path: artifacts/nuget/
+
+ - name: Push to NuGet.org
+ shell: bash
+ env:
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ run: |
+ if [ -z "$NUGET_API_KEY" ]; then
+ echo "::error::NUGET_API_KEY secret is not set — cannot publish." >&2
+ exit 1
+ fi
+ # dotnet expands the wildcard and auto-pushes the matching .snupkg for each .nupkg.
+ dotnet nuget push "artifacts/nuget/*.nupkg" \
+ --api-key "$NUGET_API_KEY" \
+ --source https://api.nuget.org/v3/index.json \
+ --skip-duplicate
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73f4470..79de05a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## unreleased
nothing yet
+## 0.3.2
+
+### Fixed
+- **SourceLink symbols for the whole library.** `CycloneDDS.Core` and
+ `CycloneDDS.Schema` now ship their SourceLink-enabled PDBs in `lib/net8.0`
+ (previously only `CycloneDDS.Runtime` did), so consumers can step into all three
+ assemblies straight from the package.
+
+### Added
+- **Automatic NuGet publish on a version tag.** Pushing a `vX.Y.Z` tag (e.g. via a
+ GitHub Release) builds and verifies the cross-platform package on Windows and
+ Linux, then publishes it — and the `ddsmonitor` tool — to NuGet.org. Requires the
+ `NUGET_API_KEY` repository secret.
+- **CI now verifies the packaged output on both platforms** before publishing (a
+ Windows smoke test + a Linux job that consumes the finished package), and uploads
+ the `win-x64` native as a workflow artifact.
+- **`examples/PackageSmokeTest`** and **`build/test-package.{sh,ps1}`** for
+ smoke-testing the package (and the `ddsmonitor` tool) locally on either OS.
+
+### Changed
+- Packing now succeeds on Linux too (Windows-only pack entries are `Exists`-guarded),
+ so a per-platform package can be built locally on either OS.
+- CI builds set `ContinuousIntegrationBuild` for deterministic PDBs with normalized
+ SourceLink source paths.
+
## 0.3.1
### Added
diff --git a/Directory.Build.props b/Directory.Build.props
index c5f4167..6c3f1ce 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -10,6 +10,9 @@
snupkg
true
true
+
+ true
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
false
diff --git a/RELEASE-GUIDE.md b/RELEASE-GUIDE.md
index 6b9be94..6041cba 100644
--- a/RELEASE-GUIDE.md
+++ b/RELEASE-GUIDE.md
@@ -9,10 +9,97 @@ Before you begin, ensure you have:
1. **Maintainer Access** to the GitHub repository
2. **NuGet API Key** with push permissions for the CycloneDDS.NET package
3. **Development Environment** with:
- - .NET 8.0 SDK or later
- - PowerShell
+ - **.NET 10 SDK** — the code uses C# 13 language features, so building requires it; the produced binaries still target `net8.0`. (The .NET 8 runtime is also handy for running the `net8.0` tests.)
+ - **CMake 3.16+**
+ - **Windows:** PowerShell and Visual Studio 2022 (*C++ Desktop Development* workload) for native compilation.
+ - **Linux:** a C toolchain and `patchelf` — `sudo apt-get install -y cmake build-essential patchelf`.
- Git command-line tools
+## Building & Testing a Multi-Platform Package Locally
+
+The published `CycloneDDS.NET` and `CycloneDDS.NET.DdsMonitor` packages ship native
+assets for **both** `win-x64` and `linux-x64`. Every green CI run already produces
+these as artifacts, so **you rarely need to build them yourself** — the options
+below are ordered easiest first.
+
+> **How the cross-platform package is assembled.** The pack step includes whatever
+> native artifacts are present under `artifacts/native//` (Windows-only entries
+> are `Exists`-guarded), so a cross-platform package needs *both* platforms' natives
+> on one pack host. Because the packages bundle the Windows apphost `.exe` launchers
+> for the build-time tools, the final cross-platform pack runs on **Windows**.
+> Packing on Linux is fully supported but yields a `linux-x64`-only package.
+
+CI publishes three artifacts per run: **`nuget-packages`** (the finished
+cross-platform `.nupkg` + `.snupkg`), **`native-win-x64`**, and
+**`native-linux-x64`**. Download them with the GitHub CLI (`gh`, recommended —
+`winget install GitHub.cli` on Windows) or from the Actions run page.
+
+### Option A — Download the finished package from CI (easiest, no build)
+
+```bash
+# Grab the cross-platform packages from the latest green run on main:
+gh run download --repo pjanec/CycloneDds.NET -n nuget-packages -D artifacts/nuget
+build/test-package.sh # smoke-test them on this OS (PowerShell: .\build\test-package.ps1)
+```
+
+Then publish `artifacts/nuget/*.nupkg` manually (see *Publishing* below). Run
+`test-package` on both a Windows and a Linux box against the same package before
+publishing.
+
+### Option B — Assemble the package locally without a C++ toolchain
+
+Download **both** natives from CI and pack — no Visual Studio / CMake needed, just
+the .NET 10 SDK:
+
+```powershell
+gh run download --repo pjanec/CycloneDds.NET -n native-win-x64 -D artifacts\native\win-x64
+gh run download --repo pjanec/CycloneDds.NET -n native-linux-x64 -D artifacts\native\linux-x64
+dotnet build CycloneDDS.NET.Core.slnf -c Release
+dotnet pack src\CycloneDDS.Runtime\CycloneDDS.Runtime.csproj -c Release --no-build -o artifacts\nuget
+dotnet pack tools\DdsMonitor\DdsMonitor.Blazor\DdsMonitor.csproj -c Release --no-build -o artifacts\nuget
+.\build\test-package.ps1
+```
+
+### Option C — Full local build from source
+
+```powershell
+# Windows (needs VS 2022 C++ workload + CMake): builds win native, downloads linux native, packs both
+.\build\native-win.ps1
+gh run download --repo pjanec/CycloneDds.NET -n native-linux-x64 -D artifacts\native\linux-x64
+.\build\pack.ps1
+.\build\test-package.ps1
+```
+
+```bash
+# Linux (needs cmake/gcc/patchelf): produces a linux-x64-only package, enough to validate the Linux side
+build/native-linux.sh Release
+dotnet build CycloneDDS.NET.Core.slnf -c Release
+dotnet pack src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj -c Release --no-build -o artifacts/nuget
+dotnet pack tools/DdsMonitor/DdsMonitor.Blazor/DdsMonitor.csproj -c Release --no-build -o artifacts/nuget
+build/test-package.sh
+```
+
+### The smoke test (`build/test-package.{sh,ps1}`)
+
+Both scripts pick the **newest** package in the feed by write-time (so a cluttered
+`artifacts/nuget` with old builds is fine — do **not** name-sort), then:
+
+1. Consume `CycloneDDS.NET` as a real NuGet **package** via `examples/PackageSmokeTest`
+ — which declares a `[DdsTopic]` (so the packaged code generator + `idlc` run at
+ build time) and does a publish/subscribe round-trip. Expect
+ `[smoke] PASS: round-trip Id=42 ...`.
+2. Install the `ddsmonitor` global tool, confirm it serves HTTP 200 (native loaded),
+ and uninstall it.
+
+```
+build/test-package.sh [FEED_DIR] # default artifacts/nuget; NO_DDSMON=1 to skip the tool
+.\build\test-package.ps1 [-FeedDir ] [-NoDdsMon]
+```
+
+Run the smoke test on **both** a Windows and a Linux box before publishing. Both
+were validated during development — Linux locally + in CI, Windows in the CI
+`build` job.
+
## Version Management
This project uses **Nerdbank.GitVersioning (NBGV)** for automatic semantic versioning.
@@ -133,8 +220,20 @@ Expand-Archive artifacts/nuget/CycloneDDS.NET.1.0.0.nupkg -DestinationPath temp_
# - ThirdPartyNotices.txt is present
# - Native binaries are in runtimes/ folder
# - Build tools are in build/ or buildTransitive/ folder
+# - lib/net8.0/ has *.dll AND matching *.pdb for Runtime, Core and Schema
+# (SourceLink debugging), and a .snupkg was produced alongside the .nupkg
```
+**Debugging support (SourceLink).** The package embeds SourceLink so consumers can
+step into the library sources straight from GitHub. Symbols ship two ways: the
+`lib/net8.0/*.pdb` travel inside the `.nupkg` (Visual Studio loads them directly),
+and a `.snupkg` is published to the NuGet symbol server. Verify with the
+[`sourcelink`](https://www.nuget.org/packages/sourcelink) tool, e.g.
+`sourcelink print-urls lib\net8.0\CycloneDDS.Runtime.pdb` should list
+`https://raw.githubusercontent.com/pjanec/CycloneDds.NET//...` URLs.
+Publish **both** the `.nupkg` and the `.snupkg` (see below). SourceLink URLs only
+resolve for commits pushed to GitHub, so publish from a CI build on `main`/a tag.
+
**Key Things to Check:**
- [ ] Package version is correct (no `-alpha` suffix for stable)
- [ ] `icon.png` is at package root
@@ -146,13 +245,42 @@ Expand-Archive artifacts/nuget/CycloneDDS.NET.1.0.0.nupkg -DestinationPath temp_
## Publishing to NuGet.org
-### Option A: Automatic Publishing (CI/CD)
+### Option A: Automatic Publishing (CI/CD) — recommended
+
+CI publishes to NuGet.org automatically when you push a **version tag** (`vX.Y.Z`),
+but only after the cross-platform package has passed the Windows and Linux
+verification jobs. The `publish` job pushes both `CycloneDDS.NET` and
+`CycloneDDS.NET.DdsMonitor` (plus their `.snupkg` symbols).
-If CI is configured to auto-publish on tags:
+**One-time setup — store the NuGet API key as a repository secret:**
-1. **Wait for CI to complete** after pushing the tag
-2. **Verify on NuGet.org** that the new version appears
-3. **Create a GitHub Release** with release notes
+1. Create an API key at with **"Push new
+ packages and package versions"**. Scope the *Package glob* to `CycloneDDS.NET*`
+ so it covers both packages. (For the very first push of a brand-new package id,
+ a key scoped to *all* packages / your account may be required until the id
+ exists.)
+2. In GitHub: **Repo → Settings → Secrets and variables → Actions → New repository
+ secret**. Name it exactly **`NUGET_API_KEY`** and paste the key. GitHub encrypts
+ it and masks it in logs; the workflow reads it as `secrets.NUGET_API_KEY`. You
+ never commit the key.
+
+**Releasing:**
+
+1. Make sure `main` is green and at the commit you want to ship.
+2. Check the version NBGV will produce for that commit:
+ ```bash
+ nbgv get-version # e.g. 0.3.2 (dotnet tool install -g nbgv)
+ ```
+ The published version is `version.json` base + git height — the tag name does
+ **not** set it, so name the tag to match (e.g. `v0.3.2`).
+3. **Create a GitHub Release** with tag `vX.Y.Z` targeting that commit (or
+ `git tag v0.3.2 && git push origin v0.3.2`). The tag push runs CI; on success
+ the `publish` job pushes to NuGet.org.
+4. **Verify on NuGet.org** that the new version (and the DdsMonitor tool) appear.
+
+> The tag is added to `publicReleaseRefSpec` in `version.json`, so a tag build gets
+> a clean version (no `-gSHA` prerelease suffix). SourceLink URLs resolve because
+> the tagged commit is on GitHub.
### Option B: Manual Publishing
diff --git a/build/test-package.ps1 b/build/test-package.ps1
new file mode 100644
index 0000000..64a67bc
--- /dev/null
+++ b/build/test-package.ps1
@@ -0,0 +1,101 @@
+<#
+.SYNOPSIS
+ Smoke-test the CycloneDDS.NET NuGet package (and the DdsMonitor global tool)
+ from a local feed — whether built locally (build\pack.ps1) or downloaded from a
+ CI run (the 'nuget-packages' artifact).
+
+.DESCRIPTION
+ Picks the NEWEST matching package by LastWriteTime (so a cluttered
+ artifacts\nuget with many historical builds is fine), then consumes it as a real
+ PackageReference via examples\PackageSmokeTest: restore -> run the bundled code
+ generator (idlc) -> publish/subscribe round-trip. Optionally installs the
+ ddsmonitor tool, confirms it serves HTTP, and uninstalls it.
+
+.EXAMPLE
+ .\build\test-package.ps1
+ .\build\test-package.ps1 -FeedDir C:\downloads\nuget-packages -NoDdsMon
+#>
+[CmdletBinding()]
+param(
+ [string]$FeedDir = "artifacts/nuget",
+ [switch]$NoDdsMon
+)
+
+$ErrorActionPreference = "Stop"
+$RepoRoot = Split-Path -Parent $PSScriptRoot
+$Feed = (Resolve-Path $FeedDir).Path
+
+function Get-Newest($pattern) {
+ Get-ChildItem -Path (Join-Path $Feed $pattern) -ErrorAction SilentlyContinue |
+ Sort-Object LastWriteTime -Descending | Select-Object -First 1
+}
+
+$rt = Get-Newest 'CycloneDDS.NET.[0-9]*.nupkg'
+if (-not $rt) { throw "No CycloneDDS.NET..nupkg found in $Feed" }
+$rtVer = $rt.Name -replace '^CycloneDDS\.NET\.(.+)\.nupkg$', '$1'
+
+Write-Host "============================================================"
+Write-Host " Package smoke test"
+Write-Host " feed: $Feed"
+Write-Host " package: $($rt.Name) (version $rtVer)"
+Write-Host "============================================================"
+
+Remove-Item -Recurse -Force `
+ "$env:USERPROFILE\.nuget\packages\cyclonedds.net", `
+ "$env:USERPROFILE\.nuget\packages\cyclonedds.net.ddsmonitor" -ErrorAction SilentlyContinue
+Remove-Item -Recurse -Force `
+ "$RepoRoot\examples\PackageSmokeTest\bin", `
+ "$RepoRoot\examples\PackageSmokeTest\obj" -ErrorAction SilentlyContinue
+
+Write-Host "`n[1/2] Running examples/PackageSmokeTest against $rtVer ..."
+dotnet run --project "$RepoRoot\examples\PackageSmokeTest" -c Release `
+ -p:SmokePkgVersion=$rtVer -p:RestoreAdditionalSources=$Feed
+if ($LASTEXITCODE -ne 0) { throw "runtime package smoke test FAILED" }
+Write-Host " [+] runtime package smoke test PASSED"
+
+if ($NoDdsMon) { Write-Host "`nSkipping DdsMonitor tool check (-NoDdsMon)."; Write-Host "All good."; exit 0 }
+
+$mon = Get-Newest 'CycloneDDS.NET.DdsMonitor.*.nupkg'
+if (-not $mon) { Write-Host "`nNo DdsMonitor package in feed — skipping tool check."; Write-Host "All good."; exit 0 }
+$monVer = $mon.Name -replace '^CycloneDDS\.NET\.DdsMonitor\.(.+)\.nupkg$', '$1'
+
+Write-Host "`n[2/2] Testing the ddsmonitor global tool $monVer ..."
+dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor 2>$null | Out-Null
+dotnet tool install --global --add-source $Feed --version $monVer CycloneDDS.NET.DdsMonitor | Out-Null
+
+# Invoke by absolute path: the global-tools dir may not be on PATH in this session
+# (e.g. right after `dotnet tool install --global` in CI).
+$toolExe = Join-Path $env:USERPROFILE ".dotnet\tools\ddsmonitor.exe"
+if (-not (Test-Path $toolExe)) { $toolExe = "ddsmonitor" }
+
+$out = [System.IO.Path]::GetTempFileName()
+$err = [System.IO.Path]::GetTempFileName()
+$proc = Start-Process -FilePath $toolExe -ArgumentList "--NoBrowser true" `
+ -RedirectStandardOutput $out -RedirectStandardError $err -PassThru -NoNewWindow
+
+$ok = $false; $port = $null
+for ($i = 0; $i -lt 30; $i++) {
+ $txt = (Get-Content $out, $err -Raw -ErrorAction SilentlyContinue) -join "`n"
+ if ($txt -match 'Now listening on: http://127\.0\.0\.1:(\d+)') { $port = $Matches[1] }
+ if ($txt -match 'Application started') { $ok = $true; break }
+ if ($proc.HasExited) { break }
+ Start-Sleep -Milliseconds 500
+}
+
+$code = $null
+if ($port) {
+ try { $code = (Invoke-WebRequest -Uri "http://127.0.0.1:$port/" -UseBasicParsing -TimeoutSec 5).StatusCode } catch {}
+}
+
+if (-not $proc.HasExited) { $proc.Kill(); $proc.WaitForExit() }
+dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor 2>$null | Out-Null
+
+if ($ok -and $code -eq 200) {
+ Write-Host " [+] ddsmonitor started (port $port) and served HTTP 200 — native loaded OK"
+ Write-Host "`nAll good."
+ exit 0
+}
+
+Write-Host " [-] ddsmonitor check FAILED (started=$ok, http=$code). Log tail:" -ForegroundColor Red
+Get-Content $out, $err -Tail 20 -ErrorAction SilentlyContinue
+exit 1
diff --git a/build/test-package.sh b/build/test-package.sh
new file mode 100755
index 0000000..d88f1bc
--- /dev/null
+++ b/build/test-package.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# ===================================================================================
+# build/test-package.sh
+#
+# PURPOSE:
+# Smoke-test the CycloneDDS.NET NuGet package (and, optionally, the DdsMonitor
+# global tool) from a local feed — whether the packages were built locally
+# (build/pack.ps1 or dotnet pack) or downloaded from a CI run (the
+# 'nuget-packages' artifact). It always picks the NEWEST matching package by
+# modification time, so a cluttered artifacts/nuget with many historical builds
+# is fine.
+#
+# It consumes the package as a real PackageReference via examples/PackageSmokeTest:
+# restore -> run the bundled code generator (idlc) -> publish/subscribe round-trip.
+#
+# USAGE:
+# build/test-package.sh [FEED_DIR] # default FEED_DIR = artifacts/nuget
+# NO_DDSMON=1 build/test-package.sh # skip the DdsMonitor tool check
+# ===================================================================================
+
+FEED="${1:-artifacts/nuget}"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_ROOT="$(dirname "$SCRIPT_DIR")"
+
+if [ ! -d "$FEED" ]; then
+ echo "ERROR: feed directory not found: $FEED" >&2
+ exit 1
+fi
+FEED_ABS="$(cd "$FEED" && pwd)"
+
+# Newest file matching a glob (by mtime), or empty.
+newest() { ls -t "$FEED_ABS"/$1 2>/dev/null | head -n1 || true; }
+
+RT_PKG="$(newest 'CycloneDDS.NET.[0-9]*.nupkg')"
+if [ -z "$RT_PKG" ]; then
+ echo "ERROR: no CycloneDDS.NET..nupkg found in $FEED_ABS" >&2
+ exit 1
+fi
+RT_VER="$(basename "$RT_PKG" | sed -E 's/^CycloneDDS\.NET\.(.+)\.nupkg$/\1/')"
+
+echo "============================================================"
+echo " Package smoke test"
+echo " feed: $FEED_ABS"
+echo " package: $(basename "$RT_PKG") (version $RT_VER)"
+echo "============================================================"
+
+# Force a fresh restore of exactly this build.
+rm -rf "$HOME/.nuget/packages/cyclonedds.net" "$HOME/.nuget/packages/cyclonedds.net.ddsmonitor" 2>/dev/null || true
+rm -rf "$REPO_ROOT/examples/PackageSmokeTest/bin" "$REPO_ROOT/examples/PackageSmokeTest/obj"
+
+echo ""
+echo "[1/2] Running examples/PackageSmokeTest against $RT_VER ..."
+dotnet run --project "$REPO_ROOT/examples/PackageSmokeTest" -c Release \
+ -p:SmokePkgVersion="$RT_VER" \
+ -p:RestoreAdditionalSources="$FEED_ABS"
+echo " [+] runtime package smoke test PASSED"
+
+if [ "${NO_DDSMON:-0}" = "1" ]; then
+ echo ""
+ echo "Skipping DdsMonitor tool check (NO_DDSMON=1)."
+ echo "All good."
+ exit 0
+fi
+
+MON_PKG="$(newest 'CycloneDDS.NET.DdsMonitor.*.nupkg')"
+if [ -z "$MON_PKG" ]; then
+ echo ""
+ echo "No CycloneDDS.NET.DdsMonitor package in feed — skipping tool check."
+ echo "All good."
+ exit 0
+fi
+MON_VER="$(basename "$MON_PKG" | sed -E 's/^CycloneDDS\.NET\.DdsMonitor\.(.+)\.nupkg$/\1/')"
+
+echo ""
+echo "[2/2] Testing the ddsmonitor global tool $MON_VER ..."
+dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor >/dev/null 2>&1 || true
+dotnet tool install --global --add-source "$FEED_ABS" --version "$MON_VER" CycloneDDS.NET.DdsMonitor >/dev/null
+
+TOOL="$HOME/.dotnet/tools/ddsmonitor"
+LOG="$(mktemp)"
+"$TOOL" --NoBrowser true >"$LOG" 2>&1 &
+TOOL_PID=$!
+
+ok=0
+for _ in $(seq 1 30); do
+ if grep -q "Application started" "$LOG" 2>/dev/null; then ok=1; break; fi
+ if ! kill -0 "$TOOL_PID" 2>/dev/null; then break; fi
+ sleep 0.5
+done
+
+PORT="$(grep -oE 'Now listening on: http://127.0.0.1:[0-9]+' "$LOG" | grep -oE '[0-9]+$' | head -n1 || true)"
+code=""
+if [ -n "$PORT" ]; then
+ code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT/" 2>/dev/null || true)"
+fi
+
+kill "$TOOL_PID" 2>/dev/null || true
+wait "$TOOL_PID" 2>/dev/null || true
+dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor >/dev/null 2>&1 || true
+
+if [ "$ok" = "1" ] && [ "$code" = "200" ]; then
+ echo " [+] ddsmonitor started (port $PORT) and served HTTP 200 — native loaded OK"
+ echo ""
+ echo "All good."
+ exit 0
+fi
+
+echo " [-] ddsmonitor check FAILED (started=$ok, http=$code). Log:" >&2
+tail -n 20 "$LOG" >&2
+exit 1
diff --git a/examples/PackageSmokeTest/PackageSmokeTest.csproj b/examples/PackageSmokeTest/PackageSmokeTest.csproj
new file mode 100644
index 0000000..19ec19f
--- /dev/null
+++ b/examples/PackageSmokeTest/PackageSmokeTest.csproj
@@ -0,0 +1,39 @@
+
+
+
+
+ Exe
+ net8.0
+ enable
+ disable
+
+ true
+ false
+
+ false
+
+ *-*
+
+
+
+
+
+
+
diff --git a/examples/PackageSmokeTest/Program.cs b/examples/PackageSmokeTest/Program.cs
new file mode 100644
index 0000000..3f290c0
--- /dev/null
+++ b/examples/PackageSmokeTest/Program.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Threading;
+using System.Runtime.InteropServices;
+using CycloneDDS.Runtime;
+using CycloneDDS.Schema;
+
+namespace PackageSmokeTest
+{
+ // Declaring a [DdsTopic] forces the package's code generator (idlc) to run at
+ // build time — so a successful build already proves the packaged tooling works
+ // on this OS. Main() then proves the runtime native (libddsc.so / ddsc.dll)
+ // works by doing a real publish -> subscribe round-trip.
+ [DdsTopic("PackageSmokeTest_SmokeSample")]
+ public partial struct SmokeSample
+ {
+ [DdsKey] public int Id;
+ public double Value;
+ public FixedString32 Label;
+ }
+
+ public static class Program
+ {
+ public static int Main()
+ {
+ Console.WriteLine($"[smoke] {RuntimeInformation.OSDescription} / {RuntimeInformation.ProcessArchitecture}");
+ try
+ {
+ using var participant = new DdsParticipant();
+ using var writer = new DdsWriter(participant);
+ using var reader = new DdsReader(participant);
+
+ // Wait for the reader/writer to discover each other so the first
+ // sample isn't dropped. This also exercises the matched-status
+ // listener callback (the ABI fix that was broken on Linux).
+ if (!writer.WaitForReaderAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult())
+ {
+ Console.Error.WriteLine("[smoke] FAIL: reader/writer did not discover each other");
+ return 1;
+ }
+
+ var sent = new SmokeSample
+ {
+ Id = 42,
+ Value = 3.14159,
+ Label = new FixedString32("hello-cyclonedds")
+ };
+ writer.Write(sent);
+
+ SmokeSample? received = null;
+ for (int attempt = 0; attempt < 50 && received is null; attempt++)
+ {
+ using (var loan = reader.Take(maxSamples: 10))
+ {
+ foreach (var sample in loan)
+ {
+ if (sample.IsValid) { received = sample.Data; break; }
+ }
+ }
+ if (received is null) Thread.Sleep(100);
+ }
+
+ if (received is null)
+ {
+ Console.Error.WriteLine("[smoke] FAIL: no sample received within timeout");
+ return 1;
+ }
+
+ var got = received.Value;
+ bool ok = got.Id == sent.Id
+ && Math.Abs(got.Value - sent.Value) < 1e-9
+ && got.Label.ToString() == sent.Label.ToString();
+
+ if (!ok)
+ {
+ Console.Error.WriteLine(
+ $"[smoke] FAIL: round-trip mismatch: Id={got.Id} Value={got.Value} Label={got.Label}");
+ return 1;
+ }
+
+ Console.WriteLine($"[smoke] PASS: round-trip Id={got.Id} Value={got.Value} Label={got.Label}");
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[smoke] FAIL: {ex}");
+ return 1;
+ }
+ }
+ }
+}
diff --git a/examples/PackageSmokeTest/nuget.config b/examples/PackageSmokeTest/nuget.config
new file mode 100644
index 0000000..ca4635b
--- /dev/null
+++ b/examples/PackageSmokeTest/nuget.config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj b/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj
index ca1a998..7c8cce5 100644
--- a/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj
+++ b/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj
@@ -13,7 +13,7 @@
-
+
-
-
-
-
-
+
+
+
+
+
-
+
@@ -63,7 +63,7 @@
-
+
@@ -83,6 +83,14 @@
+
+
+
diff --git a/tools/DdsMonitor/version.json b/tools/DdsMonitor/version.json
index 8564c5f..cb201cd 100644
--- a/tools/DdsMonitor/version.json
+++ b/tools/DdsMonitor/version.json
@@ -4,7 +4,8 @@
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/heads/master$",
- "^refs/heads/v\\d+(?:\\.\\d+)?$"
+ "^refs/heads/v\\d+(?:\\.\\d+)?$",
+ "^refs/tags/v\\d+\\.\\d+"
],
"cloudBuild": {
"buildNumber": {
diff --git a/version.json b/version.json
index db804ce..c75a435 100644
--- a/version.json
+++ b/version.json
@@ -4,7 +4,8 @@
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/heads/master$",
- "^refs/heads/v\\d+(?:\\.\\d+)?$"
+ "^refs/heads/v\\d+(?:\\.\\d+)?$",
+ "^refs/tags/v\\d+\\.\\d+"
],
"cloudBuild": {
"buildNumber": {