| /* |
| * 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)); |
| } |
| |
| } |