* The direct mode fails to pick up new header files in some rare scenarios. See
<<_the_direct_mode,THE DIRECT MODE>> above.
-* When run via ccache, warning messages produced by GCC 4.9 and newer will only
- be colored when the environment variable *GCC_COLORS* is set. An alternative
- to setting *GCC_COLORS* is to pass `-fdiagnostics-color` explicitly when
- compiling (but then color codes will also be present when redirecting stderr
- to a file).
-* If ccache guesses that the compiler may emit colored warnings, then a
- compilation with stderr referring to a TTY will be considered different from
- a compilation with a redirected stderr, thus not sharing cache entries. This
- happens for Clang by default and for GCC when *GCC_COLORS* is set as
- mentioned above. If you want to share cache hits, you can pass
- `-f[no-]diagnostics-color` (GCC) or `-f[no-]color-diagnostics` (Clang)
- explicitly when compiling (but then color codes will be either on or off for
- both the TTY and the redirected case).
Troubleshooting
- Added a new inode cache for file hashes, allowing computed hash values to be
reused both within and between builds.
+- Compiler diagnostic messages are now always cached in color, and ccache
+ strips the color codes on the fly when requested explicitly by command-line
+ option or when stderr does not refer to a TTY. This allows IDEs and terminals
+ to share cached compilation results.
+
- (More to be written.)
// (--serialize-diagnostics)?
bool generating_diagnostics = false;
+ // Whether to strip color codes from diagnostic messages on output.
+ bool strip_diagnostics_colors = false;
+
// Have we seen -gsplit-dwarf?
bool seen_split_dwarf = false;
// Uses absolute path for some include files.
bool has_absolute_include_headers = false;
+ // Have we tried and failed to get colored diagnostics?
+ bool diagnostics_color_failed = false;
+
// The name of the temporary preprocessed file.
std::string i_tmpfile;
#include <algorithm>
#include <fstream>
+#include <regex>
#ifdef HAVE_LINUX_FS_H
# include <linux/magic.h>
}
}
+std::string
+edit_ansi_csi_seqs(string_view string, const SubstringEditor& editor)
+{
+ static const std::regex csi_regex(
+ "\x1B\\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]");
+ std::string ret, substr;
+ ret.reserve(string.size());
+ for (std::cregex_token_iterator itr(
+ string.begin(), string.end(), csi_regex, {-1, 0});
+ itr != std::cregex_token_iterator{};
+ ++itr) {
+ ret.append(itr->first, itr->second);
+ if (++itr == std::cregex_token_iterator{}) {
+ break;
+ }
+ substr.assign(itr->first, itr->second);
+ editor(itr->first - string.begin(), substr);
+ ret.append(substr);
+ }
+ return ret;
+}
+
bool
ends_with(string_view string, string_view suffix)
{
return string.starts_with(prefix);
}
+std::string
+strip_ansi_csi_seqs(string_view string, string_view strip_actions)
+{
+ return edit_ansi_csi_seqs(
+ string, [strip_actions](string_view::size_type, std::string& substr) {
+ if (strip_actions.find(substr.back()) != string_view::npos) {
+ substr.clear();
+ }
+ });
+}
+
std::string
strip_whitespace(const std::string& string)
{
const ProgressReceiver& /*progress_receiver*/)>;
using TraverseVisitor =
std::function<void(const std::string& path, bool is_dir)>;
+using SubstringEditor =
+ std::function<void(nonstd::string_view::size_type pos, std::string& substr)>;
enum class UnlinkLog { log_failure, ignore_failure };
// Get directory name of path.
nonstd::string_view dir_name(nonstd::string_view path);
+// Returns a copy of string with any contained ANSI CSI sequences edited by the
+// given SubstringEditor, which is invoked once for each ANSI CSI sequence
+// encountered in string. The original string is not modified.
+[[gnu::warn_unused_result]] std::string
+edit_ansi_csi_seqs(nonstd::string_view string, const SubstringEditor& editor);
+
// Return true if suffix is a suffix of string.
bool ends_with(nonstd::string_view string, nonstd::string_view suffix);
// Return true if prefix is a prefix of string.
bool starts_with(nonstd::string_view string, nonstd::string_view prefix);
+// Returns a copy of string with the specified ANSI CSI sequences removed.
+[[gnu::warn_unused_result]] std::string
+strip_ansi_csi_seqs(nonstd::string_view string,
+ nonstd::string_view strip_actions = "Km");
+
// Strip whitespace from left and right side of a string.
[[gnu::warn_unused_result]] std::string
strip_whitespace(const std::string& string);
namespace {
+enum class ColorDiagnostics : int8_t { automatic, always, never = -1 };
+
struct ArgumentProcessingState
{
bool found_c_opt = false;
bool found_S_opt = false;
bool found_pch = false;
bool found_fpch_preprocess = false;
- bool found_color_diagnostics = false;
+ ColorDiagnostics color_diagnostics = ColorDiagnostics::automatic;
bool found_directives_only = false;
bool found_rewrite_includes = false;
return nullopt;
}
- if (args[i] == "-fcolor-diagnostics" || args[i] == "-fno-color-diagnostics"
- || args[i] == "-fdiagnostics-color"
- || args[i] == "-fdiagnostics-color=always"
- || args[i] == "-fno-diagnostics-color"
+ if (args[i] == "-fcolor-diagnostics" || args[i] == "-fdiagnostics-color"
+ || args[i] == "-fdiagnostics-color=always") {
+ state.color_diagnostics = ColorDiagnostics::always;
+ return nullopt;
+ }
+ if (args[i] == "-fno-color-diagnostics" || args[i] == "-fno-diagnostics-color"
|| args[i] == "-fdiagnostics-color=never") {
- state.common_args.push_back(args[i]);
- state.found_color_diagnostics = true;
+ state.color_diagnostics = ColorDiagnostics::never;
return nullopt;
}
-
if (args[i] == "-fdiagnostics-color=auto") {
- if (color_output_possible()) {
- // Output is redirected, so color output must be forced.
- state.common_args.push_back("-fdiagnostics-color=always");
- add_extra_arg(ctx, "-fdiagnostics-color=always");
- cc_log("Automatically forcing colors");
- } else {
- state.common_args.push_back(args[i]);
- }
- state.found_color_diagnostics = true;
+ state.color_diagnostics = ColorDiagnostics::automatic;
return nullopt;
}
state.cpp_args.push_back(state.explicit_language);
}
+ args_info.strip_diagnostics_colors =
+ state.color_diagnostics != ColorDiagnostics::automatic
+ ? state.color_diagnostics == ColorDiagnostics::never
+ : !color_output_possible();
// Since output is redirected, compilers will not color their output by
- // default, so force it explicitly if it would be otherwise done.
- if (!state.found_color_diagnostics && color_output_possible()) {
- if (ctx.guessed_compiler == GuessedCompiler::clang) {
- if (args_info.actual_language != "assembler") {
- state.common_args.push_back("-fcolor-diagnostics");
- add_extra_arg(ctx, "-fcolor-diagnostics");
- cc_log("Automatically enabling colors");
- }
- } else if (ctx.guessed_compiler == GuessedCompiler::gcc) {
- // GCC has it since 4.9, but that'd require detecting what GCC version is
- // used for the actual compile. However it requires also GCC_COLORS to be
- // set (and not empty), so use that for detecting if GCC would use colors.
- const char* gcc_colors = getenv("GCC_COLORS");
- if (gcc_colors && gcc_colors[0] != '\0') {
- state.common_args.push_back("-fdiagnostics-color");
- add_extra_arg(ctx, "-fdiagnostics-color");
- cc_log("Automatically enabling colors");
+ // default, so force it explicitly.
+ if (ctx.guessed_compiler == GuessedCompiler::clang) {
+ if (args_info.actual_language != "assembler") {
+ if (!config.run_second_cpp()) {
+ state.cpp_args.push_back("-fcolor-diagnostics");
}
+ state.compiler_only_args.push_back("-fcolor-diagnostics");
+ add_extra_arg(ctx, "-fcolor-diagnostics");
+ }
+ } else if (ctx.guessed_compiler == GuessedCompiler::gcc) {
+ if (!config.run_second_cpp()) {
+ state.cpp_args.push_back("-fdiagnostics-color");
}
+ state.compiler_only_args.push_back("-fdiagnostics-color");
+ add_extra_arg(ctx, "-fdiagnostics-color");
+ } else {
+ // Other compilers shouldn't output color, so no need to strip it.
+ args_info.strip_diagnostics_colors = false;
}
if (args_info.generating_dependencies) {
return d;
}
+// Execute the compiler/preprocessor, with logic to retry without requesting
+// colored diagnostics messages if that fails.
+static int
+execute(Context& ctx,
+ Args& args,
+ const std::string& stdout_path,
+ int stdout_fd,
+ const std::string& stderr_path,
+ int stderr_fd)
+{
+ if (ctx.diagnostics_color_failed
+ && ctx.guessed_compiler == GuessedCompiler::gcc) {
+ args.erase_with_prefix("-fdiagnostics-color");
+ }
+ int status =
+ execute(args.to_argv().data(), stdout_fd, stderr_fd, &ctx.compiler_pid);
+ if (status != 0 && !ctx.diagnostics_color_failed
+ && ctx.guessed_compiler == GuessedCompiler::gcc) {
+ auto errors = Util::read_file(stderr_path);
+ if (errors.find("unrecognized command-line option") != std::string::npos
+ && errors.find("-fdiagnostics-color") != std::string::npos) {
+ // Old versions of GCC did not support colored diagnostics.
+ cc_log("-fdiagnostics-color is unsupported; trying again without it");
+ if (ftruncate(stdout_fd, 0) < 0 || lseek(stdout_fd, 0, SEEK_SET) < 0) {
+ cc_log(
+ "Failed to truncate %s: %s", stdout_path.c_str(), strerror(errno));
+ failed(STATS_ERROR);
+ }
+ if (ftruncate(stderr_fd, 0) < 0 || lseek(stderr_fd, 0, SEEK_SET) < 0) {
+ cc_log(
+ "Failed to truncate %s: %s", stderr_path.c_str(), strerror(errno));
+ failed(STATS_ERROR);
+ }
+ ctx.diagnostics_color_failed = true;
+ return execute(ctx, args, stdout_path, stdout_fd, stderr_path, stderr_fd);
+ }
+ }
+ return status;
+}
+
// Send cached stderr, if any, to stderr.
static void
-send_cached_stderr(const char* path_stderr)
+send_cached_stderr(const std::string& path_stderr, bool strip_colors)
{
- int fd_stderr = open(path_stderr, O_RDONLY | O_BINARY);
- if (fd_stderr != -1) {
- copy_fd(fd_stderr, STDERR_FILENO);
- close(fd_stderr);
+ if (strip_colors) {
+ try {
+ auto stripped = Util::strip_ansi_csi_seqs(Util::read_file(path_stderr));
+ write_fd(STDERR_FILENO, stripped.data(), stripped.size());
+ } catch (const Error&) {
+ // Fall through
+ }
+ } else {
+ int fd_stderr = open(path_stderr.c_str(), O_RDONLY | O_BINARY);
+ if (fd_stderr != -1) {
+ copy_fd(fd_stderr, STDERR_FILENO);
+ close(fd_stderr);
+ }
}
}
int status;
if (!ctx.config.depend_mode()) {
- status = execute(
- args.to_argv().data(), tmp_stdout_fd, tmp_stderr_fd, &ctx.compiler_pid);
+ status =
+ execute(ctx, args, tmp_stdout, tmp_stdout_fd, tmp_stderr, tmp_stderr_fd);
args.pop_back(3);
} else {
// Use the original arguments (including dependency options) in depend
add_prefix(ctx, depend_mode_args, ctx.config.prefix_command());
ctx.time_of_compilation = time(nullptr);
- status = execute(depend_mode_args.to_argv().data(),
+ status = execute(ctx,
+ depend_mode_args,
+ tmp_stdout,
tmp_stdout_fd,
- tmp_stderr_fd,
- &ctx.compiler_pid);
+ tmp_stderr,
+ tmp_stderr_fd);
}
MTR_END("execute", "compiler");
if (status != 0) {
cc_log("Compiler gave exit status %d", status);
- int fd = open(tmp_stderr.c_str(), O_RDONLY | O_BINARY);
- if (fd != -1) {
- // We can output stderr immediately instead of rerunning the compiler.
- copy_fd(fd, STDERR_FILENO);
- close(fd);
- }
+ // We can output stderr immediately instead of rerunning the compiler.
+ send_cached_stderr(tmp_stderr, ctx.args_info.strip_diagnostics_colors);
failed(STATS_STATUS, status);
}
}
// Everything OK.
- send_cached_stderr(tmp_stderr.c_str());
+ send_cached_stderr(tmp_stderr, ctx.args_info.strip_diagnostics_colors);
}
// Find the result name by running the compiler in preprocessor mode and
add_prefix(ctx, args, ctx.config.prefix_command_cpp());
cc_log("Running preprocessor");
MTR_BEGIN("execute", "preprocessor");
- status =
- execute(args.to_argv().data(), stdout_fd, stderr_fd, &ctx.compiler_pid);
+ status = execute(ctx, args, stdout_path, stdout_fd, stderr_path, stderr_fd);
MTR_END("execute", "preprocessor");
args.pop_back(args_added);
}
MTR_END("file", "file_get");
- send_cached_stderr(tmp_stderr.c_str());
+ send_cached_stderr(tmp_stderr, ctx.args_info.strip_diagnostics_colors);
cc_log("Succeeded getting cached result");
}
expect_file_exists() {
- if [ ! -f "$1" ]; then
+ if [ ! -e "$1" ]; then
test_failed "Expected $1 to exist, but it's missing"
fi
}
expect_file_missing() {
- if [ -f "$1" ]; then
+ if [ -e "$1" ]; then
test_failed "Expected $1 to be missing, but it exists"
fi
}
local file="$1"
local content="$2"
- if [ ! -f "$file" ]; then
+ if [ ! -e "$file" ]; then
test_failed "$file not found"
fi
if [ "$(cat $file)" != "$content" ]; then
local file="$1"
local string="$2"
- if [ ! -f "$file" ]; then
+ if [ ! -e "$file" ]; then
test_failed "$file not found"
fi
- if ! grep -q "$string" "$file"; then
+ if ! fgrep -q "$string" "$file"; then
test_failed "File $file does not contain: $string. Actual content: $(cat $file)"
fi
}
+expect_file_not_contains() {
+ local file="$1"
+ local string="$2"
+
+ if [ ! -e "$file" ]; then
+ test_failed "$file not found"
+ fi
+ if fgrep -q "$string" "$file"; then
+ test_failed "File $file contains: $string. Actual content: $(cat $file)"
+ fi
+}
+
expect_file_count() {
local expected=$1
local pattern=$2
cpp1
multi_arch
serialize_diagnostics
+color_diagnostics
sanitize_blacklist
debug_prefix_map
profiling
--- /dev/null
+if $COMPILER_TYPE_GCC ; then
+ color_diagnostics_enable='-fdiagnostics-color'
+ color_diagnostics_disable='-fno-diagnostics-color'
+elif $COMPILER_TYPE_CLANG ; then
+ color_diagnostics_enable='-fcolor-diagnostics'
+ color_diagnostics_disable='-fno-color-diagnostics'
+fi
+
+SUITE_color_diagnostics_PROBE() {
+ # Probe that real compiler actually supports colored diagnostics.
+ if [[ ! $color_diagnostics_enable || ! $color_diagnostics_disable ]] ; then
+ echo "compiler $COMPILER does not support colored diagnostics"
+ elif ! $REAL_COMPILER $color_diagnostics_enable -E - </dev/null >/dev/null 2>&1 ; then
+ echo "compiler $COMPILER (version: $compiler_version) does not support $color_diagnostics_enable"
+ elif ! $REAL_COMPILER $color_diagnostics_disable -E - </dev/null >/dev/null 2>&1 ; then
+ echo "compiler $COMPILER (version: $compiler_version) does not support $color_diagnostics_disable"
+ fi
+}
+
+SUITE_color_diagnostics_SETUP() {
+ if $run_second_cpp ; then
+ export CCACHE_CPP2=1
+ else
+ export CCACHE_NOCPP2=1
+ fi
+
+ unset GCC_COLORS
+}
+
+color_diagnostics_expect_color() {
+ expect_file_contains "${1:?}" $'\033['
+ expect_file_contains <(fgrep 'previous prototype' "$1") $'\033['
+ expect_file_contains <(fgrep 'from preprocessor' "$1") $'\033['
+}
+
+color_diagnostics_expect_no_color() {
+ expect_file_not_contains "${1:?}" $'\033['
+}
+
+color_diagnostics_generate_code() {
+ generate_code "$@"
+ echo '#warning "Warning from preprocessor"' >>"$2"
+}
+
+# Heap's permutation algorithm
+color_diagnostics_generate_permutations() {
+ local -i i k="${1:?}-1"
+ if (( k )) ; then
+ color_diagnostics_generate_permutations "$k"
+ for (( i = 0 ; i < k ; ++i )) ; do
+ if (( k & 1 )) ; then
+ local tmp=${A[$i]} ; A[$i]=${A[$k]} ; A[$k]=$tmp
+ else
+ local tmp=${A[0]} ; A[0]=${A[$k]} ; A[$k]=$tmp
+ fi
+ color_diagnostics_generate_permutations "$k"
+ done
+ else
+ echo "${A[@]}"
+ fi
+}
+
+color_diagnostics_run_on_pty() {
+ script --return --quiet --command "unset GCC_COLORS; ${1:?}" /dev/null </dev/null
+}
+
+color_diagnostics_test() {
+ # -------------------------------------------------------------------------
+ TEST "Colored diagnostics automatically disabled when stderr is not a TTY (run_second_cpp=$run_second_cpp)"
+ color_diagnostics_generate_code 1 test1.c
+ $CCACHE_COMPILE -Wmissing-prototypes -c -o test1.o test1.c 2>test1.stderr
+ color_diagnostics_expect_no_color test1.stderr
+
+ # Check that subsequently running on a TTY generates a cache hit.
+ color_diagnostics_run_on_pty "$CCACHE_COMPILE -Wmissing-prototypes -c -o test1.o test1.c" >test1.output
+ color_diagnostics_expect_color test1.output
+ expect_stat 'cache miss' 1
+ expect_stat 'cache hit (preprocessed)' 1
+
+ # -------------------------------------------------------------------------
+ TEST "Colored diagnostics automatically enabled when stderr is a TTY (run_second_cpp=$run_second_cpp)"
+ color_diagnostics_generate_code 1 test1.c
+ color_diagnostics_run_on_pty "$CCACHE_COMPILE -Wmissing-prototypes -c -o test1.o test1.c" >test1.output
+ color_diagnostics_expect_color test1.output
+
+ # Check that subsequently running without a TTY generates a cache hit.
+ $CCACHE_COMPILE -Wmissing-prototypes -c -o test1.o test1.c 2>test1.stderr
+ color_diagnostics_expect_no_color test1.stderr
+ expect_stat 'cache miss' 1
+ expect_stat 'cache hit (preprocessed)' 1
+
+ # -------------------------------------------------------------------------
+ while read -r case ; do
+ TEST "Cache object shared across ${case} (run_second_cpp=$run_second_cpp)"
+ color_diagnostics_generate_code 1 test1.c
+ local each ; for each in ${case} ; do
+ case $each in
+ color,*)
+ local color_flag=$color_diagnostics_enable color_expect=color
+ ;;
+ nocolor,*)
+ local color_flag=$color_diagnostics_disable color_expect=no_color
+ ;;
+ esac
+ case $each in
+ *,tty)
+ color_diagnostics_run_on_pty "$CCACHE_COMPILE $color_flag -Wmissing-prototypes -c -o test1.o test1.c" >test1.output
+ color_diagnostics_expect_$color_expect test1.output
+ ;;
+ *,notty)
+ $CCACHE_COMPILE $color_flag -Wmissing-prototypes -c -o test1.o test1.c 2>test1.stderr
+ color_diagnostics_expect_$color_expect test1.stderr
+ ;;
+ esac
+ done
+ expect_stat 'cache miss' 1
+ expect_stat 'cache hit (preprocessed)' 3
+ done < <(
+ A=( {color,nocolor},{tty,notty} )
+ color_diagnostics_generate_permutations "${#A[@]}"
+ )
+}
+
+SUITE_color_diagnostics() {
+ run_second_cpp=true color_diagnostics_test
+ run_second_cpp=false color_diagnostics_test
+}
CHECK(Util::dir_name("/foo/bar/f.txt") == "/foo/bar");
}
+TEST_CASE("Util::{edit,strip}_ansi_csi_seqs")
+{
+ static constexpr auto input =
+ "Normal, "
+ "\x1B[K\x1B[1mbold\x1B[m, "
+ "\x1B[31mred\x1B[m, "
+ "\x1B[1;32mbold green\x1B[m.\n";
+
+ SECTION("Remove bold attributes")
+ {
+ CHECK(Util::edit_ansi_csi_seqs(input,
+ [](nonstd::string_view::size_type,
+ std::string& substr)
+ {
+ if (substr.size() > 3 && substr.back() == 'm') {
+ nonstd::string_view attrs = substr;
+ attrs.remove_prefix(2); // ESC [
+ attrs.remove_suffix(1); // m
+ std::string edited;
+ edited.reserve(attrs.size());
+ for (auto& attr : Util::split_into_views(attrs, ";")) {
+ if (attr != "1") {
+ if (!edited.empty()) {
+ edited += ';';
+ }
+ edited.append(attr.begin(), attr.end());
+ }
+ }
+ if (edited.empty()) {
+ substr.clear();
+ } else {
+ substr.replace(2, attrs.size(), std::move(edited));
+ }
+ }
+ }) ==
+ "Normal, "
+ "\x1B[Kbold\x1B[m, "
+ "\x1B[31mred\x1B[m, "
+ "\x1B[32mbold green\x1B[m.\n");
+ }
+
+ SECTION("Strip SGR sequences only")
+ {
+ CHECK(Util::strip_ansi_csi_seqs(input, "m")
+ == "Normal, \x1B[Kbold, red, bold green.\n");
+ }
+
+ SECTION("Strip default set of CSI sequences")
+ {
+ CHECK(Util::strip_ansi_csi_seqs(input)
+ == "Normal, bold, red, bold green.\n");
+ }
+}
+
TEST_CASE("Util::ends_with")
{
CHECK(Util::ends_with("", ""));