#include "cmake.h" #include "compile_commands.h" #include "config.h" #include "dialogs.h" #include "filesystem.h" #include "terminal.h" #include CMake::CMake(const boost::filesystem::path &path) { const auto find_cmake_project = [](const boost::filesystem::path &cmake_path) { for(auto &line : filesystem::read_lines(cmake_path)) { const static std::regex project_regex(R"(^ *project *\(.*\r?$)", std::regex::icase); std::smatch sm; if(std::regex_match(line, sm, project_regex)) return true; } return false; }; auto search_path = boost::filesystem::is_directory(path) ? path : path.parent_path(); while(true) { auto search_cmake_path = search_path / "CMakeLists.txt"; if(boost::filesystem::exists(search_cmake_path)) { paths.emplace(paths.begin(), search_cmake_path); if(find_cmake_project(search_cmake_path)) { project_path = search_path; break; } } if(search_path == search_path.root_directory()) break; search_path = search_path.parent_path(); } } bool CMake::update_default_build(const boost::filesystem::path &default_build_path, bool force) { if(project_path.empty() || !boost::filesystem::exists(project_path / "CMakeLists.txt") || default_build_path.empty()) return false; if(!boost::filesystem::exists(default_build_path)) { boost::system::error_code ec; boost::filesystem::create_directories(default_build_path, ec); if(ec) { Terminal::get().print("Error: could not create " + default_build_path.string() + ": " + ec.message() + "\n", true); return false; } } if(!force && boost::filesystem::exists(default_build_path / "compile_commands.json")) return true; auto compile_commands_path = default_build_path / "compile_commands.json"; Dialog::Message message("Creating/updating default build"); auto exit_status = Terminal::get().process(Config::get().project.cmake.command + ' ' + filesystem::escape_argument(project_path.string()) + " -DCMAKE_EXPORT_COMPILE_COMMANDS=ON", default_build_path); message.hide(); if(exit_status == EXIT_SUCCESS) { #ifdef _WIN32 //Temporary fix to MSYS2's libclang auto compile_commands_file = filesystem::read(compile_commands_path); auto replace_drive = [&compile_commands_file](const std::string ¶m) { size_t pos = 0; auto param_size = param.length(); while((pos = compile_commands_file.find(param + "/", pos)) != std::string::npos) { if(pos + param_size + 1 < compile_commands_file.size()) compile_commands_file.replace(pos, param_size + 2, param + compile_commands_file[pos + param_size + 1] + ":"); else break; } }; replace_drive("-I"); replace_drive("-isystem "); filesystem::write(compile_commands_path, compile_commands_file); #endif return true; } return false; } bool CMake::update_debug_build(const boost::filesystem::path &debug_build_path, bool force) { if(project_path.empty() || !boost::filesystem::exists(project_path / "CMakeLists.txt") || debug_build_path.empty()) return false; if(!boost::filesystem::exists(debug_build_path)) { boost::system::error_code ec; boost::filesystem::create_directories(debug_build_path, ec); if(ec) { Terminal::get().print("Error: could not create " + debug_build_path.string() + ": " + ec.message() + "\n", true); return false; } } if(!force && boost::filesystem::exists(debug_build_path / "CMakeCache.txt")) return true; Dialog::Message message("Creating/updating debug build"); auto exit_status = Terminal::get().process(Config::get().project.cmake.command + ' ' + filesystem::escape_argument(project_path.string()) + " -DCMAKE_BUILD_TYPE=Debug", debug_build_path); message.hide(); if(exit_status == EXIT_SUCCESS) return true; return false; } boost::filesystem::path CMake::get_executable(const boost::filesystem::path &build_path, const boost::filesystem::path &file_path) { // CMake does not store in compile_commands.json if an object is part of an executable or not. // Therefore, executables are first attempted found in the cmake files. These executables // are then used to identify if a file in compile_commands.json is part of an executable or not auto parameters = get_functions_parameters("add_executable"); std::vector cmake_executables; for(auto ¶meter : parameters) { if(parameter.second.size() > 1 && parameter.second[0].size() > 0 && parameter.second[0].compare(0, 2, "${") != 0) { auto executable = (parameter.first.parent_path() / parameter.second[0]).string(); auto project_path_str = project_path.string(); size_t pos = executable.find(project_path_str); if(pos != std::string::npos) executable.replace(pos, project_path_str.size(), build_path.string()); cmake_executables.emplace_back(executable); } } CompileCommands compile_commands(build_path); std::vector> command_files_and_maybe_executables; for(auto &command : compile_commands.commands) { auto command_file = filesystem::get_normal_path(command.file); auto values = command.parameter_values("-o"); if(!values.empty()) { size_t pos; values[0].erase(0, 11); if((pos = values[0].find(".dir")) != std::string::npos) { auto executable = command.directory / values[0].substr(0, pos); command_files_and_maybe_executables.emplace_back(command_file, executable); } } } size_t best_match_size = -1; boost::filesystem::path best_match_executable; for(auto &cmake_executable : cmake_executables) { for(auto &command_file_and_maybe_executable : command_files_and_maybe_executables) { auto &command_file = command_file_and_maybe_executable.first; auto &maybe_executable = command_file_and_maybe_executable.second; if(cmake_executable == maybe_executable) { if(command_file == file_path) return maybe_executable; auto command_file_directory = command_file.parent_path(); if(filesystem::file_in_path(file_path, command_file_directory)) { auto size = static_cast(std::distance(command_file_directory.begin(), command_file_directory.end())); if(best_match_size == static_cast(-1) || best_match_size < size) { best_match_size = size; best_match_executable = maybe_executable; } } } } } if(!best_match_executable.empty()) return best_match_executable; for(auto &command_file_and_maybe_executable : command_files_and_maybe_executables) { auto &command_file = command_file_and_maybe_executable.first; auto &maybe_executable = command_file_and_maybe_executable.second; if(command_file == file_path) return maybe_executable; auto command_file_directory = command_file.parent_path(); if(filesystem::file_in_path(file_path, command_file_directory)) { auto size = static_cast(std::distance(command_file_directory.begin(), command_file_directory.end())); if(best_match_size == static_cast(-1) || best_match_size < size) { best_match_size = size; best_match_executable = maybe_executable; } } } return best_match_executable; } void CMake::read_files() { for(auto &path : paths) files.emplace_back(filesystem::read(path)); } void CMake::remove_tabs() { for(auto &file : files) { for(auto &chr : file) { if(chr == '\t') chr = ' '; } } } void CMake::remove_comments() { for(auto &file : files) { size_t pos = 0; size_t comment_start; bool inside_comment = false; while(pos < file.size()) { if(!inside_comment && file[pos] == '#') { comment_start = pos; inside_comment = true; } if(inside_comment && file[pos] == '\n') { file.erase(comment_start, pos - comment_start); pos -= pos - comment_start; inside_comment = false; } pos++; } if(inside_comment) file.erase(comment_start); } } void CMake::remove_newlines_inside_parentheses() { for(auto &file : files) { size_t pos = 0; bool inside_para = false; bool inside_quote = false; char last_char = 0; while(pos < file.size()) { if(!inside_quote && file[pos] == '"' && last_char != '\\') inside_quote = true; else if(inside_quote && file[pos] == '"' && last_char != '\\') inside_quote = false; else if(!inside_quote && file[pos] == '(') inside_para = true; else if(!inside_quote && file[pos] == ')') inside_para = false; else if(inside_para && file[pos] == '\n') file.replace(pos, 1, 1, ' '); last_char = file[pos]; pos++; } } } void CMake::parse_variable_parameters(std::string &data) { size_t pos = 0; bool inside_quote = false; char last_char = 0; while(pos < data.size()) { if(!inside_quote && data[pos] == '"' && last_char != '\\') { inside_quote = true; data.erase(pos, 1); //TODO: instead remove quote-mark if pasted into a quote, for instance: "test${test}test"<-remove quotes from ${test} pos--; } else if(inside_quote && data[pos] == '"' && last_char != '\\') { inside_quote = false; data.erase(pos, 1); //TODO: instead remove quote-mark if pasted into a quote, for instance: "test${test}test"<-remove quotes from ${test} pos--; } else if(!inside_quote && data[pos] == ' ' && pos + 1 < data.size() && data[pos + 1] == ' ') { data.erase(pos, 1); pos--; } if(pos != static_cast(-1)) last_char = data[pos]; pos++; } for(auto &var : variables) { auto pos = data.find("${" + var.first + '}'); while(pos != std::string::npos) { data.replace(pos, var.first.size() + 3, var.second); pos = data.find("${" + var.first + '}'); } } //Remove variables we do not know: pos = data.find("${"); auto pos_end = data.find('}', pos + 2); while(pos != std::string::npos && pos_end != std::string::npos) { data.erase(pos, pos_end - pos + 1); pos = data.find("${"); pos_end = data.find('}', pos + 2); } } void CMake::parse() { read_files(); remove_tabs(); remove_comments(); remove_newlines_inside_parentheses(); parsed = true; } std::vector CMake::get_function_parameters(std::string &data) { std::vector parameters; size_t pos = 0; size_t parameter_pos = 0; bool inside_quote = false; char last_char = 0; while(pos < data.size()) { if(!inside_quote && data[pos] == '"' && last_char != '\\') { inside_quote = true; data.erase(pos, 1); pos--; } else if(inside_quote && data[pos] == '"' && last_char != '\\') { inside_quote = false; data.erase(pos, 1); pos--; } else if(!inside_quote && pos + 1 < data.size() && data[pos] == ' ' && data[pos + 1] == ' ') { data.erase(pos, 1); pos--; } else if(!inside_quote && data[pos] == ' ') { parameters.emplace_back(data.substr(parameter_pos, pos - parameter_pos)); if(pos + 1 < data.size()) parameter_pos = pos + 1; } if(pos != static_cast(-1)) last_char = data[pos]; pos++; } parameters.emplace_back(data.substr(parameter_pos)); for(auto &var : variables) { for(auto ¶meter : parameters) { auto pos = parameter.find("${" + var.first + '}'); while(pos != std::string::npos) { parameter.replace(pos, var.first.size() + 3, var.second); pos = parameter.find("${" + var.first + '}'); } } } return parameters; } std::vector>> CMake::get_functions_parameters(const std::string &name) { const std::regex function_regex("^ *" + name + R"( *\( *(.*)\) *\r?$)", std::regex::icase); variables.clear(); if(!parsed) parse(); std::vector>> functions; for(size_t c = 0; c < files.size(); ++c) { size_t pos = 0; while(pos < files[c].size()) { auto start_line = pos; auto end_line = files[c].find('\n', start_line); if(end_line == std::string::npos) end_line = files[c].size(); if(end_line > start_line) { auto line = files[c].substr(start_line, end_line - start_line); std::smatch sm; const static std::regex set_regex(R"(^ *set *\( *([A-Za-z_][A-Za-z_0-9]*) +(.*)\) *\r?$)", std::regex::icase); const static std::regex project_regex(R"(^ *project *\( *([^ ]+).*\) *\r?$)", std::regex::icase); if(std::regex_match(line, sm, set_regex)) { auto data = sm[2].str(); while(data.size() > 0 && data.back() == ' ') data.pop_back(); parse_variable_parameters(data); variables[sm[1].str()] = data; } else if(std::regex_match(line, sm, project_regex)) { auto data = sm[1].str(); parse_variable_parameters(data); variables["CMAKE_PROJECT_NAME"] = data; //TODO: is this variable deprecated/non-standard? variables["PROJECT_NAME"] = data; } if(std::regex_match(line, sm, function_regex)) { auto data = sm[1].str(); while(data.size() > 0 && data.back() == ' ') data.pop_back(); auto parameters = get_function_parameters(data); functions.emplace_back(paths[c], parameters); } } pos = end_line + 1; } } return functions; }