/** * checkProgram invokes the Haskell compiler on a given file and reports the output. * * @param pathToProgramFile Specifies the file or folder containing that should be compiled. * (accepts .lhs and .hs files) * @param compilerName The compiler to be used (usually ghc). * @param compilerFlags Additional flags to be passed to the compiler. * @throws FileNotFoundException Is thrown when the file in pathToProgramFile cannot be opened * @throws BadCompilerSpecifiedException Is thrown when the given compiler cannot be called * @return A {@link CompilerOutput} that contains all compiler messages and flags on how the * compile run went. * @throws BadFlagException When ghc doesn't recognize a flag, this exception is thrown. */ @Override public CompilerOutput checkProgram( Path pathToProgramFile, String compilerName, List<String> compilerFlags) throws FileNotFoundException, BadCompilerSpecifiedException, BadFlagException { Process compilerProcess = null; try { // create compiler invocation. List<String> compilerInvocation = createCompilerInvocation(pathToProgramFile, compilerName, compilerFlags); ProcessBuilder compilerProcessBuilder = new ProcessBuilder(compilerInvocation); // make sure the compiler stays in its directory. compilerProcessBuilder.directory(pathToProgramFile.getParent().toFile()); compilerProcess = compilerProcessBuilder.start(); // this will never happen because createCompilerInvocation never // throws this Exception. Throw declaration needs to be in method // declaration because of the implemented Interface although we // never use it in the HaskellCompileChecker } catch (CompilerOutputFolderExistsException e) { LOGGER.severe( "A problem while compiling, which never should happen, occured" + e.getMessage()); } catch (BadCompilerSpecifiedException e) { throw new BadCompilerSpecifiedException(e.getMessage()); } catch (IOException e) { // If we cannot call the compiler we return a CompilerOutput // initialized with false, false, indicating // that the compiler wasn't invoked properly and that there was no // clean Compile. CompilerOutput compilerInvokeError = new CompilerOutput(); compilerInvokeError.setClean(false); compilerInvokeError.setCompilerInvoked(false); return compilerInvokeError; } // Now we read compiler output. If everything is ok ghc reports // nothing in the errorStream. InputStream compilerOutputStream = compilerProcess.getErrorStream(); InputStreamReader compilerStreamReader = new InputStreamReader(compilerOutputStream); BufferedReader compilerOutputBuffer = new BufferedReader(compilerStreamReader); String line; CompilerOutput compilerOutput = new CompilerOutput(); compilerOutput.setCompilerInvoked(true); List<String> compilerOutputLines = new LinkedList<>(); try { while ((line = compilerOutputBuffer.readLine()) != null) { compilerOutputLines.add(line); } // Errors are separated via an empty line (""). But after the // the last error the OutputBuffer has nothing more to write. // In order to recognize the last error we insert an empty String // at the end of the list. // Only needs to be done when there are errors. if (compilerOutputLines.size() != 0) { line = ""; compilerOutputLines.add(line); } compilerOutputStream.close(); compilerStreamReader.close(); compilerOutputBuffer.close(); compilerProcess.destroy(); } catch (IOException e) { // Reading might go wrong here if ghc should unexpectedly die LOGGER.severe("Error while reading from compiler stream."); compilerOutput.setClean(false); compilerOutput.setCompileStreamBroken(true); return compilerOutput; } // ghc -c generates a .o(object) and a .hi(haskell interface) file. // But we don't need those files so they can be deleted. // The generated files have the same name like our input file so we // can just exchange the file endings in order to get the // correct file paths for deletion if (Files.isDirectory(pathToProgramFile, LinkOption.NOFOLLOW_LINKS)) { // we use a file walker in order to find all files in the folder // and its subfolders RegexDirectoryWalker dirWalker = new RegexDirectoryWalker(".+\\.([Ll])?[Hh][Ss]"); try { Files.walkFileTree(pathToProgramFile, dirWalker); } catch (IOException e) { LOGGER.severe( "Could not walk submission " + pathToProgramFile.toString() + " while building copiler invocation: " + e.getMessage()); } for (Path candidatePath : dirWalker.getFoundFiles()) { File candidateFile = candidatePath.toFile(); if (!candidateFile.isDirectory()) { String extension = FilenameUtils.getExtension(candidateFile.toString()); if (extension.matches("[Ll]?[Hh][Ss]")) { File ghcGeneratedObject = new File(FilenameUtils.removeExtension(candidateFile.toString()) + ".o"); File ghcGeneratedInterface = new File(FilenameUtils.removeExtension(candidateFile.toString()) + ".hi"); ghcGeneratedObject.delete(); ghcGeneratedInterface.delete(); } } } } else { String extension = FilenameUtils.getExtension(pathToProgramFile.toString()); if (extension.matches("[Ll]?[Hh][Ss]")) { File ghcGeneratedObject = new File(FilenameUtils.removeExtension(pathToProgramFile.toString()) + ".o"); File ghcGeneratedInterface = new File(FilenameUtils.removeExtension(pathToProgramFile.toString()) + ".hi"); ghcGeneratedObject.delete(); ghcGeneratedInterface.delete(); } } // if there are no errors there is no Output to handle if (compilerOutputLines.size() != 0) { compilerOutput = splitCompilerOutput(compilerOutputLines, compilerOutput); } else { compilerOutput.setClean(true); } return compilerOutput; }
/** * This Method generates the command required to start the compiler. It generates a list of * strings that can be passed to a process builder. * * @param pathToProgramFile Where to look for the main file that will be compiled. * @param compilerName Which compiler to call * @param compilerFlags User supplied flags to be passed * @return List of string with the command for the process builder. * @throws BadCompilerSpecifiedException When no compiler is given. * @throws FileNotFoundException When the file to be compiled does not exist * @throws CompilerOutputFolderExistsException Due to slightly uncompatible * CompileCheckerInterface this exception is in the declaration but it is never thrown. * JavaCompileChecker uses this exception. */ private List<String> createCompilerInvocation( Path pathToProgramFile, String compilerName, List<String> compilerFlags) throws BadCompilerSpecifiedException, FileNotFoundException, CompilerOutputFolderExistsException { List<String> compilerInvocation = new LinkedList<>(); // We need a compiler name. Without it we cannot compile anything and // abort. if (("".equals(compilerName)) || (compilerName == null)) { throw new BadCompilerSpecifiedException("No compiler specified."); } else { compilerInvocation.add(compilerName); } // If compiler flags are passed, append them after the compiler name. // If we didn't get any we append nothing. if ((compilerFlags != null) && (!(compilerFlags.isEmpty()))) { compilerInvocation.addAll(compilerFlags); } // now we tell ghc to stop after compilation because we just want to // see if there are syntax errors in the code compilerInvocation.add("-c"); // Check for the existence of the program file we are trying to // compile. if ((pathToProgramFile == null) || (pathToProgramFile.compareTo(Paths.get("")) == 0)) { throw new FileNotFoundException("No file to compile specified"); } else { if (Files.isDirectory(pathToProgramFile, LinkOption.NOFOLLOW_LINKS)) { // we are supposed to compile a folder. Hence we'll scan for // lhs files and pass them to the compiler. RegexDirectoryWalker dirWalker = new RegexDirectoryWalker(".+\\.([Ll])?[Hh][Ss]"); try { Files.walkFileTree(pathToProgramFile, dirWalker); } catch (IOException e) { LOGGER.severe( "Could not walk submission " + pathToProgramFile.toString() + " while building compiler invocation: " + e.getMessage()); } for (Path matchedFile : dirWalker.getFoundFiles()) { compilerInvocation.add(matchedFile.toFile().getAbsolutePath()); } } else if (Files.exists(pathToProgramFile, LinkOption.NOFOLLOW_LINKS)) { // if the file exists, just pass the file name, since the // compiler will // be confined to the directory the file is in a few lines // down. compilerInvocation.add(pathToProgramFile.toString()); } else { throw new FileNotFoundException( "Program file that should be compiled does not exist." + "Filename : \"" + pathToProgramFile.toString() + "\""); } } return compilerInvocation; }