blob: 5a3744e02dd775385310b827946721b12b932938 [file] [log] [blame]
/*
* Copyright 2014-present Facebook, Inc.
*
* 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.facebook.buck.cxx;
import com.facebook.buck.log.Logger;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.util.Escaper;
import com.facebook.buck.util.FunctionLineProcessorThread;
import com.facebook.buck.util.MoreIterables;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.io.ByteArrayOutputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A step that preprocesses C/C++ sources.
*/
public class CxxPreprocessStep implements Step {
private static final Logger LOG = Logger.get(CxxCompileStep.class);
private final ImmutableList<String> preprocessor;
private final ImmutableList<String> flags;
private final Path output;
private final Path input;
private final ImmutableList<Path> prefixHeaders;
private final ImmutableList<Path> includes;
private final ImmutableList<Path> systemIncludes;
private final ImmutableList<Path> frameworkRoots;
private final ImmutableMap<Path, Path> replacementPaths;
private final Optional<DebugPathSanitizer> sanitizer;
public CxxPreprocessStep(
ImmutableList<String> preprocessor,
ImmutableList<String> flags,
Path output,
Path input,
ImmutableList<Path> prefixHeaders,
ImmutableList<Path> includes,
ImmutableList<Path> systemIncludes,
ImmutableList<Path> frameworkRoots,
ImmutableMap<Path, Path> replacementPaths,
Optional<DebugPathSanitizer> sanitizer) {
this.preprocessor = preprocessor;
this.flags = flags;
this.output = output;
this.input = input;
this.prefixHeaders = prefixHeaders;
this.includes = includes;
this.systemIncludes = systemIncludes;
this.frameworkRoots = frameworkRoots;
this.replacementPaths = replacementPaths;
this.sanitizer = sanitizer;
}
@Override
public String getShortName() {
return "c++ preprocess";
}
@VisibleForTesting
protected ImmutableList<String> getCommand() {
return ImmutableList.<String>builder()
.addAll(preprocessor)
.add("-E")
.addAll(flags)
.addAll(
MoreIterables.zipAndConcat(
Iterables.cycle("-include"),
Iterables.transform(prefixHeaders, Functions.toStringFunction())))
.addAll(
MoreIterables.zipAndConcat(
Iterables.cycle("-I"),
Iterables.transform(includes, Functions.toStringFunction())))
.addAll(
MoreIterables.zipAndConcat(
Iterables.cycle("-isystem"),
Iterables.transform(systemIncludes, Functions.toStringFunction())))
.addAll(
MoreIterables.zipAndConcat(
Iterables.cycle("-F"),
Iterables.transform(frameworkRoots, Functions.toStringFunction())))
.add(input.toString())
.build();
}
@VisibleForTesting
protected static Function<String, String> createOutputLineProcessor(
final Path workingDir,
final ImmutableMap<Path, Path> replacementPaths,
final Optional<DebugPathSanitizer> sanitizer) {
return new Function<String, String>() {
private final Pattern lineMarkers =
Pattern.compile("^# (?<num>\\d+) \"(?<path>[^\"]+)\"(?<rest>.*)?$");
@Override
public String apply(String line) {
if (line.startsWith("# ")) {
Matcher m = lineMarkers.matcher(line);
if (m.find()) {
String originalPath = m.group("path");
String replacementPath = originalPath;
replacementPath = Optional
.fromNullable(replacementPaths.get(Paths.get(replacementPath)))
.transform(Functions.toStringFunction())
.or(replacementPath);
if (sanitizer.isPresent()) {
replacementPath = sanitizer.get().sanitize(Optional.of(workingDir), replacementPath);
}
if (!originalPath.equals(replacementPath)) {
String num = m.group("num");
String rest = m.group("rest");
return "# " + num + " \"" + replacementPath + "\"" + rest;
}
}
}
return line;
}
};
}
@VisibleForTesting
protected Function<String, String> createErrorLineProcessor() {
return CxxDescriptionEnhancer.createErrorMessagePathProcessor(
new Function<String, String>() {
@Override
public String apply(String path) {
Path replacement = replacementPaths.get(Paths.get(path));
return replacement != null ? replacement.toString() : path;
}
});
}
@Override
@SuppressWarnings("PMD.EmptyTryBlock")
public int execute(ExecutionContext context) throws InterruptedException {
LOG.debug("Preprocessing %s -> %s", input, output);
ProcessBuilder builder = new ProcessBuilder();
builder.command(getCommand());
builder.directory(context.getProjectDirectoryRoot().toAbsolutePath().toFile());
builder.redirectOutput(ProcessBuilder.Redirect.PIPE);
builder.redirectError(ProcessBuilder.Redirect.PIPE);
Path outputPath = context.getProjectFilesystem().resolve(this.output);
Path outputTempPath = context.getProjectFilesystem().resolve(this.output + ".tmp");
try {
LOG.debug(
"Running command (pwd=%s): %s",
builder.directory(),
Joiner.on(' ').join(Iterables.transform(builder.command(), Escaper.BASH_ESCAPER)));
// Start the process.
Process process = builder.start();
// We buffer error messages in memory, as these are typically small.
ByteArrayOutputStream error = new ByteArrayOutputStream();
// Open the temp file to write the intermediate output to and also fire up managed threads
// to process the stdout and stderr lines from the preprocess command.
try (OutputStream output = Files.newOutputStream(outputTempPath);
FunctionLineProcessorThread outputProcessor =
new FunctionLineProcessorThread(
process.getInputStream(),
output,
createOutputLineProcessor(
context.getProjectDirectoryRoot(),
replacementPaths,
sanitizer));
FunctionLineProcessorThread errorProcessor =
new FunctionLineProcessorThread(
process.getErrorStream(),
error,
createErrorLineProcessor())) {
outputProcessor.start();
errorProcessor.start();
}
// Wait for the process to finish, and grab it's exit code.
int exitCode = process.waitFor();
// If the process finished successfully, move the preprocessed output into it's final place.
if (exitCode == 0) {
Files.move(
outputTempPath,
outputPath,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
}
// If we generated any error output, print that to the console.
String err = new String(error.toByteArray());
if (!err.isEmpty()) {
context.getConsole().printErrorText(err);
}
if (exitCode != 0) {
LOG.warn("Error %d preprocessing %s: %s", exitCode, input, err);
}
return exitCode;
} catch (InterruptedException | InterruptedIOException e) {
throw new InterruptedException();
} catch (Exception e) {
context.getConsole().printBuildFailureWithStacktrace(e);
return 1;
}
}
@Override
public String getDescription(ExecutionContext context) {
return Joiner.on(' ').join(
FluentIterable.from(getCommand())
.transform(Escaper.BASH_ESCAPER));
}
}