package org.elixir_lang.jps.builder; // 52.0 in openapi.jar // import com.intellij.execution.ExecutionException; // 52.0 in openapi.jar //import com.intellij.execution.configurations.GeneralCommandLine; // 50.0 in util.jar import com.intellij.execution.process.BaseOSProcessHandler; // 50.0 in util.jar import com.intellij.execution.process.ProcessAdapter; // 50.0 in util.jar import com.intellij.openapi.diagnostic.Logger; // 50.0 in util.jar import com.intellij.openapi.util.io.FileUtil; // 49.0 in util.jar import com.intellij.openapi.util.io.FileUtilRt; // 50.0 in util.jar import com.intellij.openapi.util.text.StringUtil; // 50.0 in util.jar import com.intellij.util.CommonProcessors; // 49.0 in util.jar import com.intellij.util.Function; // 50.0 in util.jar import com.intellij.util.containers.ContainerUtil; // 49.0 in trove4j.jar import gnu.trove.THashSet; import org.elixir_lang.jps.mix.JpsMixConfigurationExtension; import org.elixir_lang.jps.model.ElixirCompilerOptions; import org.elixir_lang.jps.model.JpsElixirCompilerOptionsExtension; import org.elixir_lang.jps.model.JpsElixirModuleType; import org.elixir_lang.jps.model.JpsElixirSdkType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; // 50.0 in jsp-builders.jar import org.jetbrains.jps.builders.BuildOutputConsumer; // 50.0 in jsp-builders.jar import org.jetbrains.jps.builders.DirtyFilesHolder; // 50.0 in jsp-builders.jar import org.jetbrains.jps.builders.FileProcessor; // 50.0 in jsp-builders.jar import org.jetbrains.jps.incremental.CompileContext; // 50.0 in jsp-builders.jar import org.jetbrains.jps.incremental.ProjectBuildException; // 50.0 in jsp-builders.jar import org.jetbrains.jps.incremental.TargetBuilder; // 50.0 in jsp-builders.jar import org.jetbrains.jps.incremental.messages.BuildMessage; // 50.0 in jsp-builders.jar import org.jetbrains.jps.incremental.messages.CompilerMessage; // 50.0 in jsp-builders.jar import org.jetbrains.jps.incremental.resources.ResourcesBuilder; // 50.0 in jsp-builders.jar import org.jetbrains.jps.incremental.resources.StandardResourceBuilderEnabler; // 50.0 in openapi.jar import org.jetbrains.jps.model.JpsDummyElement; // 50.0 in openapi.jar import org.jetbrains.jps.model.JpsProject; // 50.0 in openapi.jar import org.jetbrains.jps.model.java.JavaSourceRootType; // 50.0 in openapi.jar import org.jetbrains.jps.model.java.JpsJavaExtensionService; // 50.0 in openapi.jar import org.jetbrains.jps.model.library.sdk.JpsSdk; // 50.0 in openapi.jar import org.jetbrains.jps.model.module.*; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; import java.util.*; /** * Created by zyuyou on 15/7/10. * * https://github.com/ignatov/intellij-erlang/blob/master/jps-plugin/src/org/intellij/erlang/jps/builder/ErlangBuilder.java * https://github.com/ignatov/intellij-erlang/tree/master/jps-plugin/src/org/intellij/erlang/jps/rebar */ public class ElixirBuilder extends TargetBuilder<ElixirSourceRootDescriptor, ElixirTarget> { public static final String BUILDER_NAME = "Elixir Builder"; public static final String ELIXIR_SOURCE_EXTENSION = "ex"; public static final String ELIXIR_SCRIPT_EXTENSION = "exs"; public static final String ELIXIR_TEST_SOURCE_EXTENSION = ELIXIR_SCRIPT_EXTENSION; public static final String ElIXIRC_NAME = "elixirc"; public static final String MIX_NAME = "mix"; private static final String MIX_CONFIG_FILE_NAME = "mix." + ELIXIR_SCRIPT_EXTENSION; private final static Logger LOG = Logger.getInstance(ElixirBuilder.class); public static final String ADD_PATH_TO_FRONT_OF_CODE_PATH = "-pa"; public static final FileFilter ELIXIR_SOURCE_FILTER = new FileFilter() { @Override public boolean accept(File file) { return FileUtilRt.extensionEquals(file.getName(), ELIXIR_SOURCE_EXTENSION); } }; public static final FileFilter ELIXIR_TEST_SOURCE_FILTER = new FileFilter() { @Override public boolean accept(File file) { return FileUtilRt.extensionEquals(file.getName(), ELIXIR_TEST_SOURCE_EXTENSION); } }; // use JavaBuilderExtension? public static final Set<? extends JpsModuleType<?>> ourCompilableModuleTypes = Collections.singleton(JpsElixirModuleType.INSTANCE); public ElixirBuilder(){ super(Arrays.asList(ElixirTargetType.PRODUCTION, ElixirTargetType.TEST)); // disables java resource builder for elixir modules ResourcesBuilder.registerEnabler(new StandardResourceBuilderEnabler() { @Override public boolean isResourceProcessingEnabled(JpsModule module) { return !(module.getModuleType() instanceof JpsElixirModuleType); } }); } @Override public void build(@NotNull ElixirTarget target, @NotNull DirtyFilesHolder<ElixirSourceRootDescriptor, ElixirTarget> holder, @NotNull BuildOutputConsumer outputConsumer, @NotNull CompileContext context) throws ProjectBuildException, IOException { LOG.info(target.getPresentableName()); final Set<File> filesToCompile = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY); holder.processDirtyFiles(new FileProcessor<ElixirSourceRootDescriptor, ElixirTarget>() { @Override public boolean apply(ElixirTarget target, File file, ElixirSourceRootDescriptor root) throws IOException { boolean isAcceptFile = target.isTests() ? ELIXIR_TEST_SOURCE_FILTER.accept(file) : ELIXIR_SOURCE_FILTER.accept(file); if(isAcceptFile && ourCompilableModuleTypes.contains(target.getModule().getModuleType())){ filesToCompile.add(file); } return true; } }); if(filesToCompile.isEmpty() && !holder.hasRemovedFiles()) return; JpsModule module = target.getModule(); JpsProject project = module.getProject(); ElixirCompilerOptions compilerOptions = JpsElixirCompilerOptionsExtension.getOrCreateExtension(project).getOptions(); if(compilerOptions.myUseMixCompiler){ doBuildWithMix(target, context, module, compilerOptions); } else { // elixirc can not compile tests now. if(!target.isTests()){ doBuildWithElixirc(target, context, module, compilerOptions, filesToCompile); } } } @NotNull @Override public String getPresentableName() { return BUILDER_NAME; } /** * Build With elixirc. * if "isMake": compile all files of the module and dependent module. * else: just for compile the target affected file. * */ private static void doBuildWithElixirc(ElixirTarget target, CompileContext context, JpsModule module, ElixirCompilerOptions compilerOptions, Collection<File> filesToCompile) throws ProjectBuildException{ // ensure compile output directory File outputDirectory = getBuildOutputDirectory(module, target.isTests(), context); runElixirc(target, context, compilerOptions, filesToCompile, outputDirectory); } private static void doBuildWithMix(ElixirTarget target, CompileContext context, JpsModule module, ElixirCompilerOptions compilerOptions) throws ProjectBuildException, IOException { String mixPath = getMixExecutablePath(module.getProject()); if(mixPath == null){ String errorMessage = "Mix path is not set."; context.processMessage(new CompilerMessage(MIX_NAME, BuildMessage.Kind.ERROR, errorMessage)); throw new ProjectBuildException(errorMessage); } JpsSdk<JpsDummyElement> sdk = ElixirTargetBuilderUtil.getSdk(context, module); String elixirPath = JpsElixirSdkType.getScriptInterpreterExecutable(sdk.getHomePath()).getAbsolutePath(); for(String contentRootUrl: module.getContentRootsList().getUrls()){ String contentRootPath = new URL(contentRootUrl).getPath(); File contentRootDir = new File(contentRootPath); File mixConfigFile = new File(contentRootDir, MIX_CONFIG_FILE_NAME); if(!mixConfigFile.exists()) continue; runMix(target, elixirPath, mixPath, contentRootPath, compilerOptions, context); } } /*** doBuildWithElixirc releated private methods */ @NotNull private static File getBuildOutputDirectory(@NotNull JpsModule module, boolean forTests, @NotNull CompileContext context) throws ProjectBuildException{ JpsJavaExtensionService instance = JpsJavaExtensionService.getInstance(); File outputDirectory = instance.getOutputDirectory(module, forTests); if(outputDirectory == null){ String errorMessage = "No output directory for module " + module.getName(); context.processMessage(new CompilerMessage(ElIXIRC_NAME, BuildMessage.Kind.ERROR, errorMessage)); throw new ProjectBuildException(errorMessage); } if(!outputDirectory.exists()){ FileUtil.createDirectory(outputDirectory); } return outputDirectory; } private static void runElixirc(ElixirTarget target, CompileContext context, ElixirCompilerOptions compilerOptions, Collection<File> files, File outputDirectory) throws ProjectBuildException { GeneralCommandLine commandLine = getElixircCommandLine(target, context, compilerOptions, files, outputDirectory); Process process; try{ process = commandLine.createProcess(); }catch (ExecutionException e){ throw new ProjectBuildException("Failed to launch elixir compiler", e); } BaseOSProcessHandler handler = new BaseOSProcessHandler(process, commandLine.getCommandLineString(), Charset.defaultCharset()); ProcessAdapter adapter = new ElixirCompilerProcessAdapter(context, ElIXIRC_NAME, ""); handler.addProcessListener(adapter); handler.startNotify(); handler.waitFor(); } private static GeneralCommandLine getElixircCommandLine(ElixirTarget target, CompileContext context, ElixirCompilerOptions compilerOptions, Collection<File> files, File outputDirectory) throws ProjectBuildException{ GeneralCommandLine commandLine = new GeneralCommandLine(); // get executable JpsModule module = target.getModule(); JpsSdk<JpsDummyElement> sdk = ElixirTargetBuilderUtil.getSdk(context, module); File executable = JpsElixirSdkType.getByteCodeCompilerExecutable(sdk.getHomePath()); List<String> compileFilePaths = getCompileFilePaths(module, target, context, files); commandLine.withWorkDirectory(outputDirectory); commandLine.setExePath(executable.getAbsolutePath()); addDependentModuleCodePath(commandLine, module, target, context); addCompileOptions(commandLine, compilerOptions); commandLine.addParameters(compileFilePaths); return commandLine; } @NotNull private static List<String> getCompileFilePaths(@NotNull JpsModule module, @NotNull ElixirTarget target, @NotNull CompileContext context, Collection<File> files){ // make if(context.getScope().isBuildIncrementally(target.getTargetType())){ return getCompileFilePathsDefault(module, target); } // force build files return ContainerUtil.map(files, new Function<File, String>() { @Override public String fun(File file) { return file.getAbsolutePath(); } }); } @NotNull private static List<String> getCompileFilePathsDefault(@NotNull JpsModule module, @NotNull ElixirTarget target){ CommonProcessors.CollectProcessor<File> exFilesCollector = new CommonProcessors.CollectProcessor<File>(){ @Override protected boolean accept(File file) { return !file.isDirectory() && FileUtilRt.extensionEquals(file.getName(), ELIXIR_SOURCE_EXTENSION); } }; List<JpsModuleSourceRoot> sourceRoots = new ArrayList<JpsModuleSourceRoot>(); ContainerUtil.addAll(sourceRoots, module.getSourceRoots(JavaSourceRootType.SOURCE)); if(target.isTests()){ ContainerUtil.addAll(sourceRoots, module.getSourceRoots(JavaSourceRootType.TEST_SOURCE)); } for (JpsModuleSourceRoot root : sourceRoots){ FileUtil.processFilesRecursively(root.getFile(), exFilesCollector); } return ContainerUtil.map(exFilesCollector.getResults(), new Function<File, String>() { @NotNull @Override public String fun(@NotNull File file) { return file.getAbsolutePath(); } }); } private static void addDependentModuleCodePath(@NotNull GeneralCommandLine commandLine, @NotNull JpsModule module, @NotNull ElixirTarget target, @NotNull CompileContext context) throws ProjectBuildException{ ArrayList<JpsModule> codePathModules = new ArrayList<JpsModule>(); collectDependentModules(module, codePathModules, new HashSet<String>()); addModuleToCodePath(commandLine, module, target.isTests(), context); for(JpsModule codePathModule : codePathModules){ if(codePathModule != module){ addModuleToCodePath(commandLine, codePathModule, false, context); } } } private static void collectDependentModules(@NotNull JpsModule module, @NotNull Collection<JpsModule> addedModules, @NotNull Set<String> addedModuleNames){ String moduleName = module.getName(); if(addedModuleNames.contains(moduleName)) return; addedModuleNames.add(moduleName); addedModules.add(module); for (JpsDependencyElement dependency : module.getDependenciesList().getDependencies()){ if(!(dependency instanceof JpsModuleDependency)) continue; JpsModuleDependency moduleDependency = (JpsModuleDependency) dependency; JpsModule depModule = moduleDependency.getModule(); if(depModule != null){ collectDependentModules(depModule, addedModules, addedModuleNames); } } } private static void addModuleToCodePath(@NotNull GeneralCommandLine commandLine, @NotNull JpsModule module, boolean forTests, @NotNull CompileContext context) throws ProjectBuildException{ File outputDirectory = getBuildOutputDirectory(module, forTests, context); commandLine.addParameters(ADD_PATH_TO_FRONT_OF_CODE_PATH, outputDirectory.getPath()); for(String rootUrl : module.getContentRootsList().getUrls()){ try{ String path = new URL(rootUrl).getPath(); commandLine.addParameters(ADD_PATH_TO_FRONT_OF_CODE_PATH, path); }catch (MalformedURLException e){ context.processMessage(new CompilerMessage(ElIXIRC_NAME, BuildMessage.Kind.ERROR, "Failed to find content root for module: " + module.getName())); } } } private static void addCompileOptions(@NotNull GeneralCommandLine commandLine, ElixirCompilerOptions compilerOptions){ if(!compilerOptions.myAttachDocsEnabled){ commandLine.addParameter("--no-docs"); } if(!compilerOptions.myAttachDebugInfoEnabled){ commandLine.addParameter("--no-debug-info"); } if(compilerOptions.myWarningsAsErrorsEnabled){ commandLine.addParameter("--warnings-as-errors"); } if(compilerOptions.myIgnoreModuleConflictEnabled){ commandLine.addParameter("--ignore-module-conflict"); } } /*** doBuildWithMix releated private methods */ @Nullable private static String getMixExecutablePath(@Nullable JpsProject project){ JpsMixConfigurationExtension mixConfigurationExtension = JpsMixConfigurationExtension.getExtension(project); String mixPath = mixConfigurationExtension != null ? mixConfigurationExtension.getMixPath() : null; return StringUtil.isEmptyOrSpaces(mixPath) ? null : mixPath; } private static void runMix(@NotNull ElixirTarget target, @NotNull String elixirPath, @NotNull String mixPath, @Nullable String contentRootPath, @NotNull ElixirCompilerOptions compilerOptions, @NotNull CompileContext context) throws ProjectBuildException{ GeneralCommandLine commandLine = new GeneralCommandLine(); commandLine.withWorkDirectory(contentRootPath); commandLine.setExePath(elixirPath); commandLine.addParameter(mixPath); commandLine.addParameter(target.isTests()? "test":"compile"); addCompileOptions(commandLine, compilerOptions); Process process; try{ process = commandLine.createProcess(); } catch (ExecutionException e) { throw new ProjectBuildException("Failed to run mix.", e); } BaseOSProcessHandler handler = new BaseOSProcessHandler(process, commandLine.getCommandLineString(), Charset.defaultCharset()); ProcessAdapter adapter = new ElixirCompilerProcessAdapter(context, MIX_NAME, commandLine.getWorkDirectory().getPath()); handler.addProcessListener(adapter); handler.startNotify(); handler.waitFor(); } }