/* * Copyright 2012-2014 Sergey Ignatov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.intellij.erlang.jps.builder; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.BaseOSProcessHandler; import com.intellij.execution.process.ProcessAdapter; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.CommonProcessors; import com.intellij.util.containers.ContainerUtil; import org.intellij.erlang.jps.model.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.jps.builders.BuildOutputConsumer; import org.jetbrains.jps.builders.DirtyFilesHolder; import org.jetbrains.jps.incremental.CompileContext; import org.jetbrains.jps.incremental.ProjectBuildException; import org.jetbrains.jps.incremental.TargetBuilder; import org.jetbrains.jps.incremental.messages.BuildMessage; import org.jetbrains.jps.incremental.messages.CompilerMessage; import org.jetbrains.jps.incremental.resources.ResourcesBuilder; import org.jetbrains.jps.model.JpsDummyElement; import org.jetbrains.jps.model.JpsProject; import org.jetbrains.jps.model.java.JavaSourceRootType; import org.jetbrains.jps.model.java.JpsJavaExtensionService; import org.jetbrains.jps.model.library.sdk.JpsSdk; import org.jetbrains.jps.model.module.*; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import static org.intellij.erlang.jps.builder.ErlangBuilderUtil.LOG; import static org.intellij.erlang.jps.builder.ErlangBuilderUtil.isSource; public class ErlangBuilder extends TargetBuilder<ErlangSourceRootDescriptor, ErlangTarget> { public static final String NAME = "erlc"; public ErlangBuilder() { super(Collections.singletonList(ErlangTargetType.INSTANCE)); //TODO provide a way to copy erlang resources //disables java resource builder for erlang modules ResourcesBuilder.registerEnabler(module -> !(module.getModuleType() instanceof JpsErlangModuleType)); } @Override public void build(@NotNull ErlangTarget target, @NotNull DirtyFilesHolder<ErlangSourceRootDescriptor, ErlangTarget> holder, @NotNull BuildOutputConsumer outputConsumer, @NotNull CompileContext context) throws ProjectBuildException, IOException { JpsModule module = target.getModule(); JpsProject project = module.getProject(); ErlangCompilerOptions compilerOptions = ErlangBuilderUtil.getCompilerOptions(project); if (compilerOptions.myUseRebarCompiler) return; LOG.info("Build module " + target.getPresentableName()); File sourceOutput = getBuildOutputDirectory(module, false, context); File testOutput = getBuildOutputDirectory(module, true, context); buildSources(target, context, compilerOptions, outputConsumer, sourceOutput, false); buildSources(target, context, compilerOptions, outputConsumer, testOutput, true); processAppConfigFiles(holder, outputConsumer, context, sourceOutput, testOutput); } @NotNull @Override public String getPresentableName() { return NAME; } private static void buildSources(@NotNull ErlangTarget target, @NotNull CompileContext context, @NotNull ErlangCompilerOptions compilerOptions, @NotNull BuildOutputConsumer outputConsumer, @NotNull File outputDir, final boolean isTests) throws IOException, ProjectBuildException { List<String> erlangModulePathsToCompile = getErlangModulePaths(target, context, isTests); if (erlangModulePathsToCompile.isEmpty()) { String message = isTests ? "Test sources is up to date for module" : "Sources is up to date for module"; reportMessageForModule(context, message, target.getModule().getName()); return; } String message = isTests ? "Compile tests for module" : "Compile source code for module"; reportMessageForModule(context, message, target.getModule().getName()); runErlc(target, context, compilerOptions, erlangModulePathsToCompile, outputConsumer, outputDir, isTests); } private static void reportMessageForModule(@NotNull CompileContext context, @NotNull String messagePrefix, @NotNull String moduleName) { String message = messagePrefix + " \"" + moduleName + "\"."; reportMessage(context, message); } private static void reportMessage(@NotNull CompileContext context, @NotNull String message) { LOG.info(message); context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.PROGRESS, message)); context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.INFO, message)); } @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 dir for module " + module.getName(); context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.ERROR, errorMessage)); throw new ProjectBuildException(errorMessage); } if (!outputDirectory.exists()) { FileUtil.createDirectory(outputDirectory); } return outputDirectory; } private static void processAppConfigFiles(DirtyFilesHolder<ErlangSourceRootDescriptor, ErlangTarget> holder, BuildOutputConsumer outputConsumer, CompileContext context, File... outputDirectories) throws IOException { List<File> appConfigFiles = new DirtyFilesProcessor<File, ErlangTarget>() { @Nullable @Override protected File getDirtyElement(@NotNull File file) throws IOException { return ErlangBuilderUtil.isAppConfigFileName(file.getName()) ? file : null; } }.collectDirtyElements(holder); for (File appConfigSrc : appConfigFiles) { for (File outputDir : outputDirectories) { File appConfigDst = getDestinationAppConfig(outputDir, appConfigSrc.getName()); FileUtil.copy(appConfigSrc, appConfigDst); reportMessage(context, String.format("Copy %s to %s", ErlangBuilderUtil.getPath(appConfigSrc), ErlangBuilderUtil.getPath(outputDir))); outputConsumer.registerOutputFile(appConfigDst, Collections.singletonList(ErlangBuilderUtil.getPath(appConfigSrc))); } } } @NotNull private static File getDestinationAppConfig(File outputDir, @NotNull String fileName) { return new File(outputDir, getAppConfigDestinationFileName(fileName)); } @NotNull private static String getAppConfigDestinationFileName(@NotNull String sourceFileName) { return StringUtil.trimEnd(sourceFileName, ".src"); } private static void runErlc(ErlangTarget target, CompileContext context, ErlangCompilerOptions compilerOptions, List<String> erlangModulePathsToCompile, BuildOutputConsumer outputConsumer, File outputDirectory, boolean isTest) throws ProjectBuildException, IOException { GeneralCommandLine commandLine = getErlcCommandLine(target, context, compilerOptions, outputDirectory, erlangModulePathsToCompile, isTest); Process process; LOG.debug("Run erlc compiler with command " + commandLine.getCommandLineString()); try { process = commandLine.createProcess(); } catch (ExecutionException e) { throw new ProjectBuildException("Failed to launch erlang compiler", e); } BaseOSProcessHandler handler = new BaseOSProcessHandler(process, commandLine.getCommandLineString(), Charset.defaultCharset()); ProcessAdapter adapter = new ErlangCompilerProcessAdapter(context, NAME, ""); handler.addProcessListener(adapter); handler.startNotify(); handler.waitFor(); registerBeams(outputConsumer, erlangModulePathsToCompile, outputDirectory); } private static GeneralCommandLine getErlcCommandLine(ErlangTarget target, CompileContext context, ErlangCompilerOptions compilerOptions, File outputDirectory, List<String> erlangModulePaths, boolean isTest) throws ProjectBuildException { GeneralCommandLine commandLine = new GeneralCommandLine(); JpsModule module = target.getModule(); JpsSdk<JpsDummyElement> sdk = ErlangTargetBuilderUtil.getSdk(context, module); File executable = JpsErlangSdkType.getByteCodeCompilerExecutable(sdk.getHomePath()); commandLine.withWorkDirectory(outputDirectory); commandLine.setExePath(executable.getAbsolutePath()); addCodePath(commandLine, module, target, context); addParseTransforms(commandLine, module); addDebugInfo(commandLine, compilerOptions.myAddDebugInfoEnabled); addIncludePaths(commandLine, module); addMacroDefinitions(commandLine, isTest); commandLine.addParameters(compilerOptions.myAdditionalErlcArguments); commandLine.addParameters(erlangModulePaths); return commandLine; } private static void addMacroDefinitions(GeneralCommandLine commandLine, boolean isTests) { if (isTests) { commandLine.addParameters("-DTEST"); } } private static void addDebugInfo(@NotNull GeneralCommandLine commandLine, boolean addDebugInfoEnabled) { if (addDebugInfoEnabled) { commandLine.addParameter("+debug_info"); } } private static void addIncludePaths(@NotNull GeneralCommandLine commandLine, @Nullable JpsModule module) { if (module == null) return; for (JpsTypedModuleSourceRoot<JpsDummyElement> includeDirectory : module.getSourceRoots(ErlangIncludeSourceRootType.INSTANCE)) { commandLine.addParameters("-I", includeDirectory.getFile().getPath()); } } @NotNull private static List<String> getErlangModulePaths(@NotNull ErlangTarget target, @NotNull CompileContext context, boolean isTest) { List<String> erlangDirtyModules = getErlangModulePathsFromTarget(target, isTest); if (erlangDirtyModules != null) { return erlangDirtyModules; } String message = "Erlang module " + target.getModule().getName() + " will be fully rebuilt."; LOG.warn(message); context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.WARNING, message)); return getErlangModulePathsDefault(target, isTest); } @Nullable private static List<String> getErlangModulePathsFromTarget(@NotNull ErlangTarget target, boolean isTests) { ErlangModuleBuildOrder buildOrder = target.getBuildOrder(); if (buildOrder == null) return null; List<String> modules = buildOrder.myOrderedErlangFilePaths; return isTests ? ContainerUtil.concat(modules, buildOrder.myOrderedErlangTestFilePaths) : modules; } @NotNull private static List<String> getErlangModulePathsDefault(@NotNull ErlangTarget target, boolean isTests) { CommonProcessors.CollectProcessor<File> erlFilesCollector = new CommonProcessors.CollectProcessor<File>() { @Override protected boolean accept(@NotNull File file) { return !file.isDirectory() && isSource(file.getName()); } }; List<JpsModuleSourceRoot> sourceRoots = ContainerUtil.newArrayList(); JpsModule module = target.getModule(); ContainerUtil.addAll(sourceRoots, module.getSourceRoots(JavaSourceRootType.SOURCE)); if (isTests) { ContainerUtil.addAll(sourceRoots, module.getSourceRoots(JavaSourceRootType.TEST_SOURCE)); } for (JpsModuleSourceRoot root : sourceRoots) { FileUtil.processFilesRecursively(root.getFile(), erlFilesCollector); } return ContainerUtil.map(erlFilesCollector.getResults(), ErlangBuilderUtil::getPath); } private static void addParseTransforms(@NotNull GeneralCommandLine commandLine, @Nullable JpsModule module) throws ProjectBuildException { JpsErlangModuleExtension extension = JpsErlangModuleExtension.getExtension(module); List<String> parseTransforms = extension != null ? extension.getParseTransforms() : Collections.<String>emptyList(); if (parseTransforms.isEmpty()) return; for (String ptModule : parseTransforms) { commandLine.addParameter("+{parse_transform, " + ptModule + "}"); } } private static void addCodePath(@NotNull GeneralCommandLine commandLine, @NotNull JpsModule module, @NotNull ErlangTarget target, @NotNull CompileContext context) throws ProjectBuildException { List<JpsModule> codePathModules = ContainerUtil.newArrayList(); collectDependentModules(module, codePathModules, ContainerUtil.<String>newHashSet()); 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("-pa", outputDirectory.getPath()); for (String rootUrl : module.getContentRootsList().getUrls()) { try { String path = new URL(rootUrl).getPath(); commandLine.addParameters("-pa", path); } catch (MalformedURLException e) { context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.ERROR, "Failed to find content root for module: " + module.getName())); } } } private static void registerBeams(@NotNull BuildOutputConsumer outputConsumer, @NotNull List<String> erlPaths, @NotNull File outputDir) throws IOException { for (String erlPath : erlPaths) { File beam = getBeam(outputDir, erlPath); if (beam.exists()) { outputConsumer.registerOutputFile(beam, ContainerUtil.createMaybeSingletonList(erlPath)); } } } @NotNull private static File getBeam(@NotNull File outputDirectory, @NotNull String erlPath) { String name = FileUtil.getNameWithoutExtension(new File(erlPath)); return new File(outputDirectory, name + ".beam"); } }