Turn logs into rules

Run the commands on the logs of all journeys, and act on the output with the evidence levels and conventions in this file.

Suggest rules

java -jar "$R8_ASSISTANT_JAR" suggest \
  --log app/build/r8-assistant/debug-settings.json \
  --output app/build/r8-assistant/suggested.pro

Pass --log once per journey to combine logs, and pass the rule files of the project with --rules to leave out what they already keep:

java -jar "$R8_ASSISTANT_JAR" suggest \
  --log app/build/r8-assistant/debug-settings.json \
  --log app/build/r8-assistant/debug-sync.json \
  --rules app/proguard-rules.pro \
  --output app/build/r8-assistant/suggested.pro

For a crash, copy only the rules that cover the classes and members in the retraced stack trace. Do not paste the whole output into a rule file.

Query events

java -jar "$R8_ASSISTANT_JAR" query \
  --log app/build/r8-assistant/debug-settings.json \
  --class com.example.app.settings.DarkTheme \
  --stacks

Filter with --class, --member and --event, and add --stacks to print the call stack of each event. The stack ties an event to the code that made the call, which is the evidence to quote in the report.

java -jar "$R8_ASSISTANT_JAR" query \
  --log app/build/r8-assistant/debug-sync.json \
  --event CLASS_GET_DECLARED_FIELDS

Compare logs with the rules

java -jar "$R8_ASSISTANT_JAR" diff \
  --log app/build/r8-assistant/debug-settings.json \
  --log app/build/r8-assistant/debug-sync.json \
  --rules app/proguard-rules.pro \
  --output app/build/r8-assistant/proposed.pro

Pass the log of every journey and every rule file that the project owns. diff reports, for each rule, the logged events that the rule covers and the rules that no event uses.

Evidence levels

Classify every rule that you change:

  • Observed: the logs have events for the classes or members of the rule. Keep the rule, narrowed to the logged classes and members.
  • Not observed: the logs have no events for the classes of the rule, and a journey ran the code that uses them. Confirm in the code that the journey reached it. Narrow or delete the rule; the release replay decides.
  • Uncovered: no journey ran the code that uses the classes, or the log leaves out the reflection that reaches them, as listed in references/capture.md. Leave the rule unchanged and list it in the report.

“Not observed” is weak evidence. A deletion stays only when the release replay passes for the journeys that ran the code of the rule.

Rule conventions

  • Name exact classes. Never use package-wide wildcards.
  • Keep only the members that the logs show, and the constructors of classes that the app creates by reflection. In R8 full mode, -keep class without members does not keep the default constructor, so list <init> explicitly.
  • Add allowobfuscation only when no code looks up the name as a string. Gson and Moshi read field names, and Class.forName, ClassLoader.loadClass and ResourceBundle.getBundle read class names.
  • Keep the volatile modifier in rules for fields of atomic field updaters. Without it, the release build fails with IllegalArgumentException: Must be volatile type.
  • Keep the provider classes listed in META-INF/services for a SERVICE_LOADER_LOAD event, with their no-argument constructors.
  • For a RESOURCE_BUNDLE_GET_BUNDLE event, keep the bundle classes of all locales in one rule, -keep class <base>, <base>_* { <init>(); }, where <base> is the base name in the first argument. The log shows only the device locale.
  • A RESOURCE_BUNDLE_GET_BUNDLE result of java.util.PropertyResourceBundle means that the bundle is a properties file, which needs no class rule.
  • For a CLASS_GET_GENERIC_SUPERCLASS or CLASS_GET_GENERIC_INTERFACES event with a parameterized type in the debug result, add -keepattributes Signature and -keep,allowobfuscation,allowshrinking rules for the class and its generic supertype. R8 keeps attributes only for classes that a rule matches.
  • Match an anonymous class, such as SyncRepository$1, by its supertype with class * extends or class * implements. The number in its name changes when the code changes.

Place and document each rule

Put each rule in the rule file that owns the code: the proguard-rules.pro of the app module for app code, and the consumer rules file of a library module for code in that module.

To find the owning file of an existing rule, search configuration.txt, as described in references/setup.md.

Write a provenance comment on the line before each rule, with the call site and the journey, or with the retraced exception:

# r8-assistant: Class.forName in ThemeLoader.load (journey settings).
-keep class com.example.app.settings.DarkTheme {
  <init>();
}

# r8-assistant: Gson reads the fields of SyncResponse (journey sync).
-keepclassmembers class com.example.app.sync.SyncResponse {
  <fields>;
  <init>();
}

Write rules from a retraced stack trace

When the log has no event for a crash, write the rule from the retraced stack trace. Retrace first, so that every class name is an original name:

  • ClassNotFoundException or NoClassDefFoundError: R8 removed or renamed the class. Keep the class name.
  • NoSuchMethodException: keep the method with its parameter types.
  • NoSuchFieldException: keep the field.
  • InstantiationException, including Fragment$InstantiationException: keep the constructor, usually <init>().
  • IllegalArgumentException: Must be volatile type: keep the field with its volatile modifier.
  • SerializationException from kotlinx.serialization: keep the Companion field and the serializer method of the class.
  • MissingResourceException for a bundle base name: keep the bundle classes, as for a RESOURCE_BUNDLE_GET_BUNDLE event in the rule conventions.
  • ClassCastException from Class to ParameterizedType, or a library error about a missing type argument: keep the generic signature, as for the generic events in the rule conventions.
  • ExceptionInInitializerError: retrace the cause and apply the matching case in this list.
  • Data that goes missing, such as empty fields after Gson or Moshi parsing: keep the fields and the constructor of the model class.
# r8-assistant: Fragment$InstantiationException for SettingsFragment
# (retraced logcat, journey settings).
-keep class com.example.app.settings.SettingsFragment {
  public <init>();
}

For the official guidance on keep rules, see Add keep rules.