Skip to content

Commit 4ba27e8

Browse files
author
Martin Belanger
committed
libnvme: add check-public-headers test
Add tools/check-public-headers.py, a companion to the existing check-public-symbols.py, that verifies every symbol exported in a version script has a prototype declared in one of the installed headers. The check reads all *.ld files, extracts their global: symbols, then scans each installed header for a matching identifier followed by '('. Conditional headers (MI, fabrics) are silently skipped when absent, matching the same build conditionality as install_headers(). Register the script as a meson test alongside check-public-symbols so it runs automatically under 'meson test'. Signed-off-by: Martin Belanger <[email protected]> Assisted-by: Claude Sonnet 4.6 <[email protected]>
1 parent cd55dd7 commit 4ba27e8

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

libnvme/test/meson.build

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ if python3_prog.found()
1515
meson.project_source_root() / 'libnvme',
1616
],
1717
)
18+
test(
19+
'libnvme - check-public-headers',
20+
python3_prog,
21+
args: [
22+
files('../tools/check-public-headers.py'),
23+
meson.project_source_root() / 'libnvme',
24+
],
25+
)
1826
endif
1927

2028
# These tests all require interaction with a real NVMe device, so we don't
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: LGPL-2.1-or-later
3+
#
4+
# This file is part of libnvme.
5+
# Copyright (c) 2025, Dell Technologies Inc. or its subsidiaries.
6+
#
7+
# Authors: Martin Belanger <[email protected]>
8+
#
9+
# Verify that every symbol exported in a version script has a prototype
10+
# declared in one of the installed header files.
11+
#
12+
# A __public function that appears in a .ld version script but not in any
13+
# installed header is technically callable by external code, but callers have
14+
# no declaration to include — they would need to write their own prototype or
15+
# use dlsym(), which defeats the purpose of a stable public API.
16+
#
17+
# Usage (standalone):
18+
# python3 tools/check-public-headers.py [LIBNVME-SOURCE-ROOT]
19+
#
20+
# The source root defaults to the parent directory of this script.
21+
22+
import re
23+
import sys
24+
import pathlib
25+
26+
ROOT = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else \
27+
pathlib.Path(__file__).resolve().parent.parent
28+
29+
SRC_DIR = ROOT / 'src'
30+
31+
LD_FILES = [
32+
SRC_DIR / 'libnvme.ld',
33+
SRC_DIR / 'libnvmf.ld',
34+
SRC_DIR / 'accessors.ld',
35+
SRC_DIR / 'accessors-fabrics.ld',
36+
]
37+
38+
# Mirrors the install_headers() calls in src/meson.build.
39+
# Conditional headers (fabrics, MI) are included here and silently skipped
40+
# if they don't exist, matching the same conditionality as the build.
41+
INSTALLED_HEADERS = [
42+
# base — always installed
43+
SRC_DIR / 'nvme' / 'accessors.h',
44+
SRC_DIR / 'nvme' / 'endian.h',
45+
SRC_DIR / 'nvme' / 'filters.h',
46+
SRC_DIR / 'nvme' / 'ioctl.h',
47+
SRC_DIR / 'nvme' / 'lib-types.h',
48+
SRC_DIR / 'nvme' / 'lib.h',
49+
SRC_DIR / 'nvme' / 'linux.h',
50+
SRC_DIR / 'nvme' / 'nvme-cmds.h',
51+
SRC_DIR / 'nvme' / 'nvme-types.h',
52+
SRC_DIR / 'nvme' / 'tree.h',
53+
SRC_DIR / 'nvme' / 'types.h',
54+
SRC_DIR / 'nvme' / 'util.h',
55+
# fabrics — conditional on want_fabrics
56+
SRC_DIR / 'nvme' / 'accessors-fabrics.h',
57+
SRC_DIR / 'nvme' / 'fabrics.h',
58+
SRC_DIR / 'nvme' / 'nbft-types.h',
59+
SRC_DIR / 'nvme' / 'nbft.h',
60+
# MI — conditional on want_mi
61+
SRC_DIR / 'nvme' / 'mi-types.h',
62+
SRC_DIR / 'nvme' / 'mi.h',
63+
]
64+
65+
# ---------------------------------------------------------------------------
66+
# Collect all symbols listed in the version scripts
67+
# ---------------------------------------------------------------------------
68+
ld_syms = {} # symbol -> Path of the .ld file that declares it
69+
70+
for ld_path in LD_FILES:
71+
if not ld_path.exists():
72+
continue
73+
for line in ld_path.read_text().splitlines():
74+
m = re.match(r'^\s+([a-z]\w+);', line)
75+
if m:
76+
ld_syms[m.group(1)] = ld_path
77+
78+
# ---------------------------------------------------------------------------
79+
# Collect all names that appear as a prototype/declaration in installed headers
80+
# ---------------------------------------------------------------------------
81+
# Match any identifier immediately followed by '(' — this catches both
82+
# single-line and multi-line function declarations, and macro definitions
83+
# that alias a function name. The libnvme_*/libnvmf_* namespace is long
84+
# enough that false positives from comments are not a practical concern.
85+
header_syms = set()
86+
87+
for hdr_path in INSTALLED_HEADERS:
88+
if not hdr_path.exists():
89+
continue
90+
for m in re.finditer(r'\b([a-z_]\w+)\s*\(', hdr_path.read_text()):
91+
header_syms.add(m.group(1))
92+
93+
# ---------------------------------------------------------------------------
94+
# Report exported symbols with no prototype in any installed header
95+
# ---------------------------------------------------------------------------
96+
errors = 0
97+
98+
for sym, ld_path in sorted(ld_syms.items()):
99+
if sym not in header_syms:
100+
print(f'ERROR: {sym}() is exported in {ld_path.name} '
101+
f'but has no prototype in any installed header')
102+
errors += 1
103+
104+
if errors:
105+
print(f'\n{errors} error(s) found.')
106+
sys.exit(1)
107+
108+
print(f'OK: all {len(ld_syms)} exported symbols have prototypes '
109+
f'in installed headers.')

0 commit comments

Comments
 (0)