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