From 81ed5bc389b8512a92f67fa2c12172e29b846f4a Mon Sep 17 00:00:00 2001 From: binostr Date: Fri, 4 Jul 2025 15:13:33 -0400 Subject: [PATCH] Some minor improvements - Enhance `version-stats.py` to support both module and group/name parsing - Introduce `generate_dependency_report.sh` to generate formatted dependency reports - Update `.gitignore` to exclude `dependency_report.txt` - Update README with usage instructions for `generate_dependency_report.sh` --- .gitignore | 3 +- README.md | 52 ++++++++++-- generate_dependency_report.sh | 51 ++++++++++++ test/test_parse_library_line.py | 32 +++++++ version-stats.py | 142 +++++++++++++++++++------------- 5 files changed, 213 insertions(+), 67 deletions(-) create mode 100755 generate_dependency_report.sh create mode 100644 test/test_parse_library_line.py diff --git a/.gitignore b/.gitignore index d6e32c8..54812da 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,5 @@ Thumbs.db *~ # Logs -*.log \ No newline at end of file +*.log +/dependency_report.txt diff --git a/README.md b/README.md index aabe5d9..de4281f 100644 --- a/README.md +++ b/README.md @@ -19,34 +19,57 @@ Un script automatizado para verificar y comparar las versiones de dependencias M - Python 3.6+ - Bash +- jq (para generar reportes formateados) - Acceso a internet para consultas Maven ## 🛠️ Instalación 1. Clona este repositorio: ```bash -git clone https://github.com/pfranccino/gradle-deps-monitor.git -cd gradle-deps-monitor +git clone https://github.com/pfranccino/toml-deps-checker.git +cd toml-deps-checker ``` -2. Dale permisos de ejecución al script: +2. Dale permisos de ejecución a los scripts: ```bash chmod +x check-dependencies.sh +chmod +x generate_dependency_report.sh ``` ## 📖 Uso +### Verificación de dependencias + Ejecuta el script proporcionando la ruta a tu directorio Gradle que contiene `libs.versions.toml`: ```bash ./check-dependencies.sh /ruta/al/directorio/gradle ``` -### Ejemplo: +#### Ejemplo: ```bash ./check-dependencies.sh ./app/gradle ``` +### Generación de reportes + +Después de ejecutar la verificación de dependencias, puedes generar un reporte formateado usando: + +```bash +./generate_dependency_report.sh [archivo_json] +``` + +Si no se proporciona un archivo JSON, se utilizará `dependency_status.json` por defecto. + +#### Ejemplo: +```bash +./generate_dependency_report.sh +# O especificando un archivo JSON personalizado: +./generate_dependency_report.sh mi_archivo.json +``` + +El script generará un archivo `dependency_report.txt` con un formato legible de todas las dependencias y sus estados. + ## 📊 Salida El script genera un archivo `dependency_status.json` con información detallada de cada dependencia: @@ -75,14 +98,18 @@ El script genera un archivo `dependency_status.json` con información detallada ## 🏗️ Estructura del proyecto ``` -├── check-dependencies.sh # Script principal de Bash -├── version-stats.py # Script de Python para análisis -├── README.md # Este archivo -└── dependency_status.json # Archivo de salida (generado) +├── check-dependencies.sh # Script principal de Bash +├── generate_dependency_report.sh # Script para generar reportes formateados +├── version-stats.py # Script de Python para análisis +├── README.md # Este archivo +├── dependency_status.json # Archivo de salida JSON (generado) +└── dependency_report.txt # Archivo de reporte formateado (generado) ``` ## ⚙️ Cómo funciona +### check-dependencies.sh + 1. **Validación**: Verifica que existe el directorio y el archivo `libs.versions.toml` 2. **Entorno virtual**: Crea y activa un entorno virtual Python 3. **Instalación**: Instala las dependencias Python necesarias (`requests`) @@ -93,6 +120,15 @@ El script genera un archivo `dependency_status.json` con información detallada 5. **Comparación**: Evalúa el estado de cada dependencia 6. **Reporte**: Genera un archivo JSON con los resultados +### generate_dependency_report.sh + +1. **Validación**: Verifica que existe el archivo JSON de entrada (por defecto `dependency_status.json`) +2. **Procesamiento**: + - Utiliza `jq` para procesar el archivo JSON + - Extrae información de cada dependencia (nombre, versión actual, última versión, estado) +3. **Formateo**: Formatea cada dependencia según un formato legible +4. **Reporte**: Genera un archivo de texto (`dependency_report.txt`) con el reporte formateado + ## 🔧 Configuración ### Tipos de dependencias soportadas diff --git a/generate_dependency_report.sh b/generate_dependency_report.sh new file mode 100755 index 0000000..e8c0842 --- /dev/null +++ b/generate_dependency_report.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Script to generate dependency report from JSON file +# Usage: ./generate_dependency_report.sh [json_file] +# If no json_file is provided, dependency_status.json will be used by default + +echo "🚀 Iniciando generador de reporte de dependencias" + +# Check if jq is installed +if ! command -v jq &> /dev/null; then + echo "❌ Error: jq is required but not installed. Please install jq." + exit 1 +fi + +# Path to input and output files +JSON_FILE="${1:-dependency_status.json}" +REPORT_FILE="dependency_report.txt" + +echo "📂 Archivo de entrada: $JSON_FILE" +echo "📄 Archivo de salida: $REPORT_FILE" + +# Check if JSON file exists +if [ ! -f "$JSON_FILE" ]; then + echo "❌ Error: $JSON_FILE not found." + exit 1 +fi + +# Clear the report file if it exists +> "$REPORT_FILE" + +# Process each dependency in the JSON file +echo "🔎 Procesando dependencias..." +jq -r 'to_entries | .[] | @json' "$JSON_FILE" | while read -r dependency; do + # Extract dependency information + name=$(echo "$dependency" | jq -r '.key') + status=$(echo "$dependency" | jq -r '.value.status') + version_used=$(echo "$dependency" | jq -r '.value.version_used') + latest_version=$(echo "$dependency" | jq -r '.value.latest_version') + + # Format the line according to the specified format + formatted_line="$status *$name* - Actual: \`$version_used\`| Última: \`$latest_version\`" + + # Append the formatted line to the report file + echo "$formatted_line" >> "$REPORT_FILE" + + # Print the formatted line to the terminal + echo "📝 Añadiendo: $formatted_line" +done + +echo "✅ Reporte de dependencias generado: $REPORT_FILE" +echo "✨ Proceso completado" diff --git a/test/test_parse_library_line.py b/test/test_parse_library_line.py new file mode 100644 index 0000000..88ebda7 --- /dev/null +++ b/test/test_parse_library_line.py @@ -0,0 +1,32 @@ +import sys +import importlib.util + +# Import the module using the file path +spec = importlib.util.spec_from_file_location("version_stats", "../version-stats.py") +version_stats = importlib.util.module_from_spec(spec) +spec.loader.exec_module(version_stats) + +# Get the MavenVersionChecker class +MavenVersionChecker = version_stats.MavenVersionChecker + +# Create a test instance +checker = MavenVersionChecker() + +# Test versions dictionary +versions = { + "turbine": "0.12.1", + "coil": "2.2.2" +} + +# Test both formats +print("Testing module format:") +line1 = 'turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }' +result1 = checker.parse_library_line(line1, versions) +print(f"Result: {result1}") + +print("\nTesting group and name format:") +line2 = 'compose-coil = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil"}' +result2 = checker.parse_library_line(line2, versions) +print(f"Result: {result2}") + +print("\nTest completed.") diff --git a/version-stats.py b/version-stats.py index 5d8822a..ffe42d7 100644 --- a/version-stats.py +++ b/version-stats.py @@ -28,12 +28,12 @@ def get_google_version(self, group_id: str, artifact_id: str) -> Optional[str]: path_group = group_id.replace('.', '/') google_url = f"https://dl.google.com/android/maven2/{path_group}/{artifact_id}/maven-metadata.xml" print(f"🔍 Consultando Google Maven: {google_url}") - + response = requests.get(google_url, timeout=5) if response.status_code != 200: print(f"❌ Error en respuesta de Google Maven: {response.status_code}") return None - + root = ET.fromstring(response.content) versioning = root.find('versioning') if versioning is not None: @@ -42,16 +42,16 @@ def get_google_version(self, group_id: str, artifact_id: str) -> Optional[str]: if release is not None: print(f"✅ Versión release encontrada en Google Maven: {release.text}") return release.text - + # Si no hay 'release', intentamos con 'latest' latest = versioning.find('latest') if latest is not None: print(f"✅ Versión latest encontrada en Google Maven (no se encontró release): {latest.text}") return latest.text - + print("⚠️ No se encontró versión en Google Maven") return None - + except Exception as e: print(f"❌ Error consultando Google Maven: {str(e)}") return None @@ -71,33 +71,33 @@ def get_latest_version(self, group_id: str, artifact_id: str) -> Optional[str]: 'rows': '1', 'wt': 'json' } - + url = self.maven_search_url query_string = '&'.join([f'{k}={v}' for k, v in params.items()]) full_url = f"{url}?{query_string}" print(f"🔍 URL de búsqueda Maven Central: {full_url}") - + response = requests.get(self.maven_search_url, params=params, timeout=5) print(f"📡 Código de respuesta: {response.status_code}") - + if response.status_code != 200: print(f"❌ Error en la respuesta: {response.status_code}") return None - + data = response.json() print(f"📥 Respuesta recibida: {json.dumps(data, indent=2)}") - + if (data.get('response', {}).get('docs') and len(data['response']['docs']) > 0 and 'v' in data['response']['docs'][0]): - + latest = data['response']['docs'][0]['v'] print(f"✅ Última versión encontrada en Maven Central: {latest}") return latest - + print(f"⚠️ No se encontraron versiones para {group_id}:{artifact_id}") return None - + except requests.Timeout: print(f"⏰ Timeout buscando {group_id}:{artifact_id}") return None @@ -115,7 +115,7 @@ def compare_versions(self, current: str, latest: str) -> str: - ⚫ : No se puede determinar (versión no encontrada o error) """ print(f"\n🔄 Comparando versiones - Actual: {current}, Última: {latest}") - + # Si no se encontró la última versión o hay algún problema if not current or not latest or latest == "N/A": print("⚫ No se pueden comparar las versiones - versión no encontrada") @@ -124,7 +124,7 @@ def compare_versions(self, current: str, latest: str) -> str: try: current_parts = self._parse_version(current) latest_parts = self._parse_version(latest) - + if not current_parts or not latest_parts: print("⚫ Error al parsear las versiones") return "⚫" @@ -144,28 +144,28 @@ def compare_versions(self, current: str, latest: str) -> str: if latest_major > current_major: print("🔴 Diferencia en versión mayor (Major)") return "🔴" - + # Diferencia en versión menor (Minor) if latest_minor > current_minor: print("🟡 Diferencia en versión menor (Minor)") return "🟡" - + # Diferencia en versión patch if latest_patch > current_patch: patch_diff = latest_patch - current_patch print(f"📊 Diferencia en patch: {patch_diff}") - + if patch_diff > 5: print("🟡 Diferencia en patch > 5") return "🟡" else: print("🟢 Diferencia en patch ≤ 5") return "🟢" - + # Si la versión actual es más nueva que la "latest" encontrada print("🟢 Versión actual es igual o más nueva") return "🟢" - + except Exception as e: print(f"❌ Error en comparación: {str(e)}") return "⚫" @@ -177,12 +177,12 @@ def _parse_version(self, version: str) -> Optional[tuple]: version = re.sub(r'^[vV]', '', version) # Tomar solo la parte antes del primer '-' o '+' version = version.split('-')[0].split('+')[0] - + parts = version.split('.') # Asegurar que tenemos al menos 3 partes (major.minor.patch) if len(parts) < 3: parts.extend(['0'] * (3 - len(parts))) - + # Convertir a enteros solo las primeras 3 partes result = tuple(int(p) for p in parts[:3]) print(f"✅ Versión parseada: {result}") @@ -194,29 +194,55 @@ def _parse_version(self, version: str) -> Optional[tuple]: def parse_library_line(self, line: str, versions: dict) -> Optional[tuple]: print(f"\n📝 Procesando línea: {line}") try: - if '=' in line and 'module' in line: + if '=' in line: key, content = line.split('=', 1) key = key.strip() print(f"🔑 Key encontrada: {key}") - - module_match = re.search(r'module\s*=\s*"([^"]+)"', content) - version_ref_match = re.search(r'version\.ref\s*=\s*"([^"]+)"', content) - - if module_match and version_ref_match: - module = module_match.group(1) - version_ref = version_ref_match.group(1) - print(f"📦 Módulo: {module}") - print(f"🏷️ Referencia de versión: {version_ref}") - - current_version = versions.get(version_ref) - if current_version: - current_version = current_version.strip('"') - group_id, artifact_id = module.split(':', 1) - print(f"✅ Parseado - Group: {group_id}, Artifact: {artifact_id}, Version: {current_version}") - return group_id, artifact_id, current_version - else: - print(f"⚠️ No se encontró la versión para la referencia: {version_ref}") - + + # Check for module format + if 'module' in content: + module_match = re.search(r'module\s*=\s*"([^"]+)"', content) + version_ref_match = re.search(r'version\.ref\s*=\s*"([^"]+)"', content) + + if module_match and version_ref_match: + module = module_match.group(1) + version_ref = version_ref_match.group(1) + print(f"📦 Módulo: {module}") + print(f"🏷️ Referencia de versión: {version_ref}") + + current_version = versions.get(version_ref) + if current_version: + current_version = current_version.strip('"') + group_id, artifact_id = module.split(':', 1) + print(f"✅ Parseado - Group: {group_id}, Artifact: {artifact_id}, Version: {current_version}") + return group_id, artifact_id, current_version + else: + print(f"⚠️ No se encontró la versión para la referencia: {version_ref}") + + # Check for group and name format + elif 'group' in content and 'name' in content: + group_match = re.search(r'group\s*=\s*"([^"]+)"', content) + name_match = re.search(r'name\s*=\s*"([^"]+)"', content) + version_ref_match = re.search(r'version\.ref\s*=\s*"([^"]+)"', content) + + if group_match and name_match and version_ref_match: + group_id = group_match.group(1) + artifact_id = name_match.group(1) + version_ref = version_ref_match.group(1) + + # Construct module as group + ":" + name + module = f"{group_id}:{artifact_id}" + print(f"📦 Módulo construido: {module}") + print(f"🏷️ Referencia de versión: {version_ref}") + + current_version = versions.get(version_ref) + if current_version: + current_version = current_version.strip('"') + print(f"✅ Parseado - Group: {group_id}, Artifact: {artifact_id}, Version: {current_version}") + return group_id, artifact_id, current_version + else: + print(f"⚠️ No se encontró la versión para la referencia: {version_ref}") + return None except Exception as e: print(f"❌ Error parseando línea: {str(e)}") @@ -236,13 +262,13 @@ def process_toml_file(self, folder_path: str) -> Dict[str, Any]: versions = {} in_versions_section = False print(f"📖 Primera pasada: leyendo versiones del archivo: {file_path}") - + with open(file_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue - + if line == '[versions]': print("✅ Sección [versions] encontrada") in_versions_section = True @@ -250,7 +276,7 @@ def process_toml_file(self, folder_path: str) -> Dict[str, Any]: elif line.startswith('['): in_versions_section = False continue - + if in_versions_section and '=' in line: key, value = line.split('=', 1) versions[key.strip()] = value.strip() @@ -258,13 +284,13 @@ def process_toml_file(self, folder_path: str) -> Dict[str, Any]: print(f"\n📖 Segunda pasada: procesando librerías") in_libraries_section = False - + with open(file_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue - + if line == '[libraries]': print("✅ Sección [libraries] encontrada") in_libraries_section = True @@ -272,17 +298,17 @@ def process_toml_file(self, folder_path: str) -> Dict[str, Any]: elif line.startswith('['): in_libraries_section = False continue - + if in_libraries_section: parsed = self.parse_library_line(line, versions) if parsed: group_id, artifact_id, current_version = parsed dep_key = f"{group_id}:{artifact_id}" print(f"\n📦 Procesando dependencia: {dep_key}") - + latest_version = self.get_latest_version(group_id, artifact_id) status = self.compare_versions(current_version, latest_version) - + # Determinar tipo y URL basado en si es dependencia de Google if self.is_google_dependency(group_id): dep_type = "google" @@ -290,7 +316,7 @@ def process_toml_file(self, folder_path: str) -> Dict[str, Any]: else: dep_type = "maven" url = f"https://central.sonatype.com/artifact/{group_id}/{artifact_id}" - + result[dep_key] = { "url": url, "version_used": current_version, @@ -311,9 +337,9 @@ def process_toml_file(self, folder_path: str) -> Dict[str, Any]: def main(): import sys - + print("\n🚀 Iniciando verificador de versiones Maven") - + if len(sys.argv) != 2: print("❌ Error: Falta la ruta del directorio") print("Uso: python script.py ruta/al/directorio") @@ -321,19 +347,19 @@ def main(): folder_path = sys.argv[1] print(f"📂 Directorio a procesar: {folder_path}") - + checker = MavenVersionChecker() result = checker.process_toml_file(folder_path) - + # Obtener la ruta donde está el script script_dir = os.path.dirname(os.path.abspath(__file__)) output_file = os.path.join(script_dir, "dependency_status.json") print(f"\n💾 Guardando resultados en: {output_file}") - + with open(output_file, 'w', encoding='utf-8') as f: json.dump(result, f, indent=2, ensure_ascii=False) - + print("✅ Proceso completado exitosamente") if __name__ == "__main__": - main() \ No newline at end of file + main()