package net.jangaroo.ide.idea.jps;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import net.jangaroo.ide.idea.jps.util.CompilerLoader;
import net.jangaroo.ide.idea.jps.util.JpsCompileLog;
import net.jangaroo.jooc.api.CompilationResult;
import net.jangaroo.jooc.api.CompileLog;
import net.jangaroo.jooc.api.Jooc;
import net.jangaroo.jooc.config.DebugMode;
import net.jangaroo.jooc.config.JoocConfiguration;
import net.jangaroo.utils.FileLocations;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.incremental.BuilderCategory;
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.MessageHandler;
import org.jetbrains.jps.incremental.ModuleBuildTarget;
import org.jetbrains.jps.incremental.ModuleLevelBuilder;
import org.jetbrains.jps.incremental.ProjectBuildException;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.incremental.messages.CustomBuilderMessage;
import org.jetbrains.jps.model.JpsDummyElement;
import org.jetbrains.jps.model.java.JavaResourceRootType;
import org.jetbrains.jps.model.java.JavaSourceRootProperties;
import org.jetbrains.jps.model.java.JavaSourceRootType;
import org.jetbrains.jps.model.java.JpsJavaDependencyExtension;
import org.jetbrains.jps.model.java.JpsJavaDependencyScope;
import org.jetbrains.jps.model.java.JpsJavaExtensionService;
import org.jetbrains.jps.model.library.JpsLibrary;
import org.jetbrains.jps.model.library.JpsOrderRootType;
import org.jetbrains.jps.model.library.JpsTypedLibrary;
import org.jetbrains.jps.model.library.sdk.JpsSdk;
import org.jetbrains.jps.model.module.JpsDependencyElement;
import org.jetbrains.jps.model.module.JpsLibraryDependency;
import org.jetbrains.jps.model.module.JpsModule;
import org.jetbrains.jps.model.module.JpsModuleDependency;
import org.jetbrains.jps.model.module.JpsModuleSourceRoot;
import org.jetbrains.jps.model.module.JpsTypedModuleSourceRoot;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Jangaroo analog of {@link org.jetbrains.jps.incremental.java.JavaBuilder}.
*/
public class JangarooBuilder extends ModuleLevelBuilder {
public static final String BUILDER_NAME = "jooc";
public static final FileFilter AS_SOURCES_FILTER = createSuffixFileFilter(Jooc.AS_SUFFIX);
public static final String FILE_INVALIDATION_BUILDER_MESSAGE = "FILE_INVALIDATION";
private final Logger log = Logger.getInstance(JangarooBuilder.class);
public static FileFilter createSuffixFileFilter(final String suffix) {
return SystemInfo.isFileSystemCaseSensitive?
new FileFilter() {
public boolean accept(File file) {
return file.getPath().endsWith(suffix);
}
} :
new FileFilter() {
public boolean accept(File file) {
return StringUtil.endsWithIgnoreCase(file.getPath(), suffix);
}
};
}
public JangarooBuilder() {
super(BuilderCategory.TRANSLATOR);
}
@Override
public List<String> getCompilableFileExtensions() {
return Collections.singletonList(Jooc.AS_SUFFIX_NO_DOT);
}
@Override
public ExitCode build(CompileContext context, ModuleChunk chunk,
DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
OutputConsumer outputConsumer) throws ProjectBuildException, IOException {
final Map<ModuleBuildTarget, List<File>> filesToCompile = getFilesToCompile(
AS_SOURCES_FILTER,
dirtyFilesHolder
);
if (!filesToCompile.isEmpty()) {
Map<ModuleBuildTarget, String> finalOutputs = getCanonicalModuleOutputs(context, chunk);
if (finalOutputs == null) {
return ExitCode.ABORT;
}
JpsCompileLog compileLog = new JpsCompileLog(BUILDER_NAME, context);
for (ModuleBuildTarget moduleBuildTarget : finalOutputs.keySet()) {
ExitCode result = compile(context, outputConsumer, filesToCompile.get(moduleBuildTarget), compileLog,
moduleBuildTarget);
if (result != null) {
return result;
}
}
return ExitCode.OK;
}
return ExitCode.NOTHING_DONE;
}
private ExitCode compile(CompileContext context, OutputConsumer outputConsumer, List<File> filesToCompile,
JpsCompileLog compileLog, ModuleBuildTarget moduleBuildTarget) throws IOException {
JpsModule module = moduleBuildTarget.getModule();
JoocConfigurationBean joocConfigurationBean = JangarooModelSerializerExtension.getJoocSettings(module);
if (joocConfigurationBean == null) {
return null; // no Jangaroo Facet in this module: skip silently!
}
List<String> jarPaths = getJangarooSdkJarPath(joocConfigurationBean, module);
if (jarPaths == null) {
context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.WARNING,
String.format("Jangaroo module %s does not have a valid Jangaroo SDK. Compilation skipped.", module.getName())));
return ExitCode.ABORT;
}
JoocConfiguration joocConfiguration = getJoocConfiguration(joocConfigurationBean, module, filesToCompile, moduleBuildTarget.isTests());
log.info(String.format("Compiling module %s...", module.getName()));
if (log.isDebugEnabled()) {
log.debug(String.format(" module %s classpath=%s, sourcepath=%s, sourcefiles=%s", module.getName(),
joocConfiguration.getClassPath(), joocConfiguration.getSourcePath(), joocConfiguration.getSourceFiles()));
}
Jooc jooc = getJooc(context, jarPaths, joocConfiguration, compileLog);
if (jooc == null) {
log.warn(String.format("No Jangaroo build configuration found in module %s.", module.getName()));
return ExitCode.ABORT;
}
CompilationResult compilationResult = compile(moduleBuildTarget, jooc, outputConsumer);
if (compilationResult.getResultCode() == CompilationResult.RESULT_CODE_COMPILATION_FAILED) {
log.info(String.format("Compilation failed in module %s.", module.getName()));
return ExitCode.ABORT;
}
if (compilationResult.getResultCode() != CompilationResult.RESULT_CODE_OK) {
log.error(String.format("Unexpected compilation reulst %s in module %s.", compilationResult.getResultCode(), module.getName()));
return ExitCode.ABORT;
}
log.info(String.format("Compilation of module %s completed successfully.", module.getName()));
return null;
}
@Nullable
public static List<String> getJangarooSdkJarPath(JoocConfigurationBean joocConfigurationBean, JpsModule module) {
JpsTypedLibrary<JpsSdk<JpsDummyElement>> jangarooSdkLibrary = module.getProject().getModel().getGlobal().getLibraryCollection()
.findLibrary(joocConfigurationBean.jangarooSdkName, JpsJangarooSdkType.INSTANCE);
if (jangarooSdkLibrary == null) {
return null;
}
JpsSdk<JpsDummyElement> sdk = jangarooSdkLibrary.getProperties();
return JpsJangarooSdkType.getSdkJarPaths(sdk);
}
private CompilationResult compile(ModuleBuildTarget moduleBuildTarget, Jooc jooc,
OutputConsumer outputConsumer) throws IOException {
CompilationResult compilationResult = jooc.run();
for (Map.Entry<File, File> sourceToTarget : compilationResult.getOutputFileMap().entrySet()) {
if (sourceToTarget.getValue() != null) { // only non-native classes!
outputConsumer.registerOutputFile(moduleBuildTarget, sourceToTarget.getValue(), toSingletonPath(sourceToTarget.getKey()));
}
}
return compilationResult;
}
public static Set<String> toSingletonPath(File file) {
return Collections.singleton(file.getPath());
}
public static Map<ModuleBuildTarget, List<File>> getFilesToCompile(final FileFilter sourcesFilter,
DirtyFilesHolder<JavaSourceRootDescriptor,
ModuleBuildTarget> dirtyFilesHolder) throws IOException {
// a map of files, grouped by module first, then by test sources (true) versus non-test sources (false)
final Map<ModuleBuildTarget, List<File>> filesToCompile = new HashMap<ModuleBuildTarget, List<File>>();
dirtyFilesHolder.processDirtyFiles(new FileProcessor<JavaSourceRootDescriptor, ModuleBuildTarget>() {
public boolean apply(ModuleBuildTarget target, File file, JavaSourceRootDescriptor descriptor) throws IOException {
if (sourcesFilter.accept(file)) {
if (!filesToCompile.containsKey(target)) {
filesToCompile.put(target, new ArrayList<File>());
}
filesToCompile.get(target).add(file);
}
return true;
}
});
return filesToCompile;
}
protected JoocConfiguration getJoocConfiguration(JoocConfigurationBean joocConfigurationBean, JpsModule module, List<File> sourceFiles, boolean forTests) {
JoocConfiguration joocConfig = new JoocConfiguration();
joocConfig.setVerbose(joocConfigurationBean.verbose);
joocConfig.setDebugMode(joocConfigurationBean.isDebug() ? joocConfigurationBean.isDebugSource() ? DebugMode.SOURCE : DebugMode.LINES : null);
joocConfig.setAllowDuplicateLocalVariables(joocConfigurationBean.allowDuplicateLocalVariables);
joocConfig.setEnableAssertions(joocConfigurationBean.enableAssertions);
joocConfig.setApiOutputDirectory(forTests ? null : joocConfigurationBean.getApiOutputDirectory());
updateFileLocations(joocConfig, module, forTests, true);
joocConfig.setSourceFiles(sourceFiles);
joocConfig.setMergeOutput(false); // no longer supported: joocConfigurationBean.mergeOutput;
joocConfig.setOutputDirectory(forTests ? joocConfigurationBean.getTestOutputDirectory() : joocConfigurationBean.getOutputDirectory());
joocConfig.setPublicApiViolationsMode(joocConfigurationBean.publicApiViolationsMode);
return joocConfig;
}
public static void updateFileLocations(FileLocations fileLocations, JpsModule module,
boolean forTests, boolean compileGeneratedSources) {
Collection<File> classPath = new LinkedHashSet<File>();
Collection<File> sourcePath = new LinkedHashSet<File>();
addToClassOrSourcePath(module, classPath, sourcePath, forTests, compileGeneratedSources);
fileLocations.setClassPath(new ArrayList<File>(classPath));
try {
fileLocations.setSourcePath(new ArrayList<File>(sourcePath));
} catch (IOException e) {
throw new RuntimeException("while constructing Jangaroo source path", e);
}
}
public static void addToClassOrSourcePath(JpsModule module, Collection<File> classPath, Collection<File> sourcePath,
boolean forTests, boolean compileGeneratedSources) {
JavaSourceRootType sourceRootType = forTests ? JavaSourceRootType.TEST_SOURCE : JavaSourceRootType.SOURCE;
for (JpsTypedModuleSourceRoot<JavaSourceRootProperties> sourceRoot : module.getSourceRoots(sourceRootType)) {
if (compileGeneratedSources || !sourceRoot.getProperties().isForGeneratedSources()) {
sourcePath.add(sourceRoot.getFile());
} else {
classPath.add(sourceRoot.getFile());
}
}
// special case: the deprecated src/main/joo-api directory is still used, but in IDEA 13 it is no longer a source,
// but a resource directory!
for (JpsTypedModuleSourceRoot resourceRoot : module.getSourceRoots(JavaResourceRootType.RESOURCE)) {
File resourceRootFile = resourceRoot.getFile();
if ("joo-api".equals(resourceRootFile.getName())) {
classPath.add(resourceRootFile);
}
}
if (forTests) {
for (JpsTypedModuleSourceRoot<JavaSourceRootProperties> sourceRoot : module.getSourceRoots(JavaSourceRootType.SOURCE)) {
classPath.add(sourceRoot.getFile());
}
}
JpsJavaExtensionService javaExtensionService = JpsJavaExtensionService.getInstance();
List<JpsDependencyElement> dependencies = module.getDependenciesList().getDependencies();
for (JpsDependencyElement dependency : dependencies) {
JpsJavaDependencyExtension dependencyExtension = javaExtensionService.getDependencyExtension(dependency);
if (dependencyExtension == null || dependencyExtension.getScope() == JpsJavaDependencyScope.COMPILE ||
dependencyExtension.getScope() == JpsJavaDependencyScope.TEST && forTests) {
addToClassPath(classPath, dependency);
}
}
}
private static void addToClassPath(Collection<File> classPath, JpsDependencyElement dependency) {
if (dependency instanceof JpsLibraryDependency) {
JpsLibrary library = ((JpsLibraryDependency)dependency).getLibrary();
if (library != null) {
classPath.addAll(library.getFiles(JpsOrderRootType.COMPILED));
}
} else if (dependency instanceof JpsModuleDependency) {
JpsModule otherModule = ((JpsModuleDependency)dependency).getModule();
if (otherModule != null) {
for (JpsModuleSourceRoot sourceRoot : otherModule.getSourceRoots(JavaSourceRootType.SOURCE)) {
classPath.add(sourceRoot.getFile());
}
for (JpsModuleSourceRoot sourceRoot : otherModule.getSourceRoots(JavaResourceRootType.RESOURCE)) {
classPath.add(sourceRoot.getFile());
}
}
}
}
public static Jooc getJooc(MessageHandler messageHandler, List<String> jarPaths, JoocConfiguration configuration, CompileLog log) {
Jooc jooc;
try {
jooc = CompilerLoader.loadJooc(jarPaths);
} catch (FileNotFoundException e) {
messageHandler.processMessage(new CompilerMessage(BUILDER_NAME, e));
return null;
} catch (Exception e) {
// Jangaroo SDK not correctly set up or not compatible with this Jangaroo IDEA plugin:
messageHandler.processMessage(new CompilerMessage(BUILDER_NAME, e));
return null;
}
jooc.setConfig(configuration);
jooc.setLog(log);
return jooc;
}
@Nullable
public static Map<ModuleBuildTarget, String> getCanonicalModuleOutputs(CompileContext context, ModuleChunk chunk) {
Map<ModuleBuildTarget, String> finalOutputs = new HashMap<ModuleBuildTarget, String>();
for (ModuleBuildTarget target : chunk.getTargets()) {
File moduleOutputDir = target.getOutputDir();
if (moduleOutputDir == null) {
context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, "Output directory not specified for module " + target.getModule().getName()));
return null;
}
String moduleOutputPath = FileUtil.toCanonicalPath(moduleOutputDir.getPath());
assert moduleOutputPath != null;
finalOutputs.put(target, moduleOutputPath.endsWith("/") ? moduleOutputPath : moduleOutputPath + "/");
}
return finalOutputs;
}
@NotNull
@Override
public String getPresentableName() {
return "Jangaroo Compiler";
}
public static void processFileInvalidationMessage(CompileContext context, File file) {
context.processMessage(new CustomBuilderMessage(BUILDER_NAME, FILE_INVALIDATION_BUILDER_MESSAGE, file.getPath()));
}
}