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;