diff options
-rw-r--r-- | Userland/DevTools/HackStudio/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/DevTools/HackStudio/ProjectBuilder.cpp | 209 | ||||
-rw-r--r-- | Userland/DevTools/HackStudio/ProjectBuilder.h | 52 |
3 files changed, 262 insertions, 0 deletions
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. <itamar8910@gmail.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ProjectBuilder.h" +#include <AK/LexicalPath.h> +#include <LibCore/Command.h> +#include <LibCore/File.h> +#include <LibRegex/Regex.h> +#include <fcntl.h> +#include <sys/stat.h> + +namespace HackStudio { + +ProjectBuilder::ProjectBuilder(NonnullRefPtr<TerminalWrapper> 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<void> 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<void> 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<void> 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<void> 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<void> 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<String> ProjectBuilder::component_name(StringView cmake_file_path) +{ + auto content = TRY(Core::File::open(cmake_file_path, Core::OpenMode::ReadOnly))->read_all(); + + static const Regex<ECMA262> 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<void> 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<String> 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<String> arguments = { "sh", "-c", "find Userland/Libraries -name CMakeLists.txt | xargs grep serenity_lib" }; + auto res = Core::command("/bin/sh", arguments, {}); + + static const Regex<ECMA262> 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<String> arguments = { "sh", "-c", "find Userland/Libraries -name CMakeLists.txt | xargs grep target_link_libraries" }; + auto res = Core::command("/bin/sh", arguments, {}); + + static const Regex<ECMA262> 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<void> 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. <itamar8910@gmail.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "AK/Error.h" +#include "Project.h" +#include "TerminalWrapper.h" +#include <AK/Noncopyable.h> +#include <LibCore/TempFile.h> + +namespace HackStudio { +class ProjectBuilder { + + AK_MAKE_NONCOPYABLE(ProjectBuilder); + +public: + ProjectBuilder(NonnullRefPtr<TerminalWrapper>, Project const&); + ~ProjectBuilder() = default; + + ErrorOr<void> build(StringView active_file); + ErrorOr<void> run(StringView active_file); + +private: + enum class IsSerenityRepo { + No, + Yes + }; + + ErrorOr<void> build_serenity_component(); + ErrorOr<void> run_serenity_component(); + ErrorOr<void> initialize_build_directory(); + Optional<String> find_cmake_file_for(StringView file_path) const; + String generate_cmake_file_content() const; + ErrorOr<void> update_active_file(StringView active_file); + + static void generate_cmake_library_definitions(StringBuilder&); + static void generate_cmake_library_dependencies(StringBuilder&); + static ErrorOr<String> component_name(StringView cmake_file_path); + static ErrorOr<void> verify_cmake_is_installed(); + + String m_project_root; + NonnullRefPtr<TerminalWrapper> m_terminal; + IsSerenityRepo m_is_serenity { IsSerenityRepo::No }; + OwnPtr<Core::TempFile> m_build_directory; + String m_serenity_component_cmake_file; + String m_serenity_component_name; +}; +} |