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