From bb6324a9a910ddd81e7a831a117d32a7a68a9557 Mon Sep 17 00:00:00 2001 From: Itamar Date: Fri, 7 Jan 2022 17:20:28 +0200 Subject: [PATCH] HackStudio: Add ProjectBuilder component ProjectBuilder takes care of building and running the current project from Hack Studio. The existing functionality of building javascript and Makefile projects remains, and in addition to it the ability to build standalone serenity components is added. If the Hack Studio project is the serenity repository itself, ProjectBuilder will attempt building the component that the currently active file belongs to. It does so by creating a new CMake file which adds the component as a build subdirectory. It also parses all CMake files in the serenity repository to gather all available libraries. It declares the libraries and their dependencies in this CMake file. It then uses the HACKSTUDIO_BUILD CMake option to direct the build system to use this CMake file instead of doing a full system build. --- Userland/DevTools/HackStudio/CMakeLists.txt | 1 + .../DevTools/HackStudio/ProjectBuilder.cpp | 209 ++++++++++++++++++ Userland/DevTools/HackStudio/ProjectBuilder.h | 52 +++++ 3 files changed, 262 insertions(+) create mode 100644 Userland/DevTools/HackStudio/ProjectBuilder.cpp create mode 100644 Userland/DevTools/HackStudio/ProjectBuilder.h diff --git a/Userland/DevTools/HackStudio/CMakeLists.txt b/Userland/DevTools/HackStudio/CMakeLists.txt index 7dc1b35db6..f86e4e02e5 100644 --- a/Userland/DevTools/HackStudio/CMakeLists.txt +++ b/Userland/DevTools/HackStudio/CMakeLists.txt @@ -43,6 +43,7 @@ set(SOURCES LanguageClient.cpp Locator.cpp Project.cpp + ProjectBuilder.cpp ProjectDeclarations.cpp ProjectFile.cpp ProjectTemplate.cpp diff --git a/Userland/DevTools/HackStudio/ProjectBuilder.cpp b/Userland/DevTools/HackStudio/ProjectBuilder.cpp new file mode 100644 index 0000000000..fea73e59fb --- /dev/null +++ b/Userland/DevTools/HackStudio/ProjectBuilder.cpp @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2022, Itamar S. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ProjectBuilder.h" +#include +#include +#include +#include +#include +#include + +namespace HackStudio { + +ProjectBuilder::ProjectBuilder(NonnullRefPtr terminal, Project const& project) + : m_project_root(project.root_path()) + , m_terminal(move(terminal)) + , m_is_serenity(project.project_is_serenity() ? IsSerenityRepo::Yes : IsSerenityRepo::No) +{ +} + +ErrorOr ProjectBuilder::build(StringView active_file) +{ + m_terminal->clear_including_history(); + if (active_file.is_null()) + return Error::from_string_literal("no active file"sv); + + if (active_file.ends_with(".js")) { + m_terminal->run_command(String::formatted("js -A {}", active_file)); + return {}; + } + if (m_is_serenity == IsSerenityRepo::No) { + m_terminal->run_command("make"); + return {}; + } + + TRY(update_active_file(active_file)); + + return build_serenity_component(); +} + +ErrorOr ProjectBuilder::run(StringView active_file) +{ + if (active_file.is_null()) + return Error::from_string_literal("no active file"sv); + + if (active_file.ends_with(".js")) { + m_terminal->run_command(String::formatted("js {}", active_file)); + return {}; + } + + if (m_is_serenity == IsSerenityRepo::No) { + m_terminal->run_command("make run"); + return {}; + } + + TRY(update_active_file(active_file)); + + return run_serenity_component(); +} + +ErrorOr ProjectBuilder::run_serenity_component() +{ + auto relative_path_to_dir = LexicalPath::relative_path(LexicalPath::dirname(m_serenity_component_cmake_file), m_project_root); + m_terminal->run_command(LexicalPath::join(relative_path_to_dir, m_serenity_component_name).string(), LexicalPath::join(m_build_directory->path(), "Build").string()); + return {}; +} + +ErrorOr ProjectBuilder::update_active_file(StringView active_file) +{ + TRY(verify_cmake_is_installed()); + auto cmake_file = find_cmake_file_for(active_file); + if (!cmake_file.has_value()) { + warnln("did not find cmake file for: {}", active_file); + return Error::from_string_literal("did not find cmake file"sv); + } + + if (m_serenity_component_cmake_file == cmake_file.value()) + return {}; + + if (!m_serenity_component_cmake_file.is_null()) + m_build_directory.clear(); + + m_serenity_component_cmake_file = cmake_file.value(); + m_serenity_component_name = TRY(component_name(m_serenity_component_cmake_file)); + + TRY(initialize_build_directory()); + return {}; +} + +ErrorOr ProjectBuilder::build_serenity_component() +{ + m_terminal->run_command(String::formatted("make {}", m_serenity_component_name), LexicalPath::join(m_build_directory->path(), "Build"sv).string(), TerminalWrapper::WaitForExit::Yes); + if (m_terminal->child_exit_status() == 0) + return {}; + return Error::from_string_literal("Make failed"sv); +} + +ErrorOr ProjectBuilder::component_name(StringView cmake_file_path) +{ + auto content = TRY(Core::File::open(cmake_file_path, Core::OpenMode::ReadOnly))->read_all(); + + static const Regex component_name(R"~~~(serenity_component\([\s]*(\w+)[\s\S]*\))~~~"); + RegexResult result; + if (!component_name.search(StringView { content }, result)) + return Error::from_string_literal("component not found"sv); + + return String { result.capture_group_matches.at(0).at(0).view.string_view() }; +} + +ErrorOr ProjectBuilder::initialize_build_directory() +{ + m_build_directory = Core::TempFile::create(Core::TempFile::Type::Directory); + if (mkdir(LexicalPath::join(m_build_directory->path(), "Build").string().characters(), 0700)) { + return Error::from_errno(errno); + } + + auto cmake_file_path = LexicalPath::join(m_build_directory->path(), "CMakeLists.txt").string(); + auto cmake_file = TRY(Core::File::open(cmake_file_path, Core::OpenMode::WriteOnly)); + cmake_file->write(generate_cmake_file_content()); + + m_terminal->run_command(String::formatted("cmake -S {} -DHACKSTUDIO_BUILD=ON -DHACKSTUDIO_BUILD_CMAKE_FILE={}" + " -DENABLE_UNICODE_DATABASE_DOWNLOAD=OFF", + m_project_root, cmake_file_path), + LexicalPath::join(m_build_directory->path(), "Build"sv).string(), TerminalWrapper::WaitForExit::Yes); + + if (m_terminal->child_exit_status() == 0) + return {}; + return Error::from_string_literal("CMake error"sv); +} + +Optional ProjectBuilder::find_cmake_file_for(StringView file_path) const +{ + auto directory = LexicalPath::dirname(file_path); + while (!directory.is_empty()) { + auto cmake_path = LexicalPath::join(m_project_root, directory, "CMakeLists.txt"); + if (Core::File::exists(cmake_path.string())) + return cmake_path.string(); + directory = LexicalPath::dirname(directory); + } + return {}; +} + +String ProjectBuilder::generate_cmake_file_content() const +{ + StringBuilder builder; + builder.appendff("add_subdirectory({})\n", LexicalPath::dirname(m_serenity_component_cmake_file)); + generate_cmake_library_definitions(builder); + builder.append('\n'); + generate_cmake_library_dependencies(builder); + + return builder.to_string(); +} + +void ProjectBuilder::generate_cmake_library_definitions(StringBuilder& builder) +{ + Vector arguments = { "sh", "-c", "find Userland/Libraries -name CMakeLists.txt | xargs grep serenity_lib" }; + auto res = Core::command("/bin/sh", arguments, {}); + + static const Regex parse_library_definition(R"~~~(.+:serenity_lib[c]?\((\w+) (\w+)\).*)~~~"); + for (auto& line : res->stdout.split('\n')) { + + RegexResult result; + if (!parse_library_definition.search(line, result)) + continue; + if (result.capture_group_matches.size() != 1 || result.capture_group_matches[0].size() != 2) + continue; + + auto library_name = result.capture_group_matches.at(0).at(0).view.string_view(); + auto library_obj_name = result.capture_group_matches.at(0).at(1).view.string_view(); + builder.appendff("add_library({} SHARED IMPORTED GLOBAL)\n", library_name); + auto so_path = String::formatted("{}.so", LexicalPath::join("/usr/lib"sv, String::formatted("lib{}", library_obj_name)).string()); + builder.appendff("set_target_properties({} PROPERTIES IMPORTED_LOCATION {})\n", library_name, so_path); + } +} + +void ProjectBuilder::generate_cmake_library_dependencies(StringBuilder& builder) +{ + Vector arguments = { "sh", "-c", "find Userland/Libraries -name CMakeLists.txt | xargs grep target_link_libraries" }; + auto res = Core::command("/bin/sh", arguments, {}); + + static const Regex parse_library_definition(R"~~~(.+:target_link_libraries\((\w+) ([\w\s]+)\).*)~~~"); + for (auto& line : res->stdout.split('\n')) { + + RegexResult result; + if (!parse_library_definition.search(line, result)) + continue; + if (result.capture_group_matches.size() != 1 || result.capture_group_matches[0].size() != 2) + continue; + + auto library_name = result.capture_group_matches.at(0).at(0).view.string_view(); + auto dependencies = result.capture_group_matches.at(0).at(1).view.string_view(); + if (library_name == "LibCStaticWithoutDeps"sv || library_name == "DumpLayoutTree"sv) + continue; + builder.appendff("target_link_libraries({} INTERFACE {})\n", library_name, dependencies); + } +} + +ErrorOr ProjectBuilder::verify_cmake_is_installed() +{ + auto res = Core::command("cmake --version", {}); + if (res.has_value() && res->exit_code == 0) + return {}; + return Error::from_string_literal("CMake port is not installed"sv); +} + +} diff --git a/Userland/DevTools/HackStudio/ProjectBuilder.h b/Userland/DevTools/HackStudio/ProjectBuilder.h new file mode 100644 index 0000000000..3398dfa93e --- /dev/null +++ b/Userland/DevTools/HackStudio/ProjectBuilder.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022, Itamar S. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "AK/Error.h" +#include "Project.h" +#include "TerminalWrapper.h" +#include +#include + +namespace HackStudio { +class ProjectBuilder { + + AK_MAKE_NONCOPYABLE(ProjectBuilder); + +public: + ProjectBuilder(NonnullRefPtr, Project const&); + ~ProjectBuilder() = default; + + ErrorOr build(StringView active_file); + ErrorOr run(StringView active_file); + +private: + enum class IsSerenityRepo { + No, + Yes + }; + + ErrorOr build_serenity_component(); + ErrorOr run_serenity_component(); + ErrorOr initialize_build_directory(); + Optional find_cmake_file_for(StringView file_path) const; + String generate_cmake_file_content() const; + ErrorOr update_active_file(StringView active_file); + + static void generate_cmake_library_definitions(StringBuilder&); + static void generate_cmake_library_dependencies(StringBuilder&); + static ErrorOr component_name(StringView cmake_file_path); + static ErrorOr verify_cmake_is_installed(); + + String m_project_root; + NonnullRefPtr m_terminal; + IsSerenityRepo m_is_serenity { IsSerenityRepo::No }; + OwnPtr m_build_directory; + String m_serenity_component_cmake_file; + String m_serenity_component_name; +}; +}