/* * 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.event.ConsoleEvent; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.log.Logger; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.StepExecutionResult; import com.facebook.buck.util.Console; import com.facebook.buck.util.DefaultProcessExecutor; import com.facebook.buck.util.Escaper; import com.facebook.buck.util.MoreThrowables; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProcessExecutorParams; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; /** A step that preprocesses and/or compiles C/C++ sources in a single step. */ public class CxxPreprocessAndCompileStep implements Step { private static final Logger LOG = Logger.get(CxxPreprocessAndCompileStep.class); private final BuildTarget target; private final ProjectFilesystem filesystem; private final Operation operation; private final Path output; private final Optional<Path> depFile; private final Path input; private final CxxSource.Type inputType; private final ToolCommand command; private final HeaderPathNormalizer headerPathNormalizer; private final DebugPathSanitizer sanitizer; private final Compiler compiler; /** Directory to use to store intermediate/temp files used for compilation. */ private final Path scratchDir; private final boolean useArgfile; private static final FileLastModifiedDateContentsScrubber FILE_LAST_MODIFIED_DATE_SCRUBBER = new FileLastModifiedDateContentsScrubber(); public CxxPreprocessAndCompileStep( BuildTarget target, ProjectFilesystem filesystem, Operation operation, Path output, Optional<Path> depFile, Path input, CxxSource.Type inputType, ToolCommand command, HeaderPathNormalizer headerPathNormalizer, DebugPathSanitizer sanitizer, Path scratchDir, boolean useArgfile, Compiler compiler) { this.target = target; this.filesystem = filesystem; this.operation = operation; this.output = output; this.depFile = depFile; this.input = input; this.inputType = inputType; this.command = command; this.headerPathNormalizer = headerPathNormalizer; this.sanitizer = sanitizer.withProjectFilesystem(filesystem); this.scratchDir = scratchDir; this.useArgfile = useArgfile; this.compiler = compiler; } @Override public String getShortName() { return inputType.getLanguage() + " " + operation.toString().toLowerCase(); } /** * Apply common settings for our subprocesses. * * @return Half-configured ProcessExecutorParams.Builder */ private ProcessExecutorParams.Builder makeSubprocessBuilder(ExecutionContext context) { Map<String, String> env = new HashMap<>(context.getEnvironment()); env.putAll( sanitizer.getCompilationEnvironment( filesystem.getRootPath().toAbsolutePath(), shouldSanitizeOutputBinary())); // Set `TMPDIR` to `scratchDir` so the compiler/preprocessor uses this dir for it's temp and // intermediate files. env.put("TMPDIR", filesystem.resolve(scratchDir).toString()); // Add some diagnostic strings into the subprocess's env as well. // Note: the current process's env already contains `BUCK_BUILD_ID`, which will be inherited. env.put("BUCK_BUILD_TARGET", target.toString()); return ProcessExecutorParams.builder() .setDirectory(filesystem.getRootPath().toAbsolutePath()) .setRedirectError(ProcessBuilder.Redirect.PIPE) .setEnvironment(ImmutableMap.copyOf(env)); } private Path getArgfile() { return filesystem.resolve(scratchDir).resolve("ppandcompile.argsfile"); } @VisibleForTesting ImmutableList<String> getArguments(boolean allowColorsInDiagnostics) { String inputLanguage = operation == Operation.GENERATE_PCH ? inputType.getPrecompiledHeaderLanguage().get() : inputType.getLanguage(); return ImmutableList.<String>builder() .addAll(command.getArguments()) .addAll( (allowColorsInDiagnostics ? compiler.getFlagsForColorDiagnostics() : Optional.<ImmutableList<String>>empty()) .orElseGet(ImmutableList::of)) .addAll(compiler.languageArgs(inputLanguage)) .addAll(sanitizer.getCompilationFlags()) .add("-c") .addAll( depFile .map(depFile -> compiler.outputDependenciesArgs(depFile.toString())) .orElseGet(ImmutableList::of)) .add(input.toString()) .addAll(compiler.outputArgs(output.toString())) .build(); } private int executeCompilation(ExecutionContext context) throws Exception { ProcessExecutorParams.Builder builder = makeSubprocessBuilder(context); if (useArgfile) { filesystem.writeLinesToPath( Iterables.transform( getArguments(context.getAnsi().isAnsiTerminal()), Escaper.ARGFILE_ESCAPER), getArgfile()); builder.setCommand( ImmutableList.<String>builder() .addAll(command.getCommandPrefix()) .add("@" + getArgfile()) .build()); } else { builder.setCommand( ImmutableList.<String>builder() .addAll(command.getCommandPrefix()) .addAll(getArguments(context.getAnsi().isAnsiTerminal())) .build()); } ProcessExecutorParams params = builder.build(); LOG.debug("Running command (pwd=%s): %s", params.getDirectory(), getDescription(context)); // Start the process. ProcessExecutor executor = new DefaultProcessExecutor(Console.createNullConsole()); ProcessExecutor.LaunchedProcess process = executor.launchProcess(params); // We buffer error messages in memory, as these are typically small. String err; int exitCode; try (BufferedReader reader = new BufferedReader(new InputStreamReader(compiler.getErrorStream(process)))) { CxxErrorTransformer cxxErrorTransformer = new CxxErrorTransformer( filesystem, context.shouldReportAbsolutePaths(), headerPathNormalizer); err = reader.lines().map(cxxErrorTransformer::transformLine).collect(Collectors.joining("\n")); exitCode = executor.waitForLaunchedProcess(process).getExitCode(); } catch (UncheckedIOException e) { throw e.getCause(); } finally { executor.destroyLaunchedProcess(process); executor.waitForLaunchedProcess(process); } // If we generated any error output, print that to the console. if (!err.isEmpty()) { context .getBuckEventBus() .post( createConsoleEvent( context, compiler.getFlagsForColorDiagnostics().isPresent(), exitCode == 0 ? Level.WARNING : Level.SEVERE, err)); } return exitCode; } private ConsoleEvent createConsoleEvent( ExecutionContext context, boolean commandOutputsColor, Level level, String message) { if (context.getAnsi().isAnsiTerminal() && commandOutputsColor) { return ConsoleEvent.createForMessageWithAnsiEscapeCodes(level, message); } else { return ConsoleEvent.create(level, message); } } @Override public StepExecutionResult execute(ExecutionContext context) throws InterruptedException { try { LOG.debug("%s %s -> %s", operation.toString().toLowerCase(), input, output); int exitCode = executeCompilation(context); // If the compilation completed successfully and we didn't effect debug-info normalization // through #line directive modification, perform the in-place update of the compilation per // above. This locates the relevant debug section and swaps out the expanded actual // compilation directory with the one we really want. if (exitCode == 0 && shouldSanitizeOutputBinary()) { try { Path path = filesystem.getRootPath().toAbsolutePath().resolve(output); sanitizer.restoreCompilationDirectory(path, filesystem.getRootPath().toAbsolutePath()); FILE_LAST_MODIFIED_DATE_SCRUBBER.scrubFileWithPath(path); } catch (IOException e) { context.logError(e, "error updating compilation directory"); return StepExecutionResult.ERROR; } } if (exitCode != 0) { LOG.warn("error %d %s %s", exitCode, operation.toString().toLowerCase(), input); } return StepExecutionResult.of(exitCode); } catch (Exception e) { MoreThrowables.propagateIfInterrupt(e); context.logError(e, "Build error caused by exception"); return StepExecutionResult.ERROR; } } ImmutableList<String> getCommand() { // We set allowColorsInDiagnostics to false here because this function is only used by the // compilation database (its contents should not depend on how Buck was invoked) and in the // step's description. It is not used to determine what command this step runs, which needs // to decide whether to use colors or not based on whether the terminal supports them. return ImmutableList.<String>builder() .addAll(command.getCommandPrefix()) .addAll(getArguments(false)) .build(); } @Override public String getDescription(ExecutionContext context) { if (context.getVerbosity().shouldPrintCommand()) { return Stream.concat(command.getCommandPrefix().stream(), getArguments(false).stream()) .map(Escaper.SHELL_ESCAPER::apply) .collect(Collectors.joining(" ")); } return "(verbosity level disables command output)"; } private boolean shouldSanitizeOutputBinary() { return inputType.isAssembly() || (operation == Operation.PREPROCESS_AND_COMPILE && compiler.shouldSanitizeOutputBinary()); } public enum Operation { /** Run only the compiler on source files. */ COMPILE, /** Run the preprocessor and compiler on source files. */ PREPROCESS_AND_COMPILE, GENERATE_PCH, ; } public static class ToolCommand { private final ImmutableList<String> commandPrefix; private final ImmutableList<String> arguments; private final ImmutableMap<String, String> environment; public ToolCommand( ImmutableList<String> commandPrefix, ImmutableList<String> arguments, ImmutableMap<String, String> environment) { this.commandPrefix = commandPrefix; this.arguments = arguments; this.environment = environment; } public ImmutableList<String> getCommandPrefix() { return commandPrefix; } public ImmutableList<String> getArguments() { return arguments; } public ImmutableMap<String, String> getEnvironment() { return environment; } } }