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 kotlin.modules.AllModules;
026    import kotlin.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.KotlinPaths;
046    import org.jetbrains.jet.utils.PathUtil;
047    import org.jetbrains.jet.utils.UtilsPackage;
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.addAll(JVMConfigurationKeys.CLASSPATH_KEY, PathUtil.getJdkClassesRoots());
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, 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, 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                ArrayList<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 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        // Used for debug output only
239        private static String loadModuleScriptText(String moduleScriptFile) {
240            String moduleScriptText;
241            try {
242                moduleScriptText = FileUtil.loadFile(new File(moduleScriptFile));
243            }
244            catch (IOException e) {
245                moduleScriptText = "Can't load module script text:\n" + MessageRenderer.PLAIN.renderException(e);
246            }
247            return moduleScriptText;
248        }
249    
250        static void writeOutputToDirOrJar(
251                @Nullable File jar,
252                @Nullable OutputDirector outputDir,
253                boolean includeRuntime,
254                @Nullable FqName mainClass,
255                @NotNull ClassFileFactory outputFiles,
256                @NotNull MessageCollector messageCollector
257        ) {
258            if (jar != null) {
259                writeToJar(jar, includeRuntime, mainClass, outputFiles);
260            }
261            else if (outputDir != null) {
262                OutputUtilsPackage.writeAll(outputFiles, outputDir, messageCollector);
263            }
264            else {
265                throw new CompileEnvironmentException("Output directory or jar file is not specified - no files will be saved to the disk");
266            }
267        }
268    
269        private static class DescriptionToModuleAdapter implements Module {
270            private final ModuleDescription description;
271    
272            public DescriptionToModuleAdapter(ModuleDescription description) {
273                this.description = description;
274            }
275    
276            @NotNull
277            @Override
278            public String getModuleName() {
279                return description.getModuleName();
280            }
281    
282            @NotNull
283            @Override
284            public String getOutputDirectory() {
285                return description.getOutputDir();
286            }
287    
288            @NotNull
289            @Override
290            public List<String> getSourceFiles() {
291                return description.getSourceFiles();
292            }
293    
294            @NotNull
295            @Override
296            public List<String> getClasspathRoots() {
297                return description.getClasspathRoots();
298            }
299    
300            @NotNull
301            @Override
302            public List<String> getAnnotationsRoots() {
303                return description.getAnnotationsRoots();
304            }
305        }
306    }