From fa27c200bddb45a5e55c5ae8559b4f46712a7517 Mon Sep 17 00:00:00 2001 From: Richard Kiene Date: Tue, 1 Sep 2026 09:15:24 -0700 Subject: [PATCH 1/3] build: drop the CUDA toolkit dependency nvfd only needs NVML, and NVML ships with every NVIDIA driver as libnvidia-ml.so.1. The only reason the CUDA toolkit was required was nvml.h, and installing the toolkit through a distribution package (nvidia-cuda-toolkit on Debian/Ubuntu) can replace or pin the driver on the host. On Ubuntu 22.04 the packaged header is also CUDA 11.5, which predates nvmlDeviceSetFanControlPolicy, so the #ifdef in fan.c silently compiled the auto-restore path out. - Add include/nvml_api.h declaring the 17 NVML entry points nvfd calls, with the enum values and struct layouts from nvml.h. Signatures are part of the versioned ABI (_v2 suffixes), so this is stable. - Link by SONAME (-l:libnvidia-ml.so.1) so no libnvidia-ml.so dev symlink is needed either. Compile with -Werror=implicit-function-declaration so an undeclared NVML call is a build error, not a silent implicit int. - nvmlDeviceSetFanControlPolicy is always compiled in and its result checked. Minimum driver is R520: the first branch exporting every symbol (Set*FanSpeed_v2 are R515; SetFanControlPolicy is 520.61.05, backported to 515.105.01). - install.sh: stop installing nvidia-cuda-toolkit. Preflight libnvidia-ml.so.1 via ldconfig before installing anything, build, probe NVML with the fresh build/nvfd list (which also runs the v1.x migration), and only then stop the old service, install and start, so an NVML failure leaves the host untouched instead of half-upgraded. The link directory comes from the same ldconfig line, so the linker searches where ld.so actually resolves the library. - CI: build without the toolkit, and link against a stub libnvidia-ml.so.1 built from tests/nvml_stub.c, which defines every declared entry point with the same signature. This exercises the -l: link line and the symbol set, and fails to compile if nvml_api.h drifts. --- .github/workflows/ci.yml | 14 +++++- Makefile | 15 +++--- README.md | 4 +- README.zh-TW.md | 4 +- include/nvfd.h | 2 +- include/nvml_api.h | 98 ++++++++++++++++++++++++++++++++++++++++ scripts/install.sh | 42 +++++++++++------ src/fan.c | 13 ++++-- tests/nvml_stub.c | 32 +++++++++++++ 9 files changed, 194 insertions(+), 30 deletions(-) create mode 100644 include/nvml_api.h create mode 100644 tests/nvml_stub.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98c4e40..0e28816 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,23 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y build-essential libjansson-dev libncursesw5-dev nvidia-cuda-toolkit + sudo apt-get install -y build-essential libjansson-dev libncursesw5-dev - name: Compile check run: make check + # The runner has no NVIDIA driver. Link against a stub libnvidia-ml.so.1 + # that defines exactly the symbols include/nvml_api.h declares, so the + # -l:libnvidia-ml.so.1 link line and the declared symbol set are both + # exercised, not just compilation. + - name: Link against stub NVML + run: | + mkdir -p build/stub + cc -shared -fPIC -Wall -Wextra -Werror -Wl,-soname,libnvidia-ml.so.1 \ + -Iinclude -o build/stub/libnvidia-ml.so.1 tests/nvml_stub.c + make LDFLAGS=-Lbuild/stub + test -x build/nvfd + shellcheck: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 375945d..c2da3dc 100644 --- a/Makefile +++ b/Makefile @@ -7,12 +7,15 @@ BINDIR = $(PREFIX)/bin CONFDIR = /etc/nvfd UNITDIR = /etc/systemd/system -# NVIDIA CUDA paths (try standard locations) -CUDA_PATH ?= $(shell [ -d /usr/local/cuda ] && echo /usr/local/cuda || echo /usr) -CFLAGS += -I$(CUDA_PATH)/include -Iinclude -LDFLAGS += -L$(CUDA_PATH)/lib64 - -LIBS = -lnvidia-ml -ljansson -lncursesw +# NVML declarations are carried in include/nvml_api.h, so no CUDA toolkit is +# needed to build. Any NVML call that is not declared there is a hard error +# rather than an implicit declaration. +CFLAGS += -Iinclude -Werror=implicit-function-declaration + +# Link the driver's NVML by SONAME: libnvidia-ml.so.1 ships with every +# driver, whereas the unversioned libnvidia-ml.so symlink only comes with +# -dev packages or the CUDA toolkit. +LIBS = -l:libnvidia-ml.so.1 -ljansson -lncursesw SRCDIR = src BUILDDIR = build diff --git a/README.md b/README.md index 0f9eae7..301633c 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Recommendations: - Linux operating system - `libjansson-dev` — JSON library - `libncursesw5-dev` — ncurses wide-character support -- NVML headers (included with CUDA toolkit or `nvidia-cuda-toolkit` package) +- NVIDIA driver R520 or newer — NVFD links against the driver's `libnvidia-ml.so.1`; no CUDA toolkit or NVML headers are needed ## Installation @@ -329,7 +329,7 @@ The utility script requires: - `nvidia-smi` (included with NVIDIA drivers) - `nvfd` binary (installed via this package) -No CUDA toolkit required for runtime. +No CUDA toolkit is required, either to build NVFD or at runtime. ## Migration from v1.x diff --git a/README.zh-TW.md b/README.zh-TW.md index 0a803b0..3bc1a65 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -57,7 +57,7 @@ NVFD 是一款開源的 Linux NVIDIA GPU 風扇控制守護程式。透過 NVML - Linux 作業系統 - `libjansson-dev` — JSON 函式庫 - `libncursesw5-dev` — ncurses 寬字元支援 -- NVML 標頭檔(包含在 CUDA toolkit 或 `nvidia-cuda-toolkit` 套件中) +- NVIDIA 驅動程式 R520 或更新版本 — NVFD 直接連結驅動程式附帶的 `libnvidia-ml.so.1`,不需要 CUDA toolkit 或 NVML 標頭檔 ## 安裝 @@ -329,7 +329,7 @@ ExecStart=/usr/local/bin/nvfd-fan-control.sh --threshold-up 50 --threshold-down - `nvidia-smi`(包含在 NVIDIA 驅動中) - `nvfd` 二進位檔案(通過此套件安裝) -運行時不需要 CUDA toolkit。 +建置與運行皆不需要 CUDA toolkit。 ## 從 v1.x 遷移 diff --git a/include/nvfd.h b/include/nvfd.h index c7bab2a..ec81090 100644 --- a/include/nvfd.h +++ b/include/nvfd.h @@ -1,7 +1,7 @@ #ifndef NVFD_H #define NVFD_H -#include +#include "nvml_api.h" #include #define NVFD_VERSION "1.1" diff --git a/include/nvml_api.h b/include/nvml_api.h new file mode 100644 index 0000000..8545915 --- /dev/null +++ b/include/nvml_api.h @@ -0,0 +1,98 @@ +/* + * Declarations for the NVML entry points nvfd uses. + * + * nvfd deliberately does not include NVIDIA's nvml.h. That header ships only + * with the CUDA toolkit, and installing the toolkit through a distribution + * package can replace or pin the NVIDIA driver on the host. The NVML ABI is + * stable and versioned by symbol name (the _v2 suffixes below), and + * libnvidia-ml.so.1 ships with every driver, so declaring the handful of + * functions we call is sufficient. Signatures and enum values match nvml.h + * from CUDA 12.x; they are part of the ABI and do not change. + * + * Minimum driver: R520, the first branch whose libnvidia-ml.so.1 exports + * every symbol below. nvmlDeviceSetFanSpeed_v2 / SetDefaultFanSpeed_v2 + * appeared in R515; nvmlDeviceSetFanControlPolicy in 520.61.05 (backported + * to 515.105.01). Per `nm -D` on the NVIDIA rhel8 repo builds of 515.43.04, + * 515.105.01 and 520.61.05. + */ +#ifndef NVFD_NVML_API_H +#define NVFD_NVML_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct nvmlDevice_st *nvmlDevice_t; + +/* Only NVML_SUCCESS is compared against; every other code is passed straight + * to nvmlErrorString(). NVML_ERROR_UNKNOWN is listed so the enum has the full + * value range nvml.h gives it. */ +typedef enum nvmlReturn_enum { + NVML_SUCCESS = 0, + NVML_ERROR_UNKNOWN = 999 +} nvmlReturn_t; + +typedef enum nvmlEnableState_enum { + NVML_FEATURE_DISABLED = 0, + NVML_FEATURE_ENABLED = 1 +} nvmlEnableState_t; + +typedef enum nvmlTemperatureSensors_enum { + NVML_TEMPERATURE_GPU = 0 +} nvmlTemperatureSensors_t; + +typedef enum nvmlFanControlPolicy_enum { + NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW = 0, /* sic — NVIDIA's spelling */ + NVML_FAN_POLICY_MANUAL = 1 +} nvmlFanControlPolicy_t; + +typedef struct nvmlUtilization_st { + unsigned int gpu; + unsigned int memory; +} nvmlUtilization_t; + +/* v1 layout, as consumed by nvmlDeviceGetMemoryInfo (not the _v2 variant). */ +typedef struct nvmlMemory_st { + unsigned long long total; + unsigned long long free; + unsigned long long used; +} nvmlMemory_t; + +#define NVML_DEVICE_NAME_BUFFER_SIZE 64 + +nvmlReturn_t nvmlInit_v2(void); +nvmlReturn_t nvmlShutdown(void); +const char *nvmlErrorString(nvmlReturn_t result); + +nvmlReturn_t nvmlDeviceGetCount_v2(unsigned int *deviceCount); +nvmlReturn_t nvmlDeviceGetHandleByIndex_v2(unsigned int index, nvmlDevice_t *device); +nvmlReturn_t nvmlDeviceGetName(nvmlDevice_t device, char *name, unsigned int length); +nvmlReturn_t nvmlDeviceGetTemperature(nvmlDevice_t device, + nvmlTemperatureSensors_t sensorType, + unsigned int *temp); +nvmlReturn_t nvmlDeviceGetUtilizationRates(nvmlDevice_t device, + nvmlUtilization_t *utilization); +nvmlReturn_t nvmlDeviceGetMemoryInfo(nvmlDevice_t device, nvmlMemory_t *memory); +nvmlReturn_t nvmlDeviceGetPowerUsage(nvmlDevice_t device, unsigned int *power); +nvmlReturn_t nvmlDeviceGetEnforcedPowerLimit(nvmlDevice_t device, unsigned int *limit); +nvmlReturn_t nvmlDeviceSetPersistenceMode(nvmlDevice_t device, nvmlEnableState_t mode); + +nvmlReturn_t nvmlDeviceGetNumFans(nvmlDevice_t device, unsigned int *numFans); +nvmlReturn_t nvmlDeviceGetFanSpeed_v2(nvmlDevice_t device, unsigned int fan, + unsigned int *speed); +nvmlReturn_t nvmlDeviceSetFanSpeed_v2(nvmlDevice_t device, unsigned int fan, + unsigned int speed); +nvmlReturn_t nvmlDeviceSetDefaultFanSpeed_v2(nvmlDevice_t device, unsigned int fan); +nvmlReturn_t nvmlDeviceSetFanControlPolicy(nvmlDevice_t device, unsigned int fan, + nvmlFanControlPolicy_t policy); + +/* nvml.h maps the unversioned names onto the current ABI the same way. */ +#define nvmlInit nvmlInit_v2 +#define nvmlDeviceGetCount nvmlDeviceGetCount_v2 +#define nvmlDeviceGetHandleByIndex nvmlDeviceGetHandleByIndex_v2 + +#ifdef __cplusplus +} +#endif + +#endif /* NVFD_NVML_API_H */ diff --git a/scripts/install.sh b/scripts/install.sh index 10ac05e..c9bf336 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -54,21 +54,48 @@ else fi echo "Detected OS: $OS" + +# NVML comes from the NVIDIA driver, never from a CUDA toolkit package. Find +# the driver library before touching anything, and link against the directory +# the dynamic linker actually resolves it from. +NVML_LIB=$(ldconfig -p | awk '/libnvidia-ml\.so\.1 \(libc6/{print $NF; exit}') +if [ -z "$NVML_LIB" ]; then + echo "ERROR: libnvidia-ml.so.1 not found in the dynamic linker cache." >&2 + echo " Install the NVIDIA driver (R520 or newer) before installing NVFD." >&2 + exit 1 +fi +NVML_LIBDIR=$(dirname "$NVML_LIB") +echo "Using NVML from $NVML_LIB" + echo "Installing dependencies..." case "$OS" in "Ubuntu"|"Ubuntu "*|"Debian GNU/Linux"|"Debian") apt-get update - apt-get install -y build-essential libjansson-dev libncursesw5-dev nvidia-cuda-toolkit + apt-get install -y build-essential libjansson-dev libncursesw5-dev ;; "Rocky Linux"|"CentOS Linux"|"Red Hat Enterprise Linux"|"Fedora"|"Fedora Linux") dnf install -y gcc make jansson-devel ncurses-devel ;; *) - echo "Warning: Unsupported OS ($OS). Ensure gcc, make, libjansson-dev, libncurses-dev, and NVML headers are installed." + echo "Warning: Unsupported OS ($OS). Ensure gcc, make, libjansson-dev and libncurses-dev are installed." ;; esac +# Determine script directory (where the repo is) +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +echo "Building NVFD..." +cd "$SCRIPT_DIR" +make clean && make LDFLAGS="-L$NVML_LIBDIR" + +# Prove the fresh binary can initialise NVML on this host before anything on +# the system is changed. This also runs the v1.x config migration. If NVML +# cannot initialise here the daemon would not work either, and the running +# service (if any) is left untouched. +echo "Checking NVML and config migration..." +"$SCRIPT_DIR/build/nvfd" list + # Stop old service if running if systemctl is-active --quiet infinirc-gpu-fan-control.service 2>/dev/null; then echo "Stopping old v1 service..." @@ -91,20 +118,9 @@ if systemctl is-active --quiet nvfd.service 2>/dev/null; then systemctl stop nvfd.service fi -# Determine script directory (where the repo is) -SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" - -echo "Building NVFD..." -cd "$SCRIPT_DIR" -make clean && make - echo "Installing..." make install -# Run config migration -echo "Checking for config migration..." -/usr/local/bin/nvfd list >/dev/null 2>&1 || true - # Remove old alias if present if grep -q 'alias igfc=' /etc/bash.bashrc 2>/dev/null; then sed -i '/alias igfc=/d' /etc/bash.bashrc diff --git a/src/fan.c b/src/fan.c index c84cb6c..0c08069 100644 --- a/src/fan.c +++ b/src/fan.c @@ -79,13 +79,16 @@ int fan_reset_to_auto(unsigned int gpu_index) { } } - /* Restore automatic fan policy if API is available */ -#ifdef NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW + /* Also restore the driver's temperature-driven policy explicitly. */ for (int i = 0; i < num_fans; i++) { - nvmlDeviceSetFanControlPolicy(device, (unsigned int)i, - NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW); + nvmlReturn_t r = nvmlDeviceSetFanControlPolicy(device, (unsigned int)i, + NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW); + if (r != NVML_SUCCESS) { + fprintf(stderr, "Failed to restore auto fan policy for fan %d on GPU %u: %s\n", + i, gpu_index, nvmlErrorString(r)); + failures++; + } } -#endif return failures; } diff --git a/tests/nvml_stub.c b/tests/nvml_stub.c new file mode 100644 index 0000000..3dbd8a1 --- /dev/null +++ b/tests/nvml_stub.c @@ -0,0 +1,32 @@ +/* + * Stub libnvidia-ml.so.1 for CI. Every entry point nvfd declares in + * include/nvml_api.h is defined here with that exact signature, so this file + * does two things: it lets `make` link on a runner with no driver, and it + * fails to compile if nvml_api.h drifts from the definitions below. + * + * Build: cc -shared -fPIC -Wl,-soname,libnvidia-ml.so.1 -Iinclude \ + * -o /libnvidia-ml.so.1 tests/nvml_stub.c + * Then: make LDFLAGS=-L + */ +#include +#include "nvml_api.h" + +nvmlReturn_t nvmlInit_v2(void) { return NVML_SUCCESS; } +nvmlReturn_t nvmlShutdown(void) { return NVML_SUCCESS; } +const char *nvmlErrorString(nvmlReturn_t result) { (void)result; return "stub"; } + +nvmlReturn_t nvmlDeviceGetCount_v2(unsigned int *deviceCount) { *deviceCount = 0; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceGetHandleByIndex_v2(unsigned int index, nvmlDevice_t *device) { (void)index; *device = NULL; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceGetName(nvmlDevice_t device, char *name, unsigned int length) { (void)device; if (length) name[0] = '\0'; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceGetTemperature(nvmlDevice_t device, nvmlTemperatureSensors_t sensorType, unsigned int *temp) { (void)device; (void)sensorType; *temp = 0; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceGetUtilizationRates(nvmlDevice_t device, nvmlUtilization_t *utilization) { (void)device; utilization->gpu = utilization->memory = 0; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceGetMemoryInfo(nvmlDevice_t device, nvmlMemory_t *memory) { (void)device; memory->total = memory->free = memory->used = 0; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceGetPowerUsage(nvmlDevice_t device, unsigned int *power) { (void)device; *power = 0; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceGetEnforcedPowerLimit(nvmlDevice_t device, unsigned int *limit) { (void)device; *limit = 0; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceSetPersistenceMode(nvmlDevice_t device, nvmlEnableState_t mode) { (void)device; (void)mode; return NVML_SUCCESS; } + +nvmlReturn_t nvmlDeviceGetNumFans(nvmlDevice_t device, unsigned int *numFans) { (void)device; *numFans = 0; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceGetFanSpeed_v2(nvmlDevice_t device, unsigned int fan, unsigned int *speed) { (void)device; (void)fan; *speed = 0; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceSetFanSpeed_v2(nvmlDevice_t device, unsigned int fan, unsigned int speed) { (void)device; (void)fan; (void)speed; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceSetDefaultFanSpeed_v2(nvmlDevice_t device, unsigned int fan) { (void)device; (void)fan; return NVML_SUCCESS; } +nvmlReturn_t nvmlDeviceSetFanControlPolicy(nvmlDevice_t device, unsigned int fan, nvmlFanControlPolicy_t policy) { (void)device; (void)fan; (void)policy; return NVML_SUCCESS; } From 0e15c02918c1804fcc2157ae9436ab450f31c5cd Mon Sep 17 00:00:00 2001 From: Richard Kiene Date: Fri, 4 Sep 2026 14:20:06 +0000 Subject: [PATCH 2/3] build: discover the NVML library path --- .github/workflows/ci.yml | 2 +- Makefile | 17 +++++++++++++---- include/nvml_api.h | 8 ++++---- scripts/install.sh | 5 ++--- tests/test_makefile_nvml_detection.sh | 19 +++++++++++++++++++ tests/test_nvml_api_declarations.sh | 8 ++++++++ 6 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 tests/test_makefile_nvml_detection.sh create mode 100644 tests/test_nvml_api_declarations.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e28816..7dcece3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: mkdir -p build/stub cc -shared -fPIC -Wall -Wextra -Werror -Wl,-soname,libnvidia-ml.so.1 \ -Iinclude -o build/stub/libnvidia-ml.so.1 tests/nvml_stub.c - make LDFLAGS=-Lbuild/stub + make NVML_LIBDIR=build/stub test -x build/nvfd shellcheck: diff --git a/Makefile b/Makefile index c2da3dc..ccd9032 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,13 @@ CFLAGS += -Iinclude -Werror=implicit-function-declaration # Link the driver's NVML by SONAME: libnvidia-ml.so.1 ships with every # driver, whereas the unversioned libnvidia-ml.so symlink only comes with -# -dev packages or the CUDA toolkit. -LIBS = -l:libnvidia-ml.so.1 -ljansson -lncursesw +# -dev packages or the CUDA toolkit. Some distributions keep it outside +# ld's default search path, so use the directory registered with ldconfig. +LDCONFIG ?= /sbin/ldconfig +NVML_CACHE_PATTERN = libnvidia-ml\.so\.1 \(libc6, +NVML_LIBDIR ?= $(shell $(LDCONFIG) -p 2>/dev/null | awk '/$(NVML_CACHE_PATTERN)/{print $$NF; exit}' | xargs -r dirname) +LDFLAGS += $(if $(NVML_LIBDIR),-L$(NVML_LIBDIR)) +LIBS = -l:libnvidia-ml.so.1 -ljansson -lncursesw SRCDIR = src BUILDDIR = build @@ -24,7 +29,7 @@ SRCS = $(wildcard $(SRCDIR)/*.c) OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS)) TARGET = $(BUILDDIR)/nvfd -.PHONY: all clean check install uninstall install-utils uninstall-utils +.PHONY: all clean check test install uninstall install-utils uninstall-utils all: $(TARGET) @@ -37,9 +42,13 @@ $(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR) $(BUILDDIR): mkdir -p $(BUILDDIR) -check: $(OBJS) +check: $(OBJS) test @echo "All source files compiled successfully." +test: + sh tests/test_makefile_nvml_detection.sh + sh tests/test_nvml_api_declarations.sh + clean: rm -rf $(BUILDDIR) diff --git a/include/nvml_api.h b/include/nvml_api.h index 8545915..1a66215 100644 --- a/include/nvml_api.h +++ b/include/nvml_api.h @@ -41,10 +41,10 @@ typedef enum nvmlTemperatureSensors_enum { NVML_TEMPERATURE_GPU = 0 } nvmlTemperatureSensors_t; -typedef enum nvmlFanControlPolicy_enum { - NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW = 0, /* sic — NVIDIA's spelling */ - NVML_FAN_POLICY_MANUAL = 1 -} nvmlFanControlPolicy_t; +typedef unsigned int nvmlFanControlPolicy_t; +/* sic — NVIDIA's spelling */ +#define NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW 0 +#define NVML_FAN_POLICY_MANUAL 1 typedef struct nvmlUtilization_st { unsigned int gpu; diff --git a/scripts/install.sh b/scripts/install.sh index c9bf336..29a4caa 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -58,13 +58,12 @@ echo "Detected OS: $OS" # NVML comes from the NVIDIA driver, never from a CUDA toolkit package. Find # the driver library before touching anything, and link against the directory # the dynamic linker actually resolves it from. -NVML_LIB=$(ldconfig -p | awk '/libnvidia-ml\.so\.1 \(libc6/{print $NF; exit}') +NVML_LIB=$(ldconfig -p | awk '/libnvidia-ml\.so\.1 \(libc6,/{print $NF; exit}') if [ -z "$NVML_LIB" ]; then echo "ERROR: libnvidia-ml.so.1 not found in the dynamic linker cache." >&2 echo " Install the NVIDIA driver (R520 or newer) before installing NVFD." >&2 exit 1 fi -NVML_LIBDIR=$(dirname "$NVML_LIB") echo "Using NVML from $NVML_LIB" echo "Installing dependencies..." @@ -87,7 +86,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" echo "Building NVFD..." cd "$SCRIPT_DIR" -make clean && make LDFLAGS="-L$NVML_LIBDIR" +make clean && make # Prove the fresh binary can initialise NVML on this host before anything on # the system is changed. This also runs the v1.x config migration. If NVML diff --git a/tests/test_makefile_nvml_detection.sh b/tests/test_makefile_nvml_detection.sh new file mode 100644 index 0000000..a76f962 --- /dev/null +++ b/tests/test_makefile_nvml_detection.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -eu + +repo_dir=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +cat >"$tmp_dir/ldconfig" <<'EOF' +#!/bin/sh +cat <<'CACHE' + libnvidia-ml.so.1 (libc6,x86-64) => /test/nvidia/libnvidia-ml.so.1 +CACHE +EOF +chmod +x "$tmp_dir/ldconfig" + +ldflags=$(make -C "$repo_dir" --no-print-directory -s \ + LDCONFIG="$tmp_dir/ldconfig" \ + --eval='print-nvml-ldflags:;@echo $(LDFLAGS)' print-nvml-ldflags) +[ "$ldflags" = "-L/test/nvidia" ] diff --git a/tests/test_nvml_api_declarations.sh b/tests/test_nvml_api_declarations.sh new file mode 100644 index 0000000..caee64f --- /dev/null +++ b/tests/test_nvml_api_declarations.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -eu + +header=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)/include/nvml_api.h + +grep -q '^typedef unsigned int nvmlFanControlPolicy_t;' "$header" +grep -q '^#define NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW 0$' "$header" +grep -q '^#define NVML_FAN_POLICY_MANUAL 1$' "$header" From 0e734e2449507c0845ccf84409258dbe51cc48bf Mon Sep 17 00:00:00 2001 From: Richard Kiene Date: Fri, 4 Sep 2026 15:42:11 +0000 Subject: [PATCH 3/3] build: share NVML discovery with installer --- .github/workflows/ci.yml | 2 +- Makefile | 1 + scripts/find-nvml.sh | 12 ++++++++++++ scripts/install.sh | 22 ++++++++++++---------- tests/test_find_nvml.sh | 20 ++++++++++++++++++++ tests/test_makefile_nvml_detection.sh | 10 +++++++++- tests/test_nvml_api_declarations.sh | 2 +- 7 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 scripts/find-nvml.sh create mode 100644 tests/test_find_nvml.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dcece3..7cf1142 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,4 +38,4 @@ jobs: - uses: actions/checkout@v4 - name: Run ShellCheck - run: find scripts -name '*.sh' -exec shellcheck {} + + run: find scripts tests -name '*.sh' -exec shellcheck {} + diff --git a/Makefile b/Makefile index ccd9032..2bdaa50 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ check: $(OBJS) test test: sh tests/test_makefile_nvml_detection.sh sh tests/test_nvml_api_declarations.sh + sh tests/test_find_nvml.sh clean: rm -rf $(BUILDDIR) diff --git a/scripts/find-nvml.sh b/scripts/find-nvml.sh new file mode 100644 index 0000000..2db8676 --- /dev/null +++ b/scripts/find-nvml.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +find_nvml_libdir() { + if [ -n "${NVML_LIBDIR:-}" ]; then + printf '%s\n' "$NVML_LIBDIR" + return + fi + + "${LDCONFIG:-/sbin/ldconfig}" -p 2>/dev/null | + awk '/libnvidia-ml\.so\.1 \(libc6,/{print $NF; exit}' | + xargs -r dirname +} diff --git a/scripts/install.sh b/scripts/install.sh index 29a4caa..a2981f1 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -55,16 +55,21 @@ fi echo "Detected OS: $OS" +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/scripts/find-nvml.sh" + # NVML comes from the NVIDIA driver, never from a CUDA toolkit package. Find -# the driver library before touching anything, and link against the directory -# the dynamic linker actually resolves it from. -NVML_LIB=$(ldconfig -p | awk '/libnvidia-ml\.so\.1 \(libc6,/{print $NF; exit}') -if [ -z "$NVML_LIB" ]; then +# the driver library before touching anything, and pass that same directory to +# Make. NVML_LIBDIR remains an escape hatch for nonstandard driver layouts. +LDCONFIG=${LDCONFIG:-/sbin/ldconfig} +NVML_LIBDIR=$(find_nvml_libdir) +if [ -z "$NVML_LIBDIR" ]; then echo "ERROR: libnvidia-ml.so.1 not found in the dynamic linker cache." >&2 - echo " Install the NVIDIA driver (R520 or newer) before installing NVFD." >&2 + echo " Install the NVIDIA driver (R520 or newer), or set NVML_LIBDIR." >&2 exit 1 fi -echo "Using NVML from $NVML_LIB" +echo "Using NVML from $NVML_LIBDIR/libnvidia-ml.so.1" echo "Installing dependencies..." @@ -81,12 +86,9 @@ case "$OS" in ;; esac -# Determine script directory (where the repo is) -SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" - echo "Building NVFD..." cd "$SCRIPT_DIR" -make clean && make +make clean && make NVML_LIBDIR="$NVML_LIBDIR" LDCONFIG="$LDCONFIG" # Prove the fresh binary can initialise NVML on this host before anything on # the system is changed. This also runs the v1.x config migration. If NVML diff --git a/tests/test_find_nvml.sh b/tests/test_find_nvml.sh new file mode 100644 index 0000000..804c6d8 --- /dev/null +++ b/tests/test_find_nvml.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu + +repo_dir=$(cd -- "$(dirname "$0")/.." && pwd) +# shellcheck disable=SC1091 +. "$repo_dir/scripts/find-nvml.sh" + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +cat >"$tmp_dir/ldconfig" <<'EOF' +#!/bin/sh +cat <<'CACHE' + libnvidia-ml.so.1 (libc6,x86-64) => /driver/lib/libnvidia-ml.so.1 +CACHE +EOF +chmod +x "$tmp_dir/ldconfig" + +[ "$(NVML_LIBDIR=/vendor/lib find_nvml_libdir)" = "/vendor/lib" ] +[ "$(LDCONFIG="$tmp_dir/ldconfig" NVML_LIBDIR='' find_nvml_libdir)" = "/driver/lib" ] diff --git a/tests/test_makefile_nvml_detection.sh b/tests/test_makefile_nvml_detection.sh index a76f962..7f39154 100644 --- a/tests/test_makefile_nvml_detection.sh +++ b/tests/test_makefile_nvml_detection.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -repo_dir=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +repo_dir=$(cd -- "$(dirname "$0")/.." && pwd) tmp_dir=$(mktemp -d) trap 'rm -rf "$tmp_dir"' EXIT @@ -13,7 +13,15 @@ CACHE EOF chmod +x "$tmp_dir/ldconfig" +# The Make expression must reach make literally. +# shellcheck disable=SC2016 ldflags=$(make -C "$repo_dir" --no-print-directory -s \ LDCONFIG="$tmp_dir/ldconfig" \ --eval='print-nvml-ldflags:;@echo $(LDFLAGS)' print-nvml-ldflags) [ "$ldflags" = "-L/test/nvidia" ] + +# shellcheck disable=SC2016 +override_flags=$(make -C "$repo_dir" --no-print-directory -s \ + LDCONFIG=/does/not/exist NVML_LIBDIR=/vendor/lib \ + --eval='print-nvml-override:;@echo $(LDFLAGS)' print-nvml-override) +[ "$override_flags" = "-L/vendor/lib" ] diff --git a/tests/test_nvml_api_declarations.sh b/tests/test_nvml_api_declarations.sh index caee64f..11da845 100644 --- a/tests/test_nvml_api_declarations.sh +++ b/tests/test_nvml_api_declarations.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -header=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)/include/nvml_api.h +header=$(cd -- "$(dirname "$0")/.." && pwd)/include/nvml_api.h grep -q '^typedef unsigned int nvmlFanControlPolicy_t;' "$header" grep -q '^#define NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW 0$' "$header"