Skip to content

Instantly share code, notes, and snippets.

@kibotu
Last active May 16, 2025 09:16
Show Gist options
  • Save kibotu/6a7507cb248817b3159fa5f8a37dbe6c to your computer and use it in GitHub Desktop.
Save kibotu/6a7507cb248817b3159fa5f8a37dbe6c to your computer and use it in GitHub Desktop.
Consumer ProGuard Rules Report
#!/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 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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</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
}
@kibotu
Copy link
Author

kibotu commented May 16, 2025

./extract-proguard-rules.sh
Screenshot 2025-05-16 at 11 00 25

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