Output blast radius from libanalyzer

This extends libanalyzer to emitting blast radius output. A new --blast-radius-output argument is added the libanalyzer CLI.

This also adds a --maven-coord argument to the libanalyzer CLI. When adding an input to the libanalyzer using --aar or --jar, it is possible to immediately follow that argument with a --maven-coord x:y:z argument, which will then associate the given aar/jar with the x:y:z maven coordinate. The generated blast radius will include these maven coordinates.

Bug: b/483621903
Change-Id: Ieac85738618ff3acfbf4896c50f07f5d9cd0a4c3
diff --git a/src/blastradius/web/main.js b/src/blastradius/web/main.js
index fece713..277f4ce 100644
--- a/src/blastradius/web/main.js
+++ b/src/blastradius/web/main.js
@@ -235,6 +235,7 @@
       if (!tables.files.has(fileName)) {
         tables.files.set(fileName, {
           name: fileName,
+          origin: r.origin,
           rules: [],
           classIds: new Set(),
           methodIds: new Set(),
@@ -389,7 +390,7 @@
     const div = document.createElement('div');
     div.className = 'rule-item';
     div.innerHTML = `
-            <span class="rule-name">${escapeHtml(file.name)}</span>
+            <span class="rule-name">${escapeHtml(getOriginString(file.origin))}</span>
             <div class="rule-stats">
                 Total radius: ${file.totalRadius} across ${file.rules.length} rules
                 <br>(${file.classes} classes, ${file.methods} methods, ${file.fields} fields)
@@ -650,8 +651,22 @@
   return fileOrigin ? fileOrigin.filename : "Unknown";
 }
 
+function getMavenCoordinate(origin) {
+  if (!origin) return null;
+  const fileOrigin = tables.origins.get(origin.fileOriginId);
+  return fileOrigin?.mavenCoordinate;
+}
+
+function getMavenCoordinateString(mavenCoordinate) {
+  return mavenCoordinate.groupId + ":" + mavenCoordinate.artifactId + ":" + mavenCoordinate.version;
+}
+
 function getOriginString(origin) {
   if (!origin) return "Unknown";
+  const mavenCoordinate = getMavenCoordinate(origin);
+  if (mavenCoordinate) {
+    return getMavenCoordinateString(mavenCoordinate);
+  }
   const name = getFileName(origin);
   let res = name;
   if (origin.lineNumber) {
diff --git a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzer.java b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzer.java
index 2cb780f..107570a 100644
--- a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzer.java
+++ b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzer.java
@@ -113,10 +113,19 @@
     R8Command.Builder commandBuilder =
         R8Command.builder(options.reporter)
             .addProguardConfiguration(List.of("-ignorewarnings"), Origin.unknown())
+            .setClassConflictResolver((reference, origins, handler) -> origins.iterator().next())
             .setProgramConsumer(sizeConsumer);
     configure(commandBuilder);
     try {
-      R8.run(commandBuilder.build(), executorService);
+      R8.LibraryAnalyzerEntryPoint.run(
+          commandBuilder.build(),
+          executorService,
+          r8Options -> {
+            if (options.blastRadiusOutputPath != null) {
+              r8Options.getBlastRadiusOptions().outputPath =
+                  options.blastRadiusOutputPath.toString();
+            }
+          });
     } catch (CompilationFailedException e) {
       options.reporter.warning(new ExceptionDiagnostic(e));
       options.reporter.clearAbort();
diff --git a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommand.java b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommand.java
index 4970d9e..3afdd92 100644
--- a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommand.java
+++ b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommand.java
@@ -6,9 +6,11 @@
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.libanalyzer.utils.LibraryAnalyzerOptions;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AarArchiveResourceProvider;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ArchiveResourceProvider;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.ThreadUtils;
 import java.nio.file.Path;
@@ -20,6 +22,7 @@
 public final class LibraryAnalyzerCommand {
 
   private final AndroidApp app;
+  private final Path blastRadiusOutputPath;
   private final AndroidApiLevel minApiLevel;
   private final Path outputPath;
   private final Reporter reporter;
@@ -29,11 +32,13 @@
 
   private LibraryAnalyzerCommand(
       AndroidApp app,
+      Path blastRadiusOutputPath,
       AndroidApiLevel minApiLevel,
       Path outputPath,
       Reporter reporter,
       int threadCount) {
     this.app = app;
+    this.blastRadiusOutputPath = blastRadiusOutputPath;
     this.minApiLevel = minApiLevel;
     this.outputPath = outputPath;
     this.reporter = reporter;
@@ -44,6 +49,7 @@
 
   private LibraryAnalyzerCommand(boolean printHelp, boolean printVersion) {
     this.app = null;
+    this.blastRadiusOutputPath = null;
     this.minApiLevel = null;
     this.outputPath = null;
     this.reporter = new Reporter();
@@ -57,7 +63,8 @@
   }
 
   LibraryAnalyzerOptions getInternalOptions() {
-    return new LibraryAnalyzerOptions(minApiLevel, outputPath, reporter, threadCount);
+    return new LibraryAnalyzerOptions(
+        blastRadiusOutputPath, minApiLevel, outputPath, reporter, threadCount);
   }
 
   boolean isPrintHelp() {
@@ -79,6 +86,7 @@
   public static class Builder {
 
     private final AndroidApp.Builder appBuilder;
+    private Path blastRadiusOutputPath;
     private AndroidApiLevel minApiLevel = AndroidApiLevel.getDefault();
     private Path outputPath;
     private final Reporter reporter;
@@ -101,11 +109,33 @@
       return appBuilder;
     }
 
-    public Builder setAarPath(Path aarPath) {
+    public Builder addAarPath(Path aarPath) {
       appBuilder.addProgramResourceProvider(AarArchiveResourceProvider.fromArchive(aarPath));
       return this;
     }
 
+    public Builder addAarPath(Path aarPath, Origin origin) {
+      appBuilder.addProgramResourceProvider(
+          AarArchiveResourceProvider.fromArchive(aarPath, origin));
+      return this;
+    }
+
+    public Builder addJarPath(Path jarPath) {
+      appBuilder.addProgramResourceProvider(ArchiveResourceProvider.fromArchive(jarPath, true));
+      return this;
+    }
+
+    public Builder addJarPath(Path jarPath, Origin origin) {
+      appBuilder.addProgramResourceProvider(
+          ArchiveResourceProvider.fromArchive(jarPath, true, origin));
+      return this;
+    }
+
+    public Builder setBlastRadiusOutputPath(Path blastRadiusOutputPath) {
+      this.blastRadiusOutputPath = blastRadiusOutputPath;
+      return this;
+    }
+
     public Builder setOutputPath(Path outputPath) {
       this.outputPath = outputPath;
       return this;
@@ -141,7 +171,12 @@
       }
       validate();
       return new LibraryAnalyzerCommand(
-          appBuilder.build(), minApiLevel, outputPath, reporter, threadCount);
+          appBuilder.build(),
+          blastRadiusOutputPath,
+          minApiLevel,
+          outputPath,
+          reporter,
+          threadCount);
     }
 
     private void validate() {
diff --git a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommandParser.java b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommandParser.java
index 2c91e96..b47858f 100644
--- a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommandParser.java
+++ b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommandParser.java
@@ -12,34 +12,52 @@
 import com.android.tools.r8.CompilerCommandParserUtils;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathBasedMavenOrigin;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableSet;
+import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.List;
 import java.util.Set;
 
 @KeepForApi
 public class LibraryAnalyzerCommandParser {
 
   private static final String AAR_FLAG = "--aar";
+  private static final String BLAST_RADIUS_OUTPUT_FLAG = "--blast-radius-output";
+  private static final String JAR_FLAG = "--jar";
 
   private static final Set<String> OPTIONS_WITH_ONE_PARAMETER =
-      ImmutableSet.of(AAR_FLAG, LIB_FLAG, MIN_API_FLAG, OUTPUT_FLAG, THREAD_COUNT_FLAG);
+      ImmutableSet.of(
+          AAR_FLAG,
+          BLAST_RADIUS_OUTPUT_FLAG,
+          JAR_FLAG,
+          LIB_FLAG,
+          MIN_API_FLAG,
+          OUTPUT_FLAG,
+          THREAD_COUNT_FLAG);
 
   private static final String USAGE_MESSAGE =
       StringUtils.lines(
           "Usage: libanalyzer [options]",
           "where options are:",
-          "  --aar <path>            # Path to Android Archive (AAR) that should be analyzed.",
-          "  --lib <path>            # Path to file or JDK home to use as a library resource.",
-          "  --min-api <major.minor> # Minimum API level to use for analysis.",
-          "  --output <path>         # Path where to write the analysis result (protobuf).",
-          "  --thread-count <int>    # Number of threads to use.",
-          "  --help                  # Print this message.",
-          "  --version               # Print the version.");
+          "  --aar <path>                 # Path to Android Archive (AAR) that should be analyzed.",
+          "  --blast-radius-output <path> # Path where to write blast radius result (protobuf).",
+          "  --jar <path>                 # Path to Java Archive (JAR) that should be analyzed.",
+          "  --lib <path>                 # Path to file or JDK home to use as a library resource.",
+          "  --maven-coord <x:y:z>        # Set the Maven coordinate of the previous --aar/--jar.",
+          "                               # Only allowed after --aar <path> or --jar <path>.",
+          "  --min-api <major.minor>      # Minimum API level to use for analysis.",
+          "  --output <path>              # Path where to write analysis result (protobuf).",
+          "  --thread-count <int>         # Number of threads to use.",
+          "  --help                       # Print this message.",
+          "  --version                    # Print the version.");
 
   public static String getUsageMessage() {
     return USAGE_MESSAGE;
@@ -68,7 +86,43 @@
       } else if (arg.equals("--version")) {
         builder.setPrintVersion(true);
       } else if (arg.equals(AAR_FLAG)) {
-        builder.setAarPath(Paths.get(nextArg));
+        Path aarPath = Paths.get(nextArg);
+        if (!FileUtils.isAarFile(aarPath)) {
+          throw new IllegalArgumentException("Expected AAR, got: " + nextArg);
+        }
+        if ("--maven-coord".equals(peekArg(expandedArgs, i + 1))) {
+          if (peekArg(expandedArgs, i + 2) == null) {
+            throw new IllegalArgumentException("Missing parameter for --maven-coord");
+          }
+          List<String> mavenCoordinate = StringUtils.split(expandedArgs[i + 2], ':');
+          builder.addAarPath(
+              aarPath,
+              new PathBasedMavenOrigin(
+                  aarPath, mavenCoordinate.get(0), mavenCoordinate.get(1), mavenCoordinate.get(2)));
+          i += 2;
+        } else {
+          builder.addAarPath(aarPath);
+        }
+      } else if (arg.equals(BLAST_RADIUS_OUTPUT_FLAG)) {
+        builder.setBlastRadiusOutputPath(Paths.get(nextArg));
+      } else if (arg.equals(JAR_FLAG)) {
+        Path jarPath = Paths.get(nextArg);
+        if (!FileUtils.isJarFile(jarPath)) {
+          throw new IllegalArgumentException("Expected JAR, got: " + nextArg);
+        }
+        if ("--maven-coord".equals(peekArg(expandedArgs, i + 1))) {
+          if (peekArg(expandedArgs, i + 2) == null) {
+            throw new IllegalArgumentException("Missing parameter for --maven-coord");
+          }
+          List<String> mavenCoordinate = StringUtils.split(expandedArgs[i + 2], ':');
+          builder.addJarPath(
+              jarPath,
+              new PathBasedMavenOrigin(
+                  jarPath, mavenCoordinate.get(0), mavenCoordinate.get(1), mavenCoordinate.get(2)));
+          i += 2;
+        } else {
+          builder.addJarPath(jarPath);
+        }
       } else if (arg.equals(LIB_FLAG)) {
         CompilerCommandParserUtils.addLibraryArgument(
             builder.getAppBuilder(), nextArg, origin, reporter);
@@ -85,4 +139,8 @@
     }
     return builder;
   }
+
+  private static String peekArg(String[] expandedArgs, int index) {
+    return ArrayUtils.getOrDefault(expandedArgs, index, null);
+  }
 }
diff --git a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/utils/LibraryAnalyzerOptions.java b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/utils/LibraryAnalyzerOptions.java
index ee053d9..b2be3df 100644
--- a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/utils/LibraryAnalyzerOptions.java
+++ b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/utils/LibraryAnalyzerOptions.java
@@ -10,6 +10,7 @@
 
 public class LibraryAnalyzerOptions {
 
+  public final Path blastRadiusOutputPath;
   public final AndroidApiLevel minApiLevel;
   public final Path outputPath;
   public final Reporter reporter;
@@ -18,7 +19,12 @@
   private ThreadingModule lazyThreadingModule = null;
 
   public LibraryAnalyzerOptions(
-      AndroidApiLevel minApiLevel, Path outputPath, Reporter reporter, int threadCount) {
+      Path blastRadiusOutputPath,
+      AndroidApiLevel minApiLevel,
+      Path outputPath,
+      Reporter reporter,
+      int threadCount) {
+    this.blastRadiusOutputPath = blastRadiusOutputPath;
     this.minApiLevel = minApiLevel;
     this.outputPath = outputPath;
     this.reporter = reporter;
diff --git a/src/main/java/com/android/tools/r8/DataDirectoryResource.java b/src/main/java/com/android/tools/r8/DataDirectoryResource.java
index 78a0d49..8faec7f 100644
--- a/src/main/java/com/android/tools/r8/DataDirectoryResource.java
+++ b/src/main/java/com/android/tools/r8/DataDirectoryResource.java
@@ -29,6 +29,10 @@
     return new ZipDataDirectoryResource(zip, entry);
   }
 
+  static DataDirectoryResource fromZip(ZipFile zip, ZipEntry entry, Origin zipOrigin) {
+    return new ZipDataDirectoryResource(zip, entry, zipOrigin);
+  }
+
   class NamedDataDirectoryResource implements DataDirectoryResource {
     private final String name;
     private final Origin origin;
@@ -52,19 +56,23 @@
   }
 
   class ZipDataDirectoryResource implements DataDirectoryResource {
-    private final ZipFile zip;
     private final ZipEntry entry;
+    private final Origin zipOrigin;
 
     private ZipDataDirectoryResource(ZipFile zip, ZipEntry entry) {
+      this(zip, entry, new PathOrigin(Paths.get(zip.getName())));
+    }
+
+    private ZipDataDirectoryResource(ZipFile zip, ZipEntry entry, Origin zipOrigin) {
       assert zip != null;
       assert entry != null;
-      this.zip = zip;
       this.entry = entry;
+      this.zipOrigin = zipOrigin;
     }
 
     @Override
     public Origin getOrigin() {
-      return new ArchiveEntryOrigin(entry.getName(), new PathOrigin(Paths.get(zip.getName())));
+      return new ArchiveEntryOrigin(entry.getName(), zipOrigin);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/DataEntryResource.java b/src/main/java/com/android/tools/r8/DataEntryResource.java
index 966d360..899c5a6 100644
--- a/src/main/java/com/android/tools/r8/DataEntryResource.java
+++ b/src/main/java/com/android/tools/r8/DataEntryResource.java
@@ -49,6 +49,10 @@
     return new ZipDataEntryResource(zip, entry);
   }
 
+  static DataEntryResource fromZip(ZipFile zip, ZipEntry entry, Origin zipOrigin) {
+    return new ZipDataEntryResource(zip, entry, zipOrigin);
+  }
+
   default DataEntryResource withName(String name) {
     return new NestedDataEntryResource(name, null, this);
   }
@@ -84,17 +88,23 @@
   class ZipDataEntryResource implements DataEntryResource {
     private final ZipFile zip;
     private final ZipEntry entry;
+    private final Origin zipOrigin;
 
     private ZipDataEntryResource(ZipFile zip, ZipEntry entry) {
+      this(zip, entry, new PathOrigin(Paths.get(zip.getName())));
+    }
+
+    private ZipDataEntryResource(ZipFile zip, ZipEntry entry, Origin zipOrigin) {
       assert zip != null;
       assert entry != null;
       this.zip = zip;
       this.entry = entry;
+      this.zipOrigin = zipOrigin;
     }
 
     @Override
     public Origin getOrigin() {
-      return new ArchiveEntryOrigin(entry.getName(), new PathOrigin(Paths.get(zip.getName())));
+      return new ArchiveEntryOrigin(entry.getName(), zipOrigin);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 116af49..1651bf7 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -133,6 +133,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 import javax.xml.parsers.ParserConfigurationException;
 import org.xml.sax.SAXException;
@@ -1397,6 +1398,28 @@
     }
   }
 
+  public static class LibraryAnalyzerEntryPoint {
+
+    public static void run(
+        R8Command command,
+        ExecutorService executorService,
+        Consumer<InternalOptions> optionsModification)
+        throws CompilationFailedException {
+      AndroidApp app = command.getInputApp();
+      InternalOptions options = command.getInternalOptions();
+      optionsModification.accept(options);
+      ExceptionUtils.withR8CompilationHandler(
+          options.reporter,
+          () -> {
+            try {
+              runInternal(app, options, executorService);
+            } finally {
+              executorService.shutdown();
+            }
+          });
+    }
+  }
+
   private static class R8PassThroughAndroidResource extends R8AndroidResourceBase {
 
     public R8PassThroughAndroidResource(AndroidResourceInput androidResource, Reporter reporter) {
diff --git a/src/main/java/com/android/tools/r8/blastradius/BlastRadiusOptions.java b/src/main/java/com/android/tools/r8/blastradius/BlastRadiusOptions.java
new file mode 100644
index 0000000..1d3cc65
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/blastradius/BlastRadiusOptions.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.blastradius;
+
+import com.android.tools.r8.utils.SystemPropertyUtils;
+
+public class BlastRadiusOptions {
+
+  public final String outputDirectory =
+      System.getProperty("com.android.tools.r8.dumpblastradiustodirectory");
+  public String outputPath = System.getProperty("com.android.tools.r8.dumpblastradiustofile");
+  public final boolean enableSubsumptionAnalysis =
+      SystemPropertyUtils.parseSystemPropertyOrDefault(
+          "com.android.tools.r8.blastradius.enablesubsumptionanalysis", true);
+}
diff --git a/src/main/java/com/android/tools/r8/blastradius/KeepRuleSubsumptionAnalysis.java b/src/main/java/com/android/tools/r8/blastradius/KeepRuleSubsumptionAnalysis.java
index f567137..bdf1d24 100644
--- a/src/main/java/com/android/tools/r8/blastradius/KeepRuleSubsumptionAnalysis.java
+++ b/src/main/java/com/android/tools/r8/blastradius/KeepRuleSubsumptionAnalysis.java
@@ -24,7 +24,11 @@
     this.blastRadius = blastRadius;
   }
 
-  Map<RootSetBlastRadiusForRule, Collection<RootSetBlastRadiusForRule>> run() {
+  Map<RootSetBlastRadiusForRule, Collection<RootSetBlastRadiusForRule>> run(
+      BlastRadiusOptions options) {
+    if (!options.enableSubsumptionAnalysis) {
+      return subsumedBy;
+    }
     // Visit all keep rule pairs.
     // TODO(b/441055269): Parallelize.
     // TODO(b/441055269): Consider narrowing the candidate pairs by syntactic analysis of the keep
diff --git a/src/main/java/com/android/tools/r8/blastradius/RootSetBlastRadius.java b/src/main/java/com/android/tools/r8/blastradius/RootSetBlastRadius.java
index 42a2436..380394f 100644
--- a/src/main/java/com/android/tools/r8/blastradius/RootSetBlastRadius.java
+++ b/src/main/java/com/android/tools/r8/blastradius/RootSetBlastRadius.java
@@ -46,7 +46,7 @@
       AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer.Mode mode) {
     InternalOptions options = appView.options();
     return options.hasProguardConfiguration()
-            && options.getProguardConfiguration().isPrintBlastRadius()
+            && options.getProguardConfiguration().isPrintBlastRadius(options)
             && mode.isFinalTreeShaking()
         ? new Builder()
         : null;
@@ -61,14 +61,16 @@
     return ListUtils.sort(getBlastRadius(), Comparator.comparing(x -> x.getRule().getSource()));
   }
 
-  public Map<RootSetBlastRadiusForRule, Collection<RootSetBlastRadiusForRule>> getSubsumedByInfo() {
-    return new KeepRuleSubsumptionAnalysis(this).run();
+  public Map<RootSetBlastRadiusForRule, Collection<RootSetBlastRadiusForRule>> getSubsumedByInfo(
+      BlastRadiusOptions options) {
+    return new KeepRuleSubsumptionAnalysis(this).run(options);
   }
 
   public void writeToFile(
       AppView<?> appView, EnqueuerResult enqueuerResult, Path printBlastRadiusFile) {
     BlastRadiusContainer collection =
-        new RootSetBlastRadiusSerializer(appView, enqueuerResult).serialize(this);
+        new RootSetBlastRadiusSerializer(appView, enqueuerResult)
+            .serialize(this, appView.options().getBlastRadiusOptions());
     try (OutputStream output = Files.newOutputStream(printBlastRadiusFile)) {
       collection.writeTo(output);
     } catch (IOException e) {
diff --git a/src/main/java/com/android/tools/r8/blastradius/RootSetBlastRadiusSerializer.java b/src/main/java/com/android/tools/r8/blastradius/RootSetBlastRadiusSerializer.java
index 06d0c2a..ae8a20a 100644
--- a/src/main/java/com/android/tools/r8/blastradius/RootSetBlastRadiusSerializer.java
+++ b/src/main/java/com/android/tools/r8/blastradius/RootSetBlastRadiusSerializer.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.blastradius.proto.KeptClassInfo;
 import com.android.tools.r8.blastradius.proto.KeptFieldInfo;
 import com.android.tools.r8.blastradius.proto.KeptMethodInfo;
+import com.android.tools.r8.blastradius.proto.MavenCoordinate;
 import com.android.tools.r8.blastradius.proto.MethodReference;
 import com.android.tools.r8.blastradius.proto.ProtoReference;
 import com.android.tools.r8.blastradius.proto.TextFileOrigin;
@@ -30,6 +31,7 @@
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.origin.MavenOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.position.TextRange;
@@ -40,6 +42,7 @@
 import com.android.tools.r8.shaking.ProguardKeepRuleBase;
 import com.android.tools.r8.shaking.ProguardKeepRuleModifiers;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.OriginUtils;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Iterables;
@@ -86,11 +89,12 @@
     this.appInfo = enqueuerResult.getAppInfo();
   }
 
-  public BlastRadiusContainer serialize(RootSetBlastRadius blastRadius) {
+  public BlastRadiusContainer serialize(
+      RootSetBlastRadius blastRadius, BlastRadiusOptions options) {
     Collection<RootSetBlastRadiusForRule> sortedBlastRadius =
         blastRadius.getBlastRadiusWithDeterministicOrder();
     Map<RootSetBlastRadiusForRule, Collection<RootSetBlastRadiusForRule>> subsumedByInfo =
-        blastRadius.getSubsumedByInfo();
+        blastRadius.getSubsumedByInfo(options);
     for (RootSetBlastRadiusForRule blastRadiusForRule : sortedBlastRadius) {
       ruleIds.put(blastRadiusForRule, ruleIds.size());
     }
@@ -275,9 +279,18 @@
         origin,
         o -> {
           // TODO(b/441055269): Set the filename correctly.
-          // TODO(b/441055269): Set maven coordinate.
-          FileOrigin fileOrigin =
-              FileOrigin.newBuilder().setId(origins.size()).setFilename(o.toString()).build();
+          FileOrigin.Builder fileOriginBuilder =
+              FileOrigin.newBuilder().setId(origins.size()).setFilename(o.toString());
+          MavenOrigin mavenOrigin = OriginUtils.getMavenOrigin(origin);
+          if (mavenOrigin != null) {
+            fileOriginBuilder.setMavenCoordinate(
+                MavenCoordinate.newBuilder()
+                    .setArtifactId(mavenOrigin.getModule())
+                    .setGroupId(mavenOrigin.getGroup())
+                    .setVersion(mavenOrigin.getVersion())
+                    .build());
+          }
+          FileOrigin fileOrigin = fileOriginBuilder.build();
           container.addFileOriginTable(fileOrigin);
           return fileOrigin;
         });
diff --git a/src/main/java/com/android/tools/r8/origin/PathBasedMavenOrigin.java b/src/main/java/com/android/tools/r8/origin/PathBasedMavenOrigin.java
new file mode 100644
index 0000000..a48435d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/origin/PathBasedMavenOrigin.java
@@ -0,0 +1,40 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.origin;
+
+import java.nio.file.Path;
+
+public class PathBasedMavenOrigin extends PathOrigin implements MavenOrigin {
+
+  private final String group;
+  private final String module;
+  private final String version;
+
+  public PathBasedMavenOrigin(Path path, String group, String module, String version) {
+    super(path);
+    this.group = group;
+    this.module = module;
+    this.version = version;
+  }
+
+  @Override
+  public String getDisplayName() {
+    return group + ":" + module + ":" + version;
+  }
+
+  @Override
+  public String getGroup() {
+    return group;
+  }
+
+  @Override
+  public String getModule() {
+    return module;
+  }
+
+  @Override
+  public String getVersion() {
+    return version;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 9795ac1..5c56e7d 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -4618,7 +4618,8 @@
 
   private void reportBlastRadius(EnqueuerResult result) {
     if (blastRadius != null) {
-      Path printBlastRadiusFile = options.getProguardConfiguration().getPrintBlastRadiusFile();
+      Path printBlastRadiusFile =
+          options.getProguardConfiguration().getPrintBlastRadiusFile(options);
       if (printBlastRadiusFile != null) {
         blastRadius.build(appView).writeToFile(appView, result, printBlastRadiusFile);
       } else {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index aa8e133..d1f30ae 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.shaking.ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS;
 
+import com.android.tools.r8.blastradius.BlastRadiusOptions;
 import com.android.tools.r8.errors.dontwarn.DontWarnConfiguration;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.naming.DictionaryReader;
@@ -13,10 +14,10 @@
 import com.android.tools.r8.position.TextPosition;
 import com.android.tools.r8.shaking.ProguardConfigurationParser.IncludeWorkItem;
 import com.android.tools.r8.shaking.ProguardConfigurationParser.ProguardConfigurationSourceParser;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.SystemPropertyUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -771,27 +772,26 @@
     return dontShrinkRules.isEmpty();
   }
 
-  public boolean isPrintBlastRadius() {
-    return printBlastRadius
-        || SystemPropertyUtils.isSystemPropertySet(
-        "com.android.tools.r8.dumpblastradiustodirectory")
-        || SystemPropertyUtils.isSystemPropertySet(
-            "com.android.tools.r8.dumpblastradiustofile");
+  public boolean isPrintBlastRadius(InternalOptions options) {
+    if (printBlastRadius) {
+      return true;
+    }
+    BlastRadiusOptions blastRadiusOptions = options.getBlastRadiusOptions();
+    return blastRadiusOptions.outputDirectory != null || blastRadiusOptions.outputPath != null;
   }
 
-  public Path getPrintBlastRadiusFile() {
-    assert isPrintBlastRadius();
+  public Path getPrintBlastRadiusFile(InternalOptions options) {
+    assert isPrintBlastRadius(options);
     if (printBlastRadius) {
       return printBlastRadiusFile;
     }
-    if (SystemPropertyUtils.isSystemPropertySet(
-        "com.android.tools.r8.dumpblastradiustodirectory")) {
-      return Paths.get(System.getProperty("com.android.tools.r8.dumpblastradiustodirectory"))
+    BlastRadiusOptions blastRadiusOptions = options.getBlastRadiusOptions();
+    if (blastRadiusOptions.outputDirectory != null) {
+      return Paths.get(blastRadiusOptions.outputDirectory)
           .resolve("blastradius" + System.nanoTime() + ".pb");
     }
-    assert SystemPropertyUtils.isSystemPropertySet(
-        "com.android.tools.r8.dumpblastradiustofile");
-    return Paths.get(System.getProperty("com.android.tools.r8.dumpblastradiustofile"));
+    assert blastRadiusOptions.outputPath != null;
+    return Paths.get(blastRadiusOptions.outputPath);
   }
 
   public boolean isPrintConfiguration() {
diff --git a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
index 5c3c41e..5072fda 100644
--- a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
@@ -37,12 +37,15 @@
   private final Path archive;
 
   public static AarArchiveResourceProvider fromArchive(Path archive) {
-    return new AarArchiveResourceProvider(archive);
+    return fromArchive(archive, new PathOrigin(archive));
   }
 
-  AarArchiveResourceProvider(Path archive) {
+  public static AarArchiveResourceProvider fromArchive(Path archive, Origin origin) {
+    return new AarArchiveResourceProvider(archive, origin);
+  }
+
+  AarArchiveResourceProvider(Path archive, Origin origin) {
     assert isArchive(archive);
-    PathOrigin origin = new PathOrigin(archive);
     this.classesOrigin = new ArchiveEntryOrigin("classes.jar", origin);
     this.origin = origin;
     this.archive = archive;
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
index 08dc3fc..da23e02 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -41,7 +41,13 @@
   final boolean ignoreDexInArchive;
 
   public static ArchiveResourceProvider fromArchive(Path archive, boolean ignoreDexInArchive) {
-    return new ArchiveResourceProvider(FilteredClassPath.unfiltered(archive), ignoreDexInArchive);
+    return fromArchive(archive, ignoreDexInArchive, new PathOrigin(archive));
+  }
+
+  public static ArchiveResourceProvider fromArchive(
+      Path archive, boolean ignoreDexInArchive, Origin origin) {
+    return new ArchiveResourceProvider(
+        FilteredClassPath.unfiltered(archive), ignoreDexInArchive, origin);
   }
 
   ArchiveResourceProvider(FilteredClassPath archive, boolean ignoreDexInArchive) {
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProviderUtils.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProviderUtils.java
index 6fcd6dc..9436e84 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProviderUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProviderUtils.java
@@ -120,9 +120,9 @@
         ZipEntry entry = entries.nextElement();
         if (predicate.test(entry)) {
           if (entry.isDirectory()) {
-            visitor.visit(DataDirectoryResource.fromZip(zipFile, entry));
+            visitor.visit(DataDirectoryResource.fromZip(zipFile, entry, origin));
           } else {
-            visitor.visit(DataEntryResource.fromZip(zipFile, entry));
+            visitor.visit(DataEntryResource.fromZip(zipFile, entry, origin));
           }
         }
       }
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index 05a160e..b81551d 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -29,6 +29,10 @@
     return false;
   }
 
+  public static <T> T getOrDefault(T[] array, int index, T defaultValue) {
+    return index < array.length ? array[index] : defaultValue;
+  }
+
   public static <T> boolean none(T[] array, Predicate<T> predicate) {
     return !any(array, predicate);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index b7f8efc..bb18ead 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -27,6 +27,7 @@
 import com.android.tools.r8.SyntheticInfoConsumer;
 import com.android.tools.r8.Version;
 import com.android.tools.r8.androidapi.AndroidApiModelingOptions;
+import com.android.tools.r8.blastradius.BlastRadiusOptions;
 import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.classmerging.Policy;
 import com.android.tools.r8.debuginfo.DebugRepresentation;
@@ -1071,6 +1072,7 @@
   }
 
   private final AccessModifierOptions accessModifierOptions = new AccessModifierOptions(this);
+  private final BlastRadiusOptions blastRadiusOptions = new BlastRadiusOptions();
   private final RewriteArrayOptions rewriteArrayOptions = new RewriteArrayOptions();
   private final CallSiteOptimizationOptions callSiteOptimizationOptions =
       new CallSiteOptimizationOptions();
@@ -1200,6 +1202,10 @@
     return accessModifierOptions;
   }
 
+  public BlastRadiusOptions getBlastRadiusOptions() {
+    return blastRadiusOptions;
+  }
+
   public CfCodeAnalysisOptions getCfCodeAnalysisOptions() {
     return cfCodeAnalysisOptions;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/OriginUtils.java b/src/main/java/com/android/tools/r8/utils/OriginUtils.java
new file mode 100644
index 0000000..60afe34
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/OriginUtils.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.origin.MavenOrigin;
+import com.android.tools.r8.origin.Origin;
+
+public class OriginUtils {
+
+  public static MavenOrigin getMavenOrigin(Origin origin) {
+    if (origin instanceof MavenOrigin) {
+      return (MavenOrigin) origin;
+    }
+    if (origin.parent() != null) {
+      return getMavenOrigin(origin.parent());
+    }
+    return null;
+  }
+}