|
| 1 | +Below you can read the AI-generated, and human-improved explanation of the shadow JAR configuration, that's |
| 2 | +result of [#21](https://github.com/wordpress-mobile/WordPress-Lint-Android/pull/21). |
| 3 | + |
| 4 | +# Shadow JAR Logic Explanation |
| 5 | + |
| 6 | +## Overview |
| 7 | +This document explains the logic behind the Shadow JAR configuration in the WordPressLint/build.gradle file, specifically the `shadowJar`, `cleanServiceFile` task, and publishing setup. |
| 8 | + |
| 9 | +## The Problem Being Solved |
| 10 | + |
| 11 | +The WordPress Lint library has a unique requirement: |
| 12 | +1. It needs to include **selected** lint rules from Slack's lint library (not all of them) |
| 13 | +2. It must bundle all necessary classes into a single JAR file for distribution, **without declaring Slack Lint as a regular dependency**, to prevent Android Lint from loading its full issue registry and enabling all of its checks. |
| 14 | +3. It needs to ensure proper service registration with Android Lint framework |
| 15 | + |
| 16 | +## How Android Lint Service Discovery Works |
| 17 | + |
| 18 | +Android Lint uses Java's ServiceLoader mechanism to discover lint rule providers. It looks for files in: |
| 19 | +``` |
| 20 | +META-INF/services/com.android.tools.lint.client.api.IssueRegistry |
| 21 | +``` |
| 22 | + |
| 23 | +These files contain fully qualified class names of classes that implement `IssueRegistry`. |
| 24 | + |
| 25 | +## The WordPress Approach |
| 26 | + |
| 27 | +### 1. WordPressIssueRegistry Implementation |
| 28 | +```kotlin |
| 29 | +class WordPressIssueRegistry : IssueRegistry() { |
| 30 | + private val slackIssueRegistry = SlackIssueRegistry() |
| 31 | + |
| 32 | + override val issues: List<Issue> |
| 33 | + get() { |
| 34 | + val allOwnIssues = listOf(/* WordPress-specific issues */) |
| 35 | + val selectedSlackIssues = slackIssueRegistry.issues.filter { /* only specific ones */ } |
| 36 | + return allOwnIssues + selectedSlackIssues |
| 37 | + } |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +**Key Point**: WordPress programmatically creates a SlackIssueRegistry instance and selectively includes only the rules it wants. |
| 42 | + |
| 43 | +### 2. Service Registration |
| 44 | +The WordPress service file contains only: |
| 45 | +``` |
| 46 | +org.wordpress.android.lint.WordPressIssueRegistry |
| 47 | +``` |
| 48 | + |
| 49 | +## The Shadow JAR Process |
| 50 | + |
| 51 | +### Step 1: shadowJar Task |
| 52 | +```gradle |
| 53 | +shadowJar { |
| 54 | + mergeServiceFiles() |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +**What happens:** |
| 59 | +- Creates a "fat JAR" containing all dependencies (including Slack lint library) |
| 60 | +- `mergeServiceFiles()` combines all `META-INF/services/*` files from all JARs |
| 61 | +- **Problem**: This would merge WordPress's service file with Slack's service file, resulting in: |
| 62 | + ``` |
| 63 | + org.wordpress.android.lint.WordPressIssueRegistry |
| 64 | + slack.lint.SlackIssueRegistry |
| 65 | + ``` |
| 66 | + |
| 67 | +### Step 2: cleanServiceFile Task |
| 68 | +```gradle |
| 69 | +tasks.register('cleanServiceFile') { |
| 70 | + doLast { |
| 71 | + // Extract the JAR |
| 72 | + // Find the service file |
| 73 | + // Remove lines containing "slack.lint.SlackIssueRegistry" |
| 74 | + // Repackage the JAR |
| 75 | + } |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +**What happens:** |
| 80 | +- Extracts the shadow JAR to a temporary directory |
| 81 | +- Locates the merged service file |
| 82 | +- Removes any lines containing `slack.lint.SlackIssueRegistry` |
| 83 | +- Repackages the JAR |
| 84 | + |
| 85 | +**Result**: The final JAR contains only: |
| 86 | +``` |
| 87 | +org.wordpress.android.lint.WordPressIssueRegistry |
| 88 | +``` |
| 89 | + |
| 90 | +## Why This Approach? |
| 91 | + |
| 92 | +### Without the cleanServiceFile task: |
| 93 | +1. Both `WordPressIssueRegistry` and `SlackIssueRegistry` would be registered |
| 94 | +2. Android Lint would load both registries |
| 95 | +3. This could cause conflicts, performance issues, or unexpected behavior. And actually enabling all issues from `SlackIssueRegistry` |
| 96 | + |
| 97 | +### With the cleanServiceFile task: |
| 98 | +1. Only `WordPressIssueRegistry` is registered as a service |
| 99 | +2. WordPress has full control over which Slack rules are included |
| 100 | +3. No duplication or conflicts |
| 101 | +4. Clean, predictable behavior |
| 102 | + |
| 103 | +## Publishing Configuration: Why `components["shadow"]` instead of `components.java`? |
| 104 | + |
| 105 | +```gradle |
| 106 | +publishing { |
| 107 | + publications { |
| 108 | + maven(MavenPublication) { |
| 109 | + from(components["shadow"]) // Uses the cleaned shadow JAR |
| 110 | + groupId = 'org.wordpress' |
| 111 | + artifactId = 'lint' |
| 112 | + } |
| 113 | + } |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +### The Key Difference |
| 118 | + |
| 119 | +**If we used `from components.java`:** |
| 120 | +- Would publish only the compiled classes from this project (WordPress lint rules) |
| 121 | +- Dependencies would be listed as external dependencies in the POM file |
| 122 | +- Consumers would need to resolve and download the Slack lint library separately |
| 123 | +- The original service file would be published (not the cleaned one) |
| 124 | +- Result: A regular JAR that requires dependency resolution and all Slack rules are enabled |
| 125 | + |
| 126 | +**Using `from(components["shadow"])`:** |
| 127 | +- Publishes the complete shadow JAR with all dependencies included (fat JAR) |
| 128 | +- All Slack lint classes are embedded directly in the published JAR |
| 129 | +- The cleaned service file is included (with SlackIssueRegistry removed) |
| 130 | +- Consumers get a single, self-contained JAR with no external dependencies |
| 131 | +- Result: A fat JAR ready for immediate use and only selected Slack rules are enabled |
| 132 | + |
| 133 | +### Why This Matters |
| 134 | + |
| 135 | +1. **Not all Slack rules are enabled**: Only selected ones in `WordPressIssueRegistry` |
| 136 | +1. **Service File Integrity**: The cleaned service file (after `cleanServiceFile` task) must be the one that gets published, not the original |
| 137 | +2. **Self-Contained Distribution**: Consumers don't need to worry about resolving the Slack lint dependency |
| 138 | +3. **Controlled Dependency**: The Slack dependency is embedded and controlled, not exposed as a transitive dependency |
| 139 | +4. **Simplified Consumption**: One JAR file contains everything needed to run the WordPress lint rules |
| 140 | + |
| 141 | +### The Publishing Flow |
| 142 | + |
| 143 | +1. `shadowJar` task creates the fat JAR with merged dependencies |
| 144 | +2. `cleanServiceFile` task removes unwanted service registrations |
| 145 | +3. `components["shadow"]` references the final, cleaned shadow JAR |
| 146 | +4. Publishing uploads this processed JAR to the repository |
| 147 | + |
| 148 | +This ensures that what gets published is exactly what was intended: a self-contained JAR with WordPress rules + selected Slack rules, with proper service registration. |
| 149 | + |
| 150 | +## Summary |
| 151 | + |
| 152 | +This configuration solves a complex dependency management problem: |
| 153 | +- **Goal**: Include selected rules from Slack lint library in WordPress lint |
| 154 | +- **Challenge**: Avoid service registration conflicts and rule duplication |
| 155 | +- **Solution**: Use Shadow JAR to bundle dependencies, then clean up service files to ensure only WordPress registry is registered |
| 156 | +- **Result**: A single JAR with WordPress rules + selected Slack rules, properly registered with Android Lint |
| 157 | + |
| 158 | +The seemingly complex build logic is actually an elegant solution to ensure clean service discovery while reusing valuable lint rules from the Slack library. |
0 commit comments