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; + } +}