|
root / base / usr / src / tools / scripts / i86pc-rename-analysis.py
i86pc-rename-analysis.py Python 658 lines 25.3 KB
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
#!/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()