| #!/usr/bin/env python3 |
| # Copyright (C) 2026 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| """Generate a clang header map (.hmap) for a list of include directories. |
| |
| This script builds a JSON manifest and passes it off to LLVM's hmaptool. |
| |
| On basename collisions the first directory listed wins, matching -I search order. |
| """ |
| |
| import argparse |
| import json |
| import os |
| import subprocess |
| import sys |
| |
| HEADER_EXTENSIONS = ('.h', '.hh', '.hpp', '.hxx', '.inc', '.def') |
| HMAPTOOL = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hmaptool') |
| |
| |
| def collect_mappings(directories, framework=None): |
| mappings = {} |
| lowercase_owners = {} |
| # When --framework NAME is set, also scan a `<dir>/<NAME>/` subdirectory if |
| # present. This catches the framework-staging convention where headers live |
| # at `<some-include-root>/<Framework>/<Header>.h` (e.g. cmake's |
| # `WebCore.framework/PrivateHeaders/WebCore/StyleTextEdge.h` symlink tree). |
| # Scanning these *first* makes the curated framework-staged path win over |
| # raw source-tree dirs on basename collisions. |
| scan_order = [] |
| if framework: |
| for d in directories: |
| d_abs = os.path.abspath(d) |
| if framework not in d_abs.split(os.sep): |
| continue |
| sub = os.path.join(d_abs, framework) |
| if os.path.isdir(sub): |
| scan_order.append(sub) |
| scan_order.extend(directories) |
| |
| for directory in scan_order: |
| directory = os.path.abspath(directory) |
| if not os.path.isdir(directory): |
| continue |
| for entry in sorted(os.scandir(directory), key=lambda e: e.name): |
| if not entry.name.endswith(HEADER_EXTENSIONS): |
| continue |
| if entry.name in mappings or not entry.is_file(): |
| continue |
| key = entry.name.lower() |
| owner = lowercase_owners.get(key) |
| if owner and owner != entry.name: |
| sys.stderr.write( |
| 'generate-header-map: warning: case-insensitive collision between {!r} and {!r}; ' |
| 'clang header maps are case-insensitive, so one will shadow the other\n'.format( |
| mappings[owner], entry.path)) |
| lowercase_owners.setdefault(key, entry.name) |
| mappings[entry.name] = entry.path |
| if framework: |
| # Also emit a framework-style key so `<Framework/Header.h>` |
| # resolves to the source/derived path even when the header is |
| # not staged into the framework's PrivateHeaders/. This matches |
| # Xcode's hmap (HEADERMAP_INCLUDES_FRAMEWORK_ENTRIES_FOR_TARGET_BEING_BUILT=YES). |
| mappings[f'{framework}/{entry.name}'] = entry.path |
| return mappings |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument('-o', '--output', required=True, help='output .hmap path') |
| parser.add_argument('--dirs-file', required=True, |
| help='file containing one include directory per line') |
| parser.add_argument('--framework', |
| help='if set, also emit FRAMEWORK/Header.h entries so ' |
| '<FRAMEWORK/Header.h> resolves to the source/derived path ' |
| 'without requiring a physical PrivateHeaders/ copy') |
| args = parser.parse_args() |
| |
| with open(args.dirs_file) as f: |
| directories = [line.strip() for line in f if line.strip()] |
| |
| manifest = args.output + '.json' |
| with open(manifest, 'w') as f: |
| json.dump({'mappings': collect_mappings(directories, args.framework)}, f, indent=2, sort_keys=True) |
| |
| return subprocess.call([sys.executable, HMAPTOOL, 'write', manifest, args.output]) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |