/*
* Copyright 2016 Google Inc.
*
* 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.template.soy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.errorprone.annotations.ForOverride;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.template.soy.MainClassUtils.Main;
import com.google.template.soy.msgs.SoyMsgPlugin;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.CheckReturnValue;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
/**
* Base class for the Soy Compilers.
*
* <p>Defines common flags and performs shared initialization routines.
*
* <p>TODO(lukes): make all compilers subclass this type and move all MainClassUtils logic into this
* class.
*/
abstract class AbstractSoyCompiler {
/** The string to prepend to the usage message. */
private final String usagePrefix =
"Usage:\n"
+ "java "
+ getClass().getName()
+ " \\\n"
+ " [<flag1> <flag2> ...] --jar <jarName> \\\n"
+ " --srcs <soyFilePath>,... [--deps <soyFilePath>,...]\n";
private final Function<String, Void> exitWithErrorFn =
new Function<String, Void>() {
@Override
public Void apply(String errorMsg) {
exitWithError(errorMsg);
return null;
}
};
@Option(
name = "--inputPrefix",
usage =
"If provided, this path prefix will be prepended to each input file path"
+ " listed on the command line. This is a literal string prefix, so you'll need"
+ " to include a trailing slash if necessary."
)
protected String inputPrefix = "";
@Option(
name = "--srcs",
usage =
"The list of source Soy files. Extra arguments are treated as srcs. Sources"
+ " are required from either this flag or as extra arguments.",
handler = MainClassUtils.StringListOptionHandler.class
)
private List<String> srcs = new ArrayList<>();
@Option(
name = "--deps",
usage =
"The list of dependency Soy files (if applicable). The compiler needs deps for"
+ " analysis/checking, but will not generate code for dep files.",
handler = MainClassUtils.StringListOptionHandler.class
)
private List<String> deps = new ArrayList<>();
@Option(
name = "--indirectDeps",
usage = "Soy files required by deps, but which may not be used by srcs.",
handler = MainClassUtils.StringListOptionHandler.class
)
private List<String> indirectDeps = new ArrayList<>();
@Option(
name = "--compileTimeGlobalsFile",
usage =
"The path to a file containing the mappings for global names to be substituted"
+ " at compile time. Each line of the file should have the format"
+ " \"<global_name> = <primitive_data>\" where primitive_data is a valid Soy"
+ " expression literal for a primitive type (null, boolean, integer, float, or"
+ " string). Empty lines and lines beginning with \"//\" are ignored. The file"
+ " should be encoded in UTF-8. If you need to generate a file in this format"
+ " from Java, consider using the utility"
+ " SoyUtils.generateCompileTimeGlobalsFile()."
)
private File globalsFile = null;
@Option(
name = "--pluginModules",
usage =
"Specifies the full class names of Guice modules for function plugins and"
+ " print directive plugins (comma-delimited list).",
handler = MainClassUtils.ModuleListOptionHandler.class
)
private List<Module> pluginModules = new ArrayList<>();
@Option(
name = "--protoFileDescriptors",
usage =
"Location of protocol buffer definitions in the form of a file descriptor set."
+ "The compiler needs defs for parameter type checking and generating direct "
+ "access support for proto types.",
handler = MainClassUtils.FileListOptionHandler.class
)
private static final List<File> protoFileDescriptors = new ArrayList<>();
@Option(
name = "--enableExperimentalFeatures",
usage =
"Enable experimental features that are not generally available. "
+ "These experimental features may change, break, or disappear at any time. "
+ "We make absolutely no guarantees about what may happen if you turn one of these "
+ "experiments on. Please proceed with caution at your own risk.",
handler = MainClassUtils.StringListOptionHandler.class
)
private static final List<String> experimentalFeatures = new ArrayList<>();
/** The remaining arguments after parsing command-line flags. */
@Argument private final List<String> arguments = new ArrayList<>();
private CmdLineParser cmdLineParser;
final void runMain(String... args) {
int status = run(args);
System.exit(status);
}
@VisibleForTesting
@CheckReturnValue
int run(final String... args) {
// TODO(lukes): inline this method once all mains have been migrated to this base class.
return MainClassUtils.runInternal(
new Main() {
@Override
public void main() throws IOException {
doMain(args);
}
});
}
private void doMain(String[] args) throws IOException {
this.cmdLineParser = MainClassUtils.parseFlags(this, args, usagePrefix);
validateFlags();
if (!arguments.isEmpty() && !acceptsSourcesAsArguments()) {
exitWithError("Found old style sources passed on the command line, use --srcs=... instead");
}
List<Module> modules = new ArrayList<>();
modules.addAll(pluginModules);
modules.addAll(msgPluginModule().asSet());
Injector injector = MainClassUtils.createInjector(modules);
SoyFileSet.Builder sfsBuilder = injector.getInstance(SoyFileSet.Builder.class);
if (!protoFileDescriptors.isEmpty()) {
sfsBuilder.addProtoDescriptorsFromFiles(protoFileDescriptors);
}
MainClassUtils.addSoyFilesToBuilder(
sfsBuilder, inputPrefix, srcs, arguments, deps, indirectDeps, exitWithErrorFn);
if (globalsFile != null) {
sfsBuilder.setCompileTimeGlobals(globalsFile);
}
// Set experimental features that are not generally available.
sfsBuilder.setExperimentalFeatures(experimentalFeatures);
compile(sfsBuilder, injector);
}
/**
* Returns {@code true} if old style sources should be supported. {@code true} is the default.
*
* <p>Old style srcs are for when source files are passed as arguments directly, rather than
* passed to the {@code --srcs} flag.
*
* <p>TODO(lukes): eliminate support for old-style srcs
*/
@ForOverride
boolean acceptsSourcesAsArguments() {
return true;
}
/**
* Returns an additional plugin module to support the {@link SoyMsgPlugin}. This is only neccesary
* if the compiler needs to perform msg extraction.
*/
@ForOverride
Optional<Module> msgPluginModule() {
return Optional.absent();
}
/**
* Extension point for subtypes to perform additional logic to validate compiler specific flags.
*/
@ForOverride
void validateFlags() {}
/**
* Performs the actual compilation.
*
* @param sfsBuilder The builder, already populated with sources, globals (if set) and plugins.
* subclasses may set additional compilation options on the builder.
* @param injector The injector
* @throws IOException
*/
@ForOverride
void compile(SoyFileSet.Builder sfsBuilder, Injector injector) throws IOException {
compile(sfsBuilder);
}
/**
* Performs the actual compilation.
*
* @param sfsBuilder The builder, already populated with sources, globals (if set) and plugins.
* subclasses may set additional compilation options on the builder.
* @throws IOException
*/
@ForOverride
void compile(SoyFileSet.Builder sfsBuilder) throws IOException {
throw new AbstractMethodError("must override at least one overload of compile()");
}
/**
* Prints an error message and the usage string, and then exits.
*
* @param errorMsg The error message to print.
*/
final RuntimeException exitWithError(String errorMsg) {
return MainClassUtils.exitWithError(errorMsg, cmdLineParser, usagePrefix);
}
}