001/*
002 * Copyright 2010-2013 JetBrains s.r.o.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.jetbrains.jet.cli.jvm.compiler;
018
019import com.google.common.base.Predicate;
020import com.google.common.base.Predicates;
021import com.intellij.openapi.Disposable;
022import com.intellij.openapi.project.Project;
023import com.intellij.openapi.util.Disposer;
024import com.intellij.psi.PsiFile;
025import jet.Function0;
026import jet.modules.AllModules;
027import jet.modules.Module;
028import org.jetbrains.annotations.NotNull;
029import org.jetbrains.annotations.Nullable;
030import org.jetbrains.jet.analyzer.AnalyzeExhaust;
031import org.jetbrains.jet.cli.common.CLIConfigurationKeys;
032import org.jetbrains.jet.cli.common.CompilerPlugin;
033import org.jetbrains.jet.cli.common.CompilerPluginContext;
034import org.jetbrains.jet.cli.common.messages.*;
035import org.jetbrains.jet.cli.jvm.JVMConfigurationKeys;
036import org.jetbrains.jet.codegen.*;
037import org.jetbrains.jet.codegen.state.GenerationState;
038import org.jetbrains.jet.codegen.state.Progress;
039import org.jetbrains.jet.config.CommonConfigurationKeys;
040import org.jetbrains.jet.config.CompilerConfiguration;
041import org.jetbrains.jet.lang.parsing.JetScriptDefinition;
042import org.jetbrains.jet.lang.parsing.JetScriptDefinitionProvider;
043import org.jetbrains.jet.lang.psi.JetFile;
044import org.jetbrains.jet.lang.psi.JetPsiUtil;
045import org.jetbrains.jet.lang.resolve.AnalyzerScriptParameter;
046import org.jetbrains.jet.lang.resolve.BindingTrace;
047import org.jetbrains.jet.lang.resolve.ScriptNameUtil;
048import org.jetbrains.jet.lang.resolve.java.AnalyzerFacadeForJVM;
049import org.jetbrains.jet.lang.resolve.java.PackageClassUtils;
050import org.jetbrains.jet.lang.resolve.name.FqName;
051import org.jetbrains.jet.plugin.JetMainDetector;
052import org.jetbrains.jet.utils.ExceptionUtils;
053import org.jetbrains.jet.utils.KotlinPaths;
054import org.jetbrains.jet.utils.PathUtil;
055
056import java.io.File;
057import java.io.FileNotFoundException;
058import java.io.FileOutputStream;
059import java.io.IOException;
060import java.net.URISyntaxException;
061import java.net.URL;
062import java.net.URLClassLoader;
063import java.util.Collection;
064import java.util.Collections;
065import java.util.LinkedList;
066import java.util.List;
067
068public class KotlinToJVMBytecodeCompiler {
069
070    private KotlinToJVMBytecodeCompiler() {
071    }
072
073    @Nullable
074    public static ClassFileFactory compileModule(CompilerConfiguration configuration, Module moduleBuilder, File directory) {
075        if (moduleBuilder.getSourceFiles().isEmpty()) {
076            throw new CompileEnvironmentException("No source files where defined in module " + moduleBuilder.getModuleName());
077        }
078
079        CompilerConfiguration compilerConfiguration = configuration.copy();
080        for (String sourceFile : moduleBuilder.getSourceFiles()) {
081            File source = new File(sourceFile);
082            if (!source.isAbsolute()) {
083                source = new File(directory, sourceFile);
084            }
085
086            if (!source.exists()) {
087                throw new CompileEnvironmentException("'" + source + "' does not exist in module " + moduleBuilder.getModuleName());
088            }
089
090            compilerConfiguration.add(CommonConfigurationKeys.SOURCE_ROOTS_KEY, source.getPath());
091        }
092
093        for (String classpathRoot : moduleBuilder.getClasspathRoots()) {
094            compilerConfiguration.add(JVMConfigurationKeys.CLASSPATH_KEY, new File(classpathRoot));
095        }
096
097        for (String annotationsRoot : moduleBuilder.getAnnotationsRoots()) {
098            compilerConfiguration.add(JVMConfigurationKeys.ANNOTATIONS_PATH_KEY, new File(annotationsRoot));
099        }
100
101        Disposable parentDisposable = CompileEnvironmentUtil.createMockDisposable();
102        JetCoreEnvironment moduleEnvironment = null;
103        try {
104            moduleEnvironment = new JetCoreEnvironment(parentDisposable, compilerConfiguration);
105
106
107            GenerationState generationState = analyzeAndGenerate(moduleEnvironment);
108            if (generationState == null) {
109                return null;
110            }
111            return generationState.getFactory();
112        } finally {
113            if (moduleEnvironment != null) {
114                Disposer.dispose(parentDisposable);
115            }
116        }
117    }
118
119    public static boolean compileModules(
120            CompilerConfiguration configuration,
121            @NotNull List<Module> modules,
122            @NotNull File directory,
123            @Nullable File jarPath,
124            @Nullable File outputDir,
125            boolean jarRuntime) {
126
127        for (Module moduleBuilder : modules) {
128            ClassFileFactory moduleFactory = compileModule(configuration, moduleBuilder, directory);
129            if (moduleFactory == null) {
130                return false;
131            }
132            if (outputDir != null) {
133                CompileEnvironmentUtil.writeToOutputDirectory(moduleFactory, outputDir);
134            }
135            else {
136                File path = jarPath != null ? jarPath : new File(directory, moduleBuilder.getModuleName() + ".jar");
137                FileOutputStream outputStream = null;
138                try {
139                    outputStream = new FileOutputStream(path);
140                    CompileEnvironmentUtil.writeToJar(moduleFactory, outputStream, null, jarRuntime);
141                    outputStream.close();
142                }
143                catch (FileNotFoundException e) {
144                    throw new CompileEnvironmentException("Invalid jar path " + path, e);
145                }
146                catch (IOException e) {
147                    throw ExceptionUtils.rethrow(e);
148                }
149                finally {
150                    ExceptionUtils.closeQuietly(outputStream);
151                }
152            }
153        }
154        return true;
155    }
156
157    @Nullable
158    private static FqName findMainClass(@NotNull List<JetFile> files) {
159        FqName mainClass = null;
160        for (JetFile file : files) {
161            if (JetMainDetector.hasMain(file.getDeclarations())) {
162                if (mainClass != null) {
163                    // more than one main
164                    return null;
165                }
166                FqName fqName = JetPsiUtil.getFQName(file);
167                mainClass = PackageClassUtils.getPackageClassFqName(fqName);
168            }
169        }
170        return mainClass;
171    }
172
173    public static boolean compileBunchOfSources(
174            JetCoreEnvironment environment,
175            @Nullable File jar,
176            @Nullable File outputDir,
177            boolean includeRuntime
178    ) {
179
180        FqName mainClass = findMainClass(environment.getSourceFiles());
181
182        GenerationState generationState = analyzeAndGenerate(environment);
183        if (generationState == null) {
184            return false;
185        }
186
187        try {
188            ClassFileFactory factory = generationState.getFactory();
189            if (jar != null) {
190                FileOutputStream os = null;
191                try {
192                    os = new FileOutputStream(jar);
193                    CompileEnvironmentUtil.writeToJar(factory, new FileOutputStream(jar), mainClass, includeRuntime);
194                    os.close();
195                }
196                catch (FileNotFoundException e) {
197                    throw new CompileEnvironmentException("Invalid jar path " + jar, e);
198                }
199                catch (IOException e) {
200                    throw ExceptionUtils.rethrow(e);
201                }
202                finally {
203                    ExceptionUtils.closeQuietly(os);
204                }
205            }
206            else if (outputDir != null) {
207                CompileEnvironmentUtil.writeToOutputDirectory(factory, outputDir);
208            }
209            else {
210                throw new CompileEnvironmentException("Output directory or jar file is not specified - no files will be saved to the disk");
211            }
212            return true;
213        }
214        finally {
215            generationState.destroy();
216        }
217    }
218
219    public static boolean compileAndExecuteScript(
220            @NotNull KotlinPaths paths,
221            @NotNull JetCoreEnvironment environment,
222            @NotNull List<String> scriptArgs) {
223        Class<?> scriptClass = compileScript(paths, environment, null);
224        if(scriptClass == null)
225            return false;
226
227        try {
228            scriptClass.getConstructor(String[].class).newInstance(new Object[]{scriptArgs.toArray(new String[0])});
229        }
230        catch (RuntimeException e) {
231            throw e;
232        }
233        catch (Exception e) {
234            throw new RuntimeException("Failed to evaluate script: " + e, e);
235        }
236        return true;
237    }
238
239    private static Class<?> compileScript(
240            @NotNull KotlinPaths paths, @NotNull JetCoreEnvironment environment, @Nullable ClassLoader parentLoader) {
241
242        GenerationState generationState = analyzeAndGenerate(environment);
243        if (generationState == null) {
244            return null;
245        }
246
247        GeneratedClassLoader classLoader = null;
248        try {
249            ClassFileFactory factory = generationState.getFactory();
250            classLoader = new GeneratedClassLoader(factory,
251                    new URLClassLoader(new URL[] {
252                        // TODO: add all classpath
253                        paths.getRuntimePath().toURI().toURL()
254                    },
255                    parentLoader == null ? AllModules.class.getClassLoader() : parentLoader));
256
257            JetFile scriptFile = environment.getSourceFiles().get(0);
258            return classLoader.loadClass(ScriptNameUtil.classNameForScript(scriptFile));
259        }
260        catch (Exception e) {
261            throw new RuntimeException("Failed to evaluate script: " + e, e);
262        }
263        finally {
264            if (classLoader != null) {
265                classLoader.dispose();
266            }
267            generationState.destroy();
268        }
269    }
270
271    @Nullable
272    public static GenerationState analyzeAndGenerate(JetCoreEnvironment environment) {
273        return analyzeAndGenerate(environment, environment.getConfiguration().get(JVMConfigurationKeys.STUBS, false),
274                                  environment.getConfiguration().getList(JVMConfigurationKeys.SCRIPT_PARAMETERS));
275    }
276
277    @Nullable
278    public static GenerationState analyzeAndGenerate(
279            JetCoreEnvironment environment,
280            boolean stubs
281    ) {
282        return analyzeAndGenerate(environment, stubs,
283                                  environment.getConfiguration().getList(JVMConfigurationKeys.SCRIPT_PARAMETERS));
284    }
285
286    @Nullable
287    public static GenerationState analyzeAndGenerate(
288            JetCoreEnvironment environment,
289            boolean stubs,
290            List<AnalyzerScriptParameter> scriptParameters
291    ) {
292        AnalyzeExhaust exhaust = analyze(environment, scriptParameters, stubs);
293
294        if (exhaust == null) {
295            return null;
296        }
297
298        exhaust.throwIfError();
299
300        return generate(environment, exhaust, stubs);
301    }
302
303    @Nullable
304    private static AnalyzeExhaust analyze(
305            final JetCoreEnvironment environment,
306            final List<AnalyzerScriptParameter> scriptParameters,
307            boolean stubs) {
308        AnalyzerWithCompilerReport analyzerWithCompilerReport = new AnalyzerWithCompilerReport(
309                environment.getConfiguration().get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY));
310        final Predicate<PsiFile> filesToAnalyzeCompletely =
311                stubs ? Predicates.<PsiFile>alwaysFalse() : Predicates.<PsiFile>alwaysTrue();
312        analyzerWithCompilerReport.analyzeAndReport(
313                new Function0<AnalyzeExhaust>() {
314                    @NotNull
315                    @Override
316                    public AnalyzeExhaust invoke() {
317                        BindingTrace sharedTrace = CliLightClassGenerationSupport.getInstanceForCli(environment.getProject()).getTrace();
318                        return AnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
319                                environment.getProject(),
320                                environment.getSourceFiles(),
321                                sharedTrace,
322                                scriptParameters,
323                                filesToAnalyzeCompletely,
324                                false
325                        );
326                    }
327                }, environment.getSourceFiles()
328        );
329
330        return analyzerWithCompilerReport.hasErrors() ? null : analyzerWithCompilerReport.getAnalyzeExhaust();
331    }
332
333    @NotNull
334    private static GenerationState generate(
335            JetCoreEnvironment environment,
336            AnalyzeExhaust exhaust,
337            boolean stubs) {
338        Project project = environment.getProject();
339        final CompilerConfiguration configuration = environment.getConfiguration();
340        Progress backendProgress = new Progress() {
341            @Override
342            public void reportOutput(@NotNull Collection<File> sourceFiles, @Nullable File outputFile) {
343                if (outputFile == null) return;
344
345                MessageCollector messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY);
346                if (messageCollector == null) return;
347
348                messageCollector.report(
349                        CompilerMessageSeverity.OUTPUT,
350                        OutputMessageUtil.formatOutputMessage(sourceFiles, outputFile),
351                        CompilerMessageLocation.NO_LOCATION);
352            }
353        };
354        GenerationState generationState = new GenerationState(
355                project, ClassBuilderFactories.binaries(stubs), backendProgress, exhaust.getBindingContext(), environment.getSourceFiles(),
356                configuration.get(JVMConfigurationKeys.BUILTIN_TO_JAVA_TYPES_MAPPING_KEY, BuiltinToJavaTypesMapping.ENABLED),
357                configuration.get(JVMConfigurationKeys.GENERATE_NOT_NULL_ASSERTIONS, false),
358                configuration.get(JVMConfigurationKeys.GENERATE_NOT_NULL_PARAMETER_ASSERTIONS, false),
359                /*generateDeclaredClasses = */true
360        );
361        KotlinCodegenFacade.compileCorrectFiles(generationState, CompilationErrorHandler.THROW_EXCEPTION);
362
363        CompilerPluginContext context = new CompilerPluginContext(project, exhaust.getBindingContext(), environment.getSourceFiles());
364        for (CompilerPlugin plugin : configuration.getList(CLIConfigurationKeys.COMPILER_PLUGINS)) {
365            plugin.processFiles(context);
366        }
367        return generationState;
368    }
369
370    public static Class compileScript(
371            @NotNull ClassLoader parentLoader,
372            @NotNull KotlinPaths paths,
373            @NotNull String scriptPath,
374            @Nullable List<AnalyzerScriptParameter> scriptParameters,
375            @Nullable List<JetScriptDefinition> scriptDefinitions) {
376        MessageRenderer messageRenderer = MessageRenderer.PLAIN;
377        GroupingMessageCollector messageCollector = new GroupingMessageCollector(new PrintingMessageCollector(System.err, messageRenderer, false));
378        Disposable rootDisposable = CompileEnvironmentUtil.createMockDisposable();
379        try {
380            CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
381            compilerConfiguration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector);
382            compilerConfiguration.addAll(JVMConfigurationKeys.CLASSPATH_KEY, getClasspath(parentLoader));
383            compilerConfiguration.add(JVMConfigurationKeys.CLASSPATH_KEY, PathUtil.findRtJar());
384            compilerConfiguration.addAll(JVMConfigurationKeys.ANNOTATIONS_PATH_KEY, Collections.singletonList(
385                    paths.getJdkAnnotationsPath()));
386            compilerConfiguration.add(CommonConfigurationKeys.SOURCE_ROOTS_KEY, scriptPath);
387            compilerConfiguration.addAll(CommonConfigurationKeys.SCRIPT_DEFINITIONS_KEY,
388                                         scriptDefinitions != null ? scriptDefinitions : Collections.<JetScriptDefinition>emptyList());
389            compilerConfiguration.put(JVMConfigurationKeys.SCRIPT_PARAMETERS, scriptParameters);
390
391            JetCoreEnvironment environment = new JetCoreEnvironment(rootDisposable, compilerConfiguration);
392
393            try {
394                JetScriptDefinitionProvider.getInstance(environment.getProject()).markFileAsScript(environment.getSourceFiles().get(0));
395                return compileScript(paths, environment, parentLoader);
396            }
397            catch (CompilationException e) {
398                messageCollector.report(CompilerMessageSeverity.EXCEPTION, MessageRenderer.PLAIN.renderException(e),
399                                        MessageUtil.psiElementToMessageLocation(e.getElement()));
400                return null;
401            }
402            catch (Throwable t) {
403                MessageCollectorUtil.reportException(messageCollector, t);
404                return null;
405            }
406
407        }
408        finally {
409            messageCollector.flush();
410            Disposer.dispose(rootDisposable);
411        }
412    }
413
414    private static Collection<File> getClasspath(ClassLoader loader) {
415        return getClasspath(loader, new LinkedList<File>());
416    }
417
418    private static Collection<File> getClasspath(ClassLoader loader, LinkedList<File> files) {
419        ClassLoader parent = loader.getParent();
420        if(parent != null)
421            getClasspath(parent, files);
422
423        if(loader instanceof URLClassLoader) {
424            for (URL url : ((URLClassLoader) loader).getURLs()) {
425                String urlFile = url.getFile();
426
427                if (urlFile.contains("%")) {
428                    try {
429                        urlFile = url.toURI().getPath();
430                    }
431                    catch (URISyntaxException e) {
432                        throw ExceptionUtils.rethrow(e);
433                    }
434                }
435
436                File file = new File(urlFile);
437                if(file.exists() && (file.isDirectory() || file.getName().endsWith(".jar"))) {
438                    files.add(file);
439                }
440            }
441        }
442        return files;
443    }
444}