From 34b10f41aa285e423cccb161342b68ae7275da4b Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Sat, 15 Apr 2023 08:43:30 +0200 Subject: [PATCH] Retrieve updatable packages --- CMakeLists.txt | 43 +++++++++ build.ninja | 16 ---- src/command.cpp | 67 +++++++------ src/command.h | 21 +---- src/component.cpp | 76 +++++++++++++++ src/component.h | 20 ++++ src/config.h | 2 + src/package.cpp | 30 ++++++ src/package.h | 23 +++++ src/sbo.cpp | 232 ++++++++++++++++++++++++++++++++++++++++++++++ src/sbo.h | 73 +++++++++++++++ 11 files changed, 541 insertions(+), 62 deletions(-) create mode 100644 CMakeLists.txt delete mode 100644 build.ninja create mode 100644 src/component.cpp create mode 100644 src/component.h create mode 100644 src/sbo.cpp create mode 100644 src/sbo.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cb5e349 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.21) +project(katja + VERSION 1.0 +) + +include(FindBoost) +include(FetchContent) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +add_executable(katja + src/component.h + src/command.h + src/package.h + src/sbo.h + src/component.cpp + src/command.cpp + src/package.cpp + src/sbo.cpp + src/main.cpp +) +target_include_directories(katja PRIVATE src) + +find_package(Boost 1.78.0 REQUIRED COMPONENTS filesystem) +include_directories(${Boost_INCLUDE_DIRS}) + +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui + GIT_TAG v4.0.0 +) +FetchContent_GetProperties(ftxui) +if(NOT ftxui_POPULATED) + FetchContent_Populate(ftxui) + add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + +target_link_libraries(katja + PRIVATE Boost::filesystem + PRIVATE ftxui::screen + PRIVATE ftxui::dom + PRIVATE ftxui::component +) diff --git a/build.ninja b/build.ninja deleted file mode 100644 index 81e0472..0000000 --- a/build.ninja +++ /dev/null @@ -1,16 +0,0 @@ -rule cxx - command = g++ -c -o $out $in - description = CXX $out - -rule link - command = g++ -o $out $in -lboost_filesystem -lboost_system - description = LINK $out - -build build/package.o: cxx src/package.cpp | src/package.h -build build/command.o: cxx src/command.cpp | src/command.h build/package.o src/config.h - -build build/main.o: cxx src/main.cpp - -build build/slackbuilder: link build/main.o build/command.o build/package.o - -default build/slackbuilder diff --git a/src/command.cpp b/src/command.cpp index 0fa6bf1..9443372 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -1,18 +1,52 @@ #include "command.h" #include #include "config.h" +#include "sbo.h" +#include +#include +#include "component.h" namespace katja { void list::execute() const { - for (const auto& package : katja::read_package_database()) + sbo sbo_repository; + std::unordered_map packages = katja::read_package_database(); + std::vector> table_data; + std::set installed_packages; + std::unordered_map package_database = read_package_database(); + + for (const auto& package : package_database) { - std::cout << package.second.name() - << " " << package.second.version() - << " (" << package.second.tag() << ")" - << std::endl; + std::string tag = package.second.tag(); + if (tag.find("_SBo") == std::string::npos) + { + continue; + } + installed_packages.insert(package.first); } + for (const auto& package : sbo_repository.list(installed_packages)) + { + if (package_database.find(package.first)->second.version() == package.second.version()) + { + continue; + } + table_data.push_back({ + package.second.name(), + package.second.version(), + package.second.tag(), + package.second.architecture(), + "SBo" + }); + } + + auto screen = ftxui::ScreenInteractive::Fullscreen(); + PackageList list_component{ + { "Package name", "Version", "Tag", "Architecture", "Repository" }, + table_data + }; + + screen.Loop(std::make_shared(list_component)); } void help::execute() const @@ -21,30 +55,9 @@ namespace katja "\tkatja {list|update|help} [OPTIONS]\n\n"; } - update::update() - : git_binary(boost::process::search_path("git")) - { - } - void update::execute() const { - std::filesystem::path workdir{ WORKDIR }; - std::filesystem::path repository = workdir / "sbo/repository"; - std::filesystem::file_status repository_status = std::filesystem::status(repository); - - if (std::filesystem::exists(repository_status) - && !std::filesystem::is_directory(repository_status)) - { - throw std::runtime_error("The working directory path \"" - + repository.string() + "\" exists, but it isn't a directory."); - } - else if (!std::filesystem::exists(repository_status)) - { - git("clone", std::filesystem::path(), - "git://git.slackbuilds.org/slackbuilds.git", repository.native()); - } - git("remote", repository.native(), "update", "--prune"); - git("reset", repository.native(), "--hard", "origin/master"); + sbo().refresh(); } command_exception::command_exception(const command_exception_t exception_type, diff --git a/src/command.h b/src/command.h index f03281f..b168f04 100644 --- a/src/command.h +++ b/src/command.h @@ -1,10 +1,10 @@ +#pragma once + #include #include #include #include #include "package.h" -#include -#include namespace katja { @@ -28,24 +28,7 @@ namespace katja class update final : public command { - boost::filesystem::path git_binary; - - template - void git(const std::string& command, const std::filesystem::path& cwd, const Args&... args) const - { - if (cwd.empty()) - { - boost::process::system(git_binary, command, args...); - } - else - { - boost::process::system(git_binary, "-C", cwd.native(), command, args...); - } - } - public: - explicit update(); - void execute() const override; }; diff --git a/src/component.cpp b/src/component.cpp new file mode 100644 index 0000000..9247a1d --- /dev/null +++ b/src/component.cpp @@ -0,0 +1,76 @@ +#include +#include "component.h" + +namespace katja +{ + PackageList::PackageList(const std::vector& header, const std::vector>& data) + : m_data(data), m_header(header) + { + } + + ftxui::Element PackageList::Render() + { + std::vector> data{ m_header }; + + std::size_t dimy = ftxui::ScreenInteractive::Active()->dimy(); + if (dimy == 0) + { + dimy = ftxui::Terminal::Size().dimy; + } + if (dimy > 4) // 4 = headers and borders. + { + std::size_t row_count = std::min(m_data.size() - top_line, dimy - 4 + top_line); + + std::copy(m_data.cbegin() + top_line, m_data.cbegin() + row_count, std::back_inserter(data)); + } + ftxui::Table table{ data }; + table.SelectAll().Border(ftxui::LIGHT); + + // Add border around the first column. + table.SelectColumn(0).Border(ftxui::LIGHT); + + // Make first row bold with a double border. + table.SelectRow(0).Decorate(ftxui::bold); + table.SelectRow(0).SeparatorVertical(ftxui::LIGHT); + table.SelectRow(0).Border(ftxui::DOUBLE); + + // Align right the "Release date" column. + table.SelectColumn(2).DecorateCells(ftxui::align_right); + + // Select row from the second to the last. + auto content = table.SelectRows(1, -1); + // Alternate in between 3 colors. + content.DecorateCellsAlternateRow(color(ftxui::Color::Blue), 3, 0); + content.DecorateCellsAlternateRow(color(ftxui::Color::Cyan), 3, 1); + content.DecorateCellsAlternateRow(color(ftxui::Color::White), 3, 2); + + return table.Render(); + } + + bool PackageList::OnEvent(ftxui::Event event) + { + if (event == ftxui::Event::Character('q') || event == ftxui::Event::Escape) + { + event.screen_->ExitLoopClosure()(); + } + else if (event == ftxui::Event::Character('j') || event == ftxui::Event::ArrowDown) + { + if (m_data.size() > 0 && top_line < (m_data.size() - 1)) + { + ++top_line; + } + } + else if (event == ftxui::Event::Character('k') || event == ftxui::Event::ArrowUp) + { + if (top_line > 0) + { + --top_line; + } + } + else + { + return false; + } + return true; + } +} diff --git a/src/component.h b/src/component.h new file mode 100644 index 0000000..d1a587a --- /dev/null +++ b/src/component.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace katja +{ + class PackageList final : public ftxui::ComponentBase + { + std::vector> m_data; + std::vector m_header; + std::size_t top_line{ 0 }; + + public: + PackageList(const std::vector& header, const std::vector>& data); + + virtual ftxui::Element Render() override; + virtual bool OnEvent(ftxui::Event event) override; + }; +} diff --git a/src/config.h b/src/config.h index c156081..5be3df7 100644 --- a/src/config.h +++ b/src/config.h @@ -1 +1,3 @@ +#pragma once + #define WORKDIR "./var" diff --git a/src/package.cpp b/src/package.cpp index 0734a5e..9fc99e4 100644 --- a/src/package.cpp +++ b/src/package.cpp @@ -73,4 +73,34 @@ namespace katja } return packages; } + + package_exception::package_exception(const std::string& package_name, const std::string& reason) noexcept + : m_message(package_name) + { + m_message.reserve(m_message.size() + 2 + reason.size()); + m_message += ": " + reason; + } + + const std::string_view package_exception::package_name() const noexcept + { + std::size_t colon_position = m_message.find(':'); + + if (colon_position == std::string::npos) + { + return std::string_view(m_message); + } + else if (colon_position == 0) + { + return std::string_view(""); + } + else + { + return std::string_view(m_message.c_str(), colon_position - 1); + } + } + + const char *package_exception::what() const noexcept + { + return m_message.c_str(); + } } diff --git a/src/package.h b/src/package.h index 5ffd060..109f065 100644 --- a/src/package.h +++ b/src/package.h @@ -1,6 +1,10 @@ +#pragma once + #include #include #include +#include +#include namespace katja { @@ -23,5 +27,24 @@ namespace katja static std::optional parse(const std::string& full_name) noexcept; }; + class repository + { + public: + virtual std::unordered_map list(const std::set& package_names) = 0; + virtual void refresh() = 0; + virtual std::forward_list check_dependencies(const std::string& package_name) = 0; + }; + std::unordered_map read_package_database(); + + class package_exception : public std::exception + { + std::string m_message; + + public: + explicit package_exception(const std::string& package_name, const std::string& reason) noexcept; + + const std::string_view package_name() const noexcept; + const char *what() const noexcept override; + }; } diff --git a/src/sbo.cpp b/src/sbo.cpp new file mode 100644 index 0000000..086ba79 --- /dev/null +++ b/src/sbo.cpp @@ -0,0 +1,232 @@ +#include "config.h" +#include "sbo.h" +#include +#include +#include + +namespace katja +{ + sbo::sbo() + : git_binary(boost::process::search_path("git")) + , cache_path(std::filesystem::path{ WORKDIR } / "sbo/repository") + { + } + + info sbo::parse_info_file( + const std::filesystem::path& package_directory) const + { + std::map> results; + std::filesystem::path info_path = cache_path + / package_directory + / package_directory.filename().replace_extension(".info"); + + if (!std::filesystem::exists(info_path)) + { + throw package_exception(package_directory.filename().string(), "Package doesn't exist."); + } + // Parse the info file. + std::ifstream info_file{ info_path, std::ios::in }; + if (!info_file.is_open()) + { + throw package_exception(package_directory.filename().string(), "Unable to open the .info file."); + } + std::string line; + std::string variable_name; + std::vector variable_values; + + while (std::getline(info_file, line)) + { + std::string::const_iterator current_char = line.cbegin(); + + if (variable_name.empty()) + { + current_char = std::find(current_char, line.cend(), '='); + if (current_char == line.cbegin() || current_char == line.cend()) + { + break; // Expected 'variable name' or '='. + } + ++current_char; + if (current_char == line.cend() || *current_char != '"') + { + break; // Expected '"'. + } + variable_name = std::string(line.cbegin(), current_char - 1); + + variable_values.push_back(""); + } + + for (current_char += 1; current_char != line.end(); ++current_char) + { + if (*current_char == ' ') + { + if (!variable_values.back().empty()) + { + variable_values.push_back(""); + } + } + else if (*current_char == '"') + { + if (variable_values.back().empty()) + { + variable_values.pop_back(); + } + results.insert_or_assign(variable_name, variable_values); + variable_name = ""; + variable_values.clear(); + break; + } + else if (*current_char == '\\') + { + break; + } + else + { + variable_values.back().push_back(*current_char); + } + } + if (current_char == line.cend()) + { + break; // Expected '\' or '"'. + } + } + info _info; + + _info.prgnam = results["PRGNAM"][0]; + _info.version = results["VERSION"][0]; + _info.homepage = results["HOMEPAGE"].size() == 0 ? "" : results["HOMEPAGE"][0]; + _info.download = results["DOWNLOAD"]; + _info.md5sum = results["DOWNLOAD"]; + _info.download_x86_64 = results["DOWNLOAD_X86_64"]; + _info.md5sum_x86_64 = results["MD5SUM_X86_64"]; + _info.requires = results["REQUIRES"]; + _info.maintainer = results["MAINTAINER"][0]; + _info.email = results["EMAIL"][0]; + + return _info; + } + + std::unordered_map sbo::collect_packages() const + { + std::unordered_map results; + + for (const auto& category_directory : std::filesystem::directory_iterator(cache_path)) + { + if (!category_directory.is_directory()) + { + continue; + } + for (const auto& package_directory : std::filesystem::directory_iterator(category_directory.path())) + { + results.insert({ package_directory.path().filename().string(), + category_directory.path().filename() }); + } + } + return results; + } + + std::unordered_map sbo::list(const std::set& package_names) + { + std::string line; + std::unordered_map packages; + boost::process::environment slackbuild_environment; + slackbuild_environment["PRINT_PACKAGE_NAME"] = "y"; + + std::unordered_map package_categories = collect_packages(); + + for (const std::string& package_name : package_names) + { + std::unordered_map::iterator package_category = + package_categories.find(package_name); + + if (package_category == package_categories.cend()) + { + continue; + } + std::filesystem::path package_directory_name{ package_category->first }; + std::filesystem::path slackbuild_cwd = cache_path + / package_category->second / package_directory_name; + + // Execute the SlackBuild to get package information. + std::filesystem::path slackbuild_file_name = + package_directory_name.replace_extension(".SlackBuild"); + boost::process::ipstream slackbuild_output; + + boost::process::child slackbuild_process("/bin/bash", slackbuild_file_name.native(), + slackbuild_environment, + boost::process::start_dir(slackbuild_cwd.native()), + boost::process::std_out > slackbuild_output, + boost::process::std_in.close(), + boost::process::std_err > boost::process::null); + + std::optional maybe_package; + if (slackbuild_process.running() && std::getline(slackbuild_output, line) && !line.empty()) + { + maybe_package = package::parse(std::filesystem::path(line).replace_extension("")); + } + slackbuild_process.terminate(); + + if (maybe_package.has_value()) + { + packages.insert({ maybe_package.value().name(), maybe_package.value() }); + } + } + return packages; + } + + void sbo::refresh() + { + std::filesystem::file_status repository_status = std::filesystem::status(cache_path); + + if (std::filesystem::exists(repository_status) + && !std::filesystem::is_directory(repository_status)) + { + throw std::runtime_error("The working directory path \"" + + cache_path.string() + "\" exists, but it isn't a directory."); + } + else if (!std::filesystem::exists(repository_status)) + { + git("clone", std::filesystem::path(), + "git://git.slackbuilds.org/slackbuilds.git", cache_path.native()); + } + git("remote", cache_path.native(), "update", "--prune"); + git("reset", cache_path.native(), "--hard", "origin/master"); + } + + std::forward_list sbo::check_dependencies(const std::string& package_name) + { + std::unordered_map package_categories = collect_packages(); + ordered_set resolved; + + resolve_dependencies(package_name, package_categories, resolved); + std::forward_list results; + + for (const auto& dependency : resolved.get<1>()) + { + results.push_front(dependency); + } + return results; + } + + void sbo::resolve_dependencies(const std::string& package_name, + const std::unordered_map& package_categories, + ordered_set& resolved) const + { + std::unordered_map::const_iterator package_category = + package_categories.find(package_name); + + if (package_category == package_categories.cend()) + { + throw package_exception(package_name, "Package not found."); + } + info info_file = parse_info_file(package_category->second / package_category->first); + + for (const auto& package_dependency : info_file.requires) + { + if (resolved.find(package_dependency) == resolved.cend()) + { + resolve_dependencies(package_dependency, package_categories, resolved); + } + } + resolved.insert(package_name); + } +} diff --git a/src/sbo.h b/src/sbo.h new file mode 100644 index 0000000..95a1c53 --- /dev/null +++ b/src/sbo.h @@ -0,0 +1,73 @@ +#pragma once + +#include "package.h" +#include +#include +#include +#include +#include +#include + +namespace katja +{ + struct info + { + std::string prgnam; + std::string version; + std::string homepage; + std::vector download; + std::vector md5sum; + std::vector download_x86_64; + std::vector md5sum_x86_64; + std::vector requires; + std::string maintainer; + std::string email; + }; + + class sbo : public repository + { + using ordered_set = boost::multi_index_container< + std::string, + boost::multi_index::indexed_by< + boost::multi_index::ordered_unique>, + boost::multi_index::sequenced<> + > + >; + boost::filesystem::path git_binary; + std::filesystem::path cache_path; + + template + void git(const std::string& command, const std::filesystem::path& cwd, const Args&... args) const + { + if (cwd.empty()) + { + boost::process::system(git_binary, command, args...); + } + else + { + boost::process::system(git_binary, "-C", cwd.native(), command, args...); + } + } + + info parse_info_file(const std::filesystem::path& package_directory) const; + + /** + * Returns the list of packages in the repository. + * + * @return A map containing package names as key, and its category + * directory name as value. + */ + std::unordered_map collect_packages() const; + + void resolve_dependencies(const std::string& package_name, + const std::unordered_map& package_categories, + ordered_set& resolved) const; + + public: + explicit sbo(); + + virtual std::unordered_map list(const std::set& package_names) override; + virtual void refresh() override; + virtual std::forward_list check_dependencies(const std::string& package_name) override; + }; +}