package org.jetbrains.jps.clojure.build; import com.intellij.execution.process.BaseOSProcessHandler; import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.util.ArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.jps.ModuleChunk; import org.jetbrains.jps.builders.DirtyFilesHolder; import org.jetbrains.jps.builders.FileProcessor; import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor; import org.jetbrains.jps.builders.storage.SourceToOutputMapping; import org.jetbrains.jps.clojure.model.JpsClojureCompilerSettingsExtension; import org.jetbrains.jps.clojure.model.JpsClojureExtensionService; import org.jetbrains.jps.incremental.*; import org.jetbrains.jps.incremental.messages.BuildMessage; import org.jetbrains.jps.incremental.messages.CompilerMessage; import org.jetbrains.jps.incremental.messages.ProgressMessage; import org.jetbrains.jps.model.JpsDummyElement; import org.jetbrains.jps.model.JpsProject; import org.jetbrains.jps.model.java.JpsJavaExtensionService; import org.jetbrains.jps.model.java.JpsJavaModuleType; import org.jetbrains.jps.model.java.JpsJavaSdkType; import org.jetbrains.jps.model.library.sdk.JpsSdk; import org.jetbrains.jps.model.module.JpsModule; import org.jetbrains.jps.model.module.JpsModuleSourceRoot; import org.jetbrains.jps.service.SharedThreadPool; import java.io.*; import java.util.*; import java.util.concurrent.Future; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author nik, Alefas * @since 02.11.12 */ public class ClojureBuilder extends ModuleLevelBuilder { public static final String COMPILER_NAME = "Clojure Compiler"; public static final String CLOJURE_MAIN = "clojure.main"; public static final String COMPILING_PREFIX = "[compiling]:"; public static final String COMPILED_PREFIX = "[compiled]:"; public static final String ERROR_PREFIX = "[error]:"; public static final String WRITING_PREFIX = "[writing]:"; private final boolean myBeforeJava; public ClojureBuilder(boolean isBeforeJava) { super(isBeforeJava ? BuilderCategory.SOURCE_PROCESSOR : BuilderCategory.OVERWRITING_TRANSLATOR); myBeforeJava = isBeforeJava; } @Override public ExitCode build(final CompileContext context, final ModuleChunk chunk, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder, final OutputConsumer outputConsumer) throws ProjectBuildException, IOException { JpsProject project = context.getProjectDescriptor().getProject(); JpsClojureCompilerSettingsExtension extension = JpsClojureExtensionService.getExtension(project); if (myBeforeJava && (extension == null || !extension.isClojureBefore())) return ExitCode.NOTHING_DONE; if (!myBeforeJava && extension != null && extension.isClojureBefore()) return ExitCode.NOTHING_DONE; if (extension != null && !extension.isCompileClojure()) return ExitCode.NOTHING_DONE; final LinkedHashSet<JpsModule> javaModules = new LinkedHashSet<JpsModule>(); for (JpsModule module : chunk.getModules()) { if (module.getModuleType().equals(JpsJavaModuleType.INSTANCE)) { javaModules.add(module); } } final List<File> toCompile = new ArrayList<File>(); final HashMap<File, String> toCompileNamespace = new HashMap<File, String>(); dirtyFilesHolder.processDirtyFiles(new FileProcessor<JavaSourceRootDescriptor, ModuleBuildTarget>() { public boolean apply(ModuleBuildTarget target, File file, JavaSourceRootDescriptor sourceRoot) throws IOException { if (javaModules.contains(target.getModule()) && file.getName().endsWith(".clj")) { if (!hasGenClass(file)) { return true; } toCompile.add(file); String filePath = file.getAbsolutePath(); File rootFile = sourceRoot.getRootFile(); String relPath = filePath.substring(rootFile.getAbsolutePath().length() + 1, filePath.length() - ".clj".length()); toCompileNamespace.put(file, relPath.replace(File.separator, ".")); } return true; } }); if (toCompile.isEmpty()) return ExitCode.NOTHING_DONE; JpsSdk<JpsDummyElement> sdk = chunk.representativeTarget().getModule().getSdk(JpsJavaSdkType.INSTANCE); if (sdk == null) { context.processMessage(new CompilerMessage(COMPILER_NAME, BuildMessage.Kind.ERROR, "JDK is not specified")); return ExitCode.ABORT; } String javaExecutable = JpsJavaSdkType.getJavaExecutable(sdk); List<String> classpath = new ArrayList<String>(); for (File root : JpsJavaExtensionService.getInstance().enumerateDependencies(javaModules).classes().getRoots()) { classpath.add(root.getAbsolutePath()); } for (JpsModule module : javaModules) { for (JpsModuleSourceRoot sourceRoot : module.getSourceRoots()) { classpath.add(sourceRoot.getFile().getAbsolutePath()); } } List<String> vmParams = new ArrayList<String>(); // vmParams.add("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5239"); List<String> programParams = new ArrayList<String>(); File outputDir = chunk.representativeTarget().getOutputDir(); outputDir.mkdirs(); File fileWithCompileScript = FileUtil.createTempFile("clojurekul", ".clj"); fillFileWithClojureCompilerParams(toCompile, toCompileNamespace, fileWithCompileScript, outputDir); programParams.add(fileWithCompileScript.getAbsolutePath()); List<String> commandLine = ExternalProcessUtil.buildJavaCommandLine(javaExecutable, CLOJURE_MAIN, Collections.<String>emptyList(), classpath, vmParams, programParams); Process process = Runtime.getRuntime().exec(ArrayUtil.toStringArray(commandLine)); BaseOSProcessHandler handler = new BaseOSProcessHandler(process, null, null) { @Override protected Future<?> executeOnPooledThread(Runnable task) { return SharedThreadPool.getInstance().executeOnPooledThread(task); } }; final SourceToOutputMapping sourceToOutputMap = context.getProjectDescriptor().dataManager.getSourceToOutputMap(chunk.representativeTarget()); final HashSet<String> outputs = new HashSet<String>(); handler.addProcessListener(new ProcessAdapter() { @Override public void onTextAvailable(ProcessEvent event, Key outputType) { if (outputType != ProcessOutputTypes.STDERR) return; String text = event.getText().trim(); context.processMessage(new ProgressMessage(text)); // context.processMessage(new CompilerMessage(COMPILER_NAME, BuildMessage.Kind.WARNING, text)); if (text.startsWith(ERROR_PREFIX)) { String errorDescription = text.substring(ERROR_PREFIX.length()); Pattern pattern = Pattern.compile("(.*)@@((.*) compiling:[(](.*):([\\d]*)(:([\\d]*))?[)])"); Matcher matcher = pattern.matcher(errorDescription); if (matcher.matches()) { String sourceName = matcher.group(1); String lineNumber = matcher.group(5); String columnNumber = matcher.group(6); Long locationLine = -1L; Long column = 0L; try { locationLine = Long.parseLong(lineNumber); if (columnNumber != null) { column = Long.parseLong(columnNumber); } } catch (Exception ignore) {} String errorMessage = matcher.group(2); context.processMessage(new CompilerMessage(COMPILER_NAME, BuildMessage.Kind.ERROR, errorMessage, sourceName, -1L, -1L, -1L, locationLine, column)); } else { matcher = Pattern.compile("(.*)@@(.*)").matcher(errorDescription); if (matcher.matches()) { context.processMessage(new CompilerMessage(COMPILER_NAME, BuildMessage.Kind.ERROR, matcher.group(2), matcher.group(1))); } else { context.processMessage(new CompilerMessage(COMPILER_NAME, BuildMessage.Kind.ERROR, text)); } } } else if (text.startsWith(COMPILING_PREFIX)) { //we don't need to do anything } else if (text.startsWith(WRITING_PREFIX)) { outputs.add(text.substring(WRITING_PREFIX.length())); } else if (text.startsWith(COMPILED_PREFIX)) { for (String output : outputs) { try { outputConsumer.registerOutputFile(chunk.representativeTarget(), new File(output), Collections.singleton(text.substring(COMPILED_PREFIX.length()))); } catch (IOException e) { context.processMessage(new BuildMessage(e.getMessage(), BuildMessage.Kind.ERROR) {}); } } outputs.clear(); } } }); handler.startNotify(); handler.waitFor(); if (process.exitValue() != 0) { context.processMessage(new CompilerMessage(COMPILER_NAME, BuildMessage.Kind.ERROR, "Clojure compiler returned code " + process.exitValue())); } return ExitCode.OK; } @Override public List<String> getCompilableFileExtensions() { return Arrays.asList("clj"); } private static boolean hasGenClass(File file) throws IOException { return new String(FileUtilRt.loadFileText(file)).contains("gen-class"); } private void fillFileWithClojureCompilerParams(List<File> toCompile, HashMap<File, String> toCompileNamespace, File fileWithCompileScript, File outputDir) throws FileNotFoundException { PrintStream printer = new PrintStream(new FileOutputStream(fileWithCompileScript)); printer.print("(import (java.io File))\n" + "(import (java.util HashSet))\n"); //print output path printer.print("(binding [*compile-path* "); String outputDirPath = outputDir.getAbsolutePath().replace("\\", "\\\\"); printer.print("\"" + outputDirPath + "\" *compile-files* true]\n"); for (File file : toCompile) { //collecting current outputs in output directory printer.print( "(def outputDir \"" + outputDirPath + "\")\n" + "(def output (new HashSet))\n" + "(def outputFile (new File outputDir))\n" + "(defn scanOutput [#^HashSet out #^File file]\n" + " (if (.isDirectory file)\n" + " (doseq [i (.listFiles file)] (scanOutput out i))\n" + " (.add out (.getAbsolutePath file))))\n" + "\n" + "(scanOutput output outputFile)\n"); printer.print("(try "); String absolutePath = file.getAbsolutePath().replace("\\", "\\\\"); printer.print("(. *err* println "); printer.print("\"" + COMPILING_PREFIX + absolutePath + "\""); printer.print(")\n"); printer.print("(load-file \""); printer.print(absolutePath); printer.print("\")\n"); printer.print("(catch Exception e (. *err* println (str \"" + ERROR_PREFIX + absolutePath + "@@" + "\" (let [msg (.getMessage e)] msg) ) ) )"); printer.print(")\n"); //we need to compile namespace init class, otherwise we will get CNFE on Runtime String namespace = toCompileNamespace.get(file); if (namespace != null) { printer.print("(try "); printer.print("(compile \'"); printer.print(namespace); printer.print(")\n"); printer.print("(catch Exception e ())"); //all compile error should be found in file compilation printer.print(")\n"); } //let's print information about all new created files in output directory printer.print("(defn printNewFiles [#^File file]\n" + " (if (.isDirectory file)\n" + " (doseq [i (.listFiles file)] (printNewFiles i))\n" + " (if (not (.contains output (.getAbsolutePath file))) (. *err* println " + "(.concat \"" + WRITING_PREFIX + "\" (.getAbsolutePath file))))))\n" + "(printNewFiles outputFile)\n" + "(.clear output)"); printer.print("(. *err* println "); printer.print("\"" + COMPILED_PREFIX + absolutePath + "\""); printer.print(")\n"); } printer.print(")"); printer.close(); } @NotNull @Override public String getPresentableName() { return COMPILER_NAME; } }