Files

280 строки
9.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 manager for tracking configuration file changes.
Can be used standalone or imported by the master workflow.
Usage:
python hash_manager.py --compute-all <configs_dir> [hash_file]
python hash_manager.py --detect-changes <configs_dir> [hash_file]
python hash_manager.py --update <arch_name> <configs_dir> [hash_file]
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from pathlib import Path
from typing import Optional
DEFAULT_HASH_DB = "src/utils/.config_hashes.json"
def compute_file_hash(filepath: Path) -> str:
"""Compute MD5 hash of a file."""
md5 = hashlib.md5()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
md5.update(chunk)
return md5.hexdigest()
def compute_arch_hashes(arch_dir: Path) -> dict:
"""
Compute hashes for all YAML files in an arch directory.
Returns dict: {"files": {filename: hash}, "delta_hash": <md5 or None>}
"""
arch_path = Path(arch_dir)
if not arch_path.is_dir():
return {"files": {}, "delta_hash": None}
file_hashes: dict[str, str] = {}
for yaml_file in sorted(arch_path.glob("*.yaml")):
file_hashes[yaml_file.name] = compute_file_hash(yaml_file)
# Check for delta file (assume exactly one *_diff.yaml)
delta_dir = arch_path / "config_delta"
delta_hash: Optional[str] = None
if delta_dir.is_dir():
delta_files = list(delta_dir.glob("*_diff.yaml"))
if delta_files:
delta_hash = compute_file_hash(delta_files[0])
return {"files": file_hashes, "delta_hash": delta_hash}
def load_hash_db(hash_file: Path) -> dict:
"""Load hash database from file (or initialize)."""
hash_path = Path(hash_file)
if not hash_path.exists():
return {"archs": {}}
with open(hash_path) as f:
return json.load(f)
def save_hash_db(hash_file: Path, data: dict) -> None:
"""Save hash database to file."""
hash_path = Path(hash_file)
hash_path.parent.mkdir(parents=True, exist_ok=True)
with open(hash_path, "w") as f:
json.dump(data, f, indent=2, sort_keys=True)
def detect_changes(configs_dir: Path, hash_file: Path) -> dict:
"""
Detect changes in architecture configs.
Returns dict with keys:
- new_archs: list[str]
- modified_archs: dict[str, list[str]]
- delta_files: dict[str, str] # arch -> delta file path
- deleted_archs: list[str]
"""
configs_path = Path(configs_dir)
hash_db = load_hash_db(hash_file)
current_archs = {
d.name
for d in configs_path.iterdir()
if d.is_dir() and d.name.startswith("gfx")
}
stored_archs = set(hash_db.get("archs", {}).keys())
changes = {
"new_archs": sorted(current_archs - stored_archs),
"modified_archs": {},
"delta_files": {},
"deleted_archs": sorted(stored_archs - current_archs),
}
# Compare existing archs
for arch in sorted(current_archs & stored_archs):
arch_dir = configs_path / arch
current_hashes = compute_arch_hashes(arch_dir)
stored_hashes = hash_db["archs"].get(arch, {"files": {}, "delta_hash": None})
modified_files: list[str] = []
current_files = set(current_hashes["files"].keys())
stored_files = set(stored_hashes.get("files", {}).keys())
# New files
for f in sorted(current_files - stored_files):
modified_files.append(f)
# Modified files
for f in sorted(current_files & stored_files):
if current_hashes["files"][f] != stored_hashes["files"][f]:
modified_files.append(f)
# Deleted files (mark as "[DELETED] <name>")
for f in sorted(stored_files - current_files):
modified_files.append(f"[DELETED] {f}")
if modified_files:
changes["modified_archs"][arch] = modified_files
# Delta changes
delta_dir = arch_dir / "config_delta"
if delta_dir.is_dir():
delta_files = list(delta_dir.glob("*_diff.yaml"))
if delta_files:
current_delta_hash = compute_file_hash(delta_files[0])
stored_delta_hash = stored_hashes.get("delta_hash")
if current_delta_hash != stored_delta_hash:
changes["delta_files"][arch] = str(delta_files[0])
return changes
def update_hashes(arch_name: str, configs_dir: Path, hash_file: Path) -> bool:
"""Update hashes for a specific architecture."""
hash_db = load_hash_db(hash_file)
arch_dir = Path(configs_dir) / arch_name
if not arch_dir.is_dir():
print(f"Error: {arch_dir} is not a directory")
return False
arch_hashes = compute_arch_hashes(arch_dir)
hash_db.setdefault("archs", {})[arch_name] = arch_hashes
save_hash_db(hash_file, hash_db)
print(f"Updated hashes for {arch_name}")
return True
def compute_all_hashes(configs_dir: Path, hash_file: Path) -> bool:
"""Compute and store hashes for all architectures under configs_dir."""
configs_path = Path(configs_dir)
if not configs_path.is_dir():
print(f"Error: {configs_dir} is not a directory")
return False
hash_db = {"archs": {}}
for arch_dir in sorted(configs_path.iterdir()):
if arch_dir.is_dir() and arch_dir.name.startswith("gfx"):
arch_name = arch_dir.name
hash_db["archs"][arch_name] = compute_arch_hashes(arch_dir)
print(f"Computed hashes for {arch_name}")
save_hash_db(hash_file, hash_db)
print(f"\nHash database saved to {hash_file}")
return True
def _print_change_summary(changes: dict) -> None:
print("Change Detection Results")
print("=" * 80)
if changes["new_archs"]:
print("\nNew Architectures")
for arch in changes["new_archs"]:
print(f" • {arch}")
if changes["modified_archs"]:
print("\nModified Architectures")
for arch, files in changes["modified_archs"].items():
print(f" • {arch}")
for f in files:
print(f" - {f}")
if changes["delta_files"]:
print("\nDelta Files Detected")
for arch, delta_file in changes["delta_files"].items():
print(f" • {arch}: {delta_file}")
if changes["deleted_archs"]:
print("\nDeleted Architectures")
for arch in changes["deleted_archs"]:
print(f" • {arch}")
if not any([
changes["new_archs"],
changes["modified_archs"],
changes["delta_files"],
changes["deleted_archs"],
]):
print("\nNo changes detected")
def main() -> int:
parser = argparse.ArgumentParser(
description="Manage configuration file hashes for change detection"
)
parser.add_argument(
"--compute-all",
action="store_true",
help="Compute hashes for all architectures",
)
parser.add_argument(
"--detect-changes", action="store_true", help="Detect changes in configurations"
)
parser.add_argument(
"--update", metavar="ARCH", help="Update hashes for specific architecture"
)
parser.add_argument("configs_dir", help="Path to analysis_configs directory")
parser.add_argument(
"hash_file",
nargs="?",
default=DEFAULT_HASH_DB,
help="Path to hash database file",
)
args = parser.parse_args()
configs_dir = Path(args.configs_dir)
hash_file = Path(args.hash_file)
if args.compute_all:
success = compute_all_hashes(configs_dir, hash_file)
return 0 if success else 1
if args.detect_changes:
changes = detect_changes(configs_dir, hash_file)
_print_change_summary(changes)
return 0
if args.update:
success = update_hashes(args.update, configs_dir, hash_file)
return 0 if success else 1
parser.print_help()
return 1
if __name__ == "__main__":
sys.exit(main())