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