/*
* 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");
}
}