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.intellij.openapi.Disposable;
020    import com.intellij.openapi.util.Disposer;
021    import com.intellij.openapi.util.io.FileUtil;
022    import com.intellij.openapi.util.io.FileUtilRt;
023    import com.intellij.util.Function;
024    import com.intellij.util.containers.ContainerUtil;
025    import jet.modules.AllModules;
026    import jet.modules.Module;
027    import org.jetbrains.annotations.NotNull;
028    import org.jetbrains.annotations.Nullable;
029    import org.jetbrains.jet.cli.common.CLIConfigurationKeys;
030    import org.jetbrains.jet.cli.common.messages.*;
031    import org.jetbrains.jet.cli.common.modules.ModuleDescription;
032    import org.jetbrains.jet.cli.common.modules.ModuleXmlParser;
033    import org.jetbrains.jet.cli.jvm.JVMConfigurationKeys;
034    import org.jetbrains.jet.codegen.ClassFileFactory;
035    import org.jetbrains.jet.codegen.GeneratedClassLoader;
036    import org.jetbrains.jet.codegen.state.GenerationState;
037    import org.jetbrains.jet.config.CommonConfigurationKeys;
038    import org.jetbrains.jet.config.CompilerConfiguration;
039    import org.jetbrains.jet.lang.resolve.java.PackageClassUtils;
040    import org.jetbrains.jet.lang.resolve.name.FqName;
041    import org.jetbrains.jet.utils.ExceptionUtils;
042    import org.jetbrains.jet.utils.KotlinPaths;
043    import org.jetbrains.jet.utils.PathUtil;
044    
045    import java.io.*;
046    import java.lang.reflect.Method;
047    import java.net.MalformedURLException;
048    import java.net.URL;
049    import java.net.URLClassLoader;
050    import java.util.ArrayList;
051    import java.util.Collection;
052    import java.util.List;
053    import java.util.jar.*;
054    
055    import static org.jetbrains.jet.cli.common.messages.CompilerMessageLocation.NO_LOCATION;
056    import static org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity.ERROR;
057    
058    public class CompileEnvironmentUtil {
059    
060        @Nullable
061        private static File getRuntimeJarPath() {
062            File runtimePath = PathUtil.getKotlinPathsForCompiler().getRuntimePath();
063            return runtimePath.exists() ? runtimePath : null;
064        }
065    
066        @NotNull
067        public static ModuleChunk loadModuleDescriptions(KotlinPaths paths, String moduleDefinitionFile, MessageCollector messageCollector) {
068            File file = new File(moduleDefinitionFile);
069            if (!file.exists()) {
070                messageCollector.report(ERROR, "Module definition file does not exist: " + moduleDefinitionFile, NO_LOCATION);
071                return ModuleChunk.EMPTY;
072            }
073            String extension = FileUtilRt.getExtension(moduleDefinitionFile);
074            if ("kts".equalsIgnoreCase(extension)) {
075                return new ModuleChunk(loadModuleScript(paths, moduleDefinitionFile, messageCollector));
076            }
077            if ("xml".equalsIgnoreCase(extension)) {
078                return new ModuleChunk(ContainerUtil.map(
079                        ModuleXmlParser.parse(moduleDefinitionFile, messageCollector),
080                        new Function<ModuleDescription, Module>() {
081                            @Override
082                            public Module fun(ModuleDescription description) {
083                                return new DescriptionToModuleAdapter(description);
084                            }
085                        }));
086            }
087            messageCollector.report(ERROR, "Unknown module definition type: " + moduleDefinitionFile, NO_LOCATION);
088            return ModuleChunk.EMPTY;
089        }
090    
091        @NotNull
092        private static List<Module> loadModuleScript(KotlinPaths paths, String moduleScriptFile, MessageCollector messageCollector) {
093            CompilerConfiguration configuration = new CompilerConfiguration();
094            File runtimePath = paths.getRuntimePath();
095            if (runtimePath.exists()) {
096                configuration.add(JVMConfigurationKeys.CLASSPATH_KEY, runtimePath);
097            }
098            configuration.add(JVMConfigurationKeys.CLASSPATH_KEY, PathUtil.findRtJar());
099            File jdkAnnotationsPath = paths.getJdkAnnotationsPath();
100            if (jdkAnnotationsPath.exists()) {
101                configuration.add(JVMConfigurationKeys.ANNOTATIONS_PATH_KEY, jdkAnnotationsPath);
102            }
103            configuration.add(CommonConfigurationKeys.SOURCE_ROOTS_KEY, moduleScriptFile);
104            configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector);
105    
106            List<Module> modules;
107    
108            Disposable disposable = Disposer.newDisposable();
109            try {
110                JetCoreEnvironment scriptEnvironment = JetCoreEnvironment.createForProduction(disposable, configuration);
111                GenerationState generationState = KotlinToJVMBytecodeCompiler.analyzeAndGenerate(scriptEnvironment);
112                if (generationState == null) {
113                    throw new CompileEnvironmentException("Module script " + moduleScriptFile + " analyze failed:\n" +
114                                                          loadModuleScriptText(moduleScriptFile));
115                }
116    
117                modules = runDefineModules(paths, moduleScriptFile, generationState.getFactory());
118            }
119            finally {
120                Disposer.dispose(disposable);
121            }
122    
123            if (modules == null) {
124                throw new CompileEnvironmentException("Module script " + moduleScriptFile + " compilation failed");
125            }
126    
127            if (modules.isEmpty()) {
128                throw new CompileEnvironmentException("No modules where defined by " + moduleScriptFile);
129            }
130            return modules;
131        }
132    
133        private static List<Module> runDefineModules(KotlinPaths paths, String moduleFile, ClassFileFactory factory) {
134            File stdlibJar = paths.getRuntimePath();
135            GeneratedClassLoader loader;
136            if (stdlibJar.exists()) {
137                try {
138                    loader = new GeneratedClassLoader(factory, new URLClassLoader(new URL[]{stdlibJar.toURI().toURL()},
139                                                                                  AllModules.class.getClassLoader()));
140                }
141                catch (MalformedURLException e) {
142                    throw new RuntimeException(e);
143                }
144            }
145            else {
146                loader = new GeneratedClassLoader(factory, KotlinToJVMBytecodeCompiler.class.getClassLoader());
147            }
148            try {
149                Class namespaceClass = loader.loadClass(PackageClassUtils.getPackageClassName(FqName.ROOT));
150                Method method = namespaceClass.getDeclaredMethod("project");
151                if (method == null) {
152                    throw new CompileEnvironmentException("Module script " + moduleFile + " must define project() function");
153                }
154    
155                method.setAccessible(true);
156                method.invoke(null);
157    
158                ArrayList<Module> answer = new ArrayList<Module>(AllModules.modules.get());
159                AllModules.modules.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 factory, 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 (String file : factory.files()) {
182                    stream.putNextEntry(new JarEntry(file));
183                    stream.write(factory.asBytes(file));
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 moduleFactory) {
196            FileOutputStream outputStream = null;
197            try {
198                outputStream = new FileOutputStream(jarPath);
199                doWriteToJar(moduleFactory, 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 ExceptionUtils.rethrow(e);
207            }
208            finally {
209                ExceptionUtils.closeQuietly(outputStream);
210            }
211        }
212    
213        private static void writeRuntimeToJar(final JarOutputStream stream) throws IOException {
214            File runtimeJarPath = getRuntimeJarPath();
215            if (runtimeJarPath != null) {
216                JarInputStream jis = new JarInputStream(new FileInputStream(runtimeJarPath));
217                try {
218                    while (true) {
219                        JarEntry e = jis.getNextJarEntry();
220                        if (e == null) {
221                            break;
222                        }
223                        if (FileUtilRt.extensionEquals(e.getName(), "class")) {
224                            stream.putNextEntry(e);
225                            FileUtil.copy(jis, stream);
226                        }
227                    }
228                }
229                finally {
230                    jis.close();
231                }
232            }
233            else {
234                throw new CompileEnvironmentException("Couldn't find runtime library");
235            }
236        }
237    
238        public interface OutputDirector {
239            @NotNull
240            File getOutputDirectory(@NotNull Collection<File> sourceFiles);
241        }
242    
243        public static OutputDirector singleDirectory(@Nullable final File file) {
244            if (file == null) return null;
245            return new OutputDirector() {
246                @NotNull
247                @Override
248                public File getOutputDirectory(@NotNull Collection<File> sourceFiles) {
249                    return file;
250                }
251            };
252        }
253    
254        public static void writeToOutputWithDirector(
255                ClassFileFactory factory,
256                @NotNull OutputDirector outputDirector,
257                @NotNull MessageCollector messageCollector
258        ) {
259            List<String> files = factory.files();
260            for (String file : files) {
261                List<File> sourceFiles = factory.getSourceFiles(file);
262                File target = new File(outputDirector.getOutputDirectory(sourceFiles), file);
263                messageCollector.report(
264                        CompilerMessageSeverity.OUTPUT,
265                        OutputMessageUtil.formatOutputMessage(sourceFiles, target),
266                        CompilerMessageLocation.NO_LOCATION);
267                try {
268                    FileUtil.writeToFile(target, factory.asBytes(file));
269                }
270                catch (IOException e) {
271                    throw new CompileEnvironmentException(e);
272                }
273            }
274        }
275    
276        public static void writeToOutputDirectory(ClassFileFactory factory, @NotNull File outputDir) {
277            writeToOutputWithDirector(factory, singleDirectory(outputDir), MessageCollector.NONE);
278        }
279    
280        // Used for debug output only
281        private static String loadModuleScriptText(String moduleScriptFile) {
282            String moduleScriptText;
283            try {
284                moduleScriptText = FileUtil.loadFile(new File(moduleScriptFile));
285            }
286            catch (IOException e) {
287                moduleScriptText = "Can't load module script text:\n" + MessageRenderer.PLAIN.renderException(e);
288            }
289            return moduleScriptText;
290        }
291    
292        static void writeOutputToDirOrJar(
293                @Nullable File jar,
294                @Nullable OutputDirector outputDir,
295                boolean includeRuntime,
296                @Nullable FqName mainClass,
297                @NotNull ClassFileFactory factory,
298                @NotNull MessageCollector messageCollector
299        ) {
300            if (jar != null) {
301                writeToJar(jar, includeRuntime, mainClass, factory);
302            }
303            else if (outputDir != null) {
304                writeToOutputWithDirector(factory, outputDir, messageCollector);
305            }
306            else {
307                throw new CompileEnvironmentException("Output directory or jar file is not specified - no files will be saved to the disk");
308            }
309        }
310    
311        private static class DescriptionToModuleAdapter implements Module {
312            private final ModuleDescription description;
313    
314            public DescriptionToModuleAdapter(ModuleDescription description) {
315                this.description = description;
316            }
317    
318            @Override
319            public String getModuleName() {
320                return description.getModuleName();
321            }
322    
323            @Override
324            public String getOutputDirectory() {
325                return description.getOutputDir();
326            }
327    
328            @Override
329            public List<String> getSourceFiles() {
330                return description.getSourceFiles();
331            }
332    
333            @Override
334            public List<String> getClasspathRoots() {
335                return description.getClasspathRoots();
336            }
337    
338            @Override
339            public List<String> getAnnotationsRoots() {
340                return description.getAnnotationsRoots();
341            }
342        }
343    }