From fc8f6c07b463526b03a77a1ecd4b0984bc721962 Mon Sep 17 00:00:00 2001 From: Gurkirat Singh Date: Thu, 21 Dec 2023 16:02:33 +0530 Subject: [PATCH] Utilities/pkg: Integrate LibSemVer for update information Along with this, Port.h is include which helps generalising common information for the port package, like it's name and version. With SemVer complaint versions, it is possible to show positive change (upgrade) or negative change (downgrade) in the installed ports. However, for some non-complaint versions (eg. using git commit hash), non-equality (`!=`) is used to notify upgrade. Since there is no algorithm (without git history) to check the order of commits, it is not possible to inform whether it is an upgrade or downgrade. --- Userland/Utilities/pkg/AvailablePort.cpp | 66 +++++++++++++--------- Userland/Utilities/pkg/AvailablePort.h | 15 ++--- Userland/Utilities/pkg/CMakeLists.txt | 2 +- Userland/Utilities/pkg/InstalledPort.cpp | 25 ++++----- Userland/Utilities/pkg/InstalledPort.h | 19 ++++--- Userland/Utilities/pkg/Port.h | 70 ++++++++++++++++++++++++ Userland/Utilities/pkg/main.cpp | 62 ++++++++++++++------- 7 files changed, 176 insertions(+), 83 deletions(-) create mode 100644 Userland/Utilities/pkg/Port.h diff --git a/Userland/Utilities/pkg/AvailablePort.cpp b/Userland/Utilities/pkg/AvailablePort.cpp index 4acb4e1919..79f0c4a66c 100644 --- a/Userland/Utilities/pkg/AvailablePort.cpp +++ b/Userland/Utilities/pkg/AvailablePort.cpp @@ -28,44 +28,56 @@ #include #include -static bool is_installed(HashMap& installed_ports_database, StringView package_name) +void AvailablePort::query_details_for_package(HashMap& available_ports, HashMap const& installed_ports, StringView package_name, bool verbose) { - auto port = installed_ports_database.find(package_name); - return port != installed_ports_database.end(); -} - -static Optional find_port_package(HashMap& available_ports, StringView package_name) -{ - auto port = available_ports.find(package_name); - if (port == available_ports.end()) - return {}; - return port->value; -} - -ErrorOr AvailablePort::query_details_for_package(HashMap& available_ports, HashMap& installed_ports, StringView package_name, bool verbose) -{ - auto possible_available_port = find_port_package(available_ports, package_name); - if (!possible_available_port.has_value()) { + auto possible_available_port = available_ports.find(package_name); + if (possible_available_port == available_ports.end()) { outln("pkg: No match for queried name \"{}\"", package_name); - return 0; + return; } - auto& available_port = possible_available_port.release_value(); + auto& available_port = possible_available_port->value; - outln("{}: {}, {}", available_port.name(), available_port.version(), available_port.website()); + outln("{}: {}, {}", available_port.name(), available_port.version_string(), available_port.website()); if (verbose) { out("Installed: "); - if (is_installed(installed_ports, package_name)) + auto installed_port = installed_ports.find(package_name); + if (installed_port != installed_ports.end()) { outln("Yes"); - else + + out("Update Status: "); + + auto error_or_available_version = available_port.version_semver(); + auto error_or_installed_version = installed_port->value.version_semver(); + + if (error_or_available_version.is_error() || error_or_installed_version.is_error()) { + auto ip_version = installed_port->value.version_string(); + auto ap_version = available_port.version_string(); + + if (ip_version == ap_version) { + outln("Already on latest version"); + } else { + outln("Update to {} available", ap_version); + } + return; + } + + auto available_version = error_or_available_version.value(); + auto installed_version = error_or_installed_version.value(); + if (available_version.is_same(installed_version, SemVer::CompareType::Patch)) { + outln("Already on latest version"); + } else if (available_version.is_greater_than(installed_version)) { + outln("Update to {} available", available_port.version_string()); + } + } else { outln("No"); + } } - return 0; } static Optional get_column_in_table(Markdown::Table const& ports_table, StringView column_name) { - for (auto& column : ports_table.columns()) { + for (auto const& column : ports_table.columns()) { if (column_name == column.header.render_for_terminal()) return column; } @@ -151,9 +163,9 @@ ErrorOr> AvailablePort::read_available_ports_list if (!possible_port_website_column.has_value()) return Error::from_string_literal("pkg: Website column not found /usr/Ports/AvailablePorts.md"); - auto& port_name_column = possible_port_name_column.release_value(); - auto& port_version_column = possible_port_version_column.release_value(); - auto& port_website_column = possible_port_website_column.release_value(); + auto const& port_name_column = possible_port_name_column.release_value(); + auto const& port_version_column = possible_port_version_column.release_value(); + auto const& port_website_column = possible_port_website_column.release_value(); VERIFY(port_name_column.rows.size() == port_version_column.rows.size()); VERIFY(port_version_column.rows.size() == port_website_column.rows.size()); diff --git a/Userland/Utilities/pkg/AvailablePort.h b/Userland/Utilities/pkg/AvailablePort.h index 6d65ff28b0..570e01a6d3 100644 --- a/Userland/Utilities/pkg/AvailablePort.h +++ b/Userland/Utilities/pkg/AvailablePort.h @@ -12,25 +12,20 @@ #include #include -class AvailablePort { +class AvailablePort : public Port { public: - static ErrorOr query_details_for_package(HashMap& available_ports, HashMap& installed_ports, StringView package_name, bool verbose); + static void query_details_for_package(HashMap& available_ports, HashMap const& installed_ports, StringView package_name, bool verbose); static ErrorOr> read_available_ports_list(); static ErrorOr update_available_ports_list_file(); - AvailablePort(String name, String version, String website) - : m_name(name) - , m_website(move(website)) - , m_version(move(version)) + AvailablePort(String const& name, String const& version, String const& website) + : Port(name, version) + , m_website(website) { } - StringView name() const { return m_name.bytes_as_string_view(); } - StringView version() const { return m_version.bytes_as_string_view(); } StringView website() const { return m_website.bytes_as_string_view(); } private: - String m_name; String m_website; - String m_version; }; diff --git a/Userland/Utilities/pkg/CMakeLists.txt b/Userland/Utilities/pkg/CMakeLists.txt index 276ce18d77..88fcdcacf5 100644 --- a/Userland/Utilities/pkg/CMakeLists.txt +++ b/Userland/Utilities/pkg/CMakeLists.txt @@ -12,4 +12,4 @@ set(SOURCES ) serenity_app(PackageManager ICON app-assistant) -target_link_libraries(PackageManager PRIVATE LibCore LibMain LibFileSystem LibProtocol LibHTTP LibMarkdown LibShell) +target_link_libraries(PackageManager PRIVATE LibCore LibSemVer LibMain LibFileSystem LibProtocol LibHTTP LibMarkdown LibShell) diff --git a/Userland/Utilities/pkg/InstalledPort.cpp b/Userland/Utilities/pkg/InstalledPort.cpp index 8496929eec..34037c5dcb 100644 --- a/Userland/Utilities/pkg/InstalledPort.cpp +++ b/Userland/Utilities/pkg/InstalledPort.cpp @@ -38,29 +38,24 @@ ErrorOr> InstalledPort::read_ports_database() // FIXME: Skip over invalid entries instead? return Error::from_string_view("Database entry too short"sv); } - auto string_type = parts[0]; - auto name = TRY(String::from_utf8(parts[1])); + auto install_type_string = parts[0]; + auto port_name = TRY(String::from_utf8(parts[1])); - if (auto maybe_type = type_from_string(string_type); maybe_type.has_value()) { + if (auto maybe_type = type_from_string(install_type_string); maybe_type.has_value()) { auto const type = maybe_type.release_value(); if (parts.size() < 3) return Error::from_string_view("Port is missing a version specification"sv); - auto version = TRY(String::from_utf8(parts[2])); - auto& port = ports.ensure(name, [&] { return InstalledPort { name, {}, {} }; }); - port.m_type = type; - port.m_version = move(version); - } else if (string_type == "dependency"sv) { - // Accept an empty dependency list. - auto dependency_views = parts.span().slice(2); + ports.ensure(port_name, [=] { return InstalledPort { port_name, MUST(String::from_utf8(parts[2])), type }; }); + } else if (install_type_string == "dependency"sv) { Vector dependencies; - TRY(dependencies.try_ensure_capacity(dependency_views.size())); - for (auto const& view : dependency_views) - dependencies.unchecked_append(TRY(String::from_utf8(view))); - + TRY(dependencies.try_ensure_capacity(parts.size() - 2)); + for (auto const& dependency : parts.span().slice(2)) { + dependencies.unchecked_append(TRY(String::from_utf8(dependency))); + } // Assume the port as automatically installed if the "dependency" line occurs before the "manual"/"auto" line. // This is fine since these entries override the port type in any case. - auto& port = ports.ensure(name, [&] { return InstalledPort { name, Type::Auto, {} }; }); + auto& port = ports.ensure(port_name, [&] { return InstalledPort { port_name, {}, Type::Auto }; }); port.m_dependencies = move(dependencies); } else { return Error::from_string_literal("Unknown installed port type"); diff --git a/Userland/Utilities/pkg/InstalledPort.h b/Userland/Utilities/pkg/InstalledPort.h index d38d89a8fd..cee70d1e23 100644 --- a/Userland/Utilities/pkg/InstalledPort.h +++ b/Userland/Utilities/pkg/InstalledPort.h @@ -6,14 +6,17 @@ #pragma once +#include "Port.h" #include #include #include #include +#include +#include constexpr StringView ports_database = "/usr/Ports/installed.db"sv; -class InstalledPort { +class InstalledPort : public Port { public: enum class Type { Auto, @@ -24,14 +27,16 @@ public: static ErrorOr> read_ports_database(); static ErrorOr for_each_by_type(HashMap&, Type type, Function(InstalledPort const&)> callback); - InstalledPort(String name, Type type, String version) - : m_name(move(name)) + InstalledPort(String const& name, String const& version, Type type) + : Port(name, version) , m_type(type) - , m_version(move(version)) { } - Type type() const { return m_type; } + Type type() const + { + return m_type; + } StringView type_as_string_view() const { if (m_type == Type::Auto) @@ -41,13 +46,9 @@ public: VERIFY_NOT_REACHED(); } - StringView name() const { return m_name.bytes_as_string_view(); } - StringView version() const { return m_version.bytes_as_string_view(); } ReadonlySpan dependencies() const { return m_dependencies.span(); } private: - String m_name; Type m_type; - String m_version; Vector m_dependencies; }; diff --git a/Userland/Utilities/pkg/Port.h b/Userland/Utilities/pkg/Port.h new file mode 100644 index 0000000000..5b0659a86b --- /dev/null +++ b/Userland/Utilities/pkg/Port.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023, Gurkirat Singh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +constexpr auto normal_version_separators = ".-"sv; + +class Port { + +public: + [[nodiscard]] String name() const { return m_name; } + [[nodiscard]] String version_string() const { return m_version.has() ? m_version.get() : m_version.get().to_string(); } + [[nodiscard]] ErrorOr version_semver() const + { + if (m_version.has()) + return Error::from_string_view("This does not have semver"sv); + return m_version.get(); + } + + Port(String const& name, String const& version) + : m_name(name) + , m_version(version) + { + set_version(version); + } + Port(String const& name, String const& version, char normal_version_separator) + : m_name(name) + , m_version(version) + { + set_version(version, normal_version_separator); + } + + void set_name(String const& name) + { + if (!name.is_empty()) + m_name = name; + } + + void set_version(StringView const& version) + { + for (auto const& normal_version_separator : normal_version_separators) { + auto semver_parsed = SemVer::from_string_view(version, normal_version_separator); + if (!semver_parsed.is_error()) { + m_version = semver_parsed.value(); + return; + } + } + + m_version = MUST(String::from_utf8(version)); + } + + void set_version(StringView const& version, char normal_version_separator) + { + // If the user has provided the separator, it is safe to assume that they are certain about it. + // Therefore, it is ideal to crash, indicating that their assumption is incorrect. + m_version = MUST(SemVer::from_string_view(version, normal_version_separator)); + } + +private: + String m_name; + Variant m_version; +}; diff --git a/Userland/Utilities/pkg/main.cpp b/Userland/Utilities/pkg/main.cpp index 0079f7c515..157a71a671 100644 --- a/Userland/Utilities/pkg/main.cpp +++ b/Userland/Utilities/pkg/main.cpp @@ -11,9 +11,30 @@ #include #include -static void print_port_details(InstalledPort const& port) +static void print_port_details(InstalledPort const& port, Optional const& available_port) { - outln("{}, installed as {}, version {}", port.name(), port.type_as_string_view(), port.version()); + out("{}, installed as {}, version {}", port.name(), port.type_as_string_view(), port.version_string()); + if (available_port.has_value()) { + auto const& upstream_port = available_port.value(); + auto const& upstream_version = upstream_port.version_semver(); + auto const& this_version = port.version_semver(); + + if ((this_version.is_error() || upstream_version.is_error())) { + if (upstream_port.version_string() != port.version_string()) { + outln(" (upgrade available -> {})", upstream_port.version_string()); + } + } else { + auto const& ap_version = upstream_version.value(); + auto const& ip_version = this_version.value(); + if (ip_version.is_same(ap_version)) { + outln(" (already on latest version)"); + } else if (ip_version.is_lesser_than(ap_version)) { + outln(" (upgrade available {})", ap_version.to_string()); + } + } + } else { + outln(); + } if (!port.dependencies().is_empty()) { out(" Dependencies:"); @@ -51,16 +72,6 @@ ErrorOr serenity_main(Main::Arguments arguments) return 0; } - HashMap installed_ports; - HashMap available_ports; - if (show_all_installed_ports || !query_package.is_null()) { - if (Core::System::access(ports_database, R_OK).is_error()) { - warnln("pkg: {} isn't accessible, did you install a package in the past?", ports_database); - return 1; - } - installed_ports = TRY(InstalledPort::read_ports_database()); - } - int return_value = 0; if (update_packages_db) { if (getuid() != 0) { @@ -70,18 +81,27 @@ ErrorOr serenity_main(Main::Arguments arguments) return_value = TRY(AvailablePort::update_available_ports_list_file()); } - if (!query_package.is_null()) { - if (Core::System::access("/usr/Ports/AvailablePorts.md"sv, R_OK).is_error()) { - outln("pkg: Please run this program with -u first!"); - return 0; - } - available_ports = TRY(AvailablePort::read_available_ports_list()); + if (Core::System::access(ports_database, R_OK).is_error()) { + warnln("pkg: {} isn't accessible, did you install a package in the past?", ports_database); + return 1; } + HashMap installed_ports = TRY(InstalledPort::read_ports_database()); + + if (Core::System::access("/usr/Ports/AvailablePorts.md"sv, R_OK).is_error()) { + outln("pkg: Please run this program with -u first!"); + return 0; + } + HashMap available_ports = TRY(AvailablePort::read_available_ports_list()); if (show_all_installed_ports) { outln("Manually-installed ports:"); - TRY(InstalledPort::for_each_by_type(installed_ports, InstalledPort::Type::Manual, [](auto& port) -> ErrorOr { - print_port_details(port); + TRY(InstalledPort::for_each_by_type(installed_ports, InstalledPort::Type::Manual, [available_ports](InstalledPort const& port) -> ErrorOr { + auto available_port = available_ports.find(port.name()); + if (available_port != available_ports.end()) { + print_port_details(port, available_port->value); + } else { + print_port_details(port, {}); + } return {}; })); } @@ -91,7 +111,7 @@ ErrorOr serenity_main(Main::Arguments arguments) outln("pkg: Queried package name is empty."); return 0; } - return_value = TRY(AvailablePort::query_details_for_package(available_ports, installed_ports, query_package, verbose)); + AvailablePort::query_details_for_package(available_ports, installed_ports, query_package, verbose); } return return_value;