diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3b066dd..825bb43 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,9 +9,70 @@ on:
workflow_dispatch:
jobs:
+ # ---------------------------------------------------------------------------
+ # Linux: build the native CycloneDDS libraries + idlc, then build and test the
+ # managed code against them. Building the managed solution exercises the
+ # build-time IDL code generator (idlc) on Linux end-to-end, and the runtime
+ # tests exercise the DllImport("ddsc") -> libddsc.so path. The native artifacts
+ # are uploaded for the Windows pack job to fold into the cross-platform package.
+ # ---------------------------------------------------------------------------
+ linux-build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # Required for Nerdbank.GitVersioning to determine version
+ submodules: recursive
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ # net8.0 is the target framework, but the code uses C# 13 language
+ # features (e.g. ref structs in async methods), which require the .NET 10
+ # SDK's compiler. Install both: the 10.0 SDK builds, the 8.0 runtime runs
+ # the net8.0 test assemblies.
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
+
+ - name: Install native build prerequisites
+ run: sudo apt-get update && sudo apt-get install -y cmake build-essential patchelf
+
+ - name: Display Tools Versions
+ run: |
+ dotnet --version
+ cmake --version
+ gcc --version
+
+ - name: Build Native (linux-x64)
+ shell: bash
+ run: build/native-linux.sh Release
+
+ - name: Build Managed Code
+ run: dotnet build CycloneDDS.NET.Core.slnf -c Release
+
+ - name: Run Runtime Tests
+ run: dotnet test tests/CycloneDDS.Runtime.Tests/CycloneDDS.Runtime.Tests.csproj -c Release --no-build --logger "console;verbosity=normal"
+
+ - name: Upload linux-x64 Native Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: native-linux-x64
+ path: artifacts/native/linux-x64/*
+ if-no-files-found: error
+
+ # ---------------------------------------------------------------------------
+ # Windows: full build, test and pack via the existing pack.ps1 pipeline
+ # (builds win-x64 native, builds core, runs runtime tests, packs both the
+ # CycloneDDS.NET and DdsMonitor packages, builds the examples). The linux-x64
+ # native artifacts produced above are downloaded first so the packed NuGet
+ # carries both win-x64 and linux-x64 runtime assets.
+ # ---------------------------------------------------------------------------
build:
runs-on: windows-latest
-
+ needs: [ linux-build ]
+
env:
# This ensures the build script can find CMake if not in default path (though it usually is on runners)
CMAKE_GENERATOR: "Visual Studio 17 2022"
@@ -25,13 +86,25 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
- dotnet-version: 8.0.x
+ # net8.0 is the target framework, but the code uses C# 13 language
+ # features (e.g. ref structs in async methods), which require the .NET 10
+ # SDK's compiler. Install both: the 10.0 SDK builds, the 8.0 runtime runs
+ # the net8.0 test assemblies.
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
- name: Display Tools Versions
run: |
dotnet --version
cmake --version
+ - name: Download linux-x64 Native Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: native-linux-x64
+ path: artifacts/native/linux-x64/
+
- name: Build, Test, and Pack
shell: pwsh
run: .\build\pack.ps1
diff --git a/.gitignore b/.gitignore
index 738e33f..ebc21e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,6 +57,7 @@ nunit-*.xml
cyclone-compiled/
cyclone-compiled-debug/
build/native/
+build/native-linux/
test-extract/
test-output/
**/Generated/*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7d5f3c..73f4470 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## unreleased
nothing yet
+## 0.3.1
+
+### Added
+- **Linux x64 support.** The package now ships native assets for `linux-x64`
+ (`libddsc.so`) alongside `win-x64`, and bundles the Linux `idlc` plus its
+ `.so` dependencies under `tools/`. A new `build/native-linux.sh` compiles the
+ native CycloneDDS libraries and `idlc` and rewrites their RPATH to `$ORIGIN`
+ so they resolve in the flat NuGet `tools/` layout. `IdlcRunner` is now
+ platform-aware — it locates `idlc`/`idlc.exe` under the correct RID, sets
+ `LD_LIBRARY_PATH`, and restores the Unix execute bit that NuGet does not
+ preserve — and the MSBuild native-asset copies are guarded by
+ `IsOSPlatform`. The target framework remains `net8.0`.
+- **`ddsmonitor` global tool runs on Linux.** Its tool payload now bundles both
+ the win-x64 (`ddsc.dll`) and linux-x64 (`libddsc.so`) native runtime, so
+ `dotnet tool install -g CycloneDDS.NET.DdsMonitor` works on either OS.
+ Previously the tool carried only the pack host's native and would fail with a
+ `DllNotFoundException` on the other platform. The bundled `IdlImporter` also
+ works on Linux via the platform-aware `idlc` shipped in the package's `tools/`.
+
+### Fixed
+- **DDS matched-status events never fired on Linux.** The publication- and
+ subscription-matched listener callbacks passed their 24-byte status struct by
+ `ref`, which matches the Windows x64 ABI but not the Linux System V ABI (where
+ structs larger than 16 bytes are passed by value on the stack), so `arg` was
+ read from the wrong register and every callback was silently dropped. The
+ status is now passed by value, letting the marshaller emit the correct ABI on
+ both platforms, so `PublicationMatched`, `SubscriptionMatched` and
+ `WaitForReaderAsync` work on Linux.
+- **Mis-cased project references.** Several `ProjectReference` paths used
+ `..\..\Src\...` (capital `S`), which resolved only on case-insensitive
+ (Windows) filesystems; corrected to `src` so the solution builds on Linux.
+
+### Changed
+- CI builds native libraries on both Windows and Linux and produces a single
+ cross-platform package. It installs both the .NET 8 and .NET 10 SDKs: the code
+ uses C# 13 language features that require the newer compiler, while the target
+ framework stays `net8.0`.
+- Package description updated from "Win64" to "Windows x64, Linux x64".
+
## 0.2.3
### Fixed
diff --git a/README.md b/README.md
index 6421f42..4ac0a8f 100644
--- a/README.md
+++ b/README.md
@@ -19,11 +19,16 @@ dotnet add package CycloneDDS.NET
```
This single package includes:
-- **Runtime Library:** High-performance managed bindings.
-- **Native Assets:** Pre-compiled `ddsc.dll` and `idlc.exe` (Windows x64).
+- **Runtime Library:** High-performance managed bindings (targets `net8.0`; runs on .NET 8 and later).
+- **Native Assets:** Pre-compiled native runtime and IDL compiler for both **Windows x64** (`ddsc.dll`, `idlc.exe`) and **Linux x64** (`libddsc.so`, `idlc`). NuGet selects the correct runtime asset per platform automatically.
- **Build Tools:** Automatic C# code generation during build.
-
-**Important:** This package relies on native libraries that require the [Visual C++ Redistributable for Visual Studio 2022](https://aka.ms/vs/17/release/vc_redist.x64.exe) to be installed on the target system.
+
+### Supported platforms
+
+| Platform | Native runtime | Target requirement |
+| :--- | :--- | :--- |
+| Windows x64 | `ddsc.dll` | [Visual C++ Redistributable for Visual Studio 2022](https://aka.ms/vs/17/release/vc_redist.x64.exe) installed on the target machine. |
+| Linux x64 | `libddsc.so` | A glibc-based distribution (e.g. Ubuntu/Debian). No extra runtime install needed. |
### Working with Source Code
@@ -35,16 +40,22 @@ If you want to build the project from source or contribute:
cd CycloneDds.NET
```
-2. **Build and Test** (One-Stop Script):
- Run the developer workflow script. This will automatically check for native artifacts (building them if missing), build the solution, and run all tests.
- ```powershell
- .\build\build-and-test.ps1
+2. **Build the native libraries** for your platform:
+ - **Windows** (PowerShell): `.\build\native-win.ps1`
+ - **Linux** (bash): `build/native-linux.sh Release`
+
+ Then build and test the managed solution:
+ ```bash
+ dotnet build CycloneDDS.NET.Core.slnf -c Release
+ dotnet test CycloneDDS.NET.Core.slnf -c Release
```
+ On Windows you can instead run the one-stop script `.\build\build-and-test.ps1`, which builds the native artifacts (if missing), builds the solution, and runs the tests.
3. **Requirements:**
- - .NET 8.0 SDK
- - Visual Studio 2022 (C++ Desktop Development workload) for native compilation.
- - CMake 3.16+ in your PATH.
+ - **.NET SDK:** the solution targets `net8.0`, but building requires the **.NET 10 SDK** (the code uses C# 13 language features). Produced binaries still run on .NET 8+.
+ - **CMake 3.16+** on your `PATH`.
+ - **Windows:** Visual Studio 2022 with the *C++ Desktop Development* workload (for native compilation).
+ - **Linux:** a C toolchain and `patchelf` — e.g. `sudo apt-get install -y cmake build-essential patchelf`.
## Key Features
@@ -558,7 +569,8 @@ To run it:
The `CycloneDDS.NET` package bundles these internal components:
* **Managed Libraries:** `CycloneDDS.Core`, `CycloneDDS.Schema`, `CycloneDDS.CodeGen`, `CycloneDDS.Runtime`
-* **Native Assets:** `ddsc.dll` (Cyclone DDS), `idlc.exe` (IDL Compiler), `cycloneddsidljson.dll` (IDL JSON plugin)
+* **Native Assets (Windows x64):** `ddsc.dll` (Cyclone DDS), `idlc.exe` (IDL Compiler), `cycloneddsidljson.dll` (IDL JSON plugin)
+* **Native Assets (Linux x64):** `libddsc.so` (Cyclone DDS), `idlc` (IDL Compiler), `libcycloneddsidljson.so` (IDL JSON plugin)
## Performance Characteristics
diff --git a/build/native-linux.sh b/build/native-linux.sh
new file mode 100755
index 0000000..b86fac3
--- /dev/null
+++ b/build/native-linux.sh
@@ -0,0 +1,150 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# ===================================================================================
+# build/native-linux.sh
+#
+# PURPOSE:
+# Compiles the native (C/C++) Cyclone DDS submodule for Linux and copies the
+# resulting binaries (.so libraries + idlc) to the local 'artifacts' directory.
+# This is a prerequisite for running managed tests or packing the NuGet package
+# with linux-x64 support. It is the Linux counterpart of build/native-win.ps1.
+#
+# USAGE:
+# ./build/native-linux.sh [Release|Debug]
+# Default: Release
+# ===================================================================================
+
+CONFIG="${1:-Release}"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_ROOT="$(dirname "$SCRIPT_DIR")"
+SOURCE_DIR="$REPO_ROOT/cyclonedds"
+BUILD_DIR="$REPO_ROOT/build/native-linux"
+INSTALL_DIR="$REPO_ROOT/artifacts/native-install-linux"
+ARTIFACTS_DIR="$REPO_ROOT/artifacts/native/linux-x64"
+
+echo "============================================================"
+echo " Building Native CycloneDDS for Linux ($CONFIG)"
+echo "============================================================"
+
+# ----------------------------------------------------------------
+# Prerequisites
+# ----------------------------------------------------------------
+if ! command -v cmake &> /dev/null; then
+ echo "ERROR: cmake is not installed or not in PATH." >&2
+ exit 1
+fi
+
+if ! command -v gcc &> /dev/null && ! command -v cc &> /dev/null; then
+ echo "ERROR: no C compiler (gcc/cc) found in PATH." >&2
+ exit 1
+fi
+
+if [ ! -d "$SOURCE_DIR" ] || [ -z "$(ls -A "$SOURCE_DIR" 2>/dev/null)" ]; then
+ echo "ERROR: Native source directory is missing or empty: $SOURCE_DIR" >&2
+ echo " Run: git submodule update --init --recursive" >&2
+ exit 1
+fi
+
+mkdir -p "$BUILD_DIR" "$INSTALL_DIR" "$ARTIFACTS_DIR"
+
+# ----------------------------------------------------------------
+# [1/3] CMake Configure
+# ----------------------------------------------------------------
+echo ""
+echo "[1/3] Configuring CMake..."
+
+cmake -S "$SOURCE_DIR" -B "$BUILD_DIR" \
+ -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \
+ -DCMAKE_BUILD_TYPE="$CONFIG" \
+ -DBUILD_IDLC=ON \
+ -DBUILD_TESTING=OFF \
+ -DBUILD_EXAMPLES=OFF \
+ -DENABLE_SSL=OFF \
+ -DENABLE_SHM=OFF \
+ -DENABLE_SECURITY=OFF
+
+# ----------------------------------------------------------------
+# [2/3] Build & Install
+# ----------------------------------------------------------------
+echo ""
+echo "[2/3] Building & Installing..."
+
+NPROC="${NPROC:-$(nproc 2>/dev/null || echo 4)}"
+cmake --build "$BUILD_DIR" --config "$CONFIG" -j "$NPROC"
+cmake --install "$BUILD_DIR" --config "$CONFIG"
+
+# ----------------------------------------------------------------
+# [3/3] Copy Artifacts & Fix RPATH
+# ----------------------------------------------------------------
+echo ""
+echo "[3/3] Copying artifacts to $ARTIFACTS_DIR..."
+
+LIB_DIR="$INSTALL_DIR/lib"
+BIN_DIR="$INSTALL_DIR/bin"
+# Some distros install shared libs to lib64.
+[ -d "$LIB_DIR" ] || LIB_DIR="$INSTALL_DIR/lib64"
+
+# Copy a shared library, resolving the SONAME symlink chain to the real file so
+# we don't have to hard-code the version suffix. The file is staged as both
+# .so (linker/convention name) and .so.0 (the runtime SONAME).
+copy_lib() {
+ local base="$1"
+ local real=""
+ if [ -e "$LIB_DIR/${base}.so" ]; then
+ real="$(readlink -f "$LIB_DIR/${base}.so")"
+ elif [ -e "$LIB_DIR/${base}.so.0" ]; then
+ real="$(readlink -f "$LIB_DIR/${base}.so.0")"
+ else
+ # Last resort: pick the most specific versioned file available.
+ real="$(ls -1 "$LIB_DIR/${base}.so".* 2>/dev/null | sort | tail -n1 || true)"
+ fi
+
+ if [ -z "$real" ] || [ ! -e "$real" ]; then
+ echo " [-] Missing ${base}.so* in $LIB_DIR" >&2
+ return 1
+ fi
+
+ cp -f "$real" "$ARTIFACTS_DIR/${base}.so"
+ cp -f "$real" "$ARTIFACTS_DIR/${base}.so.0"
+ echo " [+] ${base}.so / ${base}.so.0"
+}
+
+# Runtime library and IDL-compiler support libraries.
+copy_lib libddsc
+copy_lib libcycloneddsidl
+copy_lib libcycloneddsidlc
+copy_lib libcycloneddsidljson
+
+# IDL compiler executable.
+if [ -f "$BIN_DIR/idlc" ]; then
+ cp -f "$BIN_DIR/idlc" "$ARTIFACTS_DIR/"
+ chmod +x "$ARTIFACTS_DIR/idlc"
+ echo " [+] idlc"
+else
+ echo " [-] Missing idlc in $BIN_DIR" >&2
+ exit 1
+fi
+
+# Fix RPATH: CMake sets RPATH to $ORIGIN/../lib (bin/ -> lib/), but in the flat
+# NuGet tools/ directory every file sits side by side. Rewrite RPATH to $ORIGIN/
+# so the dynamic linker finds the .so dependencies next to the executable.
+if command -v patchelf &> /dev/null; then
+ echo ""
+ echo " [+] Fixing RPATH to \$ORIGIN/..."
+ chmod +w "$ARTIFACTS_DIR/"*.so* "$ARTIFACTS_DIR/idlc" 2>/dev/null || true
+ for f in "$ARTIFACTS_DIR/"*.so* "$ARTIFACTS_DIR/idlc"; do
+ [ -e "$f" ] || continue
+ patchelf --set-rpath '$ORIGIN/' "$f" 2>/dev/null || true
+ done
+ echo " [+] RPATH fixed."
+else
+ echo " [!] patchelf not found. Run: sudo apt-get install patchelf"
+ echo " [!] Without patchelf, LD_LIBRARY_PATH must point at the idlc directory at runtime."
+ echo " [!] (IdlcRunner sets this automatically; DllImport('ddsc') resolves via the co-located .so.)"
+fi
+
+echo ""
+echo "Native build complete."
+echo "Artifacts staged at: $ARTIFACTS_DIR"
+ls -la "$ARTIFACTS_DIR"
diff --git a/examples/FeatureDemo/FeatureDemo.csproj b/examples/FeatureDemo/FeatureDemo.csproj
index 67ea7d5..9ad25f3 100644
--- a/examples/FeatureDemo/FeatureDemo.csproj
+++ b/examples/FeatureDemo/FeatureDemo.csproj
@@ -21,7 +21,11 @@
-
+
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
PreserveNewest
%(Filename)%(Extension)
diff --git a/src/CycloneDDS.Core/CycloneDDS.Core.csproj b/src/CycloneDDS.Core/CycloneDDS.Core.csproj
index 452b585..2fed15b 100644
--- a/src/CycloneDDS.Core/CycloneDDS.Core.csproj
+++ b/src/CycloneDDS.Core/CycloneDDS.Core.csproj
@@ -9,7 +9,7 @@
false
-
+
PreserveNewest
%(RecursiveDir)%(Filename)%(Extension)
@@ -19,4 +19,11 @@
%(RecursiveDir)%(Filename)%(Extension)
+
+
+
+ PreserveNewest
+ %(RecursiveDir)%(Filename)%(Extension)
+
+
\ No newline at end of file
diff --git a/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj b/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj
index ae63d4b..ca1a998 100644
--- a/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj
+++ b/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj
@@ -7,14 +7,23 @@
true
CS8600;CS8601;CS8603;CS8604
CycloneDDS.NET
- Cyclone DDS Runtime bindings for .NET (Win64)
+ Cyclone DDS Runtime bindings for .NET (Windows x64, Linux x64)
true
-
+
-
+
+
+
+
+
+
+
+
+
diff --git a/src/CycloneDDS.Runtime/DdsReader.cs b/src/CycloneDDS.Runtime/DdsReader.cs
index 1e8a462..f7f6b4e 100644
--- a/src/CycloneDDS.Runtime/DdsReader.cs
+++ b/src/CycloneDDS.Runtime/DdsReader.cs
@@ -261,7 +261,7 @@ private void EnsureListenerAttached()
}
}
- private static void OnSubscriptionMatched(int reader, ref DdsApi.DdsSubscriptionMatchedStatus status, IntPtr arg)
+ private static void OnSubscriptionMatched(int reader, DdsApi.DdsSubscriptionMatchedStatus status, IntPtr arg)
{
if (arg == IntPtr.Zero) return;
try
diff --git a/src/CycloneDDS.Runtime/DdsWriter.cs b/src/CycloneDDS.Runtime/DdsWriter.cs
index d802429..7f8815f 100644
--- a/src/CycloneDDS.Runtime/DdsWriter.cs
+++ b/src/CycloneDDS.Runtime/DdsWriter.cs
@@ -330,7 +330,7 @@ private void EnsureListenerAttached()
}
// [MonoPInvokeCallback(typeof(DdsApi.DdsOnPublicationMatched))]
- private static void OnPublicationMatched(int writer, ref DdsApi.DdsPublicationMatchedStatus status, IntPtr arg)
+ private static void OnPublicationMatched(int writer, DdsApi.DdsPublicationMatchedStatus status, IntPtr arg)
{
if (arg == IntPtr.Zero) return;
try
diff --git a/src/CycloneDDS.Runtime/Interop/DdsApi.cs b/src/CycloneDDS.Runtime/Interop/DdsApi.cs
index 98e0a82..c73e63f 100644
--- a/src/CycloneDDS.Runtime/Interop/DdsApi.cs
+++ b/src/CycloneDDS.Runtime/Interop/DdsApi.cs
@@ -401,11 +401,20 @@ public struct DdsSubscriptionMatchedStatus
public long LastPublicationHandle;
}
+ // NOTE: status is passed BY VALUE, matching the C signature
+ // void (*)(dds_entity_t, const dds_..._matched_status_t status, void *arg)
+ // These 24-byte structs are >16 bytes, so the Windows x64 ABI passes them by an
+ // implicit pointer (which a `ref` parameter happened to match) while the Linux
+ // System V ABI passes them by value on the stack. Declaring the parameter `ref`
+ // therefore worked only on Windows; on Linux it shifted `arg` into the wrong
+ // register, so GCHandle.FromIntPtr(arg) threw and the callback was silently
+ // dropped. Declaring it by value lets the .NET marshaller emit the correct
+ // per-platform ABI on both.
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
- public delegate void DdsOnPublicationMatched(int writer, ref DdsPublicationMatchedStatus status, IntPtr arg);
+ public delegate void DdsOnPublicationMatched(int writer, DdsPublicationMatchedStatus status, IntPtr arg);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
- public delegate void DdsOnSubscriptionMatched(int reader, ref DdsSubscriptionMatchedStatus status, IntPtr arg);
+ public delegate void DdsOnSubscriptionMatched(int reader, DdsSubscriptionMatchedStatus status, IntPtr arg);
[DllImport(DLL_NAME)]
public extern static void dds_lset_publication_matched(IntPtr listener, DdsOnPublicationMatched callback);
diff --git a/tests/CycloneDDS.CodeGen.Tests/CycloneDDS.CodeGen.Tests.csproj b/tests/CycloneDDS.CodeGen.Tests/CycloneDDS.CodeGen.Tests.csproj
index 5bf4f22..29d226a 100644
--- a/tests/CycloneDDS.CodeGen.Tests/CycloneDDS.CodeGen.Tests.csproj
+++ b/tests/CycloneDDS.CodeGen.Tests/CycloneDDS.CodeGen.Tests.csproj
@@ -28,7 +28,11 @@
-
+
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
PreserveNewest
%(Filename)%(Extension)
diff --git a/tests/CycloneDDS.CodeGen.Tests/ErrorHandlingTests.cs b/tests/CycloneDDS.CodeGen.Tests/ErrorHandlingTests.cs
index c6cd57a..995e43b 100644
--- a/tests/CycloneDDS.CodeGen.Tests/ErrorHandlingTests.cs
+++ b/tests/CycloneDDS.CodeGen.Tests/ErrorHandlingTests.cs
@@ -128,8 +128,13 @@ string field2 // Missing semicolons
var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
// Traverse up 5 levels: net8.0 -> Debug -> bin -> CycloneDDS.CodeGen.Tests -> tests -> RepoRoot
var repoRoot = Path.GetFullPath(Path.Combine(assemblyDir, "..", "..", "..", "..", ".."));
- var idlcPath = Path.Combine(repoRoot, "artifacts", "native", "win-x64", "idlc.exe");
-
+
+ // Native artifacts are laid out per RID; the executable name differs by OS.
+ bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ string rid = isWindows ? "win-x64" : "linux-x64";
+ string idlcName = isWindows ? "idlc.exe" : "idlc";
+ var idlcPath = Path.Combine(repoRoot, "artifacts", "native", rid, idlcName);
+
runner.IdlcPathOverride = idlcPath;
var result = runner.RunIdlc(tempIdl, Path.GetTempPath());
Assert.NotEqual(0, result.ExitCode);
diff --git a/tests/CycloneDDS.CodeGen.Tests/IdlcRunnerTests.cs b/tests/CycloneDDS.CodeGen.Tests/IdlcRunnerTests.cs
index aa910e5..3654bd6 100644
--- a/tests/CycloneDDS.CodeGen.Tests/IdlcRunnerTests.cs
+++ b/tests/CycloneDDS.CodeGen.Tests/IdlcRunnerTests.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Runtime.InteropServices;
using Xunit;
using CycloneDDS.CodeGen;
using CycloneDDS.Compiler.Common;
@@ -67,9 +68,11 @@ public void FindIdlc_ReturnsOverride_WhenExists()
[Fact]
public void FindIdlc_ChecksCurrentDirectory()
{
- // Create a dummy idlc in the current directory (where tests run)
+ // Create a dummy idlc in the current directory (where tests run).
+ // The executable name is platform-specific (idlc.exe on Windows, idlc elsewhere).
var currentDir = AppDomain.CurrentDomain.BaseDirectory;
- var dummyIdlc = Path.Combine(currentDir, "idlc.exe");
+ var idlcName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "idlc.exe" : "idlc";
+ var dummyIdlc = Path.Combine(currentDir, idlcName);
// Only run this test if idlc doesn't already exist there (to avoid messing up environment)
if (!File.Exists(dummyIdlc))
@@ -91,6 +94,12 @@ public void FindIdlc_ChecksCurrentDirectory()
[Fact]
public void RunIdlc_ExecutesProcess_AndFindsFiles()
{
+ // The mock idlc is a Windows batch file, so this plumbing test only
+ // runs on Windows. Real idlc invocation is exercised on Linux by the
+ // integration tests that point at the built native idlc.
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
// Setup
var idlPath = Path.Combine(_tempDir, "TestTopic.idl");
File.WriteAllText(idlPath, "struct TestTopic {};");
diff --git a/tests/CycloneDDS.Core.Tests/CycloneDDS.Core.Tests.csproj b/tests/CycloneDDS.Core.Tests/CycloneDDS.Core.Tests.csproj
index bfd96ee..8b3f157 100644
--- a/tests/CycloneDDS.Core.Tests/CycloneDDS.Core.Tests.csproj
+++ b/tests/CycloneDDS.Core.Tests/CycloneDDS.Core.Tests.csproj
@@ -27,7 +27,11 @@
-
+
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
PreserveNewest
%(Filename)%(Extension)
diff --git a/tests/CycloneDDS.Native.Verify/CycloneDDS.Native.Verify.csproj b/tests/CycloneDDS.Native.Verify/CycloneDDS.Native.Verify.csproj
index 2f2907b..bc9cf96 100644
--- a/tests/CycloneDDS.Native.Verify/CycloneDDS.Native.Verify.csproj
+++ b/tests/CycloneDDS.Native.Verify/CycloneDDS.Native.Verify.csproj
@@ -28,7 +28,11 @@
-
+
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
PreserveNewest
%(Filename)%(Extension)
diff --git a/tests/CycloneDDS.Runtime.Tests/CycloneDDS.Runtime.Tests.csproj b/tests/CycloneDDS.Runtime.Tests/CycloneDDS.Runtime.Tests.csproj
index aa188f7..088a9e9 100644
--- a/tests/CycloneDDS.Runtime.Tests/CycloneDDS.Runtime.Tests.csproj
+++ b/tests/CycloneDDS.Runtime.Tests/CycloneDDS.Runtime.Tests.csproj
@@ -24,14 +24,18 @@
-
-
+
+
-
+
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
PreserveNewest
%(Filename)%(Extension)
diff --git a/tests/CycloneDDS.Schema.Tests/CycloneDDS.Schema.Tests.csproj b/tests/CycloneDDS.Schema.Tests/CycloneDDS.Schema.Tests.csproj
index 3fbe5a8..777f5ee 100644
--- a/tests/CycloneDDS.Schema.Tests/CycloneDDS.Schema.Tests.csproj
+++ b/tests/CycloneDDS.Schema.Tests/CycloneDDS.Schema.Tests.csproj
@@ -22,12 +22,16 @@
-
+
-
+
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
PreserveNewest
%(Filename)%(Extension)
diff --git a/tests/DdsMonitor.Engine.Tests/DdsMonitor.Engine.Tests.csproj b/tests/DdsMonitor.Engine.Tests/DdsMonitor.Engine.Tests.csproj
index 863bb2a..98edf76 100644
--- a/tests/DdsMonitor.Engine.Tests/DdsMonitor.Engine.Tests.csproj
+++ b/tests/DdsMonitor.Engine.Tests/DdsMonitor.Engine.Tests.csproj
@@ -28,7 +28,11 @@
-
+
+ PreserveNewest
+ %(Filename)%(Extension)
+
+
PreserveNewest
%(Filename)%(Extension)
diff --git a/tools/CycloneDDS.CodeGen/CycloneDDS.CodeGen.csproj b/tools/CycloneDDS.CodeGen/CycloneDDS.CodeGen.csproj
index 73ee5ab..5b4f82a 100644
--- a/tools/CycloneDDS.CodeGen/CycloneDDS.CodeGen.csproj
+++ b/tools/CycloneDDS.CodeGen/CycloneDDS.CodeGen.csproj
@@ -16,7 +16,7 @@
-
-
+
+
diff --git a/tools/CycloneDDS.Compiler.Common/IdlcRunner.cs b/tools/CycloneDDS.Compiler.Common/IdlcRunner.cs
index 5be03e3..7680f2d 100644
--- a/tools/CycloneDDS.Compiler.Common/IdlcRunner.cs
+++ b/tools/CycloneDDS.Compiler.Common/IdlcRunner.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Runtime.InteropServices;
namespace CycloneDDS.Compiler.Common
{
@@ -9,42 +10,66 @@ public class IdlcRunner
public string? IdlcPathOverride { get; set; }
public string? IdlcExtraArgs { get; set; }
+ // The idlc executable and the native RID sub-directory both differ per
+ // platform: idlc.exe under win-x64 on Windows, idlc under linux-x64 on
+ // Linux. Every lookup below iterates these so the same search logic works
+ // on either OS.
+ private static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ private static string[] IdlcNames => IsWindows ? new[] { "idlc.exe" } : new[] { "idlc" };
+ private static string[] NativeRids => IsWindows ? new[] { "win-x64" } : new[] { "linux-x64" };
+
public string FindIdlc()
{
if (!string.IsNullOrEmpty(IdlcPathOverride))
{
if (File.Exists(IdlcPathOverride)) return IdlcPathOverride;
- throw new FileNotFoundException($"idlc.exe not found at override path: {IdlcPathOverride}");
+ throw new FileNotFoundException($"idlc not found at override path: {IdlcPathOverride}");
}
- // Check current directory (where DLLs are)
+ // Check current directory (where DLLs / .so files are copied)
string currentDir = AppDomain.CurrentDomain.BaseDirectory;
- string localIdlc = Path.Combine(currentDir, "idlc.exe");
- if (File.Exists(localIdlc)) return localIdlc;
+ foreach (var name in IdlcNames)
+ {
+ string localIdlc = Path.Combine(currentDir, name);
+ if (File.Exists(localIdlc)) return localIdlc;
+ }
- // Check NuGet package location relative to tools/ (tools/ -> ../runtimes/win-x64/native/)
- try
+ // Check NuGet package location relative to tools/ (tools/ -> ../runtimes/{rid}/native/)
+ foreach (var rid in NativeRids)
{
- string nugetNativePath = Path.Combine(currentDir, "..", "runtimes", "win-x64", "native", "idlc.exe");
- if (File.Exists(nugetNativePath)) return Path.GetFullPath(nugetNativePath);
+ foreach (var name in IdlcNames)
+ {
+ try
+ {
+ string nugetNativePath = Path.Combine(currentDir, "..", "runtimes", rid, "native", name);
+ if (File.Exists(nugetNativePath)) return Path.GetFullPath(nugetNativePath);
+ }
+ catch { }
+ }
}
- catch {}
// DEV: Check workspace location (for tests/dev)
- // Iterate up 6 levels looking for cyclonedds/install/bin/idlc.exe OR cyclone-compiled/bin/idlc.exe
+ // Iterate up 6 levels looking for cyclonedds/install/bin, cyclone-compiled/bin,
+ // or artifacts/native/{rid} — trying each platform's executable name.
var searchDir = new DirectoryInfo(currentDir);
for (int i = 0; i < 6; i++)
{
if (searchDir == null) break;
-
- string checkPath = Path.Combine(searchDir.FullName, "cyclonedds", "install", "bin", "idlc.exe");
- if (File.Exists(checkPath)) return checkPath;
-
- string repoPath = Path.Combine(searchDir.FullName, "cyclone-compiled", "bin", "idlc.exe");
- if (File.Exists(repoPath)) return repoPath;
- repoPath = Path.Combine(searchDir.FullName, "artifacts", "native", "win-x64", "idlc.exe");
- if (File.Exists(repoPath)) return repoPath;
+ foreach (var name in IdlcNames)
+ {
+ string checkPath = Path.Combine(searchDir.FullName, "cyclonedds", "install", "bin", name);
+ if (File.Exists(checkPath)) return checkPath;
+
+ string repoPath = Path.Combine(searchDir.FullName, "cyclone-compiled", "bin", name);
+ if (File.Exists(repoPath)) return repoPath;
+
+ foreach (var rid in NativeRids)
+ {
+ string artifactPath = Path.Combine(searchDir.FullName, "artifacts", "native", rid, name);
+ if (File.Exists(artifactPath)) return artifactPath;
+ }
+ }
searchDir = searchDir.Parent;
}
@@ -53,33 +78,39 @@ public string FindIdlc()
string? cycloneHome = Environment.GetEnvironmentVariable("CYCLONEDDS_HOME");
if (!string.IsNullOrEmpty(cycloneHome))
{
- string path = Path.Combine(cycloneHome, "bin", "idlc.exe");
- if (File.Exists(path))
- return path;
-
- // Try without bin?
- path = Path.Combine(cycloneHome, "idlc.exe");
- if (File.Exists(path))
- return path;
+ foreach (var name in IdlcNames)
+ {
+ string path = Path.Combine(cycloneHome, "bin", name);
+ if (File.Exists(path))
+ return path;
+
+ // Try without bin?
+ path = Path.Combine(cycloneHome, name);
+ if (File.Exists(path))
+ return path;
+ }
}
-
+
// Check PATH
string? pathEnv = Environment.GetEnvironmentVariable("PATH");
if (pathEnv != null)
{
foreach (var dir in pathEnv.Split(Path.PathSeparator))
{
- try
+ foreach (var name in IdlcNames)
{
- string path = Path.Combine(dir, "idlc.exe");
- if (File.Exists(path))
- return path;
+ try
+ {
+ string path = Path.Combine(dir, name);
+ if (File.Exists(path))
+ return path;
+ }
+ catch { /* Ignore invalid paths in PATH */ }
}
- catch { /* Ignore invalid paths in PATH */ }
}
}
-
- throw new FileNotFoundException("idlc.exe not found. Set CYCLONEDDS_HOME or add to PATH.");
+
+ throw new FileNotFoundException("idlc executable not found. Set CYCLONEDDS_HOME or add to PATH.");
}
public IdlcResult RunIdlc(string idlFilePath, string outputDir, string? includePath = null)
@@ -100,7 +131,36 @@ public IdlcResult RunIdlc(string idlFilePath, string outputDir, string? includeP
RedirectStandardError = true,
CreateNoWindow = true
};
-
+
+ if (!IsWindows)
+ {
+ // NuGet packages do not preserve the Unix execute bit, so an idlc
+ // restored from the package may not be runnable. Restore it (best effort).
+ // The inner OperatingSystem guard is what the CA1416 analyzer recognizes.
+ try
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ var mode = File.GetUnixFileMode(idlcPath);
+ File.SetUnixFileMode(idlcPath,
+ mode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
+ }
+ }
+ catch { /* best effort — may already be executable, or FS may not support it */ }
+
+ // idlc depends on the libcycloneddsidl*.so libraries shipped alongside
+ // it. The native build normally rewrites RPATH to $ORIGIN so they resolve,
+ // but prepend the idlc directory to LD_LIBRARY_PATH as a fallback (e.g.
+ // when patchelf was unavailable at native build time).
+ string? idlcDir = Path.GetDirectoryName(Path.GetFullPath(idlcPath));
+ if (!string.IsNullOrEmpty(idlcDir))
+ {
+ string existing = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH") ?? string.Empty;
+ startInfo.Environment["LD_LIBRARY_PATH"] =
+ existing.Length == 0 ? idlcDir : idlcDir + Path.PathSeparator + existing;
+ }
+ }
+
if (!string.IsNullOrWhiteSpace(IdlcExtraArgs))
{
// Simple split by whitespace is sufficient for most compiler flags like "-Werror"
diff --git a/tools/DdsMonitor/DdsMonitor.Blazor/DdsMonitor.csproj b/tools/DdsMonitor/DdsMonitor.Blazor/DdsMonitor.csproj
index 919f620..2631ba1 100644
--- a/tools/DdsMonitor/DdsMonitor.Blazor/DdsMonitor.csproj
+++ b/tools/DdsMonitor/DdsMonitor.Blazor/DdsMonitor.csproj
@@ -27,6 +27,40 @@
+
+
+
+
+ PreserveNewest
+ libddsc.so
+
+
+ PreserveNewest
+ libddsc.so.0
+
+
+
+ PreserveNewest
+ ddsc.dll
+
+
+