| // 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.libanalyzer; | |
| import com.android.tools.r8.ByteArrayConsumer; | |
| 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.CliParserUtils; | |
| 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.internal.CliParser; | |
| import com.android.tools.r8.utils.internal.FileUtils; | |
| import com.android.tools.r8.utils.internal.StringUtils; | |
| import com.android.tools.r8.utils.internal.collections.Pair; | |
| 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.function.Consumer; | |
| import java.util.stream.Stream; | |
| @KeepForApi | |
| public class LibraryAnalyzerCommandParser { | |
| public static String getUsageMessage() { | |
| return CliParserUtils.getUsageMessage(createParser()); | |
| } | |
| public static LibraryAnalyzerCommand.Builder parse(String[] args, Origin origin) { | |
| Reporter reporter = new Reporter(); | |
| LibraryAnalyzerCommand.Builder builder = LibraryAnalyzerCommand.builder(); | |
| String[] expandedArgs = FlagFile.expandFlagFiles(args, reporter::error); | |
| ParserState parserState = new ParserState(builder, origin, reporter); | |
| createParser() | |
| .parse( | |
| expandedArgs, | |
| parserState, | |
| error -> reporter.error(new StringDiagnostic(error, origin))); | |
| parserState.flushPendingPath(); | |
| return builder; | |
| } | |
| private enum PathType { | |
| Jar, | |
| Aar | |
| } | |
| private static class ParserState { | |
| private Pair<Path, PathType> pendingPath = null; | |
| private final LibraryAnalyzerCommand.Builder builder; | |
| private final Origin origin; | |
| private final Reporter reporter; | |
| private ParserState(LibraryAnalyzerCommand.Builder builder, Origin origin, Reporter reporter) { | |
| this.builder = builder; | |
| this.origin = origin; | |
| this.reporter = reporter; | |
| } | |
| void addPendingPathWithOrigin(Origin origin) { | |
| if (pendingPath.getSecond() == PathType.Jar) { | |
| if (origin != null) { | |
| builder.addJarPath(pendingPath.getFirst(), origin); | |
| } else { | |
| builder.addJarPath(pendingPath.getFirst()); | |
| } | |
| } else { | |
| assert pendingPath.getSecond() == PathType.Aar; | |
| if (origin != null) { | |
| builder.addAarPath(pendingPath.getFirst(), origin); | |
| } else { | |
| builder.addAarPath(pendingPath.getFirst()); | |
| } | |
| } | |
| } | |
| void flushPendingPath() { | |
| if (pendingPath != null) { | |
| addPendingPathWithOrigin(null); | |
| } | |
| } | |
| } | |
| private static CliParser<ParserState> createParser() { | |
| var header = StringUtils.joinLines("Usage: libanalyzer [options]", "where options are:"); | |
| return new CliParser<ParserState>(header) | |
| .option1( | |
| "--aar", | |
| "<path>", | |
| "Path to Android Archive (AAR) that should be analyzed.", | |
| (state, arg) -> { | |
| Path aarPath = Paths.get(arg); | |
| if (!FileUtils.isAarFile(aarPath)) { | |
| throw new IllegalArgumentException("Expected AAR, got: " + arg); | |
| } | |
| state.flushPendingPath(); | |
| state.pendingPath = Pair.create(aarPath, PathType.Aar); | |
| }) | |
| .option1( | |
| "--keep-radius-output", | |
| "<path>", | |
| "Path where to write keep radius result (protobuf).", | |
| (state, arg) -> state.builder.setKeepRadiusOutputPath(Paths.get(arg))) | |
| .option1( | |
| "--jar", | |
| "<path>", | |
| "Path to Java Archive (JAR) that should be analyzed.", | |
| (state, arg) -> { | |
| Path jarPath = Paths.get(arg); | |
| if (!FileUtils.isJarFile(jarPath)) { | |
| throw new IllegalArgumentException("Expected JAR, got: " + arg); | |
| } | |
| state.flushPendingPath(); | |
| state.pendingPath = Pair.create(jarPath, PathType.Jar); | |
| }) | |
| .apply( | |
| CliParserUtils.addLibOption( | |
| (state, provider) -> | |
| state.builder.getAppBuilder().addLibraryResourceProvider(provider), | |
| (state, path) -> state.builder.getAppBuilder().addLibraryFiles(path), | |
| state -> state.origin, | |
| state -> state.reporter)) | |
| .option1( | |
| "--maven-coord", | |
| "<x:y:z>", | |
| "Set the Maven coordinate of the previous --aar/--jar.", | |
| (state, arg) -> { | |
| if (state.pendingPath == null) { | |
| state.reporter.error( | |
| new StringDiagnostic( | |
| "No preceding --jar or --aar to modify (or multiple --maven-coord in a" | |
| + " row)", | |
| state.origin)); | |
| } else { | |
| PathBasedMavenOrigin mavenOrigin = | |
| parseMavenCoord( | |
| arg, | |
| state.pendingPath.getFirst(), | |
| error -> | |
| state.reporter.error( | |
| new StringDiagnostic( | |
| "Invalid argument to --maven-coord: " + error, state.origin))); | |
| state.addPendingPathWithOrigin(mavenOrigin); | |
| state.pendingPath = null; | |
| } | |
| }) | |
| .option1( | |
| "--min-api", | |
| "<major|major.minor>", | |
| "Minimum API level to use for analysis.", | |
| (state, arg) -> state.builder.setMinApiLevel(AndroidApiLevel.parseAndroidApiLevel(arg))) | |
| .option1( | |
| "--output", | |
| "<path>", | |
| "Path where to write analysis result (protobuf).", | |
| (state, arg) -> | |
| state.builder.setOutputConsumer(new ByteArrayConsumer.FileConsumer(Paths.get(arg)))) | |
| .option1( | |
| "--repo", | |
| "<path>", | |
| "Path to local Maven repository.", | |
| (state, arg) -> { | |
| Path repoPath = Paths.get(arg); | |
| if (!Files.isDirectory(repoPath)) { | |
| throw new IllegalArgumentException( | |
| "Invalid parameter for --repo. Expected directory, got: " + arg); | |
| } | |
| try (Stream<Path> paths = Files.walk(repoPath)) { | |
| paths | |
| .filter(path -> FileUtils.isAarFile(path) || FileUtils.isJarFile(path)) | |
| .forEach( | |
| path -> { | |
| if (FileUtils.isAarFile(path)) { | |
| state.builder.addAarPath( | |
| path, getMavenOriginFromArchive(repoPath, path)); | |
| } else { | |
| state.builder.addJarPath( | |
| path, getMavenOriginFromArchive(repoPath, path)); | |
| } | |
| }); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException("Failed to walk repository path: " + arg, e); | |
| } | |
| }) | |
| .apply( | |
| CliParserUtils.addThreadCountOption( | |
| (state, threadCount) -> state.builder.setThreadCount(threadCount), | |
| state -> state.reporter, | |
| state -> state.origin)) | |
| .apply(CliParserUtils.addVersionOption(state -> state.builder.setPrintVersion(true))) | |
| .apply(CliParserUtils.addHelpOption(state -> state.builder.setPrintHelp(true))); | |
| } | |
| 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 PathBasedMavenOrigin parseMavenCoord( | |
| String mavenCoord, Path path, Consumer<String> errorConsumer) { | |
| List<String> coordinates = StringUtils.split(mavenCoord, ':'); | |
| if (coordinates.size() != 3) { | |
| errorConsumer.accept("Cannot read Maven coordinate: " + mavenCoord); | |
| return null; | |
| } else { | |
| return new PathBasedMavenOrigin( | |
| path, coordinates.get(0), coordinates.get(1), coordinates.get(2)); | |
| } | |
| } | |
| } |