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 + + +