Last active
May 16, 2025 09:16
-
-
Save kibotu/6a7507cb248817b3159fa5f8a37dbe6c to your computer and use it in GitHub Desktop.
Consumer ProGuard Rules Report
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Extract consumer ProGuard rules from all dependencies and generate an HTML report | |
# Author: Jan Rabe | |
set -e | |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
cd "$SCRIPT_DIR" | |
# Print banner | |
echo "==================================================" | |
echo " ProGuard Consumer Rules Extractor" | |
echo "==================================================" | |
echo | |
# Clean up any previous build artifacts | |
echo "Cleaning previous build artifacts..." | |
rm -rf build/reports/proguard | |
# Run Gradle task to extract ProGuard rules | |
echo "Extracting ProGuard rules from dependencies..." | |
./gradlew clean --no-build-cache --no-configuration-cache -b proguard-extractor.gradle extractProguardRules || { | |
echo | |
echo "Error: Failed to extract ProGuard rules." | |
echo "Try running with ./gradlew clean --no-build-cache --no-configuration-cache -b proguard-extractor.gradle --stacktrace extractProguardRules" | |
exit 1 | |
} | |
# Find the generated HTML report | |
REPORT_PATH=$(find build -name "consumer-rules-report.html" | head -n 1) | |
if [ -z "$REPORT_PATH" ]; then | |
echo "Error: Could not find generated report." | |
exit 1 | |
fi | |
echo | |
echo "ProGuard rules report generated at: $REPORT_PATH" | |
echo | |
# Open the HTML report in the default browser | |
if [[ "$OSTYPE" == "darwin"* ]]; then | |
# macOS | |
open "$REPORT_PATH" || echo "Could not open the report. Please open it manually at: $REPORT_PATH" | |
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then | |
# Linux | |
xdg-open "$REPORT_PATH" &> /dev/null || echo "Could not open the report. Please open it manually at: $REPORT_PATH" | |
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then | |
# Windows | |
start "$REPORT_PATH" || echo "Could not open the report. Please open it manually at: $REPORT_PATH" | |
else | |
echo "Please open the report manually: $REPORT_PATH" | |
fi | |
echo "Done!" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This script extracts consumer ProGuard rules from all dependencies in the release configuration | |
// and generates an HTML report. | |
// Usage: ./gradlew -b proguard-extractor.gradle extractProguardRules | |
// Remove the startParameter reference as it's not accessible in this context | |
// startParameter.buildCacheEnabled = false | |
buildscript { | |
repositories { | |
gradlePluginPortal() | |
google() | |
mavenCentral() | |
} | |
} | |
apply plugin: 'base' // Provides 'clean' task and build directory | |
task extractProguardRules { | |
description = 'Extracts consumer ProGuard rules from all dependencies in the release configuration' | |
group = 'Reporting' | |
doLast { | |
def proguardRulesMap = [:] | |
// Find the root project | |
def rootDir = project.rootDir | |
def settingsFile = new File(rootDir, 'settings.gradle') | |
if (!settingsFile.exists()) { | |
throw new GradleException("Could not find settings.gradle in ${rootDir}") | |
} | |
// Get the app's configuration | |
def appDir = new File(rootDir, 'app') | |
def appBuildFile = new File(appDir, 'build.gradle') | |
if (!appBuildFile.exists()) { | |
throw new GradleException("Could not find app/build.gradle in ${rootDir}") | |
} | |
// Simpler approach: find all consumer-rules.pro files recursively in all potential locations | |
println "Scanning for consumer ProGuard rules..." | |
// Define all places to look for ProGuard rules | |
def searchDirectories = [ | |
new File(rootDir, '.gradle'), | |
new File(System.getProperty('user.home'), '.gradle'), | |
new File(appDir, 'build') | |
] | |
// Define all ProGuard rule file patterns to search for | |
def proguardPatterns = [ | |
'**/consumer-rules.pro', | |
'**/proguard.txt', | |
'**/proguard-rules.pro', | |
'**/proguard-project.txt', | |
'**/META-INF/proguard/*' | |
] | |
// Process each search directory | |
searchDirectories.each { searchDir -> | |
if (searchDir.exists()) { | |
println "Searching in: ${searchDir.absolutePath}" | |
proguardPatterns.each { pattern -> | |
project.fileTree(dir: searchDir, include: pattern).visit { file -> | |
if (!file.directory) { | |
processPotentialProguardFile(file.file, proguardRulesMap) | |
} | |
} | |
} | |
} | |
} | |
// Generate HTML report | |
def htmlOutput = new File(project.buildDir, "reports/proguard/consumer-rules-report.html") | |
htmlOutput.parentFile.mkdirs() | |
generateHtmlReport(htmlOutput, proguardRulesMap) | |
println "Found ${proguardRulesMap.size()} dependencies with consumer ProGuard rules" | |
println "ProGuard Rules Report generated at: ${htmlOutput.absolutePath}" | |
} | |
} | |
// Helper method to process a potential ProGuard rule file and extract the dependency info | |
def processPotentialProguardFile(File file, Map<String, String> proguardRulesMap) { | |
def path = file.absolutePath | |
// Try to extract group:artifact:version from path | |
def pathSegments = path.split(File.separator) | |
// Better path scanning algorithm that looks for the Maven structure | |
def foundMavenPath = false | |
// Look for patterns that match maven repository structure | |
// which typically has modules/group/artifact/version/files | |
for (int i = 0; i < pathSegments.length - 4; i++) { | |
// Check if we have a potential Maven structure | |
if (pathSegments[i] == "modules-2" && i + 4 < pathSegments.length) { | |
def potentialGroup = pathSegments[i+1] | |
def potentialArtifact = pathSegments[i+2] | |
def potentialVersion = pathSegments[i+3] | |
// Check if the potential version looks like a version number | |
if (potentialVersion ==~ /[0-9].+/ || potentialVersion ==~ /.+\.[0-9].+/) { | |
def dependency = "${potentialGroup}:${potentialArtifact}:${potentialVersion}" | |
// Don't override existing entries | |
if (!proguardRulesMap.containsKey(dependency)) { | |
// Skip empty files | |
def content = file.text.trim() | |
if (!content.isEmpty()) { | |
proguardRulesMap[dependency] = content | |
println "Found ProGuard rules for: ${dependency}" | |
foundMavenPath = true | |
break | |
} | |
} | |
} | |
} | |
} | |
// If we didn't find a Maven path, try another pattern common in .gradle folder | |
if (!foundMavenPath) { | |
for (int i = 0; i < pathSegments.length - 3; i++) { | |
if (pathSegments[i] == "files-2.1" && i + 3 < pathSegments.length) { | |
def potentialGroup = pathSegments[i+1] | |
def potentialArtifact = pathSegments[i+2] | |
def potentialVersion = pathSegments[i+3] | |
def dependency = "${potentialGroup}:${potentialArtifact}:${potentialVersion}" | |
// Skip dependencies that look suspicious | |
if (potentialGroup == ".gradle" || potentialArtifact == "caches") { | |
continue | |
} | |
// Don't override existing entries | |
if (!proguardRulesMap.containsKey(dependency)) { | |
// Skip empty files | |
def content = file.text.trim() | |
if (!content.isEmpty()) { | |
proguardRulesMap[dependency] = content | |
println "Found ProGuard rules for: ${dependency}" | |
foundMavenPath = true | |
break | |
} | |
} | |
} | |
} | |
} | |
// If we still couldn't determine the dependency, use an approach based on file content | |
if (!foundMavenPath) { | |
// Try to infer the library name from the rules content | |
def content = file.text.trim() | |
if (!content.isEmpty()) { | |
// Look for package names in the rules to infer the library | |
def packageMatch = content =~ /(?:(?:class|interface)\s+|keep\s+(?:class|interface)\s+)([a-zA-Z0-9_.]+)/ | |
if (packageMatch.find()) { | |
def packageName = packageMatch.group(1) | |
// Convert package name to dependency format | |
def parts = packageName.split("\\.") | |
if (parts.length >= 2) { | |
def group = parts[0] | |
def artifact = parts[1] | |
for (int i = 2; i < Math.min(parts.length, 4); i++) { | |
artifact += "." + parts[i] | |
} | |
def dependency = "${group}:${artifact}:unknown" | |
// Don't override existing entries | |
if (!proguardRulesMap.containsKey(dependency)) { | |
proguardRulesMap[dependency] = content | |
println "Found ProGuard rules for inferred dependency: ${dependency}" | |
foundMavenPath = true | |
} | |
} | |
} | |
} | |
} | |
// Last resort: use the file name as a fallback | |
if (!foundMavenPath) { | |
def fileName = file.name | |
if (fileName != "consumer-rules.pro" && fileName != "proguard.txt" && fileName != "proguard-rules.pro") { | |
// Extract a reasonable name from the path | |
def parent = file.parentFile.name | |
def dependency = "unknown:${parent}:${fileName}" | |
// Don't override existing entries | |
if (!proguardRulesMap.containsKey(dependency)) { | |
// Skip empty files | |
def content = file.text.trim() | |
if (!content.isEmpty()) { | |
proguardRulesMap[dependency] = content | |
println "Found ProGuard rules (unknown dependency): ${dependency}" | |
} | |
} | |
} else { | |
// For standard proguard file names, use more path components | |
def pathParts = [] | |
def currentDir = file.parentFile | |
for (int i = 0; i < 3 && currentDir != null; i++) { | |
pathParts.add(0, currentDir.name) | |
currentDir = currentDir.parentFile | |
} | |
if (!pathParts.isEmpty()) { | |
def dependency = "unknown:" + pathParts.join(".") + ":rules" | |
// Don't override existing entries | |
if (!proguardRulesMap.containsKey(dependency)) { | |
// Skip empty files | |
def content = file.text.trim() | |
if (!content.isEmpty()) { | |
proguardRulesMap[dependency] = content | |
println "Found ProGuard rules (unknown path): ${dependency}" | |
} | |
} | |
} | |
} | |
} | |
} | |
// Method to generate an HTML report | |
def generateHtmlReport(File outputFile, Map<String, String> proguardRulesMap) { | |
def html = """<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Consumer ProGuard Rules Report</title> | |
<style> | |
:root { | |
--primary-color: #007bff; | |
--secondary-color: #6c757d; | |
--success-color: #28a745; | |
--bg-color: #f8f9fa; | |
--card-bg: #ffffff; | |
--text-color: #343a40; | |
--border-color: #dee2e6; | |
} | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
line-height: 1.6; | |
color: var(--text-color); | |
background-color: var(--bg-color); | |
margin: 0; | |
padding: 20px; | |
} | |
.container { | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
header { | |
background-color: var(--primary-color); | |
color: white; | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 30px; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
h1 { | |
margin: 0; | |
font-size: 2.2rem; | |
} | |
.summary { | |
background-color: var(--card-bg); | |
border-radius: 8px; | |
padding: 20px; | |
margin-bottom: 30px; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
} | |
.dependency-card { | |
background-color: var(--card-bg); | |
border-radius: 8px; | |
padding: 20px; | |
margin-bottom: 20px; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
transition: transform 0.2s, box-shadow 0.2s; | |
} | |
.dependency-card:hover { | |
transform: translateY(-3px); | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
.dependency-name { | |
font-weight: bold; | |
color: var(--primary-color); | |
font-size: 1.4rem; | |
padding-bottom: 10px; | |
border-bottom: 1px solid var(--border-color); | |
margin: 0 0 15px 0; | |
} | |
.rules-container { | |
background-color: #f5f5f5; | |
border-radius: 6px; | |
padding: 15px; | |
white-space: pre-wrap; | |
font-family: 'Courier New', Courier, monospace; | |
overflow-x: auto; | |
border: 1px solid var(--border-color); | |
} | |
.no-rules { | |
color: var(--secondary-color); | |
font-style: italic; | |
} | |
.stats { | |
display: flex; | |
gap: 20px; | |
flex-wrap: wrap; | |
} | |
.stat-card { | |
flex: 1; | |
min-width: 200px; | |
background-color: var(--primary-color); | |
color: white; | |
padding: 15px; | |
border-radius: 8px; | |
text-align: center; | |
} | |
.stat-number { | |
font-size: 2rem; | |
font-weight: bold; | |
margin: 10px 0; | |
} | |
.stat-label { | |
font-size: 0.9rem; | |
opacity: 0.9; | |
} | |
footer { | |
text-align: center; | |
margin-top: 40px; | |
padding: 20px; | |
color: var(--secondary-color); | |
font-size: 0.9rem; | |
} | |
@media (prefers-color-scheme: dark) { | |
:root { | |
--primary-color: #0d6efd; | |
--bg-color: #212529; | |
--card-bg: #2b3035; | |
--text-color: #f8f9fa; | |
--border-color: #495057; | |
} | |
.rules-container { | |
background-color: #343a40; | |
color: #f8f9fa; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<header> | |
<h1>Consumer ProGuard Rules Report</h1> | |
<p>Generated on ${new Date().format("yyyy-MM-dd 'at' HH:mm:ss")}</p> | |
</header> | |
<div class="summary"> | |
<h2>Summary</h2> | |
<div class="stats"> | |
<div class="stat-card"> | |
<div class="stat-number">${proguardRulesMap.size()}</div> | |
<div class="stat-label">Dependencies with ProGuard Rules</div> | |
</div> | |
<div class="stat-card"> | |
<div class="stat-number">${proguardRulesMap.values().join(' ').count('\n') + proguardRulesMap.size()}</div> | |
<div class="stat-label">Total Rules Lines</div> | |
</div> | |
</div> | |
</div> | |
<h2>ProGuard Rules by Dependency</h2> | |
""" | |
// Sort dependencies alphabetically | |
def sortedDependencies = proguardRulesMap.keySet().sort() | |
// Add each dependency and its rules | |
sortedDependencies.each { dependency -> | |
def rules = proguardRulesMap[dependency] | |
html += """ | |
<div class="dependency-card"> | |
<h3 class="dependency-name">${dependency}</h3> | |
<div class="rules-container">${rules.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")}</div> | |
</div>""" | |
} | |
// If no dependencies with ProGuard rules found | |
if (proguardRulesMap.isEmpty()) { | |
html += """ | |
<div class="dependency-card"> | |
<p class="no-rules">No dependencies with consumer ProGuard rules found.</p> | |
</div>""" | |
} | |
html += """ | |
<footer> | |
<p>Generated by ProGuard Rules Extractor Script</p> | |
</footer> | |
</div> | |
</body> | |
</html>""" | |
outputFile.text = html | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.