Skip to content

Commit bed896f

Browse files
Martin Belangerigaw
authored andcommitted
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 079804c commit bed896f

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

libnvme/test/meson.build

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ if python3_prog.found()
1515
meson.project_source_root() / 'libnvme',
1616
],
1717
)
18+
_ld_args = ['--ld', nvme_ld, '--ld', accessors_ld]
19+
if want_fabrics
20+
_ld_args += ['--ld', nvmf_ld, '--ld', nvmf_accessors_ld]
21+
endif
22+
_hdr_args = []
23+
foreach hdr : headers
24+
_hdr_args += ['--header', meson.project_source_root() / 'libnvme' / 'src' / hdr]
25+
endforeach
26+
test(
27+
'libnvme - check-public-headers',
28+
python3_prog,
29+
args: [files('../tools/check-public-headers.py')] + _ld_args + _hdr_args,
30+
)
1831
endif
1932

2033
# These tests all require interaction with a real NVMe device, so we don't
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 (via meson — preferred, keeps meson.build as single source of truth):
18+
# python3 tools/check-public-headers.py \
19+
# --ld src/libnvme.ld --ld src/accessors.ld [--ld ...] \
20+
# --header src/nvme/lib.h --header src/nvme/tree.h [--header ...]
21+
#
22+
# Usage (standalone, auto-discovers files from the source root):
23+
# python3 tools/check-public-headers.py [LIBNVME-SOURCE-ROOT]
24+
#
25+
# In auto-discovery mode the script scans src/*.ld for version scripts and
26+
# src/nvme/*.h (excluding files whose name contains "private") for headers.
27+
# The source root defaults to the parent directory of this script.
28+
29+
import argparse
30+
import re
31+
import sys
32+
import pathlib
33+
34+
35+
def parse_args():
36+
parser = argparse.ArgumentParser(
37+
description='Check that every exported symbol has a prototype in an '
38+
'installed header.')
39+
parser.add_argument(
40+
'root', nargs='?',
41+
help='libnvme source root for auto-discovery (defaults to the parent '
42+
'of this script); ignored when --ld / --header are given')
43+
parser.add_argument(
44+
'--ld', action='append', metavar='FILE', dest='ld_files',
45+
help='version-script (.ld) file to read exported symbols from '
46+
'(may be repeated)')
47+
parser.add_argument(
48+
'--header', action='append', metavar='FILE', dest='headers',
49+
help='installed header file to search for prototypes '
50+
'(may be repeated)')
51+
return parser.parse_args()
52+
53+
54+
def main():
55+
args = parse_args()
56+
57+
if args.ld_files or args.headers:
58+
if not args.ld_files or not args.headers:
59+
sys.exit('error: --ld and --header must both be provided together')
60+
ld_files = [pathlib.Path(f) for f in args.ld_files]
61+
headers = [pathlib.Path(f) for f in args.headers]
62+
else:
63+
root = pathlib.Path(args.root) if args.root else \
64+
pathlib.Path(__file__).resolve().parent.parent
65+
src = root / 'src'
66+
ld_files = sorted(src.glob('*.ld'))
67+
headers = sorted(h for h in (src / 'nvme').glob('*.h')
68+
if 'private' not in h.name)
69+
70+
# -----------------------------------------------------------------------
71+
# Collect all symbols listed in the version scripts
72+
# -----------------------------------------------------------------------
73+
ld_syms = {} # symbol -> Path of the .ld file that declares it
74+
75+
for ld_path in ld_files:
76+
for line in ld_path.read_text().splitlines():
77+
m = re.match(r'^\s+([a-z]\w+);', line)
78+
if m:
79+
ld_syms[m.group(1)] = ld_path
80+
81+
# -----------------------------------------------------------------------
82+
# Collect all names that appear as a prototype/declaration in installed
83+
# headers. Match any identifier immediately followed by '(' — this
84+
# catches both single-line and multi-line function declarations, and macro
85+
# definitions that alias a function name. The libnvme_*/libnvmf_*
86+
# namespace is long enough that false positives from comments are not a
87+
# practical concern.
88+
# -----------------------------------------------------------------------
89+
header_syms = set()
90+
91+
for hdr_path in headers:
92+
for m in re.finditer(r'\b([a-z_]\w+)\s*\(', hdr_path.read_text()):
93+
header_syms.add(m.group(1))
94+
95+
# -----------------------------------------------------------------------
96+
# Report exported symbols with no prototype in any installed header
97+
# -----------------------------------------------------------------------
98+
errors = 0
99+
100+
for sym, ld_path in sorted(ld_syms.items()):
101+
if sym not in header_syms:
102+
print(f'ERROR: {sym}() is exported in {ld_path.name} '
103+
f'but has no prototype in any installed header')
104+
errors += 1
105+
106+
if errors:
107+
print(f'\n{errors} error(s) found.')
108+
sys.exit(1)
109+
110+
print(f'OK: all {len(ld_syms)} exported symbols have prototypes '
111+
f'in installed headers.')
112+
113+
114+
if __name__ == '__main__':
115+
main()

0 commit comments

Comments
 (0)