/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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 com.haskforce.jps;
/*
* Downloaded from https://github.com/Atsky/haskell-idea-plugin on 7 May
* 2014.
*/
import com.haskforce.jps.model.HaskellBuildOptions;
import com.haskforce.jps.model.JpsHaskellBuildOptionsExtension;
import com.haskforce.jps.model.JpsHaskellModuleType;
import com.haskforce.utils.SystemUtil;
import com.intellij.execution.ExecutionException;
import org.jetbrains.jps.incremental.BuilderCategory;
import org.jetbrains.jps.incremental.ModuleLevelBuilder;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.ModuleChunk;
import org.jetbrains.jps.builders.DirtyFilesHolder;
import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.ModuleBuildTarget;
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.ProgressMessage;
import org.jetbrains.jps.model.module.JpsModule;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* First stop that does any work in the Cabal builder.
*/
public class CabalBuilder extends ModuleLevelBuilder {
// Messages go to the log available in Help -> Show log in finder,
// "build-log" subdirectory.
private final static Logger LOG = Logger.getInstance(CabalBuilder.class);
public CabalBuilder() {
super(BuilderCategory.TRANSLATOR);
}
/**
* Build the project including all modules with Cabal.
*/
public ExitCode build(final CompileContext context,
final ModuleChunk chunk,
final DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
final OutputConsumer outputConsumer) throws ProjectBuildException {
try {
for (JpsModule module : chunk.getModules()) {
// Builder gets called for pure java projects as well. Silently
// skip those since processMessage() of error will halt the
// entire compilation. Funny..
if (!module.getModuleType().equals(JpsHaskellModuleType.INSTANCE)) {
continue;
}
HaskellBuildOptions buildOptions = JpsHaskellBuildOptionsExtension.getOrCreateExtension(module.getProject()).getOptions();
// If we're not using Cabal, don't use Cabal.
if (!buildOptions.myUseCabal || buildOptions.myUseStack) {
continue;
}
File cabalFile = getCabalFile(module);
if (cabalFile == null) {
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.ERROR,
"Can not find cabal file in " + getContentRootPath(module)));
continue;
}
//noinspection ObjectAllocationInLoop
CabalJspInterface cabal = new CabalJspInterface(cabalFile, buildOptions);
//noinspection ObjectAllocationInLoop
if (buildOptions.myUseCabalSandbox) {
if (runSandboxInit(context, module, cabal, cabalFile)) return ExitCode.ABORT;
}
if (buildOptions.myInstallCabalDependencies) {
if (runInstallDependencies(context, module, cabal)) return ExitCode.ABORT;
}
if (runConfigure(context, module, cabal)) return ExitCode.ABORT;
if (runBuild(context, module, cabal)) return ExitCode.ABORT;
}
return ExitCode.OK;
} catch (IOException e) {
processCabalError(context, e);
} catch (InterruptedException e) {
processCabalError(context, e);
} catch (ExecutionException e) {
processCabalError(context, e);
}
return ExitCode.ABORT;
}
private static void processCabalError(final CompileContext context, final Exception e) {
e.printStackTrace();
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.ERROR, e.getMessage()));
}
/**
* Runs cabal build.
*/
private static boolean runBuild(CompileContext context, JpsModule module, CabalJspInterface cabal)
throws IOException, InterruptedException, ExecutionException {
context.processMessage(new ProgressMessage("cabal build"));
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.INFO, "Start build"));
Process buildProcess = cabal.build();
processOut(context, buildProcess, module);
if (buildProcess.waitFor() != 0) {
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.ERROR, "build errors."));
return true;
}
return false;
}
/**
* Runs cabal configure.
*/
private static boolean runConfigure(CompileContext context, JpsModule module, CabalJspInterface cabal)
throws IOException, InterruptedException, ExecutionException {
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.INFO, "Start configure"));
Process configureProcess = cabal.configure();
processOut(context, configureProcess, module);
if (configureProcess.waitFor() != 0) {
context.processMessage(new CompilerMessage(
"cabal",
BuildMessage.Kind.ERROR,
"configure failed."));
return true;
}
return false;
}
/**
* Runs cabal install --only-dependencies
*/
private static boolean runInstallDependencies(CompileContext context, JpsModule module, CabalJspInterface cabal)
throws IOException, InterruptedException, ExecutionException {
if (!installDependenciesRequired(cabal)) {
return false;
}
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.INFO, "Install dependencies"));
Process installDependenciesProcess = cabal.installDependencies();
processOut(context, installDependenciesProcess, module, true);
if (installDependenciesProcess.waitFor() != 0) {
context.processMessage(new CompilerMessage(
"cabal",
BuildMessage.Kind.ERROR,
"install dependencies failed."));
return true;
}
return false;
}
private static boolean installDependenciesRequired(CabalJspInterface cabal) throws IOException, ExecutionException {
Process dryRun = cabal.installDependencies(true);
Iterator<String> dryRunOut = collectOutput(dryRun);
String line;
while (dryRunOut.hasNext()) {
line = dryRunOut.next();
// TODO: Is there a more elegant way to do this?
if (line.contains("already installed")) {
return false;
}
}
return true;
}
/**
* Runs cabal sandbox init
*/
private static boolean runSandboxInit(CompileContext context, JpsModule module, CabalJspInterface cabal, File cabalFile)
throws IOException, InterruptedException, ExecutionException {
// If sandbox already exists, no need to run init - just bail out.
if (new File(cabalFile.getParent(), "cabal.sandbox.config").isFile()) {
return false;
}
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.INFO, "Create sandbox"));
Process sandboxProcess = cabal.sandboxInit();
if (sandboxProcess.waitFor() != 0) {
context.processMessage(new CompilerMessage(
"cabal",
BuildMessage.Kind.ERROR,
"sandbox init failed."));
return true;
}
return false;
}
/**
* Parses output from cabal and signals errors/warnings to the IDE.
*/
private static void processOut(CompileContext context, Process process, JpsModule module) {
processOut(context, process, module, false);
}
private static void processOut(CompileContext context, Process process, JpsModule module, boolean logAll) {
final String warningPrefix = "Warning: ";
final String cabalPrefix = "cabal: ";
boolean oneBehind = false;
String line = "";
Iterator<String> processOut = collectOutput(process);
StringBuilder msg = new StringBuilder(1000);
Pattern compiledPattern = Pattern.compile("^([^:]+):(\\d+):(\\d+:)?(.*$)?",
Pattern.MULTILINE);
Pattern progressPattern = Pattern.compile("^\\[(\\d+) of (\\d+)\\]");
while (processOut.hasNext() || oneBehind) {
if (oneBehind) {
oneBehind = false;
} else {
line = processOut.next();
}
// See comment after this method for example warning message.
Matcher matcher = compiledPattern.matcher(line);
Matcher progressMatcher = progressPattern.matcher(line);
if (line.startsWith(warningPrefix)) {
// Cabal warnings.
String text = line.substring(warningPrefix.length()) + SystemUtil.LINE_SEPARATOR + processOut.next();
//noinspection ObjectAllocationInLoop
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.WARNING, text));
} else if (line.startsWith(cabalPrefix)) {
// Unknown cabal messages. Exit code will tell if they were
// errors. Just forward to user.
String text = line.substring(cabalPrefix.length()) + SystemUtil.LINE_SEPARATOR + processOut.next();
//noinspection ObjectAllocationInLoop
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.WARNING, text));
} else if (matcher.find()) {
// GHC Messages
String file = matcher.group(1);
long lineNum = Long.parseLong(matcher.group(2));
// We might not have a column, but if we do it ends with a colon.
// Make sure to filter that colon out.
long colNum = matcher.group(3) == null ? 0 :
Long.parseLong(matcher.group(3).substring(0, matcher.group(3).length() - 1));
msg.setLength(0);
msg.append(matcher.group(4));
while (processOut.hasNext()) {
line = processOut.next();
if (line.endsWith("warning generated.") ||
line.trim().length() == 0) {
break;
}
if (line.startsWith("[") || line.startsWith("In-place")) {
// Fresh line starting, save to process next.
oneBehind = true;
break;
}
msg.append(line).append(SystemUtil.LINE_SEPARATOR);
}
// RootPath necessary for reasonable error messages by Intellij.
String sourcePath = getContentRootPath(module) + File.separator + file.replace('\\', File.separatorChar);
// GHC emits warning messages that look like:
// File.hs:line:column: Warning..
// Messages that do not start with "Warning" are errors. Other
// tools (happy for example) do not emit columns and messages
// not starting with "Warning" can still be warnings. Assume
// tools without column information warn.
//
// If our assumption is wrong that will be corrected by the
// return value from cabal build. If we assume the opposite we
// will halt working builds with a (faulty) error.
BuildMessage.Kind kind = matcher.group(4).trim().startsWith("Warning") ||
matcher.group(3) == null ? BuildMessage.Kind.WARNING : BuildMessage.Kind.ERROR;
final String trimmedMessage = msg.toString().trim();
if (trimmedMessage.isEmpty()) { // DEBUG code for #31.
//noinspection ObjectAllocationInLoop
context.processMessage(new CompilerMessage(
"ghc",
BuildMessage.Kind.WARNING,
"INTERNAL HaskForce ERROR: Got an empty compiler message from:\n" + msg + "\nWith line:\n" + line + '\n',
sourcePath,
-1L, -1L, -1L,
lineNum, colNum));
} else {
//noinspection ObjectAllocationInLoop
context.processMessage(new CompilerMessage(
"ghc",
kind,
trimmedMessage,
sourcePath,
-1L, -1L, -1L,
lineNum, colNum));
}
} else if (logAll) {
//noinspection ObjectAllocationInLoop
context.processMessage(new CompilerMessage("cabal", BuildMessage.Kind.INFO, processOut.next()));
} else if (progressMatcher.find()) {
// Update progress tracker. Seems pretty coarse-grained.
int fileNum = Integer.parseInt(progressMatcher.group(1));
int totalFiles = Integer.parseInt(progressMatcher.group(2));
if (totalFiles != 0) {
context.setDone((float) fileNum / totalFiles);
}
}
if (context.getCancelStatus().isCanceled()) {
LOG.info("Build cancelled, terminating..");
process.destroy();
break;
}
}
}
/*
Path warning:
cabal: The program 'happy' version >=1.17 is required but it could not be found
Example warning:
Preprocessing library feldspar-language-0.6.1.0...
[74 of 92] Compiling Feldspar.Core.UntypedRepresentation ( src/Feldspar/Core/UntypedRepresentation.hs, dist/build/Feldspar/Core/UntypedRepresentation.o )
src/Feldspar/Core/UntypedRepresentation.hs:483:5: Warning:
Pattern match(es) are overlapped
In an equation for `typeof': typeof e = ...
[74 of 92] Compiling Feldspar.Core.UntypedRepresentation ( src/Feldspar/Core/UntypedRepresentation.hs, dist/build/Feldspar/Core/UntypedRepresentation.p_o )
<same warning again>
Example errors:
src/Main.hs:14:3:
Could not deduce (ToJSON (ModulePragma l0))
arising from a use of `toJSON'
from the context (ToJSON l0)
bound by the instance declaration at src/Main.hs:14:3-38
Possible fix:
add an instance declaration for (ToJSON (ModulePragma l0))
In the third argument of `vector-0.10.9.1:Data.Vector.Mutable.unsafeWrite', namely
`toJSON arg3_a4Di'
In a stmt of a 'do' block:
vector-0.10.9.1:Data.Vector.Mutable.unsafeWrite
mv_a4Dn 2 (toJSON arg3_a4Di)
In the first argument of `vector-0.10.9.1:Data.Vector.create', namely
`do { mv_a4Dn <- vector-0.10.9.1:Data.Vector.Mutable.unsafeNew 7;
vector-0.10.9.1:Data.Vector.Mutable.unsafeWrite
mv_a4Dn 0 (toJSON arg1_a4Dg);
vector-0.10.9.1:Data.Vector.Mutable.unsafeWrite
mv_a4Dn 1 (toJSON arg2_a4Dh);
vector-0.10.9.1:Data.Vector.Mutable.unsafeWrite
mv_a4Dn 2 (toJSON arg3_a4Di);
.... }'
src/Main.hs..
Happy error:
Preprocessing library haskell-src-exts-1.15.0.1...
src/Language/Haskell/Exts/InternalParser.ly:962: Parse error
Nothing to do error:
Peters-MacBook-Air-472:haskell-src-exts pj$ cabal build -j5
Building haskell-src-exts-1.15.0.1...
Preprocessing library haskell-src-exts-1.15.0.1...
In-place registering haskell-src-exts-1.15.0.1...
Preprocessing test suite 'test' for haskell-src-exts-1.15.0.1...
Peters-MacBook-Air-472:haskell-src-exts pj$
*/
private static Iterator<String> collectOutput(Process process) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
return new Iterator<String>() {
String line = null;
@Override
public boolean hasNext() {
return fetch() != null;
}
private String fetch() {
if (line == null) {
try {
line = reader.readLine();
} catch (IOException e) {
e.printStackTrace();
}
}
return line;
}
@Override
public String next() throws NoSuchElementException {
String result = fetch();
line = null;
return result;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Searches for the cabal file in the module top directory.
*/
private static File getCabalFile(JpsModule module) {
String pathname = getContentRootPath(module);
//noinspection ConstantConditions
for (File file : new File(pathname).listFiles()) {
if (file.getName().endsWith(".cabal")) {
return file;
}
}
return null;
}
private static String getContentRootPath(JpsModule module) {
String url = module.getContentRootsList().getUrls().get(0);
return url.substring("file://".length());
}
/**
* Reports that we can compile hs and lhs files.
*/
@Override
public List<String> getCompilableFileExtensions() {
return Arrays.asList("hs", "lhs");
}
@Override
public String toString() {
return getPresentableName();
}
@NotNull
public String getPresentableName() {
return "Cabal builder";
}
}