blob: fa4feca080c7656503d8b5137c285d480f8ef1bb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.codehaus.groovy.tools;
import groovy.lang.GroovySystem;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.ConfigurationException;
import org.codehaus.groovy.runtime.DefaultGroovyStaticMethods;
import org.codehaus.groovy.runtime.StringGroovyMethods;
import org.codehaus.groovy.tools.javac.JavaAwareCompilationUnit;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.IVersionProvider;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.ParseResult;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static groovy.ui.GroovyMain.buildConfigScriptText;
import static groovy.ui.GroovyMain.processConfigScriptText;
import static groovy.ui.GroovyMain.processConfigScripts;
/**
* Command-line compiler (aka. <tt>groovyc</tt>).
*/
public class FileSystemCompiler {
private static boolean displayStackTraceOnError = false;
private final CompilationUnit unit;
public FileSystemCompiler(CompilerConfiguration configuration) throws ConfigurationException {
this(configuration, null);
}
public FileSystemCompiler(CompilerConfiguration configuration, CompilationUnit cu) throws ConfigurationException {
if (cu != null) {
unit = cu;
} else if (configuration.getJointCompilationOptions() != null) {
unit = new JavaAwareCompilationUnit(configuration);
} else {
unit = new CompilationUnit(configuration);
}
}
/**
* Prints the usage help message for {@link CompilationOptions} to stderr.
*
* @see #displayHelp(PrintWriter)
* @since 2.5
*/
public static void displayHelp() {
displayHelp(new PrintWriter(System.err, true));
}
/**
* Prints the usage help message for the {@link CompilationOptions} to the specified PrintWriter.
*
* @since 2.5
*/
public static void displayHelp(PrintWriter writer) {
configureParser(new CompilationOptions()).usage(writer);
}
/**
* Prints version information to stderr.
*
* @see #displayVersion(PrintWriter)
*/
public static void displayVersion() {
displayVersion(new PrintWriter(System.err, true));
}
/**
* Prints version information to the specified PrintWriter.
*
* @since 2.5
*/
public static void displayVersion(PrintWriter writer) {
for (String line : new VersionProvider().getVersion()) {
writer.println(line);
}
}
public static int checkFiles(String[] filenames) {
int errors = 0;
for (String filename : filenames) {
File file = new File(filename);
if (!file.exists()) {
System.err.println("error: file not found: " + file);
errors += 1;
} else if (!file.canRead()) {
System.err.println("error: file not readable: " + file);
errors += 1;
}
}
return errors;
}
public static boolean validateFiles(String[] filenames) {
return checkFiles(filenames) == 0;
}
/**
* Same as main(args) except that exceptions are thrown out instead of causing
* the VM to exit.
*/
public static void commandLineCompile(String[] args) throws Exception {
commandLineCompile(args, true);
}
/**
* Same as main(args) except that exceptions are thrown out instead of causing
* the VM to exit and the lookup for .groovy files can be controlled
*/
public static void commandLineCompile(String[] args, boolean lookupUnnamedFiles) throws Exception {
CompilationOptions options = new CompilationOptions();
CommandLine parser = configureParser(options);
ParseResult parseResult = parser.parseArgs(args);
if (CommandLine.printHelpIfRequested(parseResult)) {
return;
}
displayStackTraceOnError = options.printStack;
CompilerConfiguration configuration = options.toCompilerConfiguration();
// Load the file name list
String[] filenames = options.generateFileNames();
boolean fileNameErrors = filenames == null;
if (!fileNameErrors && (filenames.length == 0)) {
parser.usage(System.err);
return;
}
fileNameErrors = fileNameErrors && !validateFiles(filenames);
if (!fileNameErrors) {
doCompilation(configuration, null, filenames, lookupUnnamedFiles);
}
}
public static CommandLine configureParser(CompilationOptions options) {
CommandLine parser = new CommandLine(options);
parser.getCommandSpec().parser()
.unmatchedArgumentsAllowed(true)
.unmatchedOptionsArePositionalParams(true)
.expandAtFiles(false)
.toggleBooleanFlags(false);
return parser;
}
/**
* Primary entry point for compiling from the command line
* (using the groovyc script).
* <p>
* If calling inside a process and you don't want the JVM to exit on an
* error call commandLineCompile(String[]), which this method simply wraps
*
* @param args command line arguments
*/
public static void main(String[] args) {
commandLineCompileWithErrorHandling(args, true);
}
/**
* Primary entry point for compiling from the command line
* (using the groovyc script).
* <p>
* If calling inside a process and you don't want the JVM to exit on an
* error call commandLineCompile(String[]), which this method simply wraps
*
* @param args command line arguments
* @param lookupUnnamedFiles do a lookup for .groovy files not part of
* the given list of files to compile
*/
public static void commandLineCompileWithErrorHandling(String[] args, boolean lookupUnnamedFiles) {
try {
commandLineCompile(args, lookupUnnamedFiles);
} catch (Throwable e) {
new ErrorReporter(e, displayStackTraceOnError).write(System.err);
System.exit(1);
}
}
public static void doCompilation(CompilerConfiguration configuration, CompilationUnit unit, String[] filenames) throws Exception {
doCompilation(configuration, unit, filenames, true);
}
public static void doCompilation(CompilerConfiguration configuration, CompilationUnit unit, String[] filenames, boolean lookupUnnamedFiles) throws Exception {
File tmpDir = null;
// if there are any joint compilation options set stubDir if not set
try {
if ((configuration.getJointCompilationOptions() != null)
&& !configuration.getJointCompilationOptions().containsKey("stubDir")) {
tmpDir = DefaultGroovyStaticMethods.createTempDir(null, "groovy-generated-", "-java-source");
configuration.getJointCompilationOptions().put("stubDir", tmpDir);
}
FileSystemCompiler compiler = new FileSystemCompiler(configuration, unit);
if (lookupUnnamedFiles) {
for (String filename : filenames) {
File file = new File(filename);
if (file.isFile()) {
URL url = file.getAbsoluteFile().getParentFile().toURI().toURL();
compiler.unit.getClassLoader().addURL(url);
}
}
} else {
compiler.unit.getClassLoader()
.setResourceLoader(filename -> null);
}
compiler.compile(filenames);
} finally {
try {
if (tmpDir != null) deleteRecursive(tmpDir);
} catch (Throwable t) {
System.err.println("error: could not delete temp files - " + tmpDir.getPath());
}
}
}
private static String[] generateFileNamesFromOptions(List<String> filenames) {
if (filenames == null) {
return new String[0];
}
List<String> fileList = new ArrayList<>(filenames.size());
boolean errors = false;
for (String filename : filenames) {
if (filename.startsWith("@")) {
String fn = filename.substring(1);
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(fn));
for (String file; (file = br.readLine()) != null; ) {
fileList.add(file);
}
} catch (IOException ioe) {
System.err.println("error: file not readable: " + fn);
errors = true;
} finally {
if (null != br) {
try {
br.close();
} catch (IOException e) {
System.err.println("error: failed to close buffered reader: " + fn);
errors = true;
}
}
}
} else {
fileList.add(filename);
}
}
if (errors) {
return null;
} else {
return fileList.toArray(new String[0]);
}
}
public static void deleteRecursive(File file) {
if (!file.exists()) {
return;
}
if (file.isFile()) {
file.delete();
} else if (file.isDirectory()) {
File[] files = file.listFiles();
for (File value : files) {
deleteRecursive(value);
}
file.delete();
}
}
public void compile(String[] paths) throws Exception {
unit.addSources(paths);
unit.compile();
}
public void compile(File[] files) throws Exception {
unit.addSources(files);
unit.compile();
}
/**
* @since 2.5
*/
static class VersionProvider implements IVersionProvider {
@Override
public String[] getVersion() {
return new String[]{
"Groovy compiler version " + GroovySystem.getVersion(),
"Copyright 2003-2020 The Apache Software Foundation. http://groovy-lang.org/",
"",
};
}
}
/**
* @since 2.5
*/
@Command(name = "groovyc",
customSynopsis = "groovyc [options] <source-files>",
sortOptions = false,
versionProvider = VersionProvider.class)
public static class CompilationOptions {
// IMPLEMENTATION NOTE:
// classpath must be the first argument, so that the `startGroovy(.bat)` script
// can extract it and the JVM can be started with the classpath already correctly set.
// This saves us from having to fork a new JVM process with the classpath set from the processed arguments.
@Option(names = {"-cp", "-classpath", "--classpath"}, paramLabel = "<path>", description = "Specify where to find the class files - must be first argument")
private String classpath;
@Option(names = {"-sourcepath", "--sourcepath"}, paramLabel = "<path>", description = "Specify where to find the source files")
private File sourcepath;
@Option(names = {"--temp"}, paramLabel = "<temp>", description = "Specify temporary directory")
private File temp;
@Option(names = {"--encoding"}, description = "Specify the encoding of the user class files")
private String encoding;
@Option(names = "-d", paramLabel = "<dir>", description = "Specify where to place generated class files")
private File targetDir;
@Option(names = {"-e", "--exception"}, description = "Print stack trace on error")
private boolean printStack;
@Option(names = {"-pa", "--parameters"}, description = "Generate metadata for reflection on method parameter names (jdk8+ only)")
private boolean parameterMetadata;
@Option(names = {"-pr", "--enable-preview"}, description = "Enable preview Java features (JEP 12) (jdk12+ only) - must be after classpath but before other arguments")
private boolean previewFeatures;
@Option(names = {"-j", "--jointCompilation"}, description = "Attach javac compiler to compile .java files")
private boolean jointCompilation;
@Option(names = {"-b", "--basescript"}, paramLabel = "<class>", description = "Base class name for scripts (must derive from Script)")
private String scriptBaseClass;
@Option(names = "-J", paramLabel = "<property=value>", description = "Name-value pairs to pass to javac")
private Map<String, String> javacOptionsMap;
@Option(names = "-F", paramLabel = "<flag>", description = "Passed to javac for joint compilation")
private List<String> flags;
@Option(names = {"--indy"}, description = "Enables compilation using invokedynamic")
private boolean indy;
@Option(names = {"--configscript"}, paramLabel = "<script>", description = "A script for tweaking the configuration options")
private String configScript;
@Option(names = {"-h", "--help"}, usageHelp = true, description = "Show this help message and exit")
private boolean helpRequested;
@Option(names = {"-v", "--version"}, versionHelp = true, description = "Print version information and exit")
private boolean versionRequested;
@Parameters(description = "The groovy source files to compile, or @-files containing a list of source files to compile",
paramLabel = "<source-files>")
private List<String> files;
@Option(names = {"--compile-static"}, description = "Use CompileStatic")
private boolean compileStatic;
@Option(names = {"--type-checked"}, description = "Use TypeChecked")
private boolean typeChecked;
public CompilerConfiguration toCompilerConfiguration() throws IOException {
// Setup the configuration data
CompilerConfiguration configuration = new CompilerConfiguration();
if (classpath != null) {
configuration.setClasspath(classpath);
}
if (targetDir != null && targetDir.getName().length() > 0) {
configuration.setTargetDirectory(targetDir);
}
configuration.setParameters(parameterMetadata);
configuration.setPreviewFeatures(previewFeatures);
configuration.setSourceEncoding(encoding);
configuration.setScriptBaseClass(scriptBaseClass);
// joint compilation parameters
if (jointCompilation) {
Map<String, Object> compilerOptions = new HashMap<>();
compilerOptions.put("flags", javacFlags());
compilerOptions.put("namedValues", javacNamedValues());
configuration.setJointCompilationOptions(compilerOptions);
}
if (indy) {
configuration.getOptimizationOptions().put("int", Boolean.FALSE);
configuration.getOptimizationOptions().put("indy", Boolean.TRUE);
}
final List<String> transformations = new ArrayList<>();
if (compileStatic) {
transformations.add("ast(groovy.transform.CompileStatic)");
}
if (typeChecked) {
transformations.add("ast(groovy.transform.TypeChecked)");
}
if (!transformations.isEmpty()) {
processConfigScriptText(buildConfigScriptText(transformations), configuration);
}
String configScripts = System.getProperty("groovy.starter.configscripts", null);
if (configScript != null || (configScripts != null && !configScripts.isEmpty())) {
List<String> scripts = new ArrayList<>();
if (configScript != null) {
scripts.add(configScript);
}
if (configScripts != null) {
scripts.addAll(StringGroovyMethods.tokenize(configScripts, ','));
}
processConfigScripts(scripts, configuration);
}
return configuration;
}
public String[] generateFileNames() {
return generateFileNamesFromOptions(files);
}
private String[] javacNamedValues() {
List<String> result = new ArrayList<>();
if (javacOptionsMap != null) {
for (Map.Entry<String, String> entry : javacOptionsMap.entrySet()) {
result.add(entry.getKey());
result.add(entry.getValue());
}
}
return result.isEmpty() ? null : result.toArray(new String[0]);
}
private String[] javacFlags() {
List<String> result = new ArrayList<>();
if (flags != null) {
result.addAll(flags);
}
if (parameterMetadata) {
result.add("parameters");
}
if (previewFeatures) {
result.add("-enable-preview");
}
return result.isEmpty() ? null : result.toArray(new String[0]);
}
}
}