summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugen Wissner <belka@caraus.de>2023-04-15 08:43:30 +0200
committerEugen Wissner <belka@caraus.de>2023-04-15 08:43:30 +0200
commit34b10f41aa285e423cccb161342b68ae7275da4b (patch)
treee021c107400ec467b59a019c45d6659110c677cf
parentdbf14caee2f3ffbcfb21d5ca4d1566e0f57a1aed (diff)
downloadslackbuilder-34b10f41aa285e423cccb161342b68ae7275da4b.tar.gz
Retrieve updatable packages
-rw-r--r--CMakeLists.txt43
-rw-r--r--build.ninja16
-rw-r--r--src/command.cpp67
-rw-r--r--src/command.h21
-rw-r--r--src/component.cpp76
-rw-r--r--src/component.h20
-rw-r--r--src/config.h2
-rw-r--r--src/package.cpp30
-rw-r--r--src/package.h23
-rw-r--r--src/sbo.cpp232
-rw-r--r--src/sbo.h73
11 files changed, 541 insertions, 62 deletions
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 <cassert>
#include "config.h"
+#include "sbo.h"
+#include <ftxui/dom/elements.hpp>
+#include <ftxui/component/screen_interactive.hpp>
+#include "component.h"
namespace katja
{
void list::execute() const
{
- for (const auto& package : katja::read_package_database())
+ sbo sbo_repository;
+ std::unordered_map<std::string, package> packages = katja::read_package_database();
+ std::vector<std::vector<std::string>> table_data;
+ std::set<std::string> installed_packages;
+ std::unordered_map<std::string, package> 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<PackageList>(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 <iostream>
#include <cstring>
#include <memory>
#include <vector>
#include "package.h"
-#include <boost/process.hpp>
-#include <filesystem>
namespace katja
{
@@ -28,24 +28,7 @@ namespace katja
class update final : public command
{
- boost::filesystem::path git_binary;
-
- template<typename... Args>
- 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 <ftxui/component/screen_interactive.hpp>
+#include "component.h"
+
+namespace katja
+{
+ PackageList::PackageList(const std::vector<std::string>& header, const std::vector<std::vector<std::string>>& data)
+ : m_data(data), m_header(header)
+ {
+ }
+
+ ftxui::Element PackageList::Render()
+ {
+ std::vector<std::vector<std::string>> 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 <ftxui/component/component.hpp>
+#include <ftxui/dom/table.hpp>
+
+namespace katja
+{
+ class PackageList final : public ftxui::ComponentBase
+ {
+ std::vector<std::vector<std::string>> m_data;
+ std::vector<std::string> m_header;
+ std::size_t top_line{ 0 };
+
+ public:
+ PackageList(const std::vector<std::string>& header, const std::vector<std::vector<std::string>>& 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 <string>
#include <optional>
#include <unordered_map>
+#include <set>
+#include <forward_list>
namespace katja
{
@@ -23,5 +27,24 @@ namespace katja
static std::optional<package> parse(const std::string& full_name) noexcept;
};
+ class repository
+ {
+ public:
+ virtual std::unordered_map<std::string, package> list(const std::set<std::string>& package_names) = 0;
+ virtual void refresh() = 0;
+ virtual std::forward_list<std::string> check_dependencies(const std::string& package_name) = 0;
+ };
+
std::unordered_map<std::string, package> 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 <algorithm>
+#include <fstream>
+#include <vector>
+
+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<std::string, std::vector<std::string>> 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<std::string> 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<std::string, std::filesystem::path> sbo::collect_packages() const
+ {
+ std::unordered_map<std::string, std::filesystem::path> 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<std::string, package> sbo::list(const std::set<std::string>& package_names)
+ {
+ std::string line;
+ std::unordered_map<std::string, package> packages;
+ boost::process::environment slackbuild_environment;
+ slackbuild_environment["PRINT_PACKAGE_NAME"] = "y";
+
+ std::unordered_map<std::string, std::filesystem::path> package_categories = collect_packages();
+
+ for (const std::string& package_name : package_names)
+ {
+ std::unordered_map<std::string, std::filesystem::path>::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<package> 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<std::string> sbo::check_dependencies(const std::string& package_name)
+ {
+ std::unordered_map<std::string, std::filesystem::path> package_categories = collect_packages();
+ ordered_set resolved;
+
+ resolve_dependencies(package_name, package_categories, resolved);
+ std::forward_list<std::string> 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<std::string, std::filesystem::path>& package_categories,
+ ordered_set& resolved) const
+ {
+ std::unordered_map<std::string, std::filesystem::path>::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 <boost/process.hpp>
+#include <filesystem>
+#include <map>
+#include <boost/multi_index_container.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index/sequenced_index.hpp>
+
+namespace katja
+{
+ struct info
+ {
+ std::string prgnam;
+ std::string version;
+ std::string homepage;
+ std::vector<std::string> download;
+ std::vector<std::string> md5sum;
+ std::vector<std::string> download_x86_64;
+ std::vector<std::string> md5sum_x86_64;
+ std::vector<std::string> 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::identity<std::string>>,
+ boost::multi_index::sequenced<>
+ >
+ >;
+ boost::filesystem::path git_binary;
+ std::filesystem::path cache_path;
+
+ template<typename... Args>
+ 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<std::string, std::filesystem::path> collect_packages() const;
+
+ void resolve_dependencies(const std::string& package_name,
+ const std::unordered_map<std::string, std::filesystem::path>& package_categories,
+ ordered_set& resolved) const;
+
+ public:
+ explicit sbo();
+
+ virtual std::unordered_map<std::string, package> list(const std::set<std::string>& package_names) override;
+ virtual void refresh() override;
+ virtual std::forward_list<std::string> check_dependencies(const std::string& package_name) override;
+ };
+}