|
|
|
|
#include "cmake.hpp"
|
|
|
|
|
#include "compile_commands.hpp"
|
|
|
|
|
#include "config.hpp"
|
|
|
|
|
#include "dialogs.hpp"
|
|
|
|
|
#include "filesystem.hpp"
|
|
|
|
|
#include "terminal.hpp"
|
|
|
|
|
#include "utility.hpp"
|
|
|
|
|
#include <boost/optional.hpp>
|
|
|
|
|
#include <regex>
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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("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", ec))
|
|
|
|
|
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) {
|
|
|
|
|
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("Error: could not create " + debug_build_path.string() + ": " + ec.message() + "\n", true);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!force && boost::filesystem::exists(debug_build_path / "CMakeCache.txt", ec))
|
|
|
|
|
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<boost::filesystem::path> cmake_executables;
|
|
|
|
|
for(auto ¶meter : parameters) {
|
|
|
|
|
if(parameter.second.size() > 1 && parameter.second[0].size() > 0 && !starts_with(parameter.second[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<std::pair<boost::filesystem::path, boost::filesystem::path>> 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;
|
|
|
|
|
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);
|
|
|
|
|
command_files_and_maybe_executables.emplace_back(command_file, executable);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boost::optional<size_t> best_match_size;
|
|
|
|
|
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<size_t>(std::distance(command_file_directory.begin(), command_file_directory.end()));
|
|
|
|
|
if(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<size_t>(std::distance(command_file_directory.begin(), command_file_directory.end()));
|
|
|
|
|
if(!best_match_size || 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<size_t>(-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<std::string> CMake::get_function_parameters(std::string &data) {
|
|
|
|
|
std::vector<std::string> 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<size_t>(-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<std::pair<boost::filesystem::path, std::vector<std::string>>> 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<std::pair<boost::filesystem::path, std::vector<std::string>>> 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;
|
|
|
|
|
}
|