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.intellij.openapi.Disposable;
021    import com.intellij.openapi.project.Project;
022    import com.intellij.openapi.util.Disposer;
023    import com.intellij.openapi.util.io.FileUtil;
024    import com.intellij.openapi.util.io.FileUtilRt;
025    import com.intellij.openapi.vfs.StandardFileSystems;
026    import com.intellij.openapi.vfs.VirtualFile;
027    import com.intellij.openapi.vfs.VirtualFileManager;
028    import com.intellij.openapi.vfs.VirtualFileSystem;
029    import com.intellij.psi.PsiFile;
030    import com.intellij.psi.PsiManager;
031    import com.intellij.util.Function;
032    import com.intellij.util.containers.ContainerUtil;
033    import kotlin.Function1;
034    import kotlin.Unit;
035    import kotlin.io.IoPackage;
036    import kotlin.modules.AllModules;
037    import kotlin.modules.Module;
038    import org.jetbrains.annotations.NotNull;
039    import org.jetbrains.annotations.Nullable;
040    import org.jetbrains.jet.OutputFile;
041    import org.jetbrains.jet.cli.common.CLIConfigurationKeys;
042    import org.jetbrains.jet.cli.common.messages.MessageCollector;
043    import org.jetbrains.jet.cli.common.messages.MessageRenderer;
044    import org.jetbrains.jet.cli.common.modules.ModuleDescription;
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.Collections;
068    import java.util.List;
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 List<Module> 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 Collections.emptyList();
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 ContainerUtil.map(
095                        ModuleXmlParser.parse(moduleDefinitionFile, messageCollector),
096                        new Function<ModuleDescription, Module>() {
097                            @Override
098                            public Module fun(ModuleDescription description) {
099                                return new DescriptionToModuleAdapter(description);
100                            }
101                        });
102            }
103            messageCollector.report(ERROR, "Unknown module definition type: " + moduleDefinitionFile, NO_LOCATION);
104            return Collections.emptyList();
105        }
106    
107        @NotNull
108        private static List<Module> loadModuleScript(KotlinPaths paths, String moduleScriptFile, MessageCollector messageCollector) {
109            CompilerConfiguration configuration = new CompilerConfiguration();
110            File runtimePath = paths.getRuntimePath();
111            if (runtimePath.exists()) {
112                configuration.add(JVMConfigurationKeys.CLASSPATH_KEY, runtimePath);
113            }
114            configuration.addAll(JVMConfigurationKeys.CLASSPATH_KEY, PathUtil.getJdkClassesRoots());
115            File jdkAnnotationsPath = paths.getJdkAnnotationsPath();
116            if (jdkAnnotationsPath.exists()) {
117                configuration.add(JVMConfigurationKeys.ANNOTATIONS_PATH_KEY, jdkAnnotationsPath);
118            }
119            configuration.add(CommonConfigurationKeys.SOURCE_ROOTS_KEY, moduleScriptFile);
120            configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector);
121    
122            List<Module> modules;
123    
124            Disposable disposable = Disposer.newDisposable();
125            try {
126                JetCoreEnvironment scriptEnvironment = JetCoreEnvironment.createForProduction(disposable, configuration);
127                GenerationState generationState = KotlinToJVMBytecodeCompiler.analyzeAndGenerate(scriptEnvironment);
128                if (generationState == null) {
129                    throw new CompileEnvironmentException("Module script " + moduleScriptFile + " analyze failed:\n" +
130                                                          loadModuleScriptText(moduleScriptFile));
131                }
132    
133                modules = runDefineModules(paths, generationState.getFactory());
134            }
135            finally {
136                Disposer.dispose(disposable);
137            }
138    
139            if (modules == null) {
140                throw new CompileEnvironmentException("Module script " + moduleScriptFile + " compilation failed");
141            }
142    
143            if (modules.isEmpty()) {
144                throw new CompileEnvironmentException("No modules where defined by " + moduleScriptFile);
145            }
146            return modules;
147        }
148    
149        private static List<Module> runDefineModules(KotlinPaths paths, ClassFileFactory factory) {
150            File stdlibJar = paths.getRuntimePath();
151            GeneratedClassLoader loader;
152            if (stdlibJar.exists()) {
153                try {
154                    loader = new GeneratedClassLoader(factory, new URLClassLoader(new URL[]{stdlibJar.toURI().toURL()},
155                                                                                  AllModules.class.getClassLoader()));
156                }
157                catch (MalformedURLException e) {
158                    throw new RuntimeException(e);
159                }
160            }
161            else {
162                loader = new GeneratedClassLoader(factory, KotlinToJVMBytecodeCompiler.class.getClassLoader());
163            }
164            try {
165                Class<?> packageClass = loader.loadClass(PackageClassUtils.getPackageClassName(FqName.ROOT));
166                Method method = packageClass.getDeclaredMethod("project");
167    
168                method.setAccessible(true);
169                method.invoke(null);
170    
171                ArrayList<Module> answer = new ArrayList<Module>(AllModules.instance$.get());
172                AllModules.instance$.get().clear();
173                return answer;
174            }
175            catch (Exception e) {
176                throw new ModuleExecutionException(e);
177            }
178            finally {
179                loader.dispose();
180            }
181        }
182    
183        // TODO: includeRuntime should be not a flag but a path to runtime
184        private static void doWriteToJar(ClassFileFactory outputFiles, OutputStream fos, @Nullable FqName mainClass, boolean includeRuntime) {
185            try {
186                Manifest manifest = new Manifest();
187                Attributes mainAttributes = manifest.getMainAttributes();
188                mainAttributes.putValue("Manifest-Version", "1.0");
189                mainAttributes.putValue("Created-By", "JetBrains Kotlin");
190                if (mainClass != null) {
191                    mainAttributes.putValue("Main-Class", mainClass.asString());
192                }
193                JarOutputStream stream = new JarOutputStream(fos, manifest);
194                for (OutputFile outputFile : outputFiles.asList()) {
195                    stream.putNextEntry(new JarEntry(outputFile.getRelativePath()));
196                    stream.write(outputFile.asByteArray());
197                }
198                if (includeRuntime) {
199                    writeRuntimeToJar(stream);
200                }
201                stream.finish();
202            }
203            catch (IOException e) {
204                throw new CompileEnvironmentException("Failed to generate jar file", e);
205            }
206        }
207    
208        public static void writeToJar(File jarPath, boolean jarRuntime, FqName mainClass, ClassFileFactory outputFiles) {
209            FileOutputStream outputStream = null;
210            try {
211                outputStream = new FileOutputStream(jarPath);
212                doWriteToJar(outputFiles, outputStream, mainClass, jarRuntime);
213                outputStream.close();
214            }
215            catch (FileNotFoundException e) {
216                throw new CompileEnvironmentException("Invalid jar path " + jarPath, e);
217            }
218            catch (IOException e) {
219                throw UtilsPackage.rethrow(e);
220            }
221            finally {
222                UtilsPackage.closeQuietly(outputStream);
223            }
224        }
225    
226        private static void writeRuntimeToJar(JarOutputStream stream) throws IOException {
227            File runtimeJarPath = getRuntimeJarPath();
228            if (runtimeJarPath != null) {
229                JarInputStream jis = new JarInputStream(new FileInputStream(runtimeJarPath));
230                try {
231                    while (true) {
232                        JarEntry e = jis.getNextJarEntry();
233                        if (e == null) {
234                            break;
235                        }
236                        if (FileUtilRt.extensionEquals(e.getName(), "class")) {
237                            stream.putNextEntry(e);
238                            FileUtil.copy(jis, stream);
239                        }
240                    }
241                }
242                finally {
243                    jis.close();
244                }
245            }
246            else {
247                throw new CompileEnvironmentException("Couldn't find runtime library");
248            }
249        }
250    
251        // Used for debug output only
252        private static String loadModuleScriptText(String moduleScriptFile) {
253            String moduleScriptText;
254            try {
255                moduleScriptText = FileUtil.loadFile(new File(moduleScriptFile));
256            }
257            catch (IOException e) {
258                moduleScriptText = "Can't load module script text:\n" + MessageRenderer.PLAIN.renderException(e);
259            }
260            return moduleScriptText;
261        }
262    
263        static void writeOutputToDirOrJar(
264                @Nullable File jar,
265                @Nullable File outputDir,
266                boolean includeRuntime,
267                @Nullable FqName mainClass,
268                @NotNull ClassFileFactory outputFiles,
269                @NotNull MessageCollector messageCollector
270        ) {
271            if (jar != null) {
272                writeToJar(jar, includeRuntime, mainClass, outputFiles);
273            }
274            else if (outputDir != null) {
275                OutputUtilsPackage.writeAll(outputFiles, outputDir, messageCollector);
276            }
277            else {
278                throw new CompileEnvironmentException("Output directory or jar file is not specified - no files will be saved to the disk");
279            }
280        }
281    
282        @NotNull
283        public static List<JetFile> getJetFiles(
284                @NotNull final Project project,
285                @NotNull List<String> sourceRoots,
286                @NotNull Function1<String, Unit> reportError
287        ) {
288            final VirtualFileSystem localFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL);
289    
290            final List<JetFile> result = Lists.newArrayList();
291    
292            for (String sourceRootPath : sourceRoots) {
293                if (sourceRootPath == null) {
294                    continue;
295                }
296    
297                VirtualFile vFile = localFileSystem.findFileByPath(sourceRootPath);
298                if (vFile == null) {
299                    reportError.invoke("Source file or directory not found: " + sourceRootPath);
300                    continue;
301                }
302                if (!vFile.isDirectory() && vFile.getFileType() != JetFileType.INSTANCE) {
303                    reportError.invoke("Source entry is not a Kotlin file: " + sourceRootPath);
304                    continue;
305                }
306    
307                IoPackage.recurse(new File(sourceRootPath), new Function1<File, Unit>() {
308                    @Override
309                    public Unit invoke(File file) {
310                        if (file.isFile()) {
311                            VirtualFile fileByPath = localFileSystem.findFileByPath(file.getAbsolutePath());
312                            if (fileByPath != null) {
313                                PsiFile psiFile = PsiManager.getInstance(project).findFile(fileByPath);
314                                if (psiFile instanceof JetFile) {
315                                    result.add((JetFile) psiFile);
316                                }
317                            }
318                        }
319                        return Unit.VALUE;
320                    }
321                });
322            }
323    
324            return result;
325        }
326    
327        private static class DescriptionToModuleAdapter implements Module {
328            private final ModuleDescription description;
329    
330            public DescriptionToModuleAdapter(ModuleDescription description) {
331                this.description = description;
332            }
333    
334            @NotNull
335            @Override
336            public String getModuleName() {
337                return description.getModuleName();
338            }
339    
340            @NotNull
341            @Override
342            public String getOutputDirectory() {
343                return description.getOutputDir();
344            }
345    
346            @NotNull
347            @Override
348            public List<String> getSourceFiles() {
349                return description.getSourceFiles();
350            }
351    
352            @NotNull
353            @Override
354            public List<String> getClasspathRoots() {
355                return description.getClasspathRoots();
356            }
357    
358            @NotNull
359            @Override
360            public List<String> getAnnotationsRoots() {
361                return description.getAnnotationsRoots();
362            }
363        }
364    }