#include "cmake.hpp" #include "compile_commands.hpp" #include "config.hpp" #include "dialog.hpp" #include "filesystem.hpp" #include "terminal.hpp" #include "utility.hpp" #include #include #include #include CMake::CMake(const boost::filesystem::path &path) { const auto find_cmake_project = [](const boost::filesystem::path &file_path) { std::ifstream input(file_path.string(), std::ios::binary); if(input) { std::string line; while(std::getline(input, line)) { const static std::regex project_regex("^ *project *\\(.*$", std::regex::icase | std::regex::optimize); std::smatch sm; if(std::regex_match(line, sm, project_regex)) return true; } } return false; }; boost::system::error_code ec; auto search_path = boost::filesystem::is_directory(path, ec) ? path : path.parent_path(); while(true) { auto search_cmake_path = search_path / "CMakeLists.txt"; if(boost::filesystem::exists(search_cmake_path, ec)) { 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) { boost::system::error_code ec; if(project_path.empty() || !boost::filesystem::exists(project_path / "CMakeLists.txt", ec) || default_build_path.empty()) return false; if(!boost::filesystem::exists(default_build_path, ec)) { boost::system::error_code ec; boost::filesystem::create_directories(default_build_path, ec); if(ec) { Terminal::get().print("\e[31mError\e[m: could not create " + filesystem::get_short_path(default_build_path).string() + ": " + ec.message() + "\n", true); return false; } } if(!force && boost::filesystem::exists(default_build_path / "compile_commands.json", ec)) return true; auto compile_commands_path = default_build_path / "compile_commands.json"; bool canceled = false; Dialog::Message message("Creating/updating default build", [&canceled] { canceled = true; }); boost::optional exit_status; auto process = Terminal::get().async_process(Config::get().project.cmake.command + ' ' + filesystem::escape_argument(project_path.string()) + " -DCMAKE_EXPORT_COMPILE_COMMANDS=ON", default_build_path, [&exit_status](int exit_status_) { exit_status = exit_status_; }); bool killed = false; while(!exit_status) { if(canceled && !killed) { process->kill(); killed = true; } while(Gtk::Main::events_pending()) Gtk::Main::iteration(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } message.hide(); if(exit_status == 0) { #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) { boost::system::error_code ec; if(project_path.empty() || !boost::filesystem::exists(project_path / "CMakeLists.txt", ec) || debug_build_path.empty()) return false; if(!boost::filesystem::exists(debug_build_path, ec)) { boost::system::error_code ec; boost::filesystem::create_directories(debug_build_path, ec); if(ec) { Terminal::get().print("\e[31mError\e[m: could not create " + filesystem::get_short_path(debug_build_path).string() + ": " + ec.message() + "\n", true); return false; } } if(!force && boost::filesystem::exists(debug_build_path / "CMakeCache.txt", ec)) return true; bool canceled = false; Dialog::Message message("Creating/updating debug build", [&canceled] { canceled = true; }); boost::optional exit_status; auto process = Terminal::get().async_process(Config::get().project.cmake.command + ' ' + filesystem::escape_argument(project_path.string()) + " -DCMAKE_BUILD_TYPE=Debug", debug_build_path, [&exit_status](int exit_status_) { exit_status = exit_status_; }); bool killed = false; while(!exit_status) { if(canceled && !killed) { process->kill(); killed = true; } while(Gtk::Main::events_pending()) Gtk::Main::iteration(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } message.hide(); return exit_status == 0; } 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 CompileCommands compile_commands(build_path); std::vector> source_files_and_maybe_executables; for(auto &command : compile_commands.commands) { auto source_file = filesystem::get_normal_path(command.file); auto values = command.parameter_values("-o"); if(!values.empty()) { size_t pos; if((pos = values[0].find("CMakeFiles/")) != std::string::npos) values[0].erase(pos, 11); if((pos = values[0].find(".dir")) != std::string::npos) { auto executable = command.directory / values[0].substr(0, pos); source_files_and_maybe_executables.emplace_back(source_file, executable); } } } std::vector cmake_executables; // Parse cmake files std::map> variables; for(auto &path : paths) { parse_file(filesystem::read(path), variables, [this, &build_path, &cmake_executables, &path](Function function) { if(function.name == "add_executable") { if(!function.parameters.empty() && !function.parameters.front().empty()) { auto executable = (path.parent_path() / function.parameters.front()).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); } } }); } ssize_t best_match_size = -1; boost::filesystem::path best_match_executable; for(auto &cmake_executable : cmake_executables) { for(auto &source_file_and_maybe_executable : source_files_and_maybe_executables) { auto &source_file = source_file_and_maybe_executable.first; auto &maybe_executable = source_file_and_maybe_executable.second; if(cmake_executable == maybe_executable) { if(source_file == file_path) return maybe_executable; auto source_file_directory = source_file.parent_path(); if(filesystem::file_in_path(file_path, source_file_directory)) { auto size = std::distance(source_file_directory.begin(), source_file_directory.end()); if(size > best_match_size) { best_match_size = size; best_match_executable = maybe_executable; } } } } } if(!best_match_executable.empty()) return best_match_executable; for(auto &source_file_and_maybe_executable : source_files_and_maybe_executables) { auto &source_file = source_file_and_maybe_executable.first; auto &maybe_executable = source_file_and_maybe_executable.second; if(source_file == file_path) return maybe_executable; auto source_file_directory = source_file.parent_path(); if(filesystem::file_in_path(file_path, source_file_directory)) { auto size = std::distance(source_file_directory.begin(), source_file_directory.end()); if(size > best_match_size) { best_match_size = size; best_match_executable = maybe_executable; } } } return best_match_executable; } void CMake::parse_file(const std::string &src, std::map> &variables, std::function &&on_function) { size_t i = 0; auto parse_comment = [&] { if(src[i] == '#') { while(i < src.size() && src[i] != '\n') ++i; return true; } return false; }; auto is_whitespace = [&] { return src[i] == ' ' || src[i] == '\t' || src[i] == '\r' || src[i] == '\n'; }; auto forward_passed_whitespace = [&] { while(i < src.size() && is_whitespace()) ++i; return i < src.size(); }; auto parse_variable_name = [&]() -> boost::optional { if(src[i] == '$' && i + 1 < src.size() && src[i + 1] == '{') { auto start = i + 2; auto end = src.find('}', start); if(end != std::string::npos) { i = end; auto variable_name = src.substr(start, end - start); boost::algorithm::to_upper(variable_name); return variable_name; } } return {}; }; auto parse_function = [&]() -> boost::optional { Function function; if((src[i] >= 'A' && src[i] <= 'Z') || (src[i] >= 'a' && src[i] <= 'z') || src[i] == '_') { function.name += src[i++]; while(i < src.size() && ((src[i] >= 'A' && src[i] <= 'Z') || (src[i] >= 'a' && src[i] <= 'z') || (src[i] >= '0' && src[i] <= '9') || src[i] == '_')) function.name += src[i++]; if(forward_passed_whitespace() && src[i] == '(') { ++i; // Parse parameters for(; forward_passed_whitespace(); ++i) { if(src[i] == ')') return function; else if(src[i] == '"') { // Parse parameter within "" std::string parameter; ++i; for(; i < src.size(); ++i) { if(src[i] == '\\' && i + 1 < src.size()) parameter += src[++i]; else if(src[i] == '"') break; else if(auto variable_name = parse_variable_name()) { auto it = variables.find(*variable_name); if(it != variables.end()) { bool first = true; for(auto &value : it->second) { parameter += (first ? "" : ";") + value; first = false; } } } else parameter += src[i]; } function.parameters.emplace_back(std::move(parameter)); } else { // Parse parameter not within "" auto parameter_it = function.parameters.end(); for(; i < src.size() && !is_whitespace() && src[i] != ')'; ++i) { if(parameter_it == function.parameters.end()) parameter_it = function.parameters.emplace(parameter_it); if(src[i] == '\\' && i + 1 < src.size()) *parameter_it += src[++i]; else if(auto variable_name = parse_variable_name()) { auto variable_it = variables.find(*variable_name); if(variable_it != variables.end()) { if(variable_it->second.size() == 1) *parameter_it += variable_it->second.front(); else if(variable_it->second.size() > 1) { *parameter_it += variable_it->second.front(); function.parameters.insert(function.parameters.end(), std::next(variable_it->second.begin()), variable_it->second.end()); parameter_it = std::prev(function.parameters.end()); } } } else *parameter_it += src[i]; } if(src[i] == ')') return function; } } } } return {}; }; for(; forward_passed_whitespace(); ++i) { if(parse_comment()) continue; if(auto function = parse_function()) { boost::algorithm::to_lower(function->name); if(function->name == "set" && !function->parameters.empty() && !function->parameters.front().empty()) { auto variable_name = std::move(function->parameters.front()); boost::algorithm::to_upper(variable_name); function->parameters.erase(function->parameters.begin()); variables.emplace(std::move(variable_name), std::move(function->parameters)); } else if(function->name == "project") { if(!function->parameters.empty()) { variables.emplace("CMAKE_PROJECT_NAME", function->parameters); variables.emplace("PROJECT_NAME", function->parameters); } } if(on_function) on_function(std::move(*function)); } } }