216 lines
8.1 KiB
Python
216 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
##############################################################################
|
|
# MIT License
|
|
#
|
|
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
|
|
##############################################################################
|
|
|
|
"""
|
|
Hash consistency guard for rocprofiler-compute.
|
|
|
|
Errors (per arch):
|
|
- If latest-arch panels changed but its delta did not (and there are older archs)
|
|
- If latest-arch delta changed but its panels did not AND no new arch was added
|
|
- If an older arch's panels changed but its delta did not
|
|
- If an older arch's delta changed but neither latest panels nor this arch's
|
|
panels changed
|
|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
try:
|
|
from . import hash_manager # type: ignore
|
|
except Exception:
|
|
import importlib.util
|
|
|
|
_HERE = Path(__file__).resolve().parent
|
|
_SPEC = importlib.util.spec_from_file_location(
|
|
"hash_manager", str(_HERE / "hash_manager.py")
|
|
)
|
|
hash_manager = importlib.util.module_from_spec(_SPEC) # type: ignore[assignment]
|
|
assert _SPEC and _SPEC.loader is not None
|
|
_SPEC.loader.exec_module(hash_manager) # type: ignore[attr-defined]
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Subproject root: .../projects/rocprofiler-compute
|
|
SUBROOT = Path(__file__).resolve().parents[2]
|
|
|
|
CONFIGS_ROOT: Path = SUBROOT / "src" / "rocprof_compute_soc" / "analysis_configs"
|
|
HASH_FILE: Path = SUBROOT / "tools" / "config_management" / ".config_hashes.json"
|
|
TEMPLATE_FILE: Path = (
|
|
SUBROOT / "tools" / "config_management" / "analysis_config_template.yaml"
|
|
)
|
|
|
|
|
|
# ---------- helpers ----------
|
|
|
|
|
|
def _latest_arch(template_file: Path) -> str:
|
|
if not template_file.is_file():
|
|
return ""
|
|
with open(template_file, "r", encoding="utf-8") as f:
|
|
data = yaml.safe_load(f) or {}
|
|
return str(data.get("latest_arch") or "")
|
|
|
|
|
|
def _all_archs(cfg_root: Path) -> list[str]:
|
|
if not cfg_root.is_dir():
|
|
return []
|
|
return sorted(
|
|
p.name for p in cfg_root.iterdir() if p.is_dir() and p.name.startswith("gfx")
|
|
)
|
|
|
|
|
|
def _cur_panels_and_delta(arch_dir: Path) -> tuple[dict[str, str], str]:
|
|
"""
|
|
Current (on-disk) hashes using hash_manager.compute_arch_hashes API:
|
|
returns {"files": {...}, "delta_hash": <md5 or None>}
|
|
"""
|
|
cur = hash_manager.compute_arch_hashes(arch_dir)
|
|
panels = dict(cur.get("files") or {})
|
|
delta_hash = cur.get("delta_hash") or ""
|
|
return panels, str(delta_hash)
|
|
|
|
|
|
def _prev_panels_and_delta(
|
|
hashes_path: Path, arch_name: str
|
|
) -> tuple[dict[str, str], str]:
|
|
"""
|
|
Previous (DB) hashes saved in .config_hashes.json:
|
|
stored as {"files": {...}, "delta_hash": <md5 or None>}
|
|
"""
|
|
db: dict = hash_manager.load_hash_db(hashes_path)
|
|
prev_arch: dict = (db.get("archs") or {}).get(arch_name, {}) # type: ignore[assignment]
|
|
panels = dict(prev_arch.get("files") or {})
|
|
delta_hash = prev_arch.get("delta_hash") or ""
|
|
return panels, str(delta_hash)
|
|
|
|
|
|
def _changed_panel_files(cur: dict[str, str], prev: dict[str, str]) -> list[str]:
|
|
"""
|
|
Return a small list of changed panel filenames (added/removed/modified).
|
|
"""
|
|
# structural changes (added/removed)
|
|
changed = sorted(set(cur) ^ set(prev))
|
|
if not changed:
|
|
# content changes for existing files
|
|
changed = sorted(k for k in cur.keys() & prev.keys() if cur[k] != prev[k])
|
|
return changed
|
|
|
|
|
|
# ---------- main ----------
|
|
|
|
|
|
def main() -> int:
|
|
if not CONFIGS_ROOT.is_dir():
|
|
print(f"ERROR: analysis_configs directory not found at: {CONFIGS_ROOT}")
|
|
return 2
|
|
|
|
latest = _latest_arch(TEMPLATE_FILE)
|
|
all_archs = _all_archs(CONFIGS_ROOT)
|
|
older_archs = [a for a in all_archs if a != latest]
|
|
|
|
# detect new archs via hash_manager.detect_changes if available
|
|
try:
|
|
changes: dict = hash_manager.detect_changes(CONFIGS_ROOT, HASH_FILE) # type: ignore[call-arg]
|
|
except TypeError:
|
|
# old/new signatures both accept (cfg_root, hashes_path)
|
|
changes = hash_manager.detect_changes(CONFIGS_ROOT, HASH_FILE) # type: ignore[call-arg]
|
|
new_archs: list = changes.get("new_archs") or []
|
|
|
|
errors: list[str] = []
|
|
|
|
# Track whether latest panels changed (used for older-arch delta rule)
|
|
latest_panels_changed = False
|
|
|
|
for arch in all_archs:
|
|
arch_dir = CONFIGS_ROOT / arch
|
|
cur_panels, cur_delta = _cur_panels_and_delta(arch_dir)
|
|
prev_panels, prev_delta = _prev_panels_and_delta(HASH_FILE, arch)
|
|
|
|
panel_changed = cur_panels != prev_panels
|
|
delta_changed = cur_delta != prev_delta
|
|
|
|
if arch == latest:
|
|
latest_panels_changed = panel_changed
|
|
|
|
# A) Latest panels changed but no delta changed (and there ARE older archs)
|
|
if panel_changed and not delta_changed and older_archs:
|
|
snippet = ", ".join(_changed_panel_files(cur_panels, prev_panels)[:5])
|
|
errors.append(
|
|
f"Panels changed in latest arch '{latest}' "
|
|
"but its delta file did not change.\n"
|
|
f"Changed panels (sample): {snippet}\n"
|
|
"Run the workflow to regenerate deltas for previous archs."
|
|
)
|
|
|
|
# B) Latest delta changed but panels did not AND no new arch was added
|
|
if delta_changed and not panel_changed and latest not in new_archs:
|
|
errors.append(
|
|
"Delta file changed for latest, but panels "
|
|
"didn't change and no new arch was added.\n"
|
|
"This usually means deltas were edited/regenerated "
|
|
"without corresponding latest updates."
|
|
)
|
|
|
|
else:
|
|
# C) Arch panels changed but its delta did not
|
|
if panel_changed and not delta_changed:
|
|
snippet = ", ".join(_changed_panel_files(cur_panels, prev_panels)[:5])
|
|
errors.append(
|
|
f"Panels changed in arch '{arch}' "
|
|
"but its delta file did not change.\n"
|
|
f"Changed panels (sample): {snippet}\n"
|
|
"Regenerate deltas for this arch (diff vs latest) "
|
|
"and commit them."
|
|
)
|
|
|
|
# D) Older arch delta changed without either latest panels changing
|
|
# OR this arch's panels changing -> error
|
|
# (allow if latest panels changed: deltas can legitimately change then)
|
|
if delta_changed and not panel_changed and not latest_panels_changed:
|
|
errors.append(
|
|
f"Delta file changed under older arch '{arch}' "
|
|
"but neither latest nor this arch's panels changed.\n"
|
|
"This suggests stray delta edits; "
|
|
"verify latest panels or this arch's panels, or revert."
|
|
)
|
|
|
|
if errors:
|
|
print("\nHASH CONSISTENCY ERRORS:")
|
|
for e in errors:
|
|
print(" - " + e)
|
|
return 1
|
|
|
|
print("Hash consistency check passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|