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