#!/usr/bin/env python3
"""
i86pc-rename-analysis.py

Analyzes the effort required to rename i86pc source directories to amd64
(e.g., uts/i86pc -> uts/amd64, cmd/mdb/i86pc -> cmd/mdb/amd64,
lib/fm/topo/modules/i86pc -> lib/fm/topo/modules/amd64).

Finds all i86pc directories, counts their contents, and locates all external
references categorized by type.

Usage:
    python3 i86pc-rename-analysis.py [source-tree-root]

Default source tree root: /home/ctusa/repos/hammerhead/base/usr/src
"""

import os
import re
import sys
import textwrap
from collections import defaultdict

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

DEFAULT_SRC_ROOT = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    "..", ".."  # tools/scripts -> usr/src
)

# Extensions to scan for references.  Skipping binary/object files.
TEXT_EXTENSIONS = {
    # C/C++ source and headers
    ".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp",
    # Assembly
    ".s", ".S",
    # Make / build system
    "",                     # bare filenames like "Makefile", "Targetdirs"
    ".com", ".targ", ".rules", ".files", ".flg", ".cap",
    ".workarounds", ".intel", ".i86pc", ".i86hvm",
    ".master", ".mach", ".cmd", ".lib", ".uts",
    # Scripts
    ".sh", ".ksh", ".pl", ".py",
    # Data / config
    ".conf", ".xml", ".dtd", ".json", ".yaml", ".toml",
    ".msg", ".ld", ".mf", ".p5m", ".rc", ".4th",
    # Manual pages (all numbered extensions)
    ".1", ".2", ".3", ".3c", ".3proc", ".4", ".4d", ".5",
    ".6", ".7", ".8", ".9", ".9f",
    # Misc
    ".run", ".guess", ".properties",
}

# Bare filenames (no extension) that should always be scanned.
BARE_SCAN_NAMES = {
    "Makefile", "Targetdirs", "mapfile", "mapfile-vers",
    "mapfile-cap", "README", "NOTES",
}

# Directories to always skip.
SKIP_DIRS = {".git", ".hg", ".svn", "CVS"}


# ---------------------------------------------------------------------------
# Reference categorization patterns
# ---------------------------------------------------------------------------
# Each entry is (category_name, compiled_regex).
# Patterns are tested in order; first match wins.

