diff --git a/README.md b/README.md index e5488c0e..e8f65117 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ The following operating systems and compilers have been tested. * Ubuntu 24.04 with GCC 13 * Windows 10 with Visual Studio 2022 +* macOS 26.7.1 (Apple Silicon) with Apple Clang 21 + +See [Building and installation](doc/user/building.md) for the C library and +the [Rust workspace guide](rust/README.md) for the daemon and GStreamer plugins. ## Contributing diff --git a/doc/user/building.md b/doc/user/building.md index 2fe2a4b9..b6a3dbf6 100644 --- a/doc/user/building.md +++ b/doc/user/building.md @@ -7,6 +7,18 @@ SPDX-License-Identifier: Apache-2.0 ## Pre-Build Requirements +### C++ Compiler + +**macOS** + +Install the Xcode Command Line Tools, which provide Apple Clang and the macOS SDK. + +```sh +xcode-select --install +``` + +macOS 26.7.1 on Apple Silicon with Apple Clang 21 has been tested. + ### Python Having Python 3 isn't an absolute requirement but it makes the subsequent steps to install the dependencies easier. @@ -22,6 +34,11 @@ Use the system package manager to install Python 3 and the `venv` module (on Deb sudo apt install -y python3 python3-venv ``` +**macOS** + +Install Python 3 from [python.org](https://www.python.org/downloads/macos/) or your package manager. +Use `python3` for the virtual environment commands below. + **Windows** Download the Python 3 installer from [python.org](https://www.python.org/downloads/windows/) and run it manually, or use the following PowerShell commands. @@ -48,7 +65,7 @@ python --version For manual builds, a **virtual environment** in the repository root (for example, `.venv`) is recommended. It isolates the Python environment used for Conan and other `pip`-installed build tools from the system interpreter, pins compatible tool versions on `PATH`, and on Linux avoids PEP 668 "externally managed" errors when using the system Python. -**Linux** +**Linux and macOS** ```sh python3 -m venv .venv @@ -77,15 +94,16 @@ python -m venv .venv python -m pip install --upgrade pip ``` -To exit the virtual environment later, run `deactivate` (same on Linux and Windows). +To exit the virtual environment later, run `deactivate` (same on Linux, macOS and Windows). ### CMake The project requires CMake 3.17 or higher; dependencies may require a higher version. There are x86_64 and arm64 packages for CMake 3.30.0 on the [Python Package Index (PyPI)](https://pypi.org/) which have been tested. +CMake 3.31.10 has been tested on macOS. -**Linux** +**Linux and macOS** ```sh pip install cmake~=3.17 @@ -125,6 +143,26 @@ compiler.version=13 os=Linux ``` +**macOS** + +```sh +pip install conan~=2.2 --upgrade +conan profile detect +``` + +Conan 2.33.0 with the following Apple Silicon profile has been tested. + +```ini +[settings] +arch=armv8 +build_type=Release +compiler=apple-clang +compiler.cppstd=gnu17 +compiler.libcxx=libc++ +compiler.version=21 +os=Macos +``` + **Windows** ```PowerShell @@ -152,7 +190,7 @@ os=Windows ## Building the NvNmos Library -**Linux** +**Linux and macOS** Prepare a `build` directory adjacent to the `src` directory. @@ -194,6 +232,10 @@ Build the library and example application. cmake --build build --parallel ``` +On macOS, the shared library is `build/libnvnmos.dylib` and the example application is `build/nvnmos-example`. +Use `--parallel 2` to limit concurrent build jobs if needed. +The shared Release build and example application have been tested on Apple Silicon. + **Windows** Prepare a `build` directory adjacent to the `src` directory. @@ -296,3 +338,7 @@ apt install -y dbus avahi-daemon Install and start the Bonjour Service. See [Download Bonjour Print Services for Windows v2.0.2](https://support.apple.com/kb/DL999). + +**macOS** + +Bonjour (mDNSResponder) is included with macOS; no separate Avahi or Bonjour installation is required. diff --git a/rust/README.md b/rust/README.md index ccbf9481..2b5d901c 100644 --- a/rust/README.md +++ b/rust/README.md @@ -20,17 +20,17 @@ Rust components for the NMOS daemon and GStreamer plugin family. ## Quick Start Three terminals on Linux: build once, start the daemon, then run a sender and -receiver. The example scripts use RTP/UDP (`transport=udp`, ST 2110-style +receiver. Linux commands are shown below, with macOS differences noted where needed. The example scripts use RTP/UDP (`transport=udp`, ST 2110-style multicast) with `auto-activate=true`, so video starts without an NMOS Controller or IS-05 PATCH step. -Tested on **Ubuntu 24.04** (same as CI). +Tested on **Ubuntu 24.04** (same as CI) and **macOS 26.7.1 (Apple Silicon)**. ### Prerequisites **System packages** — C++ compiler, CMake, Python, libclang (`bindgen` for `nvnmos-sys`), and GStreamer headers plus runtime plugins for the example -pipelines: +pipelines. On Ubuntu: ```sh sudo apt-get update @@ -45,6 +45,17 @@ sudo apt-get install --no-install-recommends -y \ `iproute2` (for `ss`), `curl`, and `jq` are only required by the example pipeline scripts and the interactive demo. +**On macOS**, install the Xcode Command Line Tools and Python as described in +[`building.md`](../doc/user/building.md), then use [Homebrew](https://brew.sh/): + +```sh +brew install gstreamer pkgconf bash jq +export LIBCLANG_PATH="$(dirname "$(xcrun --find clang)")/../lib" +``` + +This uses Xcode's libclang for bindgen. The interactive demo needs Bash 4.3+; +macOS's bundled Bash 3.2 is too old. + CMake must be **3.17+**. Ubuntu 24.04's `cmake` package is sufficient; on older distros use `pip install cmake~=3.17` inside the venv below (see [`building.md`](../doc/user/building.md#cmake)). @@ -63,11 +74,12 @@ conan profile detect # creates ~/.conan2/profiles/default ``` Keep the venv activated while running `conan install` in the next section. +On macOS, also install CMake in that venv with `pip install cmake~=3.17`. **Rust** — install [rustup](https://rustup.rs/) if `cargo` is not on your `PATH`. Building inside `rust/` picks up [`rust-toolchain.toml`](rust-toolchain.toml). -### Build `libnvnmos.so` +### Build `libnvnmos.so` or `libnvnmos.dylib` (macOS) From the **repository root** (full detail in [`building.md`](../doc/user/building.md#building-the-nvnmos-library)): @@ -87,6 +99,8 @@ cmake -B build \ cmake --build build --parallel ``` +On macOS, the same build commands produce `build/libnvnmos.dylib` ins. + Point the Rust build at the shared library: ```sh @@ -94,6 +108,13 @@ export NVNMOS_LIB_DIR=$PWD/build export LD_LIBRARY_PATH=$NVNMOS_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} ``` +**On macOS**, use `DYLD_LIBRARY_PATH` instead of `LD_LIBRARY_PATH` in each +terminal's setup throughout this guide: + +```sh +export DYLD_LIBRARY_PATH="$NVNMOS_LIB_DIR${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" +``` + ### Build `nvnmosd` and the GStreamer plugin ```sh @@ -102,6 +123,14 @@ cargo build -p nvnmosd -p gst-nmos-rs export GST_PLUGIN_PATH=$PWD/target/debug ``` +The plugin is `libgstnmos.so` on Linux or `libgstnmos.dylib` on macOS; +`GST_PLUGIN_PATH` points to the containing directory on both platforms. +If the macOS plugin scanner cannot find Homebrew runtime libraries, set: + +```sh +export DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix)/lib${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" +``` + ### Start the daemon (terminal 1) ```sh @@ -122,7 +151,7 @@ export LD_LIBRARY_PATH=$NVNMOS_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} export GST_PLUGIN_PATH=/path/to/nvnmos/rust/target/debug cd rust/gst-nmos-rs -# Optional: pick the NIC that carries multicast (defaults to first non-loopback). +# Optional: pick the NIC that carries multicast (defaults to first non-loopback and in macOS defaults to the default-route interface). # export DEMO_NIC_IP=203.0.113.1 ./scripts/example-pipelines/1080p25-sender-udp.sh ``` @@ -154,6 +183,13 @@ Use `RUST_LOG=info` on the daemon for verbose logs. Headless hosts: set **Next steps:** The interactive lab with IS-05 menu actions is [`gst-nmos-rs/scripts/gst-nmos-rs-demo.sh`](gst-nmos-rs/scripts/gst-nmos-rs-demo.sh). +On macOS, run it from `rust/` with Homebrew Bash and select UDP: + +```sh +DEMO_TRANSPORT=udp DEMO_VIDEO_SINK=fakesink DEMO_AUDIO_SINK=fakesink \ + "$(brew --prefix)/bin/bash" gst-nmos-rs/scripts/gst-nmos-rs-demo.sh +``` + For MXL (`transport=mxl`), build the [MXL SDK](https://github.com/dmf-mxl/mxl) first, then see [`pipeline-examples.md`](gst-nmos-rs/pipeline-examples.md) for example pipelines. For compliant ST 2110 (`transport=nvdsudp`) @@ -183,19 +219,22 @@ For a combined image with `nvnmosd` + `gst-nmos-rs` + plugins for `transport=mxl The workspace **MSRV** is **Rust 1.87** (`rust-version` in [`Cargo.toml`](Cargo.toml)). Development, CI, and container builds pin **1.92** in [`rust-toolchain.toml`](rust-toolchain.toml) — the minimum needed for gst-plugins-rs `0.15.2` in [`docker/gst-nmos-rs`](../docker/gst-nmos-rs/). -`nvnmos-sys` links against a pre-built `libnvnmos.so`. The Rust crate does **not** build the C library itself; use the CMake/Conan flow under `../src/` ([Quick start — Build `libnvnmos.so`](#build-libnvnmosso)), then: +`nvnmos-sys` links against a pre-built `libnvnmos.so` (`libnvnmos.dylib` on macOS). The Rust crate does **not** build the C library itself; use the CMake/Conan flow under `../src/` ([Quick start — Build `libnvnmos.so`](#build-libnvnmosso)), then: ```sh -export NVNMOS_LIB_DIR=/absolute/path/to/build # contains libnvnmos.so +export NVNMOS_LIB_DIR=/absolute/path/to/build # contains libnvnmos.so or libnvnmos.dylib cargo build --workspace ``` -`NVNMOS_LIB_DIR` is optional when `libnvnmos.so` is installed on the system linker path. +`NVNMOS_LIB_DIR` is optional when `libnvnmos` is installed on the system linker path. `NVNMOS_INCLUDE_DIR` defaults to `../src/` (where `nvnmos.h` lives in-tree). Override only to bindgen against a different header location. `protoc` is vendored via [`protobuf-src`](https://crates.io/crates/protobuf-src) — no system `protoc` is required. +For integration test prerequisites, see +[`gst-nmos-rs/tests/README.md`](gst-nmos-rs/tests/README.md). + ## Scale Benchmark (`nvnmosd-bench`) Measures daemon memory usage and RPC/HTTP latencies across preset scenarios. See @@ -216,7 +255,7 @@ RPC-only regression client — no GStreamer. For a full media walk-through, use [Quick start](#quick-start) instead. ```sh -# Terminal 1: daemon (with NVNMOS_LIB_DIR / LD_LIBRARY_PATH set) +# Terminal 1: daemon (with NVNMOS_LIB_DIR and LD_LIBRARY_PATH or DYLD_LIBRARY_PATH set) cargo run --bin nvnmosd # Terminal 2: example client diff --git a/rust/gst-nmos-rs/scripts/env.sh b/rust/gst-nmos-rs/scripts/env.sh index f651f74a..46d8627f 100644 --- a/rust/gst-nmos-rs/scripts/env.sh +++ b/rust/gst-nmos-rs/scripts/env.sh @@ -35,12 +35,18 @@ export DEMO_HOST_NAME=${DEMO_HOST_NAME:-} # Override with DEMO_NIC_IP. _default_nic_ip() { local addr - if command -v ip >/dev/null 2>&1; then + if [[ "$(uname -s)" == Darwin ]]; then + local iface + iface=$(route -n get default 2>/dev/null | awk '/interface:/ {print $2; exit}') + if [[ -n "$iface" ]]; then + addr=$(ipconfig getifaddr "$iface" 2>/dev/null) + fi + elif command -v ip >/dev/null 2>&1; then addr=$(ip -4 -o addr show 2>/dev/null \ | awk '$2 != "lo" {print $4; exit}' \ | cut -d/ -f1) fi - if [[ -z "${addr:-}" ]]; then + if [[ -z "${addr:-}" && "$(uname -s)" != Darwin ]]; then addr=$(hostname -I 2>/dev/null | awk '{print $1}') fi echo "${addr:-127.0.0.1}" @@ -237,10 +243,59 @@ require_cmd() { } } +# Native tools for Unix socket liveness and ownership checks. +_demo_require_socket_tools() { + if [[ "$(uname -s)" == Darwin ]]; then + require_cmd nc + require_cmd netstat + else + require_cmd ss iproute2 + fi +} + +# A stale socket pathname is not a listening daemon. +demo_uds_socket_listening() { + local path=$1 + if [[ "$(uname -s)" == Darwin ]]; then + nc -w 1 -U "$path" /dev/null 2>&1 + else + ss -xlpn 2>/dev/null | grep -F "LISTEN" | grep -qF "$path" + fi +} + +# Verify that the expected process owns the socket, not another daemon. +demo_uds_listener_owned_by_pid() { + local pid=$1 path=$2 + if [[ "$(uname -s)" == Darwin ]]; then + # Verbose Darwin netstat reports the owning PID and SO_ACCEPTCONN + # (0x0002). Accepted connections can have the same pathname. + netstat -anv -f unix 2>/dev/null | awk -v pid="$pid" -v path="$path" ' + $1 == "Address" { + for (i = 1; i <= NF; i++) { + if ($i == "process:pid") proc = i + if ($i == "options") opts = i + if ($i == "Addr") addr = i + } + next + } + proc && opts && addr && $2 == "stream" && + $proc ~ (":" pid "$") && $opts ~ /[2367abefABEF]$/ { + name = $0 + for (i = 1; i < addr; i++) + sub(/^[[:space:]]*[^[:space:]]+[[:space:]]*/, "", name) + if (name == path) found = 1 + } + END { exit !found } + ' + else + ss -xlpn 2>/dev/null | grep -F "$path" | grep -qE "pid=$pid[,)]" + fi +} + # Example pipelines prerequisites. require_nvnmosd() { - require_cmd ss iproute2 - if ! ss -xlpn 2>/dev/null | grep -F "LISTEN" | grep -qF "$DEMO_DAEMON_SOCK"; then + _demo_require_socket_tools + if ! demo_uds_socket_listening "$DEMO_DAEMON_SOCK"; then echo "[error] $DEMO_DAEMON_SOCK is not listening — start nvnmosd first" >&2 exit 1 fi @@ -249,7 +304,7 @@ require_nvnmosd() { # Interactive demo prerequisites. require_demo_tools() { # for daemon socket checks - require_cmd ss iproute2 + _demo_require_socket_tools # for making IS-04/IS-05/IS-08 HTTP requests require_cmd curl curl # for JSON body parsing/creation diff --git a/rust/gst-nmos-rs/scripts/gst-nmos-rs-demo.sh b/rust/gst-nmos-rs/scripts/gst-nmos-rs-demo.sh index 351b0f97..44f38cc0 100755 --- a/rust/gst-nmos-rs/scripts/gst-nmos-rs-demo.sh +++ b/rust/gst-nmos-rs/scripts/gst-nmos-rs-demo.sh @@ -81,6 +81,12 @@ # parented to the stopped script (the kernel can't reparent to # init until the parent actually exits). +# Associative arrays and namerefs below require Bash 4.3 or newer. +if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3) )); then + echo "[error] Bash 4.3 or newer is required (macOS: brew install bash, then run with Homebrew bash)" >&2 + exit 1 +fi + set -u # ---- Knobs --------------------------------------------------------- @@ -109,11 +115,22 @@ DEMO_MXL_DOMAIN_PATH=${DEMO_MXL_DOMAIN_PATH:-/dev/shm/gst-nmos-rs-demo} source "$_SCRIPT_DIR/env.sh" require_demo_tools -# `libnvnmos.so` (consumed by nvnmosd) and the gst-mxl-rs plugin + +# `libnvnmos` (consumed by nvnmosd) and the gst-mxl-rs plugin + # libmxl.so runtime (required for the MXL inner transport chain). LIB=${NVNMOS_LIB_DIR:-$REPO/build} MXL_PLUGIN_DIR=${MXL_PLUGIN_DIR:-$MXL_REPO/rust/target/debug} -MXL_RT_LIB_DIR=${MXL_RT_LIB_DIR:-$MXL_REPO/build/Linux-Clang-Debug/lib} +MXL_RT_LIB_DIR=${MXL_RT_LIB_DIR:-$MXL_REPO/build/$(uname -s)-Clang-Debug/lib} + +case "$(uname -s)" in + Darwin) + PLUGIN_LIBRARY=libgstnmos.dylib + export DYLD_LIBRARY_PATH="$LIB:$MXL_RT_LIB_DIR${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + ;; + *) + PLUGIN_LIBRARY=libgstnmos.so + export LD_LIBRARY_PATH="$LIB:$MXL_RT_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + ;; +esac SOCK=/tmp/gst-nmos-rs-demo.sock LOG_DIR=$(mktemp -d -t gst-nmos-rs-demo-XXXXXX) @@ -133,7 +150,8 @@ DAEMON_LOG="$LOG_DIR/daemon.log" # (a non-exec function wrapper leaves gst-launch as an orphan when cleanup # kills only the subshell pid). _demo_line_buffered() { - if command -v stdbuf >/dev/null 2>&1; then + # macOS system tools strip DYLD_LIBRARY_PATH before launching children. + if [[ "$(uname -s)" != Darwin ]] && command -v stdbuf >/dev/null 2>&1; then exec stdbuf -oL -eL "$@" else exec "$@" @@ -145,17 +163,16 @@ _demo_gst_launch() { _demo_line_buffered gst-launch-1.0 -e -m "$@" } -# Return 0 when ss reports a LISTEN socket on path (another nvnmosd up). -# No listener / missing path in ss → return 1. +# Return 0 when a daemon is listening on this Unix socket path. _uds_socket_listening() { local path=$1 - ss -xlpn 2>/dev/null | grep -F "LISTEN" | grep -qF "$path" + demo_uds_socket_listening "$path" } -# Return 0 when ss reports pid is listening on the socket path. +# Return 0 when pid owns the listening Unix socket. _uds_listener_owned_by_pid() { local pid=$1 path=$2 - ss -xlpn 2>/dev/null | grep -F "$path" | grep -qE "pid=$pid[,)]" + demo_uds_listener_owned_by_pid "$pid" "$path" } # Fail if more than one nvnmosd was started with this --uds path. @@ -164,13 +181,13 @@ _assert_single_nvnmosd_on_sock() { mapfile -t pids < <(pgrep -f "nvnmosd --uds $SOCK" 2>/dev/null || true) if (( ${#pids[@]} != 1 )); then echo "[error] expected exactly one nvnmosd on $SOCK, found ${#pids[@]}:" - pgrep -af "nvnmosd --uds $SOCK" 2>/dev/null || true + pgrep -fl "nvnmosd --uds $SOCK" 2>/dev/null || true echo " stop extras: pkill -9 -f 'nvnmosd --uds $SOCK'" exit 1 fi if (( pids[0] != DAEMON_PID )); then echo "[error] nvnmosd on $SOCK is pid ${pids[0]}, not demo daemon pid $DAEMON_PID" - pgrep -af "nvnmosd --uds $SOCK" 2>/dev/null || true + pgrep -fl "nvnmosd --uds $SOCK" 2>/dev/null || true exit 1 fi } @@ -416,7 +433,7 @@ TARGET_DIR=${CARGO_TARGET_DIR:-$REPO/rust/target} DAEMON_BIN="$TARGET_DIR/debug/nvnmosd" PLUGIN_DIR="$TARGET_DIR/debug" [[ -x "$DAEMON_BIN" ]] || { echo "missing $DAEMON_BIN"; exit 1; } -[[ -f "$PLUGIN_DIR/libgstnmos.so" ]] || { echo "missing $PLUGIN_DIR/libgstnmos.so"; exit 1; } +[[ -f "$PLUGIN_DIR/$PLUGIN_LIBRARY" ]] || { echo "missing $PLUGIN_DIR/$PLUGIN_LIBRARY"; exit 1; } # ---- Daemon -------------------------------------------------------- @@ -426,13 +443,12 @@ PLUGIN_DIR="$TARGET_DIR/debug" if _uds_socket_listening "$SOCK"; then echo "[error] UDS socket $SOCK is already in use (another nvnmosd listening?)." echo " Stop it, e.g.: pkill -9 -f 'nvnmosd --uds $SOCK'" - pgrep -af "nvnmosd --uds $SOCK" 2>/dev/null || true + pgrep -fl "nvnmosd --uds $SOCK" 2>/dev/null || true exit 1 fi echo "[daemon] starting nvnmosd on $SOCK" RUST_LOG=${RUST_LOG:-info} \ -LD_LIBRARY_PATH="$LIB:${LD_LIBRARY_PATH:-}" \ _demo_line_buffered "$DAEMON_BIN" --uds "$SOCK" > "$DAEMON_LOG" 2>&1 & declare -i DAEMON_PID=$! @@ -505,8 +521,12 @@ if ! _uds_socket_listening "$SOCK"; then fi if ! _uds_listener_owned_by_pid "$DAEMON_PID" "$SOCK"; then echo "[daemon] $SOCK is not owned by nvnmosd pid $DAEMON_PID" - ss -xlpn 2>/dev/null | grep -F 'gst-nmos-rs-demo.sock' || true - pgrep -af "nvnmosd --uds $SOCK" 2>/dev/null || true + if [[ "$(uname -s)" == Darwin ]]; then + netstat -anv -f unix 2>/dev/null | grep -F "$SOCK" || true + else + ss -xlpn 2>/dev/null | grep -F 'gst-nmos-rs-demo.sock' || true + fi + pgrep -fl "nvnmosd --uds $SOCK" 2>/dev/null || true cat "$DAEMON_LOG" exit 1 fi @@ -516,7 +536,6 @@ echo "[daemon] ready (pid $DAEMON_PID, listening on $SOCK)" # ---- Common gst env for nmossrc / nmossink + inner mxl* ---------- export GST_PLUGIN_PATH="$PLUGIN_DIR:$MXL_PLUGIN_DIR${GST_PLUGIN_PATH:+:$GST_PLUGIN_PATH}" -export LD_LIBRARY_PATH="$LIB:$MXL_RT_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" if ! command -v gst-inspect-1.0 >/dev/null 2>&1 \ || ! gst-inspect-1.0 nmosaudiochannelmap >/dev/null 2>&1; then diff --git a/rust/gst-nmos-rs/src/channel_mapping_session.rs b/rust/gst-nmos-rs/src/channel_mapping_session.rs index 7abf3e54..f94d593d 100644 --- a/rust/gst-nmos-rs/src/channel_mapping_session.rs +++ b/rust/gst-nmos-rs/src/channel_mapping_session.rs @@ -283,6 +283,12 @@ mod session_integration { use std::time::{Duration, Instant}; use test_skip::skip; + const LIBRARY_PATH_ENV: &str = if cfg!(target_os = "macos") { + "DYLD_LIBRARY_PATH" + } else { + "LD_LIBRARY_PATH" + }; + fn nvnmosd_bin() -> PathBuf { if let Ok(p) = std::env::var("NVNMOSD_BIN") { return PathBuf::from(p); @@ -306,7 +312,7 @@ mod session_integration { fn libnvnmos_dir() -> Option { let mut dirs: Vec = Vec::new(); - if let Ok(paths) = std::env::var("LD_LIBRARY_PATH") { + if let Ok(paths) = std::env::var(LIBRARY_PATH_ENV) { dirs.extend( paths .split(':') @@ -317,20 +323,25 @@ mod session_integration { if let Ok(dir) = std::env::var("NVNMOS_LIB_DIR") { dirs.push(PathBuf::from(dir)); } - dirs.into_iter().find(|d| d.join("libnvnmos.so").exists()) + let name = format!( + "{}nvnmos{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + ); + dirs.into_iter().find(|d| d.join(&name).exists()) } /// Mirror of `tests/common::nvnmosd_skip_reason` for this in-crate integration /// test, which needs `pub(crate)` access and so cannot live under `tests/`. - /// `nvnmosd` is built by `cargo test`, but it links `libnvnmos.so` from the C - /// build, which CI exposes on `LD_LIBRARY_PATH`; a Rust-only checkout skips. + /// `nvnmosd` is built by `cargo test`, but links native libnvnmos from the C + /// build via the platform loader path; a Rust-only checkout skips. fn skip_reason() -> Option { let bin = nvnmosd_bin(); if !bin.exists() { return Some(format!("nvnmosd not built at `{}`", bin.display())); } if libnvnmos_dir().is_none() { - return Some("libnvnmos.so not found via LD_LIBRARY_PATH or NVNMOS_LIB_DIR".into()); + return Some("libnvnmos not found via the native loader path or NVNMOS_LIB_DIR".into()); } None } @@ -360,16 +371,16 @@ mod session_integration { ) .stdout(Stdio::null()) .stderr(Stdio::inherit()); - // nvnmosd links libnvnmos.so; surface NVNMOS_LIB_DIR to the loader even - // when the caller set only it (not LD_LIBRARY_PATH). + // nvnmosd links native libnvnmos; surface NVNMOS_LIB_DIR to the loader even + // when the caller set only it (not the platform loader path). if let Ok(lib_dir) = std::env::var("NVNMOS_LIB_DIR") { - let existing = std::env::var("LD_LIBRARY_PATH").unwrap_or_default(); + let existing = std::env::var(LIBRARY_PATH_ENV).unwrap_or_default(); let value = if existing.is_empty() { lib_dir } else { format!("{lib_dir}:{existing}") }; - command.env("LD_LIBRARY_PATH", value); + command.env(LIBRARY_PATH_ENV, value); } let mut child = command .spawn() @@ -384,7 +395,7 @@ mod session_integration { let _ = child.kill(); let _ = child.wait(); panic!( - "nvnmosd UDS `{}` did not appear within 5s; check LD_LIBRARY_PATH includes libnvnmos", + "nvnmosd UDS `{}` did not appear within 5s; check the native loader path includes libnvnmos", socket.display(), ); } diff --git a/rust/gst-nmos-rs/tests/common/mod.rs b/rust/gst-nmos-rs/tests/common/mod.rs index b3f3c283..795a99cb 100644 --- a/rust/gst-nmos-rs/tests/common/mod.rs +++ b/rust/gst-nmos-rs/tests/common/mod.rs @@ -15,6 +15,12 @@ use std::time::{Duration, Instant}; use gstreamer as gst; +const LIBRARY_PATH_ENV: &str = if cfg!(target_os = "macos") { + "DYLD_LIBRARY_PATH" +} else { + "LD_LIBRARY_PATH" +}; + static REGISTER: Once = Once::new(); pub fn init() { @@ -47,10 +53,15 @@ fn ensure_gst_plugin_path() { .to_string_lossy() .into_owned() }); + let plugin_name = format!( + "{}gstnmos{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + ); for profile in ["debug", "release"] { let profile_dir = PathBuf::from(&target_dir).join(profile); - let deps_so = profile_dir.join("deps").join("libgstnmos.so"); - let profile_so = profile_dir.join("libgstnmos.so"); + let deps_so = profile_dir.join("deps").join(&plugin_name); + let profile_so = profile_dir.join(&plugin_name); let mut plugin_dirs = Vec::new(); if deps_so.exists() { plugin_dirs.push(profile_dir.join("deps")); @@ -87,7 +98,7 @@ pub fn require_factories(names: &[&str]) { assert!( missing.is_empty(), "missing element factories: {missing:?}; set `GST_PLUGIN_PATH` to include \ - `libgstnmos.so` (built in this workspace's `target/debug` or `target/release`)", + the native `nmos` plugin (built in this workspace's `target/debug` or `target/release`)", ); } @@ -114,11 +125,11 @@ pub fn nvnmosd_bin() -> PathBuf { PathBuf::from(target_dir).join("release").join("nvnmosd") } -/// Directory holding `libnvnmos.so`, searched on `LD_LIBRARY_PATH` then +/// Directory holding native libnvnmos, searched on the platform loader path then /// `NVNMOS_LIB_DIR`. `None` when the C library has not been built/placed. pub fn libnvnmos_dir() -> Option { let mut dirs: Vec = Vec::new(); - if let Ok(paths) = std::env::var("LD_LIBRARY_PATH") { + if let Ok(paths) = std::env::var(LIBRARY_PATH_ENV) { dirs.extend( paths .split(':') @@ -129,7 +140,12 @@ pub fn libnvnmos_dir() -> Option { if let Ok(dir) = std::env::var("NVNMOS_LIB_DIR") { dirs.push(PathBuf::from(dir)); } - dirs.into_iter().find(|d| d.join("libnvnmos.so").exists()) + let name = format!( + "{}nvnmos{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + ); + dirs.into_iter().find(|d| d.join(&name).exists()) } const SHM_MXL_HINT: &str = "MXL uses mkdtemp(3) under /dev/shm; run integration tests \ @@ -238,7 +254,7 @@ pub fn mxl_runtime_skip_reason() -> Option { /// Reason the `nvnmosd`-backed integration tests cannot run, or `None` when the /// prerequisites are present. `nvnmosd` itself is built by `cargo test`, but it -/// links `libnvnmos.so` from the C build, which CI exposes on `LD_LIBRARY_PATH`. +/// links native libnvnmos from the C build, exposed on the platform loader path. /// On a Rust-only checkout the caller skips rather than fails. pub fn nvnmosd_skip_reason() -> Option { let bin = nvnmosd_bin(); @@ -246,7 +262,7 @@ pub fn nvnmosd_skip_reason() -> Option { return Some(format!("nvnmosd not built at `{}`", bin.display())); } if libnvnmos_dir().is_none() { - return Some("libnvnmos.so not found via LD_LIBRARY_PATH or NVNMOS_LIB_DIR".into()); + return Some("libnvnmos not found via the native loader path or NVNMOS_LIB_DIR".into()); } None } @@ -278,16 +294,16 @@ impl DaemonGuard { ) .stdout(Stdio::null()) .stderr(Stdio::inherit()); - // nvnmosd links libnvnmos.so; surface NVNMOS_LIB_DIR to the loader even - // when the caller set only it (not LD_LIBRARY_PATH). + // nvnmosd links native libnvnmos; surface NVNMOS_LIB_DIR to the loader even + // when the caller set only it (not the platform loader path). if let Ok(lib_dir) = std::env::var("NVNMOS_LIB_DIR") { - let existing = std::env::var("LD_LIBRARY_PATH").unwrap_or_default(); + let existing = std::env::var(LIBRARY_PATH_ENV).unwrap_or_default(); let value = if existing.is_empty() { lib_dir } else { format!("{lib_dir}:{existing}") }; - command.env("LD_LIBRARY_PATH", value); + command.env(LIBRARY_PATH_ENV, value); } let mut child = command .spawn() @@ -303,7 +319,7 @@ impl DaemonGuard { let _ = child.kill(); let _ = child.wait(); panic!( - "nvnmosd UDS `{}` did not appear within 5s; check LD_LIBRARY_PATH includes libnvnmos", + "nvnmosd UDS `{}` did not appear within 5s; check the native loader path includes libnvnmos", socket.display(), ); } diff --git a/rust/nvnmosd-bench/scripts/run-nvnmosd-scale-smoke.sh b/rust/nvnmosd-bench/scripts/run-nvnmosd-scale-smoke.sh index ef5f5fe5..cac61a2e 100755 --- a/rust/nvnmosd-bench/scripts/run-nvnmosd-scale-smoke.sh +++ b/rust/nvnmosd-bench/scripts/run-nvnmosd-scale-smoke.sh @@ -55,12 +55,23 @@ PRESET_ROWS=("${SELECTED[@]}") mkdir -p "$RESULTS_DIR" LIB="${NVNMOS_LIB_DIR:-$REPO/build}" -if [[ ! -f "$LIB/libnvnmos.so" ]]; then - echo "missing $LIB/libnvnmos.so — build nvnmos in \$REPO/build first" >&2 +PLATFORM=$(uname -s) +if [[ "$PLATFORM" == Darwin ]]; then + LIBRARY_NAME=libnvnmos.dylib +else + LIBRARY_NAME=libnvnmos.so +fi +if [[ ! -f "$LIB/$LIBRARY_NAME" ]]; then + echo "missing $LIB/$LIBRARY_NAME — build nvnmos in \$REPO/build first" >&2 exit 1 fi export NVNMOS_LIB_DIR="$LIB" -export LD_LIBRARY_PATH="$LIB:${LD_LIBRARY_PATH:-}" +if [[ "$PLATFORM" == Darwin ]]; then + export DYLD_LIBRARY_PATH="$LIB${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + command -v lsof >/dev/null 2>&1 || { echo "lsof is required for port checks" >&2; exit 1; } +else + export LD_LIBRARY_PATH="$LIB:${LD_LIBRARY_PATH:-}" +fi # Fail fast if a prior nvnmosd (or other process) still owns the node HTTP port range. max_nodes=1 @@ -72,7 +83,13 @@ for row in "${PRESET_ROWS[@]}"; do done for ((i = 0; i < max_nodes; i++)); do port=$((BASE_HTTP_PORT + i)) - if command -v ss >/dev/null 2>&1; then + if [[ "$PLATFORM" == Darwin ]]; then + holder=$(lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null || true) + if [[ -n "$holder" ]]; then + echo "port $port already in use — free it before running (e.g. stale nvnmosd). $holder" >&2 + exit 1 + fi + elif command -v ss >/dev/null 2>&1; then if ss -tln "sport = :$port" 2>/dev/null | grep -q LISTEN; then holder=$(ss -tlnp "sport = :$port" 2>/dev/null | head -1) echo "port $port already in use — free it before running (e.g. stale nvnmosd). $holder" >&2 @@ -88,8 +105,8 @@ echo "[build] cargo build -p nvnmosd -p nvnmosd-bench (NVNMOS_LIB_DIR=$LIB)" NVNMOS_LIB_DIR="$LIB" cargo build --manifest-path "$RUST_MANIFEST" -p nvnmosd -p nvnmosd-bench rm -f "$SOCK" -echo "[daemon] $DAEMON_BIN --uds $SOCK (LD_LIBRARY_PATH includes $LIB)" -LD_LIBRARY_PATH="$LIB:${LD_LIBRARY_PATH:-}" NVNMOSD_UDS="$SOCK" "$DAEMON_BIN" --uds "$SOCK" & +echo "[daemon] $DAEMON_BIN --uds $SOCK (library path includes $LIB)" +NVNMOSD_UDS="$SOCK" "$DAEMON_BIN" --uds "$SOCK" & DAEMON_PID=$! cleanup() { kill -TERM "$DAEMON_PID" 2>/dev/null || true diff --git a/rust/nvnmosd/src/uds.rs b/rust/nvnmosd/src/uds.rs index 9746f2f2..a7a5a39a 100644 --- a/rust/nvnmosd/src/uds.rs +++ b/rust/nvnmosd/src/uds.rs @@ -95,7 +95,8 @@ mod tests { let dir = std::env::temp_dir().join(format!("nvnmosd-uds-stale-{}", std::process::id())); let _ = std::fs::create_dir_all(&dir); let sock = dir.join("nvnmosd.sock"); - std::fs::write(&sock, b"").expect("create stale socket file"); + let listener = StdUnixListener::bind(&sock).expect("bind test socket"); + drop(listener); prepare_listen_path(&sock).expect("stale socket should be removed"); diff --git a/rust/nvnmosd/tests/common/mod.rs b/rust/nvnmosd/tests/common/mod.rs index d6322551..43cf7c81 100644 --- a/rust/nvnmosd/tests/common/mod.rs +++ b/rust/nvnmosd/tests/common/mod.rs @@ -21,6 +21,12 @@ use tokio::net::{TcpStream as AsyncTcpStream, UnixStream}; use tonic::transport::{Channel, Endpoint, Uri}; use tower::service_fn; +const LIBRARY_PATH_ENV: &str = if cfg!(target_os = "macos") { + "DYLD_LIBRARY_PATH" +} else { + "LD_LIBRARY_PATH" +}; + const HTTP_BUDGET: Duration = Duration::from_secs(30); pub type PortRange = RangeInclusive; @@ -56,7 +62,7 @@ impl DaemonHarness { .env("NVNMOSD_HTTP_PORT_MIN", http_ports.start().to_string()) .env("NVNMOSD_HTTP_PORT_MAX", http_ports.end().to_string()) .env("RUST_LOG", "error") - .env("LD_LIBRARY_PATH", &ld_library_path) + .env(LIBRARY_PATH_ENV, &ld_library_path) .env_remove("NVNMOS_EXPERIMENTAL_SETTINGS") .stdout(Stdio::null()) .stderr(Stdio::piped()); @@ -145,8 +151,8 @@ fn find_libnvnmos_dir() -> String { } } panic!( - "could not find libnvnmos.so; build the C++ library (cmake --build build) \ - or set NVNMOS_LIB_DIR to the directory containing libnvnmos.so" + "could not find the native libnvnmos library; build the C++ library (cmake --build build) \ + or set NVNMOS_LIB_DIR to the directory containing libnvnmos" ); } @@ -157,7 +163,12 @@ fn absolutize_lib_dir(path: &Path) -> Option { std::env::current_dir().ok()?.join(path) }; let abs = abs.canonicalize().ok()?; - if abs.join("libnvnmos.so").is_file() { + let name = format!( + "{}nvnmos{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + ); + if abs.join(name).is_file() { Some(abs.to_string_lossy().into_owned()) } else { None @@ -165,7 +176,7 @@ fn absolutize_lib_dir(path: &Path) -> Option { } fn prepend_ld_library_path(dir: &str) -> String { - match std::env::var("LD_LIBRARY_PATH") { + match std::env::var(LIBRARY_PATH_ENV) { Ok(existing) if !existing.is_empty() => format!("{dir}:{existing}"), _ => dir.to_string(), } @@ -190,7 +201,7 @@ async fn wait_for_daemon(uds: &Path, child: &mut Child) { let stderr = child_stderr(child); panic!( "nvnmosd exited before binding UDS (status={status}); \ - ensure libnvnmos.so is in LD_LIBRARY_PATH (build the C++ \ + ensure libnvnmos is on the native loader path (build the C++ \ library under ../../build or set NVNMOS_LIB_DIR). stderr:\n{stderr}" ); } diff --git a/src/conan.lock b/src/conan.lock index aecdbf4e..1d46b75b 100644 --- a/src/conan.lock +++ b/src/conan.lock @@ -12,6 +12,7 @@ "mdnsresponder/878.200.35#07e02cfc2e12575c71fbd09a3dc80499%1686542085.689", "libselinux/3.6#5a78ff6ae5034eeaac8da723361a8ce4%1717655459.344", "libmount/2.39.2#64850679fa9d5ead878e5076b0cab2a8%1705591482.625", + "libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1774021608.288", "libffi/3.4.8#a045c00fb26779635e3bed40e80c5254%1753360042.396", "libevent/2.1.12#7a7a21762e2b4ed13d29aee3e10c19e7%1751271631.406", "libdaemon/0.14#0eb527aaa9c1c48668779756cf47be6c%1667512637.933",