Add support for running libanalyzer on local maven repository

Bug: b/483568398
Change-Id: Ia7ef4e45d335af95964f35723c9f9e47bf64837c
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 107570a..0eef23d 100644
--- a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzer.java
+++ b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzer.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.D8Command;
 import com.android.tools.r8.R8;
 import com.android.tools.r8.R8Command;
+import com.android.tools.r8.Version;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.libanalyzer.proto.D8CompileResult;
 import com.android.tools.r8.libanalyzer.proto.LibraryAnalysisResult;
@@ -78,8 +79,7 @@
       return;
     }
     if (command.isPrintVersion()) {
-      // TODO(b/479726064): Read the version from a field.
-      System.out.println("LibraryAnalyzer 0.0.1");
+      System.out.println("LibraryAnalyzer " + Version.getVersionString());
       return;
     }
     ExceptionUtils.withR8CompilationHandler(
@@ -96,7 +96,9 @@
   private InternalD8CompileResult runD8(ExecutorService executorService) {
     DexIndexedSizeConsumer sizeConsumer = new DexIndexedSizeConsumer();
     D8Command.Builder commandBuilder =
-        D8Command.builder(options.reporter).setProgramConsumer(sizeConsumer);
+        D8Command.builder(options.reporter)
+            .setClassConflictResolver((reference, origins, handler) -> origins.iterator().next())
+            .setProgramConsumer(sizeConsumer);
     configure(commandBuilder);
     try {
       D8.run(commandBuilder.build(), executorService);
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 b47858f..78b9c65 100644
--- a/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommandParser.java
+++ b/src/libanalyzer/java/com/android/tools/r8/libanalyzer/LibraryAnalyzerCommandParser.java
@@ -21,10 +21,15 @@
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableSet;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Stream;
 
 @KeepForApi
 public class LibraryAnalyzerCommandParser {
@@ -32,6 +37,7 @@
   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 String REPO_FLAG = "--repo";
 
   private static final Set<String> OPTIONS_WITH_ONE_PARAMETER =
       ImmutableSet.of(
@@ -41,6 +47,7 @@
           LIB_FLAG,
           MIN_API_FLAG,
           OUTPUT_FLAG,
+          REPO_FLAG,
           THREAD_COUNT_FLAG);
 
   private static final String USAGE_MESSAGE =
@@ -55,6 +62,7 @@
           "                               # 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).",
+          "  --repo <path>                # Path to local Maven repository.",
           "  --thread-count <int>         # Number of threads to use.",
           "  --help                       # Print this message.",
           "  --version                    # Print the version.");
@@ -130,6 +138,26 @@
         builder.setMinApiLevel(AndroidApiLevel.parseAndroidApiLevel(nextArg));
       } else if (arg.equals(OUTPUT_FLAG)) {
         builder.setOutputPath(Paths.get(nextArg));
+      } else if (arg.equals(REPO_FLAG)) {
+        Path repoPath = Paths.get(nextArg);
+        if (!Files.isDirectory(repoPath)) {
+          throw new IllegalArgumentException(
+              "Invalid parameter for --repo. Expected directory, got: " + nextArg);
+        }
+        try (Stream<Path> paths = Files.walk(repoPath)) {
+          paths
+              .filter(path -> FileUtils.isAarFile(path) || FileUtils.isJarFile(path))
+              .forEach(
+                  path -> {
+                    if (FileUtils.isAarFile(path)) {
+                      builder.addAarPath(path, getMavenOriginFromArchive(repoPath, path));
+                    } else {
+                      builder.addJarPath(path, getMavenOriginFromArchive(repoPath, path));
+                    }
+                  });
+        } catch (IOException e) {
+          throw new UncheckedIOException("Failed to walk repository path: " + nextArg, e);
+        }
       } else if (arg.equals(THREAD_COUNT_FLAG)) {
         parsePositiveIntArgument(
             reporter::error, THREAD_COUNT_FLAG, nextArg, origin, builder::setThreadCount);
@@ -140,6 +168,17 @@
     return builder;
   }
 
+  private static Origin getMavenOriginFromArchive(Path repoPath, Path path) {
+    String directoryName = repoPath.relativize(path).getParent().toString();
+    int lastSeparator = directoryName.lastIndexOf(File.separatorChar);
+    int prevSeparator = directoryName.lastIndexOf(File.separatorChar, lastSeparator - 1);
+    String group =
+        StringUtils.replaceAll(directoryName.substring(0, prevSeparator), File.separator, ".");
+    String module = directoryName.substring(prevSeparator + 1, lastSeparator);
+    String version = directoryName.substring(lastSeparator + 1);
+    return new PathBasedMavenOrigin(path, group, module, version);
+  }
+
   private static String peekArg(String[] expandedArgs, int index) {
     return ArrayUtils.getOrDefault(expandedArgs, index, null);
   }