ProjectBuilder.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /*
  2. * Copyright (c) 2022, Itamar S. <itamar8910@gmail.com>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "ProjectBuilder.h"
  7. #include <AK/LexicalPath.h>
  8. #include <LibCore/Command.h>
  9. #include <LibFileSystem/FileSystem.h>
  10. #include <LibRegex/Regex.h>
  11. #include <fcntl.h>
  12. #include <sys/stat.h>
  13. namespace HackStudio {
  14. ProjectBuilder::ProjectBuilder(NonnullRefPtr<TerminalWrapper> terminal, Project const& project)
  15. : m_project_root(project.root_path())
  16. , m_project(project)
  17. , m_terminal(move(terminal))
  18. , m_is_serenity(project.project_is_serenity() ? IsSerenityRepo::Yes : IsSerenityRepo::No)
  19. {
  20. }
  21. ErrorOr<void> ProjectBuilder::build(StringView active_file)
  22. {
  23. m_terminal->clear_including_history();
  24. if (auto command = m_project.config()->build_command(); command.has_value()) {
  25. TRY(m_terminal->run_command(command.value()));
  26. return {};
  27. }
  28. if (active_file.is_null())
  29. return Error::from_string_literal("no active file");
  30. if (active_file.ends_with(".js"sv)) {
  31. TRY(m_terminal->run_command(DeprecatedString::formatted("js -A {}", active_file)));
  32. return {};
  33. }
  34. if (m_is_serenity == IsSerenityRepo::No) {
  35. TRY(verify_make_is_installed());
  36. TRY(m_terminal->run_command("make"));
  37. return {};
  38. }
  39. TRY(update_active_file(active_file));
  40. return build_serenity_component();
  41. }
  42. ErrorOr<void> ProjectBuilder::run(StringView active_file)
  43. {
  44. if (auto command = m_project.config()->run_command(); command.has_value()) {
  45. TRY(m_terminal->run_command(command.value()));
  46. return {};
  47. }
  48. if (active_file.is_null())
  49. return Error::from_string_literal("no active file");
  50. if (active_file.ends_with(".js"sv)) {
  51. TRY(m_terminal->run_command(DeprecatedString::formatted("js {}", active_file)));
  52. return {};
  53. }
  54. if (m_is_serenity == IsSerenityRepo::No) {
  55. TRY(verify_make_is_installed());
  56. TRY(m_terminal->run_command("make run"));
  57. return {};
  58. }
  59. TRY(update_active_file(active_file));
  60. return run_serenity_component();
  61. }
  62. ErrorOr<void> ProjectBuilder::run_serenity_component()
  63. {
  64. auto relative_path_to_dir = LexicalPath::relative_path(LexicalPath::dirname(m_serenity_component_cmake_file), m_project_root);
  65. TRY(m_terminal->run_command(LexicalPath::join(relative_path_to_dir, m_serenity_component_name).string(), build_directory()));
  66. return {};
  67. }
  68. ErrorOr<void> ProjectBuilder::update_active_file(StringView active_file)
  69. {
  70. TRY(verify_cmake_is_installed());
  71. auto cmake_file = find_cmake_file_for(active_file);
  72. if (!cmake_file.has_value()) {
  73. warnln("did not find cmake file for: {}", active_file);
  74. return Error::from_string_literal("did not find cmake file");
  75. }
  76. if (m_serenity_component_cmake_file == cmake_file.value())
  77. return {};
  78. m_serenity_component_cmake_file = cmake_file.value();
  79. m_serenity_component_name = TRY(component_name(m_serenity_component_cmake_file));
  80. TRY(initialize_build_directory());
  81. return {};
  82. }
  83. ErrorOr<void> ProjectBuilder::build_serenity_component()
  84. {
  85. TRY(verify_make_is_installed());
  86. TRY(m_terminal->run_command(DeprecatedString::formatted("make {}", m_serenity_component_name), build_directory(), TerminalWrapper::WaitForExit::Yes, "Make failed"sv));
  87. return {};
  88. }
  89. ErrorOr<DeprecatedString> ProjectBuilder::component_name(StringView cmake_file_path)
  90. {
  91. auto file = TRY(Core::File::open(cmake_file_path, Core::File::OpenMode::Read));
  92. auto content = TRY(file->read_until_eof());
  93. static Regex<ECMA262> const component_name(R"~~~(serenity_component\([\s]*(\w+)[\s\S]*\))~~~");
  94. RegexResult result;
  95. if (!component_name.search(StringView { content }, result))
  96. return Error::from_string_literal("component not found");
  97. return DeprecatedString { result.capture_group_matches.at(0).at(0).view.string_view() };
  98. }
  99. ErrorOr<void> ProjectBuilder::initialize_build_directory()
  100. {
  101. if (!FileSystem::exists(build_directory())) {
  102. if (mkdir(LexicalPath::join(build_directory()).string().characters(), 0700)) {
  103. return Error::from_errno(errno);
  104. }
  105. }
  106. auto cmake_file_path = LexicalPath::join(build_directory(), "CMakeLists.txt"sv).string();
  107. if (FileSystem::exists(cmake_file_path))
  108. MUST(FileSystem::remove(cmake_file_path, FileSystem::RecursionMode::Disallowed));
  109. auto cmake_file = TRY(Core::File::open(cmake_file_path, Core::File::OpenMode::Write));
  110. TRY(cmake_file->write_until_depleted(generate_cmake_file_content().bytes()));
  111. TRY(m_terminal->run_command(DeprecatedString::formatted("cmake -S {} -DHACKSTUDIO_BUILD=ON -DHACKSTUDIO_BUILD_CMAKE_FILE={}"
  112. " -DENABLE_UNICODE_DATABASE_DOWNLOAD=OFF",
  113. m_project_root, cmake_file_path),
  114. build_directory(), TerminalWrapper::WaitForExit::Yes, "CMake error"sv));
  115. return {};
  116. }
  117. Optional<DeprecatedString> ProjectBuilder::find_cmake_file_for(StringView file_path) const
  118. {
  119. auto directory = LexicalPath::dirname(file_path);
  120. while (!directory.is_empty()) {
  121. auto cmake_path = LexicalPath::join(m_project_root, directory, "CMakeLists.txt"sv);
  122. if (FileSystem::exists(cmake_path.string()))
  123. return cmake_path.string();
  124. directory = LexicalPath::dirname(directory);
  125. }
  126. return {};
  127. }
  128. DeprecatedString ProjectBuilder::generate_cmake_file_content() const
  129. {
  130. StringBuilder builder;
  131. builder.appendff("add_subdirectory({})\n", LexicalPath::dirname(m_serenity_component_cmake_file));
  132. auto defined_libraries = get_defined_libraries();
  133. for (auto& library : defined_libraries) {
  134. builder.appendff("add_library({} SHARED IMPORTED GLOBAL)\n", library.key);
  135. builder.appendff("set_target_properties({} PROPERTIES IMPORTED_LOCATION {})\n", library.key, library.value->path);
  136. if (library.key == "LibCStaticWithoutDeps"sv)
  137. continue;
  138. // We need to specify the dependencies for each defined library in CMake because some applications do not specify
  139. // all of their direct dependencies in the CMakeLists file.
  140. // For example, a target may directly use LibGFX but only specify LibGUI as a dependency (which in turn depends on LibGFX).
  141. // In this example, if we don't specify the dependencies of LibGUI in the CMake file, linking will fail because of undefined LibGFX symbols.
  142. builder.appendff("target_link_libraries({} INTERFACE {})\n", library.key, DeprecatedString::join(' ', library.value->dependencies));
  143. }
  144. return builder.to_deprecated_string();
  145. }
  146. HashMap<DeprecatedString, NonnullOwnPtr<ProjectBuilder::LibraryInfo>> ProjectBuilder::get_defined_libraries()
  147. {
  148. HashMap<DeprecatedString, NonnullOwnPtr<ProjectBuilder::LibraryInfo>> libraries;
  149. for_each_library_definition([&libraries](DeprecatedString name, DeprecatedString path) {
  150. libraries.set(name, make<ProjectBuilder::LibraryInfo>(move(path)));
  151. });
  152. for_each_library_dependencies([&libraries](DeprecatedString name, Vector<StringView> const& dependencies) {
  153. auto library = libraries.get(name);
  154. if (!library.has_value())
  155. return;
  156. for (auto const& dependency : dependencies) {
  157. if (libraries.contains(dependency))
  158. library.value()->dependencies.append(dependency);
  159. }
  160. });
  161. return libraries;
  162. }
  163. void ProjectBuilder::for_each_library_definition(Function<void(DeprecatedString, DeprecatedString)> func)
  164. {
  165. Vector<DeprecatedString> arguments = { "-c", "find Userland -name CMakeLists.txt | xargs grep serenity_lib" };
  166. auto res = Core::command("/bin/sh", arguments, {});
  167. if (res.is_error()) {
  168. warnln("{}", res.error());
  169. return;
  170. }
  171. static Regex<ECMA262> const parse_library_definition(R"~~~(.+:serenity_lib[c]?\((\w+) (\w+)\).*)~~~");
  172. for (auto& line : res.value().output.split('\n')) {
  173. RegexResult result;
  174. if (!parse_library_definition.search(line, result))
  175. continue;
  176. if (result.capture_group_matches.size() != 1 || result.capture_group_matches[0].size() != 2)
  177. continue;
  178. auto library_name = result.capture_group_matches.at(0).at(0).view.string_view();
  179. auto library_obj_name = result.capture_group_matches.at(0).at(1).view.string_view();
  180. auto so_path = DeprecatedString::formatted("{}.so", LexicalPath::join("/usr/lib"sv, DeprecatedString::formatted("lib{}", library_obj_name)).string());
  181. func(library_name, so_path);
  182. }
  183. // ssp is defined with "add_library" so it doesn't get picked up with the current logic for finding library definitions.
  184. func("ssp", "/usr/lib/libssp.a");
  185. }
  186. void ProjectBuilder::for_each_library_dependencies(Function<void(DeprecatedString, Vector<StringView>)> func)
  187. {
  188. Vector<DeprecatedString> arguments = { "-c", "find Userland/Libraries -name CMakeLists.txt | xargs grep target_link_libraries" };
  189. auto res = Core::command("/bin/sh", arguments, {});
  190. if (res.is_error()) {
  191. warnln("{}", res.error());
  192. return;
  193. }
  194. static Regex<ECMA262> const parse_library_definition(R"~~~(.+:target_link_libraries\((\w+) ([\w\s]+)\).*)~~~");
  195. for (auto& line : res.value().output.split('\n')) {
  196. RegexResult result;
  197. if (!parse_library_definition.search(line, result))
  198. continue;
  199. if (result.capture_group_matches.size() != 1 || result.capture_group_matches[0].size() != 2)
  200. continue;
  201. auto library_name = result.capture_group_matches.at(0).at(0).view.string_view();
  202. auto dependencies_string = result.capture_group_matches.at(0).at(1).view.string_view();
  203. func(library_name, dependencies_string.split_view(' '));
  204. }
  205. }
  206. ErrorOr<void> ProjectBuilder::verify_cmake_is_installed()
  207. {
  208. auto res = Core::command("cmake --version", {});
  209. if (!res.is_error() && res.value().exit_code == 0)
  210. return {};
  211. return Error::from_string_literal("CMake port is not installed");
  212. }
  213. ErrorOr<void> ProjectBuilder::verify_make_is_installed()
  214. {
  215. auto res = Core::command("make --version", {});
  216. if (!res.is_error() && res.value().exit_code == 0)
  217. return {};
  218. return Error::from_string_literal("Make port is not installed");
  219. }
  220. DeprecatedString ProjectBuilder::build_directory() const
  221. {
  222. return LexicalPath::join(m_project_root, "Build"sv).string();
  223. }
  224. }