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.OutputFile;
030    import org.jetbrains.jet.cli.common.CLIConfigurationKeys;
031    import org.jetbrains.jet.cli.common.messages.MessageCollector;
032    import org.jetbrains.jet.cli.common.messages.MessageRenderer;
033    import org.jetbrains.jet.cli.common.modules.ModuleDescription;
034    import org.jetbrains.jet.cli.common.modules.ModuleXmlParser;
035    import org.jetbrains.jet.cli.common.output.OutputDirector;
036    import org.jetbrains.jet.cli.common.output.outputUtils.OutputUtilsPackage;
037    import org.jetbrains.jet.cli.jvm.JVMConfigurationKeys;
038    import org.jetbrains.jet.codegen.ClassFileFactory;
039    import org.jetbrains.jet.codegen.GeneratedClassLoader;
040    import org.jetbrains.jet.codegen.state.GenerationState;
041    import org.jetbrains.jet.config.CommonConfigurationKeys;
042    import org.jetbrains.jet.config.CompilerConfiguration;
043    import org.jetbrains.jet.lang.resolve.java.PackageClassUtils;
044    import org.jetbrains.jet.lang.resolve.name.FqName;
045    import org.jetbrains.jet.utils.ExceptionUtils;
046    import org.jetbrains.jet.utils.KotlinPaths;
047    import org.jetbrains.jet.utils.PathUtil;
048    
049    import java.io.*;
050    import java.lang.reflect.Method;
051    import java.net.MalformedURLException;
052    import java.net.URL;
053    import java.net.URLClassLoader;
054    import java.util.ArrayList;
055    import java.util.List;
056    import java.util.jar.*;
057    
058    import static org.jetbrains.jet.cli.common.messages.CompilerMessageLocation.NO_LOCATION;
059    import static org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity.ERROR;
060    
061    public class CompileEnvironmentUtil {
062    
063        @Nullable
064        private static File getRuntimeJarPath() {
065            File runtimePath = PathUtil.getKotlinPathsForCompiler().getRuntimePath();
066            return runtimePath.exists() ? runtimePath : null;
067        }
068    
069        @NotNull
070        public static ModuleChunk loadModuleDescriptions(KotlinPaths paths, String moduleDefinitionFile, MessageCollector messageCollector) {
071            File file = new File(moduleDefinitionFile);
072            if (!file.exists()) {
073                messageCollector.report(ERROR, "Module definition file does not exist: " + moduleDefinitionFile, NO_LOCATION);
074                return ModuleChunk.EMPTY;
075            }
076            String extension = FileUtilRt.getExtension(moduleDefinitionFile);
077            if ("kts".equalsIgnoreCase(extension)) {
078                return new ModuleChunk(loadModuleScript(paths, moduleDefinitionFile, messageCollector));
079            }
080            if ("xml".equalsIgnoreCase(extension)) {
081                return new ModuleChunk(ContainerUtil.map(
082                        ModuleXmlParser.parse(moduleDefinitionFile, messageCollector),
083                        new Function<ModuleDescription, Module>() {
084                            @Override
085                            public Module fun(ModuleDescription description) {
086                                return new DescriptionToModuleAdapter(description);
087                            }
088                        }));
089            }
090            messageCollector.report(ERROR, "Unknown module definition type: " + moduleDefinitionFile, NO_LOCATION);
091            return ModuleChunk.EMPTY;
092        }
093    
094        @NotNull
095        private static List<Module> loadModuleScript(KotlinPaths paths, String moduleScriptFile, MessageCollector messageCollector) {
096            CompilerConfiguration configuration = new CompilerConfiguration();
097            File runtimePath = paths.getRuntimePath();
098            if (runtimePath.exists()) {
099                configuration.add(JVMConfigurationKeys.CLASSPATH_KEY, runtimePath);
100            }
101            configuration.add(JVMConfigurationKeys.CLASSPATH_KEY, PathUtil.findRtJar());
102            File jdkAnnotationsPath = paths.getJdkAnnotationsPath();
103            if (jdkAnnotationsPath.exists()) {
104                configuration.add(JVMConfigurationKeys.ANNOTATIONS_PATH_KEY, jdkAnnotationsPath);
105            }
106            configuration.add(CommonConfigurationKeys.SOURCE_ROOTS_KEY, moduleScriptFile);
107            configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector);
108    
109            List<Module> modules;
110    
111            Disposable disposable = Disposer.newDisposable();
112            try {
113                JetCoreEnvironment scriptEnvironment = JetCoreEnvironment.createForProduction(disposable, configuration);
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, moduleScriptFile, 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 modules;
134        }
135    
136        private static List<Module> runDefineModules(KotlinPaths paths, String moduleFile, 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 namespaceClass = loader.loadClass(PackageClassUtils.getPackageClassName(FqName.ROOT));
153                Method method = namespaceClass.getDeclaredMethod("project");
154                if (method == null) {
155                    throw new CompileEnvironmentException("Module script " + moduleFile + " must define project() function");
156                }
157    
158                method.setAccessible(true);
159                method.invoke(null);
160    
161                ArrayList<Module> answer = new ArrayList<Module>(AllModules.modules.get());
162                AllModules.modules.get().clear();
163                return answer;
164            }
165            catch (Exception e) {
166                throw new ModuleExecutionException(e);
167            }
168            finally {
169                loader.dispose();
170            }
171        }
172    
173        // TODO: includeRuntime should be not a flag but a path to runtime
174        private static void doWriteToJar(ClassFileFactory outputFiles, OutputStream fos, @Nullable FqName mainClass, boolean includeRuntime) {
175            try {
176                Manifest manifest = new Manifest();
177                Attributes mainAttributes = manifest.getMainAttributes();
178                mainAttributes.putValue("Manifest-Version", "1.0");
179                mainAttributes.putValue("Created-By", "JetBrains Kotlin");
180                if (mainClass != null) {
181                    mainAttributes.putValue("Main-Class", mainClass.asString());
182                }
183                JarOutputStream stream = new JarOutputStream(fos, manifest);
184                for (OutputFile outputFile : outputFiles.asList()) {
185                    stream.putNextEntry(new JarEntry(outputFile.getRelativePath()));
186                    stream.write(outputFile.asByteArray());
187                }
188                if (includeRuntime) {
189                    writeRuntimeToJar(stream);
190                }
191                stream.finish();
192            }
193            catch (IOException e) {
194                throw new CompileEnvironmentException("Failed to generate jar file", e);
195            }
196        }
197    
198        public static void writeToJar(File jarPath, boolean jarRuntime, FqName mainClass, ClassFileFactory outputFiles) {
199            FileOutputStream outputStream = null;
200            try {
201                outputStream = new FileOutputStream(jarPath);
202                doWriteToJar(outputFiles, outputStream, mainClass, jarRuntime);
203                outputStream.close();
204            }
205            catch (FileNotFoundException e) {
206                throw new CompileEnvironmentException("Invalid jar path " + jarPath, e);
207            }
208            catch (IOException e) {
209                throw ExceptionUtils.rethrow(e);
210            }
211            finally {
212                ExceptionUtils.closeQuietly(outputStream);
213            }
214        }
215    
216        private static void writeRuntimeToJar(JarOutputStream stream) throws IOException {
217            File runtimeJarPath = getRuntimeJarPath();
218            if (runtimeJarPath != null) {
219                JarInputStream jis = new JarInputStream(new FileInputStream(runtimeJarPath));
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            else {
237                throw new CompileEnvironmentException("Couldn't find runtime library");
238            }
239        }
240    
241        // Used for debug output only
242        private static String loadModuleScriptText(String moduleScriptFile) {
243            String moduleScriptText;
244            try {
245                moduleScriptText = FileUtil.loadFile(new File(moduleScriptFile));
246            }
247            catch (IOException e) {
248                moduleScriptText = "Can't load module script text:\n" + MessageRenderer.PLAIN.renderException(e);
249            }
250            return moduleScriptText;
251        }
252    
253        static void writeOutputToDirOrJar(
254                @Nullable File jar,
255                @Nullable OutputDirector outputDir,
256                boolean includeRuntime,
257                @Nullable FqName mainClass,
258                @NotNull ClassFileFactory outputFiles,
259                @NotNull MessageCollector messageCollector
260        ) {
261            if (jar != null) {
262                writeToJar(jar, includeRuntime, mainClass, outputFiles);
263            }
264            else if (outputDir != null) {
265                OutputUtilsPackage.writeAll(outputFiles, outputDir, messageCollector);
266            }
267            else {
268                throw new CompileEnvironmentException("Output directory or jar file is not specified - no files will be saved to the disk");
269            }
270        }
271    
272        private static class DescriptionToModuleAdapter implements Module {
273            private final ModuleDescription description;
274    
275            public DescriptionToModuleAdapter(ModuleDescription description) {
276                this.description = description;
277            }
278    
279            @Override
280            public String getModuleName() {
281                return description.getModuleName();
282            }
283    
284            @Override
285            public String getOutputDirectory() {
286                return description.getOutputDir();
287            }
288    
289            @Override
290            public List<String> getSourceFiles() {
291                return description.getSourceFiles();
292            }
293    
294            @Override
295            public List<String> getClasspathRoots() {
296                return description.getClasspathRoots();
297            }
298    
299            @Override
300            public List<String> getAnnotationsRoots() {
301                return description.getAnnotationsRoots();
302            }
303        }
304    }