CATEGORY_PATTERNS = [
    # --- Makefile / build variable paths -----------------------------------
    # PLATFORM = i86pc (build variable assignment)
    ("makefile_var",    re.compile(r'PLATFORM\s*=\s*i86pc')),
    # MACH_DIR, PLATFORM_DIR, SRCDIR, CONF_SRCDIR, UNIX_DIR, etc.
    ("makefile_var",    re.compile(r'(?:MACH_DIR|PLATFORM_DIR|SRCDIR|CONF_SRCDIR|'
                                   r'UNIX_DIR|DRMACH_IO|PERL_MACH)\s*[+:]?=\s*.*i86pc')),
    # i386_SUBDIRS = intel i86pc  (subdirectory dispatch)
    ("makefile_var",    re.compile(r'\w+_SUBDIRS\s*[+:]?=.*\bi86pc\b')),
    # Any other makefile variable assignment containing i86pc
    ("makefile_var",    re.compile(r'^\s*[\w_]+\s*[+:]?=\s*.*\bi86pc\b')),

    # --- Include directives and -I flags ------------------------------------
    # #include "..." or #include <...> referencing i86pc
    ("include_directive", re.compile(r'#\s*include\s+[<"].*i86pc.*[>"]')),
    # -I flag in CPPFLAGS, INC_PATH, etc.
    ("include_directive", re.compile(r'-I\S*i86pc')),
    # include $(UTSBASE)/i86pc/...  (Makefile include directives)
    ("include_directive", re.compile(r'include\s+.*i86pc')),

    # --- Relative path references ------------------------------------------
    # ../i86pc or ../../i86pc style relative paths in Makefiles/source
    ("makefile_path",   re.compile(r'\.\./+i86pc')),
    # $(UTSBASE)/i86pc  $(SRC)/uts/i86pc  $(SRC)/i86pc  etc.
    ("makefile_path",   re.compile(r'\$\([A-Z_]+\)/(?:uts/)?i86pc')),
    # Bare path fragment: uts/i86pc  cmd/mdb/i86pc  modules/i86pc
    ("makefile_path",   re.compile(r'(?:uts|cmd|lib|modules|maps)/i86pc')),

    # --- Source files named with i86pc -------------------------------------
    # File name components: pci_prd_i86pc.c, pci_i86pc.h, Makefile.i86pc
    ("source_file_ref", re.compile(r'\bi86pc\b.*\.(?:c|h|cc|cpp|S|s|py|pl|sh|ksh)\b')),
    ("source_file_ref", re.compile(r'Makefile\.i86pc')),
    ("source_file_ref", re.compile(r'i86pc_ktest')),

    # --- Runtime string literals (compiled into binary) --------------------
    # Quoted string "i86pc"
    ("runtime_string",  re.compile(r'"i86pc"')),
    # Runtime path: /platform/i86pc/  platform/i86pc/
    ("runtime_string",  re.compile(r'/platform/i86pc/')),
    ("runtime_string",  re.compile(r'platform/i86pc/')),
    # uname -m / uname -p producing i86pc
    ("runtime_string",  re.compile(r'uname.*i86pc|i86pc.*uname')),
    # strcmp / strncmp against "i86pc"
    ("runtime_string",  re.compile(r'str(?:n?cmp|str)\s*\(.*i86pc')),
    # ktest load i86pc
    ("runtime_string",  re.compile(r'ktest\s+load\s+i86pc')),
    # microcode / ucode path: platform/i86pc/ucode
    ("runtime_string",  re.compile(r'platform/i86pc/ucode')),

    # --- Comments and documentation ----------------------------------------
    # Line-level: starts with # (shell/make comment) or // or * (C block)
    ("comment_or_doc",  re.compile(r'^\s*(?:#|//|\*|/\*)')),
    # Inline comment on a make/shell line: trailing #
    ("comment_or_doc",  re.compile(r'#.*i86pc')),
    # Prose patterns in .c/.h: See: uts/i86pc/...
    ("comment_or_doc",  re.compile(r'(?i)(?:See|cf\.|formerly|was|e\.g\.|eg\.)'
                                   r'.*i86pc')),
    # config.guess style: operating system identification strings
    ("comment_or_doc",  re.compile(r'config\.guess|AuroraUX|SunOS.*uname')),

    # --- Fallback ----------------------------------------------------------
    ("other",           re.compile(r'i86pc')),
]

# Human-readable descriptions for each category
CATEGORY_DESC = {
    "makefile_var":      "Makefile variable assignments (PLATFORM=, SRCDIR=, etc.)",
    "makefile_path":     "Makefile path references ($(UTSBASE)/i86pc, ../i86pc, etc.)",
    "include_directive": "Include directives (#include, -I flags, include Makefile)",
    "source_file_ref":   "Source file names containing i86pc",
    "runtime_string":    "Runtime string literals (compiled into binary output)",
    "comment_or_doc":    "Comments and documentation (cosmetic only)",
    "other":             "Other / unclassified",
}

# Complexity guidance per category
CATEGORY_COMPLEXITY = {
    "makefile_var":      "sed-replaceable (PLATFORM=i86pc -> PLATFORM=amd64)",
    "makefile_path":     "sed-replaceable after directory rename",
    "include_directive": "sed-replaceable after directory rename",
    "source_file_ref":   "manual review: file renames + internal references",
    "runtime_string":    "MANUAL REVIEW REQUIRED: runtime behavior change",
    "comment_or_doc":    "optional cosmetic update",
    "other":             "manual review needed",
}


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def should_scan_file(path: str) -> bool:
    """Return True if the file should be scanned for references."""
    basename = os.path.basename(path)
    if basename in BARE_SCAN_NAMES:
        return True
    # Files whose name starts with Makefile (e.g. Makefile.com, Makefile.intel)
    if basename.startswith("Makefile"):
        return True
    # mapfile variants
    if basename.startswith("mapfile"):
        return True
    _, ext = os.path.splitext(basename)
    return ext in TEXT_EXTENSIONS


def is_binary(path: str, sample_size: int = 8192) -> bool:
    """Quick binary detection: check for null bytes in first sample_size bytes."""
    try:
        with open(path, "rb") as f:
            chunk = f.read(sample_size)
        return b"\x00" in chunk
    except OSError:
        return True


def categorize_line(line: str) -> str:
    """Return the category name for a line of text containing 'i86pc'."""
    for category, pattern in CATEGORY_PATTERNS:
        if pattern.search(line):
            return category
    return "other"


def rel(path: str, root: str) -> str:
    """Return path relative to root."""
    try:
        return os.path.relpath(path, root)
    except ValueError:
        return path


def wrap(text: str, width: int = 78, indent: str = "    ") -> str:
    return textwrap.fill(text, width=width, initial_indent=indent,
                         subsequent_indent=indent)


# ---------------------------------------------------------------------------
# Main analysis
# ---------------------------------------------------------------------------

def find_i86pc_dirs(src_root: str):
    """
    Walk the source tree and return a list of absolute paths of directories
    named 'i86pc'.
    """
    results = []
    for dirpath, dirnames, _ in os.walk(src_root):
        # Prune unwanted dirs in place
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for d in dirnames:
            if d == "i86pc":
                results.append(os.path.join(dirpath, d))
    return sorted(results)


def count_files_in_dir(directory: str):
    """Return (total_files, list_of_file_paths) inside directory recursively."""
    files = []
    for dirpath, dirnames, filenames in os.walk(directory):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for f in filenames:
            files.append(os.path.join(dirpath, f))
    return len(files), files


def find_references(src_root: str, i86pc_dirs: list):
    """
    Walk the entire source tree and find every line referencing 'i86pc'.

    Returns a dict:
        {
            abs_file_path: [
                {
                    "lineno": int,
                    "line": str,
                    "category": str,
                    "inside_i86pc_dir": bool,   # is this file inside an i86pc dir?
                    "i86pc_dir_ref": str|None,  # which i86pc dir it refers to
                }
            ]
        }
    """
    # Build a set of i86pc_dir prefixes for fast membership testing
    i86pc_prefixes = tuple(d + os.sep for d in i86pc_dirs) + tuple(i86pc_dirs)

    def file_is_inside_i86pc(path: str) -> bool:
        return any(path.startswith(p) for p in i86pc_prefixes)

    # Determine which i86pc dir a reference points to (by path fragment matching)
    def find_i86pc_dir_ref(line: str, file_path: str) -> str | None:
        file_dir = os.path.dirname(file_path)
        # Try to resolve relative paths like ../i86pc or ../../i86pc
        rel_match = re.search(r'(\.\./+i86pc(?:/\S*)?)', line)
        if rel_match:
            candidate = os.path.normpath(
                os.path.join(file_dir, rel_match.group(1).rstrip(",) \t\\")))
            for d in i86pc_dirs:
                if candidate == d or candidate.startswith(d + os.sep):
                    return d
        # Match src-tree path fragments
        for d in i86pc_dirs:
            # Use relative path from src_root for matching
            rel_d = rel(d, src_root)
            if rel_d in line or os.path.basename(os.path.dirname(d)) + "/i86pc" in line:
                return d
        # Generic: if line contains any i86pc path token, return None (ambiguous)
        return None

    references: dict = defaultdict(list)

    for dirpath, dirnames, filenames in os.walk(src_root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for fname in filenames:
            fpath = os.path.join(dirpath, fname)
            if not should_scan_file(fpath):
                continue
            if is_binary(fpath):
                continue
            try:
                with open(fpath, "r", encoding="utf-8", errors="replace") as fh:
                    lines = fh.readlines()
            except OSError:
                continue

            for lineno, line in enumerate(lines, start=1):
                if "i86pc" not in line:
                    continue
                stripped = line.rstrip("\n")
                cat = categorize_line(stripped)
                inside = file_is_inside_i86pc(fpath)
                ref_dir = find_i86pc_dir_ref(stripped, fpath)
                references[fpath].append({
                    "lineno": lineno,
                    "line": stripped,
                    "category": cat,
                    "inside_i86pc_dir": inside,
                    "i86pc_dir_ref": ref_dir,
                })

    return dict(references)


def build_report(src_root: str,
                 i86pc_dirs: list,
                 dir_file_counts: dict,
                 references: dict):
    """Build and return a formatted analysis report as a string."""

    lines = []
    def emit(*args):
        lines.append(" ".join(str(a) for a in args))

    SEP = "=" * 78
    sep = "-" * 78

    emit(SEP)
    emit("i86pc -> amd64 RENAME EFFORT ANALYSIS")
    emit(f"Source tree: {src_root}")
    emit(SEP)
    emit()

    # -----------------------------------------------------------------------
    # Section 1: Directories to rename
    # -----------------------------------------------------------------------
    emit("SECTION 1: Directories Named 'i86pc'")
    emit(sep)
    emit()

    total_dir_files = 0
    for d in i86pc_dirs:
        n, _ = dir_file_counts[d]
        total_dir_files += n
        rel_d = rel(d, src_root)
        emit(f"  {rel_d}/")
        emit(f"      Files inside: {n}")
    emit()
    emit(f"  Total directories to rename : {len(i86pc_dirs)}")
    emit(f"  Total files inside those dirs: {total_dir_files}")
    emit()

    # -----------------------------------------------------------------------
    # Section 2: External references summary
    # -----------------------------------------------------------------------
    emit("SECTION 2: External References (files outside i86pc dirs that reference i86pc)")
    emit(sep)
    emit()

    # Split references into internal (inside i86pc dirs) vs external
    external_refs: dict[str, list] = {}  # file -> list of ref dicts
    internal_refs: dict[str, list] = {}
    for fpath, refs in references.items():
        ext = [r for r in refs if not r["inside_i86pc_dir"]]
        inn = [r for r in refs if r["inside_i86pc_dir"]]
        if ext:
            external_refs[fpath] = ext
        if inn:
            internal_refs[fpath] = inn

    total_external_files = len(external_refs)
    total_external_lines = sum(len(v) for v in external_refs.values())
    total_internal_files = len(internal_refs)
    total_internal_lines = sum(len(v) for v in internal_refs.values())

    emit(f"  External files referencing i86pc : {total_external_files}")
    emit(f"  External reference lines total   : {total_external_lines}")
    emit(f"  Internal files (inside i86pc dirs): {total_internal_files}")
    emit(f"  Internal reference lines total    : {total_internal_lines}")
    emit()

    # -----------------------------------------------------------------------
    # Section 3: Category breakdown
    # -----------------------------------------------------------------------
    emit("SECTION 3: Reference Category Breakdown")
    emit(sep)
    emit()

    # Count by category across external refs only
    cat_file_set: dict[str, set] = defaultdict(set)
    cat_line_count: dict[str, int] = defaultdict(int)

    for fpath, refs in external_refs.items():
        for r in refs:
            cat = r["category"]
            cat_file_set[cat].add(fpath)
            cat_line_count[cat] += 1

    # Print in a defined order
    cat_order = [
        "runtime_string",
        "source_file_ref",
        "makefile_var",
        "makefile_path",
        "include_directive",
        "comment_or_doc",
        "other",
    ]

    emit(f"  {'Category':<22} {'Files':>6} {'Lines':>6}  Description")
    emit(f"  {'-'*22} {'-'*6} {'-'*6}  {'-'*38}")
    for cat in cat_order:
        n_files = len(cat_file_set.get(cat, set()))
        n_lines = cat_line_count.get(cat, 0)
        desc = CATEGORY_DESC.get(cat, cat)
        emit(f"  {cat:<22} {n_files:>6} {n_lines:>6}  {desc}")
    emit()

    # -----------------------------------------------------------------------
    # Section 4: Per-directory external reference detail
    # -----------------------------------------------------------------------
    emit("SECTION 4: External References Per i86pc Directory")
    emit(sep)
    emit()

    for d in i86pc_dirs:
        rel_d = rel(d, src_root)
        emit(f"  Directory: {rel_d}/")
        emit()

        # Find external refs that mention this specific dir
        # (either via i86pc_dir_ref match, or generic i86pc references
        # that are ambiguous - count them all per file)
        dir_basename_parent = os.path.basename(os.path.dirname(d))
        dir_frag = dir_basename_parent + "/i86pc"

        per_dir_files: dict[str, list] = defaultdict(list)
        for fpath, refs in external_refs.items():
            matching = []
            for r in refs:
                # Explicit match
                if r["i86pc_dir_ref"] == d:
                    matching.append(r)
                    continue
                # Heuristic: line contains a path fragment unique to this dir
                line = r["line"]
                if dir_frag in line:
                    matching.append(r)
                    continue
                # For uts/i86pc (the primary dir), catch generic references
                # not matched to a specific dir
                if d.endswith("uts/i86pc") and r["i86pc_dir_ref"] is None:
                    matching.append(r)
            if matching:
                per_dir_files[fpath].extend(matching)

        if not per_dir_files:
            emit("    (no external references found)")
            emit()
            continue

        emit(f"    External files referencing this dir: {len(per_dir_files)}")
        emit()

        # Group by category
        by_cat: dict[str, list] = defaultdict(list)
        for fpath, refs in per_dir_files.items():
            for r in refs:
                by_cat[r["category"]].append((fpath, r))

        for cat in cat_order:
            items = by_cat.get(cat, [])
            if not items:
                continue
            desc = CATEGORY_DESC.get(cat, cat)
            complexity = CATEGORY_COMPLEXITY.get(cat, "")
            emit(f"    [{cat}] {desc}")
            emit(f"    Complexity: {complexity}")
            # Group by file
            by_file: dict[str, list] = defaultdict(list)
            for fpath, r in items:
                by_file[fpath].append(r)
            for fpath, refs in sorted(by_file.items()):
                rel_f = rel(fpath, src_root)
                emit(f"      {rel_f}")
                for r in refs[:5]:  # Show up to 5 lines per file
                    snippet = r["line"].strip()[:100]
                    emit(f"        L{r['lineno']:>5}: {snippet}")
                if len(refs) > 5:
                    emit(f"        ... and {len(refs) - 5} more lines")
            emit()

    # -----------------------------------------------------------------------
    # Section 5: Files named with i86pc
    # -----------------------------------------------------------------------
    emit("SECTION 5: Source Files Named with 'i86pc' (require renaming)")
    emit(sep)
    emit()

    named_files = []
    for dirpath, dirnames, filenames in os.walk(src_root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for fname in filenames:
            if "i86pc" in fname:
                named_files.append(os.path.join(dirpath, fname))

    if named_files:
        emit(f"  Files with 'i86pc' in name: {len(named_files)}")
        emit()
        for f in sorted(named_files):
            rel_f = rel(f, src_root)
            emit(f"  {rel_f}")
    else:
        emit("  (none found)")
    emit()

    # -----------------------------------------------------------------------
    # Section 6: All external reference files by category
    # -----------------------------------------------------------------------
    emit("SECTION 6: Complete List of External Reference Files by Category")
    emit(sep)
    emit()

    for cat in cat_order:
        files_in_cat = sorted(cat_file_set.get(cat, set()))
        if not files_in_cat:
            continue
        desc = CATEGORY_DESC.get(cat, cat)
        complexity = CATEGORY_COMPLEXITY.get(cat, "")
        emit(f"  [{cat}]  {desc}")
        emit(f"  Complexity: {complexity}")
        emit(f"  Files ({len(files_in_cat)}):")
        for fpath in files_in_cat:
            rel_f = rel(fpath, src_root)
            n_lines = len([r for r in external_refs[fpath]
                           if r["category"] == cat])
            emit(f"    {rel_f}  ({n_lines} line(s))")
        emit()

    # -----------------------------------------------------------------------
    # Section 7: Summary and estimated complexity
    # -----------------------------------------------------------------------
    emit("SECTION 7: Summary and Estimated Complexity")
    emit(sep)
    emit()

    n_runtime   = cat_line_count.get("runtime_string", 0)
    n_srcfile   = cat_line_count.get("source_file_ref", 0)
    n_mkvar     = cat_line_count.get("makefile_var", 0)
    n_mkpath    = cat_line_count.get("makefile_path", 0)
    n_include   = cat_line_count.get("include_directive", 0)
    n_comment   = cat_line_count.get("comment_or_doc", 0)
    n_other     = cat_line_count.get("other", 0)
    n_sed_safe  = n_mkvar + n_mkpath + n_include
    n_manual    = n_runtime + n_srcfile + n_other

    emit(f"  Directories to rename          : {len(i86pc_dirs)}")
    emit(f"  Files inside those directories : {total_dir_files}")
    emit(f"  External files needing updates : {total_external_files}")
    emit(f"  Total external reference lines : {total_external_lines}")
    emit()
    emit("  Effort breakdown:")
    emit(f"    sed-safe replacements        : {n_sed_safe} lines across external files")
    emit(f"      (Makefile vars + paths + include flags)")
    emit(f"    Runtime literals (manual)    : {n_runtime} lines  <-- behavior impact")
    emit(f"    Source file references (manual): {n_srcfile} lines (file renames)")
    emit(f"    Comments / docs (cosmetic)   : {n_comment} lines")
    emit(f"    Other (manual review)        : {n_other} lines")
    emit()
    emit("  Estimated approach:")
    emit()

    steps = [
        ("1. Git rename the directories",
         f"git mv uts/i86pc uts/amd64  (and similarly for cmd/mdb/i86pc, "
         f"lib/fm/topo/modules/i86pc). {total_dir_files} files physically moved."),
        ("2. sed pass on Makefile paths",
         f"Mechanical: s/uts\\/i86pc/uts\\/amd64/g  etc. across "
         f"~{len(cat_file_set.get('makefile_path',set())) + len(cat_file_set.get('makefile_var',set()))} "
         f"files. Review with 'git diff' after."),
        ("3. Update -I include flags",
         f"{len(cat_file_set.get('include_directive',set()))} files have -I.../i86pc flags — "
         f"all sed-safe after the directory rename."),
        ("4. Rename source files with i86pc in name",
         f"{len(named_files)} files: e.g. pci_prd_i86pc.c -> pci_prd_amd64.c. "
         f"Update any internal symbol names or string refs if appropriate."),
        ("5. Review runtime string literals",
         f"{n_runtime} reference lines contain quoted 'i86pc' strings or "
         f"/platform/i86pc/ paths compiled into binary output. Each requires "
         f"individual judgment: does the platform still respond to 'i86pc' queries "
         f"from userspace tools (uname -m, ZFS platform detection, etc.)?"),
        ("6. Update PLATFORM= build variable",
         "PLATFORM=i86pc is set in Makefile.i86pc, Makefile.intel, and per-module "
         "Makefiles. Changing to PLATFORM=amd64 affects install path logic — "
         "verify proto area layout is consistent."),
        ("7. Optional: update comments",
         f"{n_comment} comment lines mention i86pc. Cosmetic; update when convenient."),
    ]

    for title, detail in steps:
        emit(f"  {title}")
        for dline in textwrap.wrap(detail, width=70):
            emit(f"      {dline}")
        emit()

    emit(SEP)
    emit("END OF ANALYSIS")
    emit(SEP)

    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main():
    if len(sys.argv) > 1:
        src_root = os.path.abspath(sys.argv[1])
    else:
        src_root = os.path.normpath(
            os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
        )

    if not os.path.isdir(src_root):
        print(f"ERROR: source root not found: {src_root}", file=sys.stderr)
        sys.exit(1)

    print(f"Scanning source tree: {src_root}", file=sys.stderr)
    print("Step 1/3: Finding i86pc directories ...", file=sys.stderr)
    i86pc_dirs = find_i86pc_dirs(src_root)
    if not i86pc_dirs:
        print("No directories named 'i86pc' found. Nothing to do.", file=sys.stderr)
        sys.exit(0)

    print(f"         Found {len(i86pc_dirs)} i86pc director(ies).", file=sys.stderr)

    print("Step 2/3: Counting files inside i86pc directories ...", file=sys.stderr)
    dir_file_counts = {}
    for d in i86pc_dirs:
        n, files = count_files_in_dir(d)
        dir_file_counts[d] = (n, files)
        print(f"         {rel(d, src_root)}/  -> {n} files", file=sys.stderr)

    print("Step 3/3: Scanning entire tree for references to 'i86pc' ...",
          file=sys.stderr)
    references = find_references(src_root, i86pc_dirs)
    print(f"         Done. {len(references)} files contain references.",
          file=sys.stderr)
    print(file=sys.stderr)

    report = build_report(src_root, i86pc_dirs, dir_file_counts, references)
    print(report)


if __name__ == "__main__":
    main()
