package com.siberika.idea.pascal.jps.builder;
import com.intellij.execution.process.BaseOSProcessHandler;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.SmartList;
import com.siberika.idea.pascal.jps.compiler.CompilerMessager;
import com.siberika.idea.pascal.jps.compiler.DelphiBackendCompiler;
import com.siberika.idea.pascal.jps.compiler.FPCBackendCompiler;
import com.siberika.idea.pascal.jps.compiler.PascalBackendCompiler;
import com.siberika.idea.pascal.jps.model.JpsPascalModuleType;
import com.siberika.idea.pascal.jps.model.JpsPascalSdkType;
import com.siberika.idea.pascal.jps.sdk.PascalCompilerFamily;
import com.siberika.idea.pascal.jps.sdk.PascalSdkData;
import com.siberika.idea.pascal.jps.util.ParamMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.builders.BuildOutputConsumer;
import org.jetbrains.jps.builders.BuildTargetType;
import org.jetbrains.jps.builders.DirtyFilesHolder;
import org.jetbrains.jps.builders.FileProcessor;
import org.jetbrains.jps.builders.java.JavaBuilderUtil;
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.incremental.resources.StandardResourceBuilderEnabler;
import org.jetbrains.jps.model.java.JpsJavaExtensionService;
import org.jetbrains.jps.model.library.JpsOrderRootType;
import org.jetbrains.jps.model.library.sdk.JpsSdk;
import org.jetbrains.jps.model.module.JpsModule;
import org.jetbrains.jps.model.module.JpsModuleSourceRoot;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Author: George Bakhtadze
* Date: 19/05/2015
*/
public class PascalTargetBuilder extends TargetBuilder<PascalSourceRootDescriptor, PascalTarget> {
private static final String NAME = "Pascal builder";
protected PascalTargetBuilder(Collection<? extends BuildTargetType<? extends PascalTarget>> buildTargetTypes) {
super(buildTargetTypes);
//disables java resource builder for pascal modules
ResourcesBuilder.registerEnabler(new StandardResourceBuilderEnabler() {
@Override
public boolean isResourceProcessingEnabled(@NotNull JpsModule module) {
return !(module.getModuleType() instanceof JpsPascalModuleType);
}
});
}
@NotNull
@Override
public String getPresentableName() {
return NAME;
}
@Override
public void build(@NotNull PascalTarget target, @NotNull DirtyFilesHolder<PascalSourceRootDescriptor, PascalTarget> holder,
@NotNull BuildOutputConsumer outputConsumer, @NotNull CompileContext context) throws ProjectBuildException, IOException {
JpsModule module = target.getModule();
File mainFile = PascalBackendCompiler.getMainFile(ParamMap.getJpsParams(module.getProperties()));
// Force main file to compile. TODO: force only for context-based (line marker?) run configurations
// if (!holder.hasDirtyFiles() && !holder.hasRemovedFiles()) return;
final Map<PascalTarget, List<File>> files = new HashMap<PascalTarget, List<File>>();
if (mainFile != null) {
files.put(target, new SmartList<File>(mainFile));
}
collectChangedFiles(files, holder);
boolean isRebuild = JavaBuilderUtil.isForcedRecompilationAllJavaModules(context) || (!JavaBuilderUtil.isCompileJavaIncrementally(context));
if (files.isEmpty() && !isRebuild) {
context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.INFO, "No changes detected"));
return;
}
CompilerMessager messager = new PascalCompilerMessager(getPresentableName(), context);
JpsSdk<?> sdk = module.getSdk(JpsPascalSdkType.INSTANCE);
if (sdk != null) {
PascalBackendCompiler compiler = getCompiler(sdk, messager);
if (compiler != null) {
messager.info("Compiler family:" + compiler.getId(), "", -1L, -1);
List<File> sdkFiles = sdk.getParent().getFiles(JpsOrderRootType.COMPILED);
sdkFiles.addAll(sdk.getParent().getFiles(JpsOrderRootType.SOURCES));
File outputDir = getBuildOutputDirectory(module, target.isTests(), context);
for (File file : files.get(target)) {
File compiled = new File(outputDir, FileUtil.getNameWithoutExtension(file) + compiler.getCompiledUnitExt());
//messager.info(String.format("Map: %s => %s ", file.getCanonicalPath(), compiled.getCanonicalPath()), null, -1L, -1L);
outputConsumer.registerOutputFile(compiled, Collections.singleton(file.getCanonicalPath()));
}
String[] cmdLine = compiler.createStartupCommand(sdk.getHomePath(), module.getName(), outputDir.getAbsolutePath(),
sdkFiles, getFiles(module.getSourceRoots()),
files.get(target), ParamMap.getJpsParams(module.getProperties()),
isRebuild,
ParamMap.getJpsParams(sdk.getSdkProperties()));
if (cmdLine != null) {
// For Delphi workingDirectory should be null otherwise file paths in compiler messages will be relative
File workingDirectory = PascalCompilerFamily.DELPHI.equals(getCompilerFamily(sdk)) ? null : new File(FileUtil.expandUserHome("~/"));
int exitCode = launchCompiler(compiler, messager, cmdLine, workingDirectory);
if (exitCode != 0) {
messager.warning("Error. Compiler exit code: " + exitCode, null, -1L, -1L);
}
} else {
messager.warning("Error. Can't launch compiler", null, -1L, -1L);
}
} else {
messager.error("Can't determine compiler family", "", -1L, -1L);
}
} else {
log(context, "Pascal SDK is not defined for module " + module.getName());
}
}
@Nullable
private PascalBackendCompiler getCompiler(@NotNull JpsSdk<?> sdk, CompilerMessager messager) {
PascalCompilerFamily family = getCompilerFamily(sdk);
if (PascalCompilerFamily.FPC.equals(family)) {
return new FPCBackendCompiler(messager);
} else if (PascalCompilerFamily.DELPHI.equals(family)) {
return new DelphiBackendCompiler(messager);
}
return null;
}
private PascalCompilerFamily getCompilerFamily(JpsSdk<?> sdk) {
ParamMap params = ParamMap.getJpsParams(sdk.getSdkProperties());
String family = params != null ? params.get(PascalSdkData.Keys.COMPILER_FAMILY.getKey()) : null;
for (PascalCompilerFamily compilerFamily : PascalCompilerFamily.values()) {
if (compilerFamily.name().equals(family)) {
return compilerFamily;
}
}
return null;
}
private int launchCompiler(PascalBackendCompiler compiler, CompilerMessager messager, String[] cmdLine, File workingDir) throws IOException {
Process process = Runtime.getRuntime().exec(cmdLine, null, workingDir);
BaseOSProcessHandler handler = new BaseOSProcessHandler(process, cmdLine[0], Charset.defaultCharset());
ProcessAdapter adapter = compiler.getCompilerProcessAdapter(messager);
handler.addProcessListener(adapter);
handler.startNotify();
handler.waitFor();
return process.exitValue();
}
private static void log(CompileContext context, String text) {
context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.INFO, text));
}
private List<File> getFiles(List<JpsModuleSourceRoot> sourceRoots) {
List<File> result = new ArrayList<File>();
for (JpsModuleSourceRoot root : sourceRoots) {
result.add(root.getFile());
}
return result;
}
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) {
context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.ERROR, "No output dir for module " + module.getName()));
} else {
if (!outputDirectory.exists()) {
FileUtil.createDirectory(outputDirectory);
}
}
return outputDirectory;
}
private static void collectChangedFiles(final Map<PascalTarget, List<File>> result, DirtyFilesHolder<PascalSourceRootDescriptor, PascalTarget> dirtyFilesHolder) throws IOException {
dirtyFilesHolder.processDirtyFiles(new FileProcessor<PascalSourceRootDescriptor, PascalTarget>() {
public boolean apply(PascalTarget target, File file, PascalSourceRootDescriptor sourceRoot) throws IOException {
final String path = file.getPath();
if (isPascalFile(path)) { //todo file type check
List<File> toCompile = result.get(target);
if (null == toCompile) {
toCompile = new ArrayList<File>();
result.put(target, toCompile);
}
toCompile.add(file);
}
return true;
}
});
}
private static boolean isPascalFile(String path) {
for (String ext : PascalBuilderService.COMPILABLE_EXTENSIONS) {
if (path.endsWith("." + ext)) {
return true;
}
}
return false;
}
}