/* * Copyright 2010-2013 JetBrains s.r.o. * * 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 org.jetbrains.kotlin.maven; import com.google.common.base.Joiner; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.io.FileUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.Processor; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; import org.apache.maven.artifact.resolver.ArtifactResolutionResult; import org.apache.maven.model.Dependency; import org.apache.maven.model.Plugin; import org.apache.maven.model.PluginExecution; import org.apache.maven.plugin.*; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.repository.RepositorySystem; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.cli.common.CLICompiler; import org.jetbrains.kotlin.cli.common.ExitCode; import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments; import org.jetbrains.kotlin.cli.common.messages.MessageCollector; import org.jetbrains.kotlin.config.KotlinCompilerVersion; import org.jetbrains.kotlin.config.Services; import java.io.File; import java.lang.reflect.Field; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public abstract class KotlinCompileMojoBase<A extends CommonCompilerArguments> extends AbstractMojo { @Component protected PlexusContainer container; @Component protected MojoExecution mojoExecution; @Component protected RepositorySystem system; /** * The source directories containing the sources to be compiled. */ @Parameter private List<String> sourceDirs; /** * A list of kotlin compiler plugins to be applied. */ @Parameter private List<String> compilerPlugins; /** * A list of plugin options in format (pluginId):(parameter)=(value) */ @Parameter private List<String> pluginOptions; @Parameter private boolean multiPlatform = false; private List<String> getAppliedCompilerPlugins() { return (compilerPlugins == null) ? Collections.<String>emptyList() : compilerPlugins; } protected List<String> getSourceFilePaths() { if (sourceDirs != null && !sourceDirs.isEmpty()) return sourceDirs; return project.getCompileSourceRoots(); } private List<File> getSourceDirs() { List<String> sources = getSourceFilePaths(); List<File> result = new ArrayList<File>(sources.size()); for (String source : sources) { addSourceRoots(result, source); } Map<String, MavenProject> projectReferences = project.getProjectReferences(); if (projectReferences != null) { iterateDependencies: for (Dependency dependency : project.getDependencies()) { MavenProject sibling = projectReferences.get(dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion()); if (sibling != null) { Plugin plugin = sibling.getPlugin("org.jetbrains.kotlin:kotlin-maven-plugin"); if (plugin != null) { for (PluginExecution pluginExecution : plugin.getExecutions()) { if (pluginExecution.getGoals() != null && pluginExecution.getGoals().contains("metadata")) { for (String sourceRoot : orEmpty(getRelatedSourceRoots(sibling))) { addSourceRoots(result, sourceRoot); continue iterateDependencies; } } } } } } } return result; } protected abstract List<String> getRelatedSourceRoots(MavenProject project); private void addSourceRoots(List<File> result, String source) { File f = new File(source); if (f.isAbsolute()) { result.add(f); } else { result.add(new File(project.getBasedir(), source)); } } /** * Suppress all warnings. */ @Parameter(defaultValue = "false") public boolean nowarn; @Parameter(defaultValue = "${project}", required = true, readonly = true) public MavenProject project; /** * The directory for compiled classes. */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) public String output; /** * The directory for compiled tests classes. */ @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true, readonly = true) public String testOutput; /** * Kotlin compilation module, as alternative to source files or folders. */ @Parameter(readonly = true) @Deprecated public String module; /** * Kotlin compilation module, as alternative to source files or folders (for tests). */ @Parameter(readonly = true) @Deprecated public String testModule; @Parameter(property = "kotlin.compiler.languageVersion", required = false, readonly = false) protected String languageVersion; @Parameter(property = "kotlin.compiler.apiVersion", required = false, readonly = false) protected String apiVersion; /** * possible values are: enable, error, warn */ @Parameter(property = "kotlin.compiler.experimental.coroutines", required = false, readonly = false) @Nullable protected String experimentalCoroutines; /** * Additional command line arguments for Kotlin compiler. */ @Parameter public List<String> args; private final static Pattern OPTION_PATTERN = Pattern.compile("([^:]+):([^=]+)=(.*)"); static { if (System.getProperty("kotlin.compiler.X.enable.idea.logger") != null) { Logger.setFactory(IdeaCoreLoggerFactory.class); } } @Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().info("Kotlin version " + KotlinCompilerVersion.VERSION + " (JRE " + System.getProperty("java.runtime.version") + ")"); if (!hasKotlinFilesInSources()) { getLog().warn("No sources found skipping Kotlin compile"); return; } A arguments = createCompilerArguments(); CLICompiler<A> compiler = createCompiler(); List<File> sourceRoots = getSourceRoots(); configureCompilerArguments(arguments, compiler); printCompilerArgumentsIfDebugEnabled(arguments, compiler); MavenPluginLogMessageCollector messageCollector = new MavenPluginLogMessageCollector(getLog()); ExitCode exitCode = execCompiler(compiler, messageCollector, arguments, sourceRoots); if (exitCode != ExitCode.OK) { messageCollector.throwKotlinCompilerException(); } } @NotNull protected ExitCode execCompiler( CLICompiler<A> compiler, MessageCollector messageCollector, A arguments, List<File> sourceRoots ) throws MojoExecutionException { for (File sourceRoot : sourceRoots) { arguments.freeArgs.add(sourceRoot.getPath()); } return compiler.exec(messageCollector, Services.EMPTY, arguments); } private boolean hasKotlinFilesInSources() throws MojoExecutionException { List<File> sources = getSourceDirs(); if (sources == null || sources.isEmpty()) return false; for (File root : sources) { if (root.exists()) { boolean sourcesExists = !FileUtil.processFilesRecursively(root, new Processor<File>() { @Override public boolean process(File file) { return !file.getName().endsWith(".kt"); } }); if (sourcesExists) return true; } } return false; } private void printCompilerArgumentsIfDebugEnabled(@NotNull A arguments, @NotNull CLICompiler<A> compiler) { if (getLog().isDebugEnabled()) { getLog().debug("Invoking compiler " + compiler + " with arguments:"); try { Field[] fields = arguments.getClass().getFields(); for (Field f : fields) { Object value = f.get(arguments); String valueString; if (value instanceof Object[]) { valueString = Arrays.deepToString((Object[]) value); } else if (value != null) { valueString = String.valueOf(value); } else { valueString = "(null)"; } getLog().debug(f.getName() + "=" + valueString); } getLog().debug("End of arguments"); } catch (Exception e) { getLog().warn("Failed to print compiler arguments: " + e, e); } } } @NotNull protected abstract CLICompiler<A> createCompiler(); @NotNull protected abstract A createCompilerArguments(); protected abstract void configureSpecificCompilerArguments(@NotNull A arguments) throws MojoExecutionException; private List<String> getCompilerPluginClassPaths() { ArrayList<String> result = new ArrayList<String>(); List<File> files = new ArrayList<File>(); for (Dependency dependency : mojoExecution.getPlugin().getDependencies()) { Artifact artifact = system.createDependencyArtifact(dependency); ArtifactResolutionResult resolved = system.resolve(new ArtifactResolutionRequest().setArtifact(artifact)); for (Artifact resolvedArtifact : resolved.getArtifacts()) { File file = resolvedArtifact.getFile(); if (file != null && file.exists()) { files.add(file); } } } for (File file : files) { result.add(file.getAbsolutePath()); } return result; } @NotNull private Map<String, KotlinMavenPluginExtension> loadCompilerPlugins() throws PluginNotFoundException { Map<String, KotlinMavenPluginExtension> loadedPlugins = new HashMap<String, KotlinMavenPluginExtension>(); for (String pluginName : getAppliedCompilerPlugins()) { getLog().debug("Looking for plugin " + pluginName); try { KotlinMavenPluginExtension extension = container.lookup(KotlinMavenPluginExtension.class, pluginName); loadedPlugins.put(pluginName, extension); getLog().debug("Got plugin instance" + pluginName + " of type " + extension.getClass().getName()); } catch (ComponentLookupException e) { getLog().debug("Unable to get plugin instance" + pluginName); throw new PluginNotFoundException(pluginName, e); } } return loadedPlugins; } @NotNull private List<String> renderCompilerPluginOptions(@NotNull List<PluginOption> options) { List<String> renderedOptions = new ArrayList<String>(options.size()); for (PluginOption option : options) { renderedOptions.add(option.toString()); } return renderedOptions; } @NotNull private List<PluginOption> getCompilerPluginOptions() throws PluginNotFoundException, PluginOptionIllegalFormatException { if (mojoExecution == null) { throw new IllegalStateException("No mojoExecution injected"); } List<PluginOption> pluginOptions = new ArrayList<PluginOption>(); Map<String, KotlinMavenPluginExtension> plugins = loadCompilerPlugins(); // Get options for extension-provided compiler plugins for (Map.Entry<String, KotlinMavenPluginExtension> pluginEntry : plugins.entrySet()) { String pluginName = pluginEntry.getKey(); KotlinMavenPluginExtension plugin = pluginEntry.getValue(); // applied plugin (...) to info() if (plugin.isApplicable(project, mojoExecution)) { getLog().info("Applied plugin: '" + pluginName + "'"); List<PluginOption> optionsForPlugin = plugin.getPluginOptions(project, mojoExecution); if (!optionsForPlugin.isEmpty()) { pluginOptions.addAll(optionsForPlugin); } } } if (this.pluginOptions != null) { pluginOptions.addAll(parseUserProvidedPluginOptions(this.pluginOptions, plugins)); } Map<String, List<PluginOption>> optionsByPluginName = new LinkedHashMap<String, List<PluginOption>>(); for (PluginOption option : pluginOptions) { List<PluginOption> optionsForPlugin = optionsByPluginName.get(option.pluginName); if (optionsForPlugin == null) { optionsForPlugin = new ArrayList<PluginOption>(); optionsByPluginName.put(option.pluginName, optionsForPlugin); } optionsForPlugin.add(option); } for (Map.Entry<String, List<PluginOption>> entry : optionsByPluginName.entrySet()) { assert !entry.getValue().isEmpty(); String pluginName = entry.getValue().get(0).pluginName; StringBuilder renderedOptions = new StringBuilder("["); for (PluginOption option : entry.getValue()) { if (renderedOptions.length() > 1) { renderedOptions.append(", "); } renderedOptions.append(option.key).append(": ").append(option.value); } renderedOptions.append("]"); getLog().debug("Options for plugin " + pluginName + ": " + renderedOptions); } return pluginOptions; } @NotNull private static List<PluginOption> parseUserProvidedPluginOptions( @NotNull List<String> rawOptions, @NotNull Map<String, KotlinMavenPluginExtension> plugins ) throws PluginOptionIllegalFormatException, PluginNotFoundException { List<PluginOption> pluginOptions = new ArrayList<PluginOption>(rawOptions.size()); for (String rawOption : rawOptions) { Matcher matcher = OPTION_PATTERN.matcher(rawOption); if (!matcher.matches()) { throw new PluginOptionIllegalFormatException(rawOption); } String pluginName = matcher.group(1); String key = matcher.group(2); String value = matcher.group(3); KotlinMavenPluginExtension plugin = plugins.get(pluginName); if (plugin == null) { throw new PluginNotFoundException(pluginName); } pluginOptions.add(new PluginOption(pluginName, plugin.getCompilerPluginId(), key, value)); } return pluginOptions; } @NotNull private List<File> getSourceRoots() throws MojoExecutionException { List<File> sourceRoots = new ArrayList<File>(); for (File sourceDir : getSourceDirs()) { if (sourceDir.exists()) { sourceRoots.add(sourceDir); } // unfortunately there is no good way to detect generated sources directory so we simply keep hardcoded value else if (!sourceDir.getPath().contains("generated-sources")) { getLog().warn("Source root doesn't exist: " + sourceDir); } } if (sourceRoots.isEmpty()) { throw new MojoExecutionException("No source roots to compile"); } getLog().info("Compiling Kotlin sources from " + sourceRoots); return sourceRoots; } private void configureCompilerArguments(@NotNull A arguments, @NotNull CLICompiler<A> compiler) throws MojoExecutionException { if (getLog().isDebugEnabled()) { arguments.verbose = true; } arguments.suppressWarnings = nowarn; arguments.languageVersion = languageVersion; arguments.apiVersion = apiVersion; arguments.multiPlatform = multiPlatform; if (experimentalCoroutines != null) { arguments.coroutinesState = experimentalCoroutines; } configureSpecificCompilerArguments(arguments); try { compiler.parseArguments(ArrayUtil.toStringArray(args), arguments); } catch (IllegalArgumentException e) { throw new MojoExecutionException(e.getMessage()); } if (arguments.noInline) { getLog().info("Method inlining is turned off"); } List<String> pluginClassPaths = getCompilerPluginClassPaths(); if (pluginClassPaths != null && !pluginClassPaths.isEmpty()) { if (getLog().isDebugEnabled()) { getLog().debug("Plugin classpaths are: " + Joiner.on(", ").join(pluginClassPaths)); } arguments.pluginClasspaths = pluginClassPaths.toArray(new String[pluginClassPaths.size()]); } List<String> pluginArguments; try { pluginArguments = renderCompilerPluginOptions(getCompilerPluginOptions()); } catch (PluginNotFoundException e) { throw new MojoExecutionException(e.getMessage(), e); } catch (PluginOptionIllegalFormatException e) { throw new MojoExecutionException(e.getMessage(), e); } if (!pluginArguments.isEmpty()) { if (getLog().isDebugEnabled()) { getLog().debug("Plugin options are: " + Joiner.on(", ").join(pluginArguments)); } arguments.pluginOptions = pluginArguments.toArray(new String[pluginArguments.size()]); } } public static class PluginNotFoundException extends Exception { PluginNotFoundException(String pluginId, Throwable cause) { super("Plugin not found: " + pluginId, cause); } PluginNotFoundException(String pluginId) { super("Plugin not found: " + pluginId); } } public static class PluginOptionIllegalFormatException extends Exception { PluginOptionIllegalFormatException(String option) { super("Plugin option has an illegal format: " + option); } } @NotNull private static <T> List<T> orEmpty(@Nullable List<T> in) { if (in == null) { return Collections.emptyList(); } return in; } }