Skip to content

Instantly share code, notes, and snippets.

@drindt
Last active July 6, 2025 17:09
Show Gist options
  • Select an option

  • Save drindt/47f97fe6a9d2ed49346bb0e3b25c878b to your computer and use it in GitHub Desktop.

Select an option

Save drindt/47f97fe6a9d2ed49346bb0e3b25c878b to your computer and use it in GitHub Desktop.
A tool for fixing dependency verification errors.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ======================================================================================
# Gradle Guardian - A tool for fixing dependency verification errors.
# https://gemini.google.com/app/98400daeaa43b060
#
# Author: Gemini, based on ideas and feedback from the user `drindt`.
# Version: 0.13.0
#
# SCRIPT WORKFLOW & USAGE:
#
# This script automates the process of fixing the two most common Gradle dependency
# verification errors: missing checksums and untrusted GPG keys.
#
# It can be operated in two modes:
#
# 1. One-Shot Mode (Default):
# $ ./gradle_guardian.py
# - In this mode, NO Gradle build is executed automatically.
# - The script finds the most recent `dependency-verification-report.html`,
# parses it, and directly corrects the `verification-metadata.xml` file.
# - After running, you must manually trigger your build again (e.g., in your IDE
# or on the command line) to see if the issues are resolved.
#
# 2. Automated Loop Mode (Optional):
# $ ./gradle_guardian.py --task check
# - By providing a Gradle task with the `--task` argument, you enable the
# automated build-and-fix loop.
# - WHY A LOOP IS NEEDED: Fixing one dependency error (e.g., trusting a key for
# a library) can cause Gradle to resolve its dependencies, which in turn might
# reveal *new* verification errors from these "transitive" dependencies. The
# automated loop handles this by repeatedly running the build and applying
# fixes until all related errors are resolved.
# - The loop aborts on success or if it detects it's stuck on the same error.
# - Recommended tasks: `check` (faster) or `assembleDebug` (very reliable).
#
# ======================================================================================
import hashlib
import os
import re
import glob
import subprocess
import argparse
from bs4 import BeautifulSoup
import xml.etree.ElementTree as ET
from xml.dom import minidom
# --- Configuration ---
GRADLE_DIR = "gradle"
VERIFICATION_XML_PATH = os.path.join(GRADLE_DIR, "verification-metadata.xml")
# ANSI color codes for terminal output
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RESET = "\033[0m"
# --- End of Configuration ---
def get_user_identity():
"""
Tries to get the git username. If not found, falls back to the system user.
"""
try:
result = subprocess.run(["git", "config", "user.name"], capture_output=True, text=True, check=True)
username = result.stdout.strip()
if username:
return username
except (subprocess.CalledProcessError, FileNotFoundError):
pass
system_user = os.environ.get("USER")
if system_user:
return system_user
return "fixer tool"
def find_latest_report_file(search_dir="."):
"""
Recursively searches the 'build' directory to find the most recent
'dependency-verification-report.html' file.
"""
print(f"\n--- Searching for the latest report in '{os.path.join(search_dir, 'build')}' directory ---")
search_pattern = os.path.join(search_dir, "build", "**", "dependency-verification-report.html")
report_files = glob.glob(search_pattern, recursive=True)
if not report_files:
print(f"{RED}ERROR: No 'dependency-verification-report.html' found in the 'build' directory.{RESET}")
return None
latest_file = max(report_files, key=os.path.getmtime)
print(f" [Found] Using latest report: '{latest_file}'")
return latest_file
def parse_html_report(report_path):
"""
Parses the HTML report for missing checksums and untrusted keys.
"""
print(f"\n--- Parsing HTML report ---")
if not os.path.exists(report_path):
print(f"{RED}ERROR: Report file not found at '{report_path}'{RESET}")
return {}
with open(report_path, "r", encoding="utf-8") as f:
soup = BeautifulSoup(f, "html.parser")
results = {"missing_checksums": [], "untrusted_keys": []}
unique_checksum_artifacts = set()
unique_key_artifacts = set()
all_rows = soup.find_all("tr")
for tr in all_rows:
cells = tr.find_all("td")
if len(cells) < 3:
continue
module_string = cells[0].get_text(strip=True)
problem_cell = cells[2]
# Check for missing checksums
checksum_span = problem_cell.find(
"span",
string=re.compile(r"^\s*Checksums are missing from verification metadata\s*$"),
)
if checksum_span:
artifact_link = cells[1].find("a")
if module_string and artifact_link and artifact_link.has_attr("href"):
file_path = artifact_link["href"].replace("file:", "")
artifact_name = artifact_link.get_text(strip=True)
if (module_string, file_path) not in unique_checksum_artifacts:
results["missing_checksums"].append(
{
"module": module_string,
"file_path": file_path,
"artifact_name": artifact_name,
}
)
unique_checksum_artifacts.add((module_string, file_path))
# Check for untrusted keys
key_span = problem_cell.find("span", style="font-weight:bold; color: #c59434")
if key_span and "key is not in your trusted key list" in key_span.get_text():
key_id_tag = key_span.find("b")
if module_string and key_id_tag:
key_id = key_id_tag.get_text(strip=True).split(" ")[0]
if (module_string, key_id) not in unique_key_artifacts:
results["untrusted_keys"].append({"module": module_string, "key_id": key_id})
unique_key_artifacts.add((module_string, key_id))
if not results["missing_checksums"] and not results["untrusted_keys"]:
print(" No fixable problems found in the report.")
else:
print(f" [Found] {len(results['missing_checksums'])} artifacts with missing checksums.")
print(f" [Found] {len(results['untrusted_keys'])} artifacts with untrusted keys.")
return results
def calculate_checksums(file_path):
"""
Calculates the SHA-256 and SHA-512 checksums for a given file.
"""
expanded_path = os.path.expanduser(file_path)
if not os.path.exists(expanded_path):
print(f" {YELLOW}WARNING: File not found at '{expanded_path}'. Skipping checksum calculation.{RESET}")
return None
sha256, sha512 = hashlib.sha256(), hashlib.sha512()
try:
with open(expanded_path, "rb") as f:
while True:
data = f.read(65536)
if not data:
break
sha256.update(data)
sha512.update(data)
return {"sha256": sha256.hexdigest(), "sha512": sha512.hexdigest()}
except IOError as e:
print(f" {RED}ERROR reading file '{expanded_path}': {e}{RESET}")
return None
def update_verification_xml(xml_path, problems):
"""
Updates the verification-metadata.xml file and overwrites it directly.
"""
print(f"\n--- Updating '{xml_path}' ---")
if not os.path.exists(xml_path):
print(f"{RED}ERROR: XML file not found at '{xml_path}'{RESET}")
return False
with open(xml_path, "r", encoding="utf-8") as f:
original_content = f.read()
xml_start_index = original_content.find("<verification-metadata")
if xml_start_index == -1:
print(f"{RED}ERROR: Could not find the <verification-metadata> root element.{RESET}")
return False
header = original_content[:xml_start_index]
xml_body = original_content[xml_start_index:]
namespace = "https://schema.gradle.org/dependency-verification"
ET.register_namespace("", namespace)
root = ET.fromstring(xml_body)
user = get_user_identity()
origin_text = f"Checksum created by tool from {user}"
# Handle missing checksums
if problems.get("missing_checksums"):
components_node = root.find(f"{{{namespace}}}components")
if components_node is None:
return False
for dep in problems["missing_checksums"]:
group, name, version = dep["module"].split(":")
artifact_name = dep["artifact_name"]
checksums = calculate_checksums(dep["file_path"])
if not checksums:
continue
comp_xpath = f".//{{{namespace}}}component[@group='{group}'][@name='{name}'][@version='{version}']"
component_node = components_node.find(comp_xpath)
if component_node is None:
component_node = ET.SubElement(
components_node,
"component",
{"group": group, "name": name, "version": version},
)
art_xpath = f".//{{{namespace}}}artifact[@name='{artifact_name}']"
artifact_node = component_node.find(art_xpath)
if artifact_node is None:
artifact_node = ET.SubElement(component_node, "artifact", {"name": artifact_name})
else:
for sha_tag in list(artifact_node):
artifact_node.remove(sha_tag)
ET.SubElement(
artifact_node,
"sha256",
{"value": checksums["sha256"], "origin": origin_text},
)
ET.SubElement(
artifact_node,
"sha512",
{"value": checksums["sha512"], "origin": origin_text},
)
# Handle untrusted keys
if problems.get("untrusted_keys"):
trusted_keys_node = root.find(f".//{{{namespace}}}trusted-keys")
if trusted_keys_node is None:
return False
for dep in problems["untrusted_keys"]:
group, name, version = dep["module"].split(":")
key_id = dep["key_id"]
key_xpath = (
f".//{{{namespace}}}trusted-key[@id='{key_id}'][@group='{group}'][@name='{name}'][@version='{version}']"
)
if trusted_keys_node.find(key_xpath) is None:
print(f" Adding trusted key: ID {key_id} for {dep['module']}")
ET.SubElement(
trusted_keys_node,
"trusted-key",
{"id": key_id, "group": group, "name": name, "version": version},
)
xml_str = ET.tostring(root, encoding="unicode")
reparsed = minidom.parseString(xml_str)
pretty_xml_str = reparsed.toprettyxml(indent=" ", newl="\n")[reparsed.toprettyxml().find("?>") + 2 :].strip()
cleaned_xml_lines = [line for line in pretty_xml_str.split("\n") if line.strip()]
cleaned_xml_str = "\n".join(cleaned_xml_lines)
with open(xml_path, "w", encoding="utf-8") as f:
f.write(header)
f.write(cleaned_xml_str)
print(f" [SUCCESS] The file '{xml_path}' has been updated.")
return True
def run_gradle_build(task):
"""
Executes a Gradle build task that triggers dependency verification.
"""
command = ["./gradlew", task, "--continue"]
print("\n" + "=" * 60)
print(f"=== Starting Gradle build: {' '.join(command)} ===")
print("=" * 60)
try:
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
encoding="utf-8",
)
for line in iter(process.stdout.readline, ""):
print(line, end="")
process.wait()
print("=" * 60)
print(f"=== Gradle task finished with exit code: {process.returncode} ===")
print("=" * 60)
return process.returncode
except FileNotFoundError:
print(f"{RED}ERROR: './gradlew' not found. Please run this script from your project's root directory.{RESET}")
return -1
def canonicalize_problems(problems_dict):
"""Converts the problems dictionary into a canonical, hashable form for comparison."""
if not problems_dict:
return None
checksums = frozenset(frozenset(d.items()) for d in problems_dict.get("missing_checksums", []))
keys = frozenset(frozenset(d.items()) for d in problems_dict.get("untrusted_keys", []))
return (checksums, keys)
def main():
"""
Main function to orchestrate the entire build-and-fix loop.
"""
parser = argparse.ArgumentParser(
description="A tool to automatically fix Gradle dependency verification errors.",
formatter_class=argparse.RawTextHelpFormatter,
epilog="Example for one-shot fix (default): ./gradle_guardian.py\n"
"Example for automated loop: ./gradle_guardian.py --task check",
)
parser.add_argument(
"--task",
default=None,
help='(Optional) The Gradle task to run in an automated loop (e.g., "check", "assembleDebug").\n'
"If this is not provided, the script runs in one-shot mode, fixing the\n"
"latest report without running Gradle itself.",
)
args = parser.parse_args()
print("=================================================")
print(f"=== {GREEN}Gradle Guardian{RESET} ===")
print("=================================================")
if args.task:
# Automated Loop Mode
print(f"=== Automated Mode active with task: {GREEN}{args.task}{RESET} ===")
last_problems = None
build_succeeded = False
made_changes = False
attempt = 1
max_retries = 10 # Safety break
while attempt <= max_retries:
print(f"\n{YELLOW}--- Build Attempt #{attempt} ---{RESET}")
exit_code = run_gradle_build(args.task)
if exit_code == 0:
print(f"\n{GREEN}SUCCESS: Gradle build completed successfully!{RESET}")
build_succeeded = True
break
print(f"\n{YELLOW}Build failed. Attempting to fix verification metadata...{RESET}")
report_html_path = find_latest_report_file()
if not report_html_path:
print(
f"{RED}Could not find a verification report. The build might have failed for a different reason. Exiting.{RESET}"
)
break
current_problems = parse_html_report(report_html_path)
canonical_current_problems = canonicalize_problems(current_problems)
if not (current_problems.get("missing_checksums") or current_problems.get("untrusted_keys")):
print(
f"{RED}Build failed, but no fixable problems were found in the report. Cannot fix automatically. Exiting.{RESET}"
)
break
if canonical_current_problems == last_problems:
print(
f"{RED}Build is still failing with the same verification errors. The script cannot make further progress. Exiting.{RESET}"
)
break
try:
with open(VERIFICATION_XML_PATH, "r", encoding="utf-8") as f:
content_before = f.read()
except FileNotFoundError:
content_before = ""
if update_verification_xml(VERIFICATION_XML_PATH, current_problems):
with open(VERIFICATION_XML_PATH, "r", encoding="utf-8") as f:
content_after = f.read()
if content_before != content_after:
made_changes = True
else:
break
last_problems = canonical_current_problems
attempt += 1
if not build_succeeded:
print(
f"\n{RED}ERROR: Build did not succeed after {attempt-1} attempts. Please check the Gradle output for persistent errors.{RESET}"
)
if build_succeeded and made_changes:
print(f"{RED}\nIMPORTANT: Because fixing dependencies can reveal new transitive dependencies,")
print(f"you may need to run the build and this tool again if new errors appear after committing.{RESET}")
else:
# One-Shot Mode (Default)
print(f"=== One-Shot Mode (no task specified) ===")
report_html_path = find_latest_report_file()
if not report_html_path:
print("\nNo report found. Nothing to do.")
else:
problems = parse_html_report(report_html_path)
if problems.get("missing_checksums") or problems.get("untrusted_keys"):
update_verification_xml(VERIFICATION_XML_PATH, problems)
print(f"\n{GREEN}Metadata file updated. Please run your build process again to verify.{RESET}")
else:
print("\nNo fixable problems found in the latest report.")
print("\n=================================================")
print("=== Tool execution finished ===")
print("=================================================")
if __name__ == "__main__":
main()
@drindt
Copy link
Author

drindt commented Jul 6, 2025

If you know the name of the task that Gradle calls in IntelliJ to synchronize the project, please let me know. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment