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    
017    package org.jetbrains.jet.cli.jvm.compiler;
018    
019    import com.google.common.collect.Lists;
020    import com.google.common.collect.Sets;
021    import com.intellij.openapi.Disposable;
022    import com.intellij.openapi.project.Project;
023    import com.intellij.openapi.util.Disposer;
024    import com.intellij.openapi.util.io.FileUtil;
025    import com.intellij.openapi.util.io.FileUtilRt;
026    import com.intellij.openapi.vfs.StandardFileSystems;
027    import com.intellij.openapi.vfs.VirtualFile;
028    import com.intellij.openapi.vfs.VirtualFileManager;
029    import com.intellij.openapi.vfs.VirtualFileSystem;
030    import com.intellij.psi.PsiFile;
031    import com.intellij.psi.PsiManager;
032    import kotlin.Function1;
033    import kotlin.Unit;
034    import kotlin.io.IoPackage;
035    import kotlin.modules.AllModules;
036    import kotlin.modules.Module;
037    import org.jetbrains.annotations.NotNull;
038    import org.jetbrains.annotations.Nullable;
039    import org.jetbrains.jet.OutputFile;
040    import org.jetbrains.jet.cli.common.CLIConfigurationKeys;
041    import org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity;
042    import org.jetbrains.jet.cli.common.messages.MessageCollector;
043    import org.jetbrains.jet.cli.common.messages.OutputMessageUtil;
044    import org.jetbrains.jet.cli.common.modules.ModuleScriptData;
045    import org.jetbrains.jet.cli.common.modules.ModuleXmlParser;
046    import org.jetbrains.jet.cli.common.output.outputUtils.OutputUtilsPackage;
047    import org.jetbrains.jet.cli.jvm.JVMConfigurationKeys;
048    import org.jetbrains.jet.codegen.ClassFileFactory;
049    import org.jetbrains.jet.codegen.GeneratedClassLoader;
050    import org.jetbrains.jet.codegen.state.GenerationState;
051    import org.jetbrains.jet.config.CommonConfigurationKeys;
052    import org.jetbrains.jet.config.CompilerConfiguration;
053    import org.jetbrains.jet.lang.psi.JetFile;
054    import org.jetbrains.jet.lang.resolve.java.PackageClassUtils;
055    import org.jetbrains.jet.lang.resolve.name.FqName;
056    import org.jetbrains.jet.plugin.JetFileType;
057    import org.jetbrains.jet.utils.KotlinPaths;
058    import org.jetbrains.jet.utils.PathUtil;
059    import org.jetbrains.jet.utils.UtilsPackage;
060    
061    import java.io.*;
062    import java.lang.reflect.Method;
063    import java.net.MalformedURLException;
064    import java.net.URL;
065    import java.net.URLClassLoader;
066    import java.util.ArrayList;
067    import java.util.List;
068    import java.util.Set;
069    import java.util.jar.*;
070    
071    import static org.jetbrains.jet.cli.common.messages.CompilerMessageLocation.NO_LOCATION;
072    import static org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity.ERROR;
073    
074    public class CompileEnvironmentUtil {
075    
076        @Nullable
077        private static File getRuntimeJarPath() {
078            File runtimePath = PathUtil.getKotlinPathsForCompiler().getRuntimePath();
079            return runtimePath.exists() ? runtimePath : null;
080        }
081    
082        @NotNull
083        public static ModuleScriptData loadModuleDescriptions(KotlinPaths paths, String moduleDefinitionFile, MessageCollector messageCollector) {
084            File file = new File(moduleDefinitionFile);
085            if (!file.exists()) {
086                messageCollector.report(ERROR, "Module definition file does not exist: " + moduleDefinitionFile, NO_LOCATION);
087                return ModuleScriptData.EMPTY;
088            }
089            String extension = FileUtilRt.getExtension(moduleDefinitionFile);
090            if ("ktm".equalsIgnoreCase(extension)) {
091                return loadModuleScript(paths, moduleDefinitionFile, messageCollector);
092            }
093            if ("xml".equalsIgnoreCase(extension)) {
094                return ModuleXmlParser.parseModuleScript(moduleDefinitionFile, messageCollector);
095            }
096            messageCollector.report(ERROR, "Unknown module definition type: " + moduleDefinitionFile, NO_LOCATION);
097            return ModuleScriptData.EMPTY;
098        }
099    
100        @NotNull
101        private static ModuleScriptData loadModuleScript(KotlinPaths paths, String moduleScriptFile, MessageCollector messageCollector) {
102            CompilerConfiguration configuration = new CompilerConfiguration();
103            File runtimePath = paths.getRuntimePath();
104            if (runtimePath.exists()) {
105                configuration.add(JVMConfigurationKeys.CLASSPATH_KEY, runtimePath);
106            }
107            configuration.addAll(JVMConfigurationKeys.CLASSPATH_KEY, PathUtil.getJdkClassesRoots());
108            File jdkAnnotationsPath = paths.getJdkAnnotationsPath();
109            if (jdkAnnotationsPath.exists()) {
110                configuration.add(JVMConfigurationKeys.ANNOTATIONS_PATH_KEY, jdkAnnotationsPath);
111            }
112            configuration.add(CommonConfigurationKeys.SOURCE_ROOTS_KEY, moduleScriptFile);
113            configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector);
114    
115            List<Module> modules;
116    
117            Disposable disposable = Disposer.newDisposable();
118            try {
119                JetCoreEnvironment scriptEnvironment =
120                        JetCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES);
121                GenerationState generationState = KotlinToJVMBytecodeCompiler.analyzeAndGenerate(scriptEnvironment);
122                if (generationState == null) {
123                    throw new CompileEnvironmentException("Module script " + moduleScriptFile + " analyze failed:\n" +
124                                                          loadModuleScriptText(moduleScriptFile));
125                }
126    
127                modules = runDefineModules(paths, generationState.getFactory());
128            }
129            finally {
130                Disposer.dispose(disposable);
131            }
132    
133            if (modules == null) {
134                throw new CompileEnvironmentException("Module script " + moduleScriptFile + " compilation failed");
135            }
136    
137            if (modules.isEmpty()) {
138                throw new CompileEnvironmentException("No modules where defined by " + moduleScriptFile);
139            }
140            return new ModuleScriptData(modules);
141        }
142    
143        private static List<Module> runDefineModules(KotlinPaths paths, ClassFileFactory factory) {
144            File stdlibJar = paths.getRuntimePath();
145            GeneratedClassLoader loader;
146            if (stdlibJar.exists()) {
147                try {
148                    loader = new GeneratedClassLoader(factory, new URLClassLoader(new URL[]{stdlibJar.toURI().toURL()},
149                                                                                  AllModules.class.getClassLoader()));
150                }
151                catch (MalformedURLException e) {
152                    throw new RuntimeException(e);
153                }
154            }
155            else {
156                loader = new GeneratedClassLoader(factory, KotlinToJVMBytecodeCompiler.class.getClassLoader());
157            }
158            try {
159                Class<?> packageClass = loader.loadClass(PackageClassUtils.getPackageClassName(FqName.ROOT));
160                Method method = packageClass.getDeclaredMethod("project");
161    
162                method.setAccessible(true);
163                method.invoke(null);
164    
165                ArrayList<Module> answer = new ArrayList<Module>(AllModules.INSTANCE$.get());
166                AllModules.INSTANCE$.get().clear();
167                return answer;
168            }
169            catch (Exception e) {
170                throw new ModuleExecutionException(e);
171            }
172            finally {
173                loader.dispose();
174            }
175        }
176    
177        // TODO: includeRuntime should be not a flag but a path to runtime
178        private static void doWriteToJar(ClassFileFactory outputFiles, OutputStream fos, @Nullable FqName mainClass, boolean includeRuntime) {
179            try {
180                Manifest manifest = new Manifest();
181                Attributes mainAttributes = manifest.getMainAttributes();
182                mainAttributes.putValue("Manifest-Version", "1.0");
183                mainAttributes.putValue("Created-By", "JetBrains Kotlin");
184                if (mainClass != null) {
185                    mainAttributes.putValue("Main-Class", mainClass.asString());
186                }
187                JarOutputStream stream = new JarOutputStream(fos, manifest);
188                for (OutputFile outputFile : outputFiles.asList()) {
189                    stream.putNextEntry(new JarEntry(outputFile.getRelativePath()));
190                    stream.write(outputFile.asByteArray());
191                }
192                if (includeRuntime) {
193                    writeRuntimeToJar(stream);
194                }
195                stream.finish();
196            }
197            catch (IOException e) {
198                throw new CompileEnvironmentException("Failed to generate jar file", e);
199            }
200        }
201    
202        public static void writeToJar(File jarPath, boolean jarRuntime, FqName mainClass, ClassFileFactory outputFiles) {
203            FileOutputStream outputStream = null;
204            try {
205                outputStream = new FileOutputStream(jarPath);
206                doWriteToJar(outputFiles, outputStream, mainClass, jarRuntime);
207                outputStream.close();
208            }
209            catch (FileNotFoundException e) {
210                throw new CompileEnvironmentException("Invalid jar path " + jarPath, e);
211            }
212            catch (IOException e) {
213                throw UtilsPackage.rethrow(e);
214            }
215            finally {
216                UtilsPackage.closeQuietly(outputStream);
217            }
218        }
219    
220        private static void writeRuntimeToJar(JarOutputStream stream) throws IOException {
221            File runtimeJarPath = getRuntimeJarPath();
222            if (runtimeJarPath != null) {
223                JarInputStream jis = new JarInputStream(new FileInputStream(runtimeJarPath));
224                try {
225                    while (true) {
226                        JarEntry e = jis.getNextJarEntry();
227                        if (e == null) {
228                            break;
229                        }
230                        if (FileUtilRt.extensionEquals(e.getName(), "class")) {
231                            stream.putNextEntry(e);
232                            FileUtil.copy(jis, stream);
233                        }
234                    }
235                }
236                finally {
237                    jis.close();
238                }
239            }
240            else {
241                throw new CompileEnvironmentException("Couldn't find runtime library");
242            }
243        }
244    
245        // Used for debug output only
246        private static String loadModuleScriptText(String moduleScriptFile) {
247            try {
248                return FileUtil.loadFile(new File(moduleScriptFile));
249            }
250            catch (IOException e) {
251                return "Can't load module script text:\n" + OutputMessageUtil.renderException(e);
252            }
253        }
254    
255        static void writeOutputToDirOrJar(
256                @Nullable File jar,
257                @Nullable File outputDir,
258                boolean includeRuntime,
259                @Nullable FqName mainClass,
260                @NotNull ClassFileFactory outputFiles,
261                @NotNull MessageCollector messageCollector
262        ) {
263            if (jar != null) {
264                writeToJar(jar, includeRuntime, mainClass, outputFiles);
265            }
266            else {
267                OutputUtilsPackage.writeAll(outputFiles, outputDir == null ? new File(".") : outputDir, messageCollector);
268            }
269        }
270    
271        @NotNull
272        public static List<JetFile> getJetFiles(
273                @NotNull final Project project,
274                @NotNull List<String> sourceRoots,
275                @NotNull Function1<String, Unit> reportError
276        ) {
277            final VirtualFileSystem localFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL);
278    
279            final Set<VirtualFile> processedFiles = Sets.newHashSet();
280            final List<JetFile> result = Lists.newArrayList();
281    
282            for (String sourceRootPath : sourceRoots) {
283                if (sourceRootPath == null) {
284                    continue;
285                }
286    
287                VirtualFile vFile = localFileSystem.findFileByPath(sourceRootPath);
288                if (vFile == null) {
289                    reportError.invoke("Source file or directory not found: " + sourceRootPath);
290                    continue;
291                }
292                if (!vFile.isDirectory() && vFile.getFileType() != JetFileType.INSTANCE) {
293                    reportError.invoke("Source entry is not a Kotlin file: " + sourceRootPath);
294                    continue;
295                }
296    
297                IoPackage.recurse(new File(sourceRootPath), new Function1<File, Unit>() {
298                    @Override
299                    public Unit invoke(File file) {
300                        if (file.isFile()) {
301                            VirtualFile virtualFile = localFileSystem.findFileByPath(file.getAbsolutePath());
302                            if (virtualFile != null && !processedFiles.contains(virtualFile)) {
303                                processedFiles.add(virtualFile);
304                                PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
305                                if (psiFile instanceof JetFile) {
306                                    result.add((JetFile) psiFile);
307                                }
308                            }
309                        }
310                        return Unit.INSTANCE$;
311                    }
312                });
313            }
314    
315            return result;
316        }
317    
318        public static void addSourceFilesCheckingForDuplicates(@NotNull CompilerConfiguration configuration, @NotNull List<String> sourceRoots) {
319            MessageCollector messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY);
320            assert messageCollector != null : "messageCollector should be set: " + configuration;
321    
322            Set<String> uniqueSourceRoots = Sets.newLinkedHashSet();
323    
324            for (String sourceRoot : sourceRoots) {
325                if (!uniqueSourceRoots.add(sourceRoot)) {
326                    messageCollector.report(
327                            CompilerMessageSeverity.WARNING,
328                            "Duplicate source roots: " + sourceRoot,
329                            NO_LOCATION
330                    );
331                }
332            }
333    
334            configuration.put(CommonConfigurationKeys.SOURCE_ROOTS_KEY, new ArrayList<String>(uniqueSourceRoots));
335        }
336    }