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        public static Disposable createMockDisposable() {
060            return new Disposable() {
061                @Override
062                public void dispose() {
063                }
064            };
065        }
066    
067        @Nullable
068        private static File getRuntimeJarPath() {
069            File runtimePath = PathUtil.getKotlinPathsForCompiler().getRuntimePath();
070            return runtimePath.exists() ? runtimePath : null;
071        }
072    
073        @NotNull
074        public static ModuleChunk loadModuleDescriptions(KotlinPaths paths, String moduleDefinitionFile, MessageCollector messageCollector) {
075            File file = new File(moduleDefinitionFile);
076            if (!file.exists()) {
077                messageCollector.report(ERROR, "Module definition file does not exist: " + moduleDefinitionFile, NO_LOCATION);
078                return ModuleChunk.EMPTY;
079            }
080            String extension = FileUtilRt.getExtension(moduleDefinitionFile);
081            if ("kts".equalsIgnoreCase(extension)) {
082                return new ModuleChunk(loadModuleScript(paths, moduleDefinitionFile, messageCollector));
083            }
084            if ("xml".equalsIgnoreCase(extension)) {
085                return new ModuleChunk(ContainerUtil.map(
086                        ModuleXmlParser.parse(moduleDefinitionFile, messageCollector),
087                        new Function<ModuleDescription, Module>() {
088                            @Override
089                            public Module fun(ModuleDescription description) {
090                                return new DescriptionToModuleAdapter(description);
091                            }
092                        }));
093            }
094            messageCollector.report(ERROR, "Unknown module definition type: " + moduleDefinitionFile, NO_LOCATION);
095            return ModuleChunk.EMPTY;
096        }
097    
098        @NotNull
099        private static List<Module> loadModuleScript(KotlinPaths paths, String moduleScriptFile, MessageCollector messageCollector) {
100            Disposable disposable = new Disposable() {
101                @Override
102                public void dispose() {
103    
104                }
105            };
106            CompilerConfiguration configuration = new CompilerConfiguration();
107            File runtimePath = paths.getRuntimePath();
108            if (runtimePath.exists()) {
109                configuration.add(JVMConfigurationKeys.CLASSPATH_KEY, runtimePath);
110            }
111            configuration.add(JVMConfigurationKeys.CLASSPATH_KEY, PathUtil.findRtJar());
112            File jdkAnnotationsPath = paths.getJdkAnnotationsPath();
113            if (jdkAnnotationsPath.exists()) {
114                configuration.add(JVMConfigurationKeys.ANNOTATIONS_PATH_KEY, jdkAnnotationsPath);
115            }
116            configuration.add(CommonConfigurationKeys.SOURCE_ROOTS_KEY, moduleScriptFile);
117            configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector);
118    
119            List<Module> modules;
120            try {
121                JetCoreEnvironment scriptEnvironment = new JetCoreEnvironment(disposable, configuration);
122                GenerationState generationState = KotlinToJVMBytecodeCompiler.analyzeAndGenerate(scriptEnvironment);
123                if (generationState == null) {
124                    throw new CompileEnvironmentException("Module script " + moduleScriptFile + " analyze failed:\n" +
125                                                          loadModuleScriptText(moduleScriptFile));
126                }
127    
128                modules = runDefineModules(paths, moduleScriptFile, generationState.getFactory());
129            }
130            finally {
131                Disposer.dispose(disposable);
132            }
133    
134            if (modules == null) {
135                throw new CompileEnvironmentException("Module script " + moduleScriptFile + " compilation failed");
136            }
137    
138            if (modules.isEmpty()) {
139                throw new CompileEnvironmentException("No modules where defined by " + moduleScriptFile);
140            }
141            return modules;
142        }
143    
144        private static List<Module> runDefineModules(KotlinPaths paths, String moduleFile, ClassFileFactory factory) {
145            File stdlibJar = paths.getRuntimePath();
146            GeneratedClassLoader loader;
147            if (stdlibJar.exists()) {
148                try {
149                    loader = new GeneratedClassLoader(factory, new URLClassLoader(new URL[]{stdlibJar.toURI().toURL()},
150                                                                                  AllModules.class.getClassLoader()));
151                }
152                catch (MalformedURLException e) {
153                    throw new RuntimeException(e);
154                }
155            }
156            else {
157                loader = new GeneratedClassLoader(factory, KotlinToJVMBytecodeCompiler.class.getClassLoader());
158            }
159            try {
160                Class namespaceClass = loader.loadClass(PackageClassUtils.getPackageClassName(FqName.ROOT));
161                Method method = namespaceClass.getDeclaredMethod("project");
162                if (method == null) {
163                    throw new CompileEnvironmentException("Module script " + moduleFile + " must define project() function");
164                }
165    
166                method.setAccessible(true);
167                method.invoke(null);
168    
169                ArrayList<Module> answer = new ArrayList<Module>(AllModules.modules.get());
170                AllModules.modules.get().clear();
171                return answer;
172            }
173            catch (Exception e) {
174                throw new ModuleExecutionException(e);
175            }
176            finally {
177                loader.dispose();
178            }
179        }
180    
181        // TODO: includeRuntime should be not a flag but a path to runtime
182        private static void doWriteToJar(ClassFileFactory factory, OutputStream fos, @Nullable FqName mainClass, boolean includeRuntime) {
183            try {
184                Manifest manifest = new Manifest();
185                Attributes mainAttributes = manifest.getMainAttributes();
186                mainAttributes.putValue("Manifest-Version", "1.0");
187                mainAttributes.putValue("Created-By", "JetBrains Kotlin");
188                if (mainClass != null) {
189                    mainAttributes.putValue("Main-Class", mainClass.asString());
190                }
191                JarOutputStream stream = new JarOutputStream(fos, manifest);
192                for (String file : factory.files()) {
193                    stream.putNextEntry(new JarEntry(file));
194                    stream.write(factory.asBytes(file));
195                }
196                if (includeRuntime) {
197                    writeRuntimeToJar(stream);
198                }
199                stream.finish();
200            }
201            catch (IOException e) {
202                throw new CompileEnvironmentException("Failed to generate jar file", e);
203            }
204        }
205    
206        public static void writeToJar(File jarPath, boolean jarRuntime, FqName mainClass, ClassFileFactory moduleFactory) {
207            FileOutputStream outputStream = null;
208            try {
209                outputStream = new FileOutputStream(jarPath);
210                doWriteToJar(moduleFactory, outputStream, mainClass, jarRuntime);
211                outputStream.close();
212            }
213            catch (FileNotFoundException e) {
214                throw new CompileEnvironmentException("Invalid jar path " + jarPath, e);
215            }
216            catch (IOException e) {
217                throw ExceptionUtils.rethrow(e);
218            }
219            finally {
220                ExceptionUtils.closeQuietly(outputStream);
221            }
222        }
223    
224        private static void writeRuntimeToJar(final JarOutputStream stream) throws IOException {
225            File runtimeJarPath = getRuntimeJarPath();
226            if (runtimeJarPath != null) {
227                JarInputStream jis = new JarInputStream(new FileInputStream(runtimeJarPath));
228                try {
229                    while (true) {
230                        JarEntry e = jis.getNextJarEntry();
231                        if (e == null) {
232                            break;
233                        }
234                        if (FileUtilRt.extensionEquals(e.getName(), "class")) {
235                            stream.putNextEntry(e);
236                            FileUtil.copy(jis, stream);
237                        }
238                    }
239                }
240                finally {
241                    jis.close();
242                }
243            }
244            else {
245                throw new CompileEnvironmentException("Couldn't find runtime library");
246            }
247        }
248    
249        public interface OutputDirector {
250            @NotNull
251            File getOutputDirectory(@NotNull Collection<File> sourceFiles);
252        }
253    
254        public static OutputDirector singleDirectory(@Nullable final File file) {
255            if (file == null) return null;
256            return new OutputDirector() {
257                @NotNull
258                @Override
259                public File getOutputDirectory(@NotNull Collection<File> sourceFiles) {
260                    return file;
261                }
262            };
263        }
264    
265        public static void writeToOutputWithDirector(ClassFileFactory factory, @NotNull OutputDirector outputDirector) {
266            List<String> files = factory.files();
267            for (String file : files) {
268                File target = new File(outputDirector.getOutputDirectory(factory.getSourceFiles(file)), file);
269                try {
270                    FileUtil.writeToFile(target, factory.asBytes(file));
271                }
272                catch (IOException e) {
273                    throw new CompileEnvironmentException(e);
274                }
275            }
276        }
277    
278        public static void writeToOutputDirectory(ClassFileFactory factory, @NotNull File outputDir) {
279            writeToOutputWithDirector(factory, singleDirectory(outputDir));
280        }
281    
282        // Used for debug output only
283        private static String loadModuleScriptText(String moduleScriptFile) {
284            String moduleScriptText;
285            try {
286                moduleScriptText = FileUtil.loadFile(new File(moduleScriptFile));
287            }
288            catch (IOException e) {
289                moduleScriptText = "Can't load module script text:\n" + MessageRenderer.PLAIN.renderException(e);
290            }
291            return moduleScriptText;
292        }
293    
294        static void writeOutputToDirOrJar(
295                @Nullable File jar,
296                @Nullable OutputDirector outputDir,
297                boolean includeRuntime,
298                @Nullable FqName mainClass,
299                @NotNull ClassFileFactory factory,
300                @NotNull MessageCollector messageCollector
301        ) {
302            if (jar != null) {
303                writeToJar(jar, includeRuntime, mainClass, factory);
304            }
305            else if (outputDir != null) {
306                reportOutputs(factory, messageCollector);
307                writeToOutputWithDirector(factory, outputDir);
308            }
309            else {
310                throw new CompileEnvironmentException("Output directory or jar file is not specified - no files will be saved to the disk");
311            }
312        }
313    
314        private static void reportOutputs(ClassFileFactory factory, MessageCollector messageCollector) {
315            for (String outputFile : factory.files()) {
316                List<File> sourceFiles = factory.getSourceFiles(outputFile);
317                messageCollector.report(
318                        CompilerMessageSeverity.OUTPUT,
319                        OutputMessageUtil.formatOutputMessage(sourceFiles, new File(outputFile)),
320                        CompilerMessageLocation.NO_LOCATION);
321    
322            }
323        }
324    
325        private static class DescriptionToModuleAdapter implements Module {
326            private final ModuleDescription description;
327    
328            public DescriptionToModuleAdapter(ModuleDescription description) {
329                this.description = description;
330            }
331    
332            @Override
333            public String getModuleName() {
334                return description.getModuleName();
335            }
336    
337            @Override
338            public String getOutputDirectory() {
339                return description.getOutputDir();
340            }
341    
342            @Override
343            public List<String> getSourceFiles() {
344                return description.getSourceFiles();
345            }
346    
347            @Override
348            public List<String> getClasspathRoots() {
349                return description.getClasspathRoots();
350            }
351    
352            @Override
353            public List<String> getAnnotationsRoots() {
354                return description.getAnnotationsRoots();
355            }
356        }
357    }