ProjectBuilder.cpp 10 KB

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