// Copyright 2015 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.buildjar;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
/**
* Parses options that the {@link JavaLibraryBuildRequest} needs to construct a build request from
* command-line flags and options files and provides them via getters.
*/
public final class OptionsParser {
private static final Splitter SPACE_SPLITTER = Splitter.on(' ');
private final List<String> javacOpts = new ArrayList<>();
private final Map<String, JarOwner> directJarsToTargets = new HashMap<>();
private final Map<String, JarOwner> indirectJarsToTargets = new HashMap<>();
private String strictJavaDeps;
private String outputDepsProtoFile;
private final Set<String> depsArtifacts = new HashSet<>();
private boolean strictClasspathMode;
private String sourceGenDir;
private String generatedSourcesOutputJar;
private String manifestProtoPath;
private final Set<String> sourceRoots = new HashSet<>();
private final List<String> sourceFiles = new ArrayList<>();
private final List<String> sourceJars = new ArrayList<>();
private final List<String> classPath = new ArrayList<>();
private final List<String> sourcePath = new ArrayList<>();
private final List<String> bootClassPath = new ArrayList<>();
private final List<String> extClassPath = new ArrayList<>();
private final List<String> processorPath = new ArrayList<>();
private final List<String> processorNames = new ArrayList<>();
private String outputJar;
private String classDir;
private String tempDir;
private final Map<String, List<String>> postProcessors = new LinkedHashMap<>();
private boolean compressJar;
private String ruleKind;
private String targetLabel;
private boolean testOnly;
/**
* Constructs an {@code OptionsParser} from a list of command args. Sets the same JavacRunner for
* both compilation and annotation processing.
*
* @param args the list of command line args.
* @throws InvalidCommandLineException on any command line error.
*/
public OptionsParser(List<String> args) throws InvalidCommandLineException, IOException {
processCommandlineArgs(expandArguments(args));
}
/**
* Processes the command line arguments.
*
* @throws InvalidCommandLineException on an invalid option being passed.
*/
private void processCommandlineArgs(Deque<String> argQueue) throws InvalidCommandLineException {
for (String arg = argQueue.pollFirst(); arg != null; arg = argQueue.pollFirst()) {
switch (arg) {
case "--javacopts":
// Collect additional arguments to javac.
// Assumes that javac options do not start with "--".
// otherwise we have to do something like adding a "--"
// terminator to the passed arguments.
collectFlagArguments(javacOpts, argQueue, "--");
sourcePathFromJavacOpts();
break;
case "--direct_dependency":
{
String jar = getArgument(argQueue, arg);
JarOwner owner = parseJarOwner(getArgument(argQueue, arg));
directJarsToTargets.put(jar, owner);
break;
}
case "--indirect_dependency":
{
String jar = getArgument(argQueue, arg);
JarOwner owner = parseJarOwner(getArgument(argQueue, arg));
indirectJarsToTargets.put(jar, owner);
break;
}
case "--strict_java_deps":
strictJavaDeps = getArgument(argQueue, arg);
break;
case "--output_deps_proto":
outputDepsProtoFile = getArgument(argQueue, arg);
break;
case "--deps_artifacts":
collectFlagArguments(depsArtifacts, argQueue, "--");
break;
case "--reduce_classpath":
strictClasspathMode = true;
break;
case "--sourcegendir":
sourceGenDir = getArgument(argQueue, arg);
break;
case "--generated_sources_output":
generatedSourcesOutputJar = getArgument(argQueue, arg);
break;
case "--output_manifest_proto":
manifestProtoPath = getArgument(argQueue, arg);
break;
case "--source_roots":
collectFlagArguments(sourceRoots, argQueue, "-");
break;
case "--sources":
collectFlagArguments(sourceFiles, argQueue, "-");
break;
case "--source_jars":
collectFlagArguments(sourceJars, argQueue, "-");
break;
case "--classpath":
collectClassPathArguments(classPath, argQueue);
break;
// TODO(#970): Consider wether we want to use --sourcepath for resolving of #970.
case "--sourcepath":
collectClassPathArguments(sourcePath, argQueue);
break;
case "--bootclasspath":
collectClassPathArguments(bootClassPath, argQueue);
break;
case "--processorpath":
collectClassPathArguments(processorPath, argQueue);
break;
case "--processors":
collectProcessorArguments(processorNames, argQueue, "-");
break;
case "--extclasspath":
case "--extdir":
collectClassPathArguments(extClassPath, argQueue);
break;
case "--output":
outputJar = getArgument(argQueue, arg);
break;
case "--classdir":
classDir = getArgument(argQueue, arg);
break;
case "--tempdir":
tempDir = getArgument(argQueue, arg);
break;
case "--gendir":
// TODO(bazel-team) - remove when Bazel no longer passes this flag to buildjar.
getArgument(argQueue, arg);
break;
case "--post_processor":
addExternalPostProcessor(argQueue, arg);
break;
case "--compress_jar":
compressJar = true;
break;
case "--rule_kind":
ruleKind = getArgument(argQueue, arg);
break;
case "--target_label":
targetLabel = getArgument(argQueue, arg);
break;
case "--testonly":
testOnly = true;
break;
default:
throw new InvalidCommandLineException("unknown option : '" + arg + "'");
}
}
}
private void sourcePathFromJavacOpts() {
Iterator<String> it = javacOpts.iterator();
while (it.hasNext()) {
String curr = it.next();
if (curr.equals("-sourcepath") && it.hasNext()) {
it.remove();
Iterables.addAll(sourcePath, CLASSPATH_SPLITTER.split(it.next()));
it.remove();
}
}
}
private JarOwner parseJarOwner(String line) {
List<String> ownerStringParts = SPACE_SPLITTER.splitToList(line);
JarOwner owner;
Preconditions.checkState(ownerStringParts.size() == 1 || ownerStringParts.size() == 2);
if (ownerStringParts.size() == 1) {
owner = JarOwner.create(ownerStringParts.get(0));
} else {
owner = JarOwner.create(ownerStringParts.get(0), ownerStringParts.get(1));
}
return owner;
}
/**
* Pre-processes an argument list, expanding options @filename to read in the content of the file
* and add it to the list of arguments.
*
* @param args the List of arguments to pre-process.
* @return the List of pre-processed arguments.
* @throws java.io.IOException if one of the files containing options cannot be read.
*/
private static Deque<String> expandArguments(List<String> args) throws IOException {
Deque<String> expanded = new ArrayDeque<>(args.size());
for (String arg : args) {
expandArgument(expanded, arg);
}
return expanded;
}
/**
* Expands a single argument, expanding options @filename to read in the content of the file and
* add it to the list of processed arguments. The @ itself can be escaped with @@.
*
* @param expanded the list of processed arguments.
* @param arg the argument to pre-process.
* @throws java.io.IOException if one of the files containing options cannot be read.
*/
private static void expandArgument(Deque<String> expanded, String arg) throws IOException {
if (arg.startsWith("@@")) {
expanded.add(arg.substring(1));
} else if (arg.startsWith("@")) {
for (String line : Files.readAllLines(Paths.get(arg.substring(1)), UTF_8)) {
if (line.length() > 0) {
expandArgument(expanded, line);
}
}
} else {
expanded.add(arg);
}
}
/**
* Collects the arguments for a command line flag until it finds a flag that starts with the
* terminatorPrefix.
*
* @param output where to put the collected flag arguments.
* @param args
* @param terminatorPrefix the terminator prefix to stop collecting of argument flags.
*/
private static void collectFlagArguments(
Collection<String> output, Deque<String> args, String terminatorPrefix) {
for (String arg = args.pollFirst(); arg != null; arg = args.pollFirst()) {
if (arg.startsWith(terminatorPrefix)) {
args.addFirst(arg);
break;
}
output.add(arg);
}
}
private static final Splitter CLASSPATH_SPLITTER =
Splitter.on(File.pathSeparatorChar).trimResults().omitEmptyStrings();
// TODO(cushon): stop splitting classpaths once cl/127006119 is released
private static void collectClassPathArguments(Collection<String> output, Deque<String> args) {
for (String arg = args.pollFirst(); arg != null; arg = args.pollFirst()) {
if (arg.startsWith("-")) {
args.addFirst(arg);
break;
}
Iterables.addAll(output, CLASSPATH_SPLITTER.split(arg));
}
}
/**
* Collects the arguments for the --processors command line flag until it finds a flag that starts
* with the terminatorPrefix.
*
* @param output where to put the collected flag arguments.
* @param args
* @param terminatorPrefix the terminator prefix to stop collecting of argument flags.
*/
private static void collectProcessorArguments(
List<String> output, Deque<String> args, String terminatorPrefix)
throws InvalidCommandLineException {
for (String arg = args.pollFirst(); arg != null; arg = args.pollFirst()) {
if (arg.startsWith(terminatorPrefix)) {
args.addFirst(arg);
break;
}
if (arg.contains(",")) {
throw new InvalidCommandLineException("processor argument may not contain commas: " + arg);
}
output.add(arg);
}
}
private static String getArgument(Deque<String> args, String arg)
throws InvalidCommandLineException {
try {
return args.remove();
} catch (NoSuchElementException e) {
throw new InvalidCommandLineException(arg + ": missing argument");
}
}
private void addExternalPostProcessor(Deque<String> args, String arg)
throws InvalidCommandLineException {
String processorName = getArgument(args, arg);
List<String> arguments = new ArrayList<>();
collectFlagArguments(arguments, args, "--");
postProcessors.put(processorName, arguments);
}
public List<String> getJavacOpts() {
return javacOpts;
}
public Map<String, JarOwner> getDirectMappings() {
return directJarsToTargets;
}
public Map<String, JarOwner> getIndirectMappings() {
return indirectJarsToTargets;
}
public String getStrictJavaDeps() {
return strictJavaDeps;
}
public String getOutputDepsProtoFile() {
return outputDepsProtoFile;
}
public Set<String> getDepsArtifacts() {
return depsArtifacts;
}
public boolean reduceClasspath() {
return strictClasspathMode;
}
public String getSourceGenDir() {
return sourceGenDir;
}
public String getGeneratedSourcesOutputJar() {
return generatedSourcesOutputJar;
}
public String getManifestProtoPath() {
return manifestProtoPath;
}
public Set<String> getSourceRoots() {
return sourceRoots;
}
public List<String> getSourceFiles() {
return sourceFiles;
}
public List<String> getSourceJars() {
return sourceJars;
}
public List<String> getClassPath() {
return classPath;
}
public List<String> getBootClassPath() {
return bootClassPath;
}
public List<String> getSourcePath() {
return sourcePath;
}
public List<String> getExtClassPath() {
return extClassPath;
}
public List<String> getProcessorPath() {
return processorPath;
}
public List<String> getProcessorNames() {
return processorNames;
}
public String getOutputJar() {
return outputJar;
}
public String getClassDir() {
return classDir;
}
public String getTempDir() {
return tempDir;
}
public Map<String, List<String>> getPostProcessors() {
return postProcessors;
}
public boolean compressJar() {
return compressJar;
}
public String getRuleKind() {
return ruleKind;
}
public String getTargetLabel() {
return targetLabel;
}
public boolean testOnly() {
return testOnly;
}
}