From 4bbd8facc01a313f3801f2e5c71e213d73dc6663 Mon Sep 17 00:00:00 2001 From: Ophelia Beatrice de Sica Date: Tue, 10 Feb 2026 16:05:21 +0100 Subject: [PATCH] Add command line features --- bin/pkgupdates | 31 ++++++++++ pkgupdates.yaml | 5 ++ src/pkgupdates/__main__.py | 25 ++++++-- src/pkgupdates/package.py | 83 ++++++++++++++++++++++--- src/pkgupdates/remote.py | 120 ++++++++++++++++++++++++++++++------- src/pkgupdates/settings.py | 52 ++++++++++++++++ src/pkgupdates/version.py | 25 +++++++- 7 files changed, 306 insertions(+), 35 deletions(-) create mode 100755 bin/pkgupdates create mode 100644 pkgupdates.yaml diff --git a/bin/pkgupdates b/bin/pkgupdates new file mode 100755 index 0000000..a0cdf3f --- /dev/null +++ b/bin/pkgupdates @@ -0,0 +1,31 @@ +#!/usr/bin/python3.13 +# -*- coding: utf-8 -*- +import re +import sys +import os +from os.path import dirname, normpath, realpath + +LAUNCH_PATH = dirname(realpath(__file__)) + +# Prevent loading Python modules from home folder +# They can interfere with Lutris and prevent it +# from working. +sys.path = [path for path in sys.path if not path.startswith("/home") + and not path.startswith("/var/home")] + +if os.path.isdir(os.path.join(LAUNCH_PATH, "../src/pkgupdates")): + sys.dont_write_bytecode = True + SOURCE_PATH = normpath(os.path.join(LAUNCH_PATH, "../src")) + sys.path.insert(0, SOURCE_PATH) +else: + sys.path.insert(0, os.path.normpath(os.path.join(LAUNCH_PATH, + "../lib/pkgupdates"))) + +try: + from pkgupdates import main +except ImportError as ex: + sys.stderr.write(f"Error importing pkgupdates module: {ex}\n") + sys.exit(1) + +sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) +sys.exit(main()) diff --git a/pkgupdates.yaml b/pkgupdates.yaml new file mode 100644 index 0000000..1b741a5 --- /dev/null +++ b/pkgupdates.yaml @@ -0,0 +1,5 @@ +only-in-tree: true +remote: + github: + rss-get: true + diff --git a/src/pkgupdates/__main__.py b/src/pkgupdates/__main__.py index ddacb5e..679231b 100644 --- a/src/pkgupdates/__main__.py +++ b/src/pkgupdates/__main__.py @@ -1,8 +1,8 @@ """Main Module""" import argparse -from .package import get_packages, check_package -from .settings import set_gh_rss_get +from .package import get_packages, check_package, get_packages_from_tree +from .settings import set_gh_rss_get, set_verbose, set_only_in_tree, get_only_in_tree, get_verbose, get_gh_rss_get, load_config def process_arguments(): @@ -13,18 +13,35 @@ def process_arguments(): parser.add_argument('--github-rss-get', type=bool, - default=False, + default=get_gh_rss_get(), required=False, help='Get updates for Github over RSS instead of API') + parser.add_argument('--only-in-tree', + type=bool, + default=get_only_in_tree(), + required=False, + help='Only search for updates ebuilds in git tree') + parser.add_argument('--verbose', + type=bool, + default=get_verbose(), + required=False, + help='Verbose output') args = parser.parse_args() set_gh_rss_get(args.github_rss_get) + set_only_in_tree(args.only_in_tree) + set_verbose(args.verbose) def main(): """Main Function""" + load_config() + process_arguments() - pkgs = get_packages() + if get_only_in_tree(): + pkgs = get_packages_from_tree() + else: + pkgs = get_packages() for pkg, meta in pkgs.items(): check_package(pkg, meta) diff --git a/src/pkgupdates/package.py b/src/pkgupdates/package.py index 9ee0fac..06564d7 100755 --- a/src/pkgupdates/package.py +++ b/src/pkgupdates/package.py @@ -6,8 +6,10 @@ import collections import re import xml.etree.ElementTree as ET import datetime -from .version import parse_version, EVersion, SemanticVersion, DateVersion +import git +from .version import parse_version, EVersion, SemanticVersion, DateVersion, CounterVersion, VCSVersion from .remote import ERemote, Remote +from .settings import get_verbose rex_pkg_version = re.compile("-([0-9.]+)([_a-z0-9]+)*(-r[0-9]+)?[.]ebuild") @@ -16,6 +18,36 @@ PkgVersion = collections.namedtuple("PkgVersion", ["name", "category", "version"] ) +def get_packages_from_tree(): + """Get files from git tree""" + res = {} + repo = git.Repo(os.getcwd()) + if repo.bare: + return + for entry in repo.commit().tree.traverse(): + file = entry.path + root = os.path.dirname(file) + if not file.endswith('.ebuild'): + continue + + p1 = os.path.split(root) + p2 = os.path.split(p1[0]) + pkg = os.path.join(p2[1], p1[1]) + match = rex_pkg_version.search(file) + if match is None: + print(file) + continue + + if pkg in res: + res[pkg]["versions"].append(match.groups()[0]) + else: + metadata = get_metadata(root) + if metadata is None: + metadata = {} + res[pkg] = metadata + res[pkg]["versions"] = [match.groups()[0]] + return res + def get_packages(): """Scan current directory for ebuild packages""" @@ -70,6 +102,8 @@ def get_package_versions(meta): eversion = EVersion.UNKNOWN is_semantic = False is_date = False + is_counter = False + is_vcs = False if "versions" in meta: for version in meta["versions"]: @@ -78,11 +112,19 @@ def get_package_versions(meta): is_semantic = True if isinstance(version_obj, DateVersion): is_date = True + if isinstance(version_obj, CounterVersion): + is_counter = True + if isinstance(version_obj, VCSVersion): + is_vcs = True versions.append(version_obj) if is_date: eversion = EVersion.DATE elif is_semantic: eversion = EVersion.SEMANTIC + elif is_counter: + eversion = EVersion.COUNTER + elif is_vcs: + eversion = EVersion.VCS return versions, eversion @@ -99,10 +141,12 @@ def check_package(pkg, meta): # pylint: disable=R0911 match (version_type): case EVersion.DATE: if remote.TYPE == ERemote.UNKNOWN: - print(pkg + ": Unsupported backend: " + pkg_type) + print(f"{pkg}: Unsupported backend: {pkg_type}") return if not remote.support_latest_commit(): + print(f"{pkg}: Error: {pkg_type} " + "date version check not implemented!") return version = remote.get_latest_commit(meta["remote-id"]) @@ -115,32 +159,55 @@ def check_package(pkg, meta): # pylint: disable=R0911 case EVersion.SEMANTIC: if remote.TYPE == ERemote.UNKNOWN: - print(pkg + ": Unsupported backend: " + pkg_type) + print(f"{pkg}: Unsupported backend: {pkg_type}") return if not remote.support_latest_release(): + print(f"{pkg}: Error: {pkg_type} " + "semantic version check not implemented!") return tag = remote.get_latest_release(meta["remote-id"]) if tag is None or tag == "": - print(pkg + ": HTTPError! " + meta["remote-id"]) + print(f"{pkg}: HTTPError! {meta['remote-id']}") return latest_version = parse_version(tag) case EVersion.VCS: - print(pkg + ": VCS package") + print(f"{pkg}: VCS package") return + # TODO: + case EVersion.COUNTER: + if remote.TYPE == ERemote.UNKNOWN: + print(f"{pkg}: Unsupported backend: {pkg_type}") + return + + if not remote.support_latest_release(): + print(f"{pkg}: Error: {pkg_type} " + "counter version check not implemented!") + return + + tag = remote.get_latest_release(meta["remote-id"]) + + if tag is None or tag == "": + print(f"{pkg}: HTTPError! {meta['remote-id']}") + return + + latest_version = parse_version(tag) + case EVersion.UNKNOWN: - print(pkg + ": Manual Compare") + versions_str = ', '.join(str(ver.version) for ver in versions) + print(f"{pkg}: Manual Compare ({versions_str})") return is_new = True for pkg_version in versions: is_new = is_new and (not pkg_version.compare(latest_version)) if is_new: - print(pkg + ": New Version: " + latest_version.version) - else: + versions_str = ', '.join(str(ver.version) for ver in versions) + print(f"{pkg}: New Version: {latest_version.version} ({versions_str})") + elif get_verbose(): print(pkg + ": OK") diff --git a/src/pkgupdates/remote.py b/src/pkgupdates/remote.py index 3067031..8b7d735 100644 --- a/src/pkgupdates/remote.py +++ b/src/pkgupdates/remote.py @@ -2,9 +2,25 @@ import urllib.request import re +import xml +from datetime import datetime from enum import Enum from xml.dom import minidom import requests +from sourcehut.client import ( + SRHT_SERVICE, + APIVersion, + SrhtClient, + _FileUpload, + _get_upload_data, +) +from sourcehut.services.builds import BuildsSrhtClient +from sourcehut.services.git import GitSrhtClient +from sourcehut.services.lists import ListsSrhtClient +from sourcehut.services.meta import MetaSrhtClient +from sourcehut.services.pages import PagesSrhtClient +from sourcehut.services.paste import PasteSrhtClient +from sourcehut.services.todo import TodoSrhtClient from github import GithubException, Github as GithubApi from .version import rex_semantic from .settings import get_gh_rss_get @@ -115,29 +131,25 @@ class Remote: @classmethod def support_latest_release(cls): """Check if remote has latest release implemented""" - if cls.get_latest_release == Remote.get_latest_release: - print(f"Error: {cls.NAME} semantic version check not implemented!") - return False - return True + return cls.get_latest_release != Remote.get_latest_release @classmethod def support_latest_commit(cls): """Check if remote has latest commit implemented""" - if cls.get_latest_commit == Remote.get_latest_commit: - print(f"Error: {cls.NAME} date version check not implemented!") - return False - return True + return cls.get_latest_commit != Remote.get_latest_commit @classmethod - def get_latest_release(cls, pkg_repo): # pylint: disable=unused-argument + def get_latest_release(cls, pkg_repo): """Get latest release - mostly for Semantic versioning""" - print(f"Error: {cls.NAME} semantic version check not implemented!") + print(f"{pkg_repo}: Error: {cls.NAME} " + "semantic version check not implemented!") return "" @classmethod - def get_latest_commit(cls, pkg_repo): # pylint: disable=unused-argument + def get_latest_commit(cls, pkg_repo): """Get latest commit - mostly for date versioning""" - print(f"Error: {cls.NAME} date version check not implemented!") + print(f"{pkg_repo}: Error: {cls.NAME} " + "date version check not implemented!") return "" @@ -153,8 +165,8 @@ class Bitbucket(Remote): """Download releases""" version = "" - url = "https://api.bitbucket.org/2.0/repositories/" + pkg_repo - url += "/refs/tags?sort=-target.date" + url = ((f"https://api.bitbucket.org/2.0/repositories/{pkg_repo}" + "/refs/tags?sort=-target.date")) response = requests.get(url, timeout=5) @@ -338,7 +350,7 @@ class Github(Remote): """ if get_gh_rss_get(): - url = "https://github.com/" + pkg_repo + "/commits.atom" + url = f"https://github.com/{pkg_repo}/commits.atom" try: with urllib.request.urlopen(url) as req: feed = req.read() @@ -367,7 +379,7 @@ class Github(Remote): """ if get_gh_rss_get(): - url = "https://github.com/" + pkg_repo + "/tags.atom" + url = f"https://github.com/{pkg_repo}/tags.atom" try: with urllib.request.urlopen(url) as req: feed = req.read() @@ -414,7 +426,7 @@ class Gitlab(Remote): @classmethod def get_latest_release(cls, pkg_repo): ident = pkg_repo.replace("/", "%2F") - url = cls.URL + "/projects/" + ident + "/repository/tags" + url = f"{cls.URL}/projects/{ident}/repository/tags" version = "" try: with requests.get(url, timeout=5) as resp: @@ -428,7 +440,7 @@ class Gitlab(Remote): @classmethod def get_latest_commit(cls, pkg_repo): ident = pkg_repo.replace("/", "%2F") - url = cls.URL + "/projects/" + ident + "/repository/commits" + url = f"{cls.URL}/projects/{ident}/repository/commits" version = "" try: with requests.get(url, timeout=5) as resp: @@ -558,7 +570,6 @@ class Pecl(Remote): # TODO: get_latest_commit -# TODO: get_latest_release class Pypi(Remote): """Remote for pypi.org hosted Python projects @@ -567,6 +578,48 @@ class Pypi(Remote): TYPE = ERemote.PYPI NAME = "pypi" + @classmethod + def get_latest_release(cls, pkg_repo): + """Download releases + + Downloads the releases from the RSS feed. + """ + + if get_gh_rss_get(): + url = f"https://pypi.org/rss/project/{pkg_repo}/releases.xml" + try: + with urllib.request.urlopen(url) as req: + feed = req.read() + except urllib.request.HTTPError: + return "" + dom = minidom.parseString(feed) + nodelist = dom.getElementsByTagName("rss") + nodes = ["channel", "item", "title"] + for node in nodes: + if len(nodelist) < 1: + return "" + nodelist = nodelist[0].getElementsByTagName(node) + if len(nodelist) < 1: + return "" + version = nodelist[0].firstChild.nodeValue + + else: + version = "" + url = f"https://pypi.org/simple/{pkg_repo}/" + response = requests.get( + url, + timeout=5, + headers={"Accept": "application/vnd.pypi.simple.v1+json"}, + ) + if response.status_code == 200: + data = response.json() + versions = data["versions"] + if len(versions) > 1: + version = versions[-1] + else: + print(f"Error: {response.status_code}") + return version + # TODO: get_latest_commit # TODO: get_latest_release @@ -636,8 +689,33 @@ class Sourcehut(Remote): TYPE = ERemote.SOURCEHUT NAME = "sourcehut" - # @classmethod - # def get_latest_commit(cls, pkg_repo): + @classmethod + def get_latest_commit(cls, pkg_repo): + url = f"https://git.sr.ht/{pkg_repo}/log/rss.xml" + try: + with urllib.request.urlopen(url) as req: + feed = req.read() + except urllib.request.HTTPError: + return "" + dom = minidom.parseString(feed) + date = "" + try: + node = dom.getElementsByTagName("rss") + node = node[0].getElementsByTagName("channel") + node = node[0].getElementsByTagName("item") + node = node[0].getElementsByTagName("pubDate") + date = node[0].firstChild.nodeValue + except Exception as e: + print(e) + return "" + date = datetime.strptime(date, "%a, %d %b %Y %H:%M:%S %z") + version = date.isoformat() + + return version + # user, reponame = cls.split_user_repo(pkg_repo) + # repo = GitSrhtClient.get_repository(GitSrhtClient, user, reponame) + # repo.cr_await() + # print(repo) # url = "https://git.sr.ht/api/" # body = """ # query repositoryByDiskPath { diff --git a/src/pkgupdates/settings.py b/src/pkgupdates/settings.py index c0153ac..be77057 100644 --- a/src/pkgupdates/settings.py +++ b/src/pkgupdates/settings.py @@ -1,6 +1,58 @@ """Module Settings""" +import yaml +import xdg +from xdg import BaseDirectory + +VERBOSE = False GITHUB_RSS_GET = False +ONLY_IN_TREE = False + + +def load_config(): + """"POOP""" + global GITHUB_RSS_GET + global VERBOSE + global ONLY_IN_TREE + + filename = BaseDirectory.load_first_config('pkgupdates/config.yml') + if filename is None: + return + + with open(filename, 'r') as file: + config_data = yaml.safe_load(file) + if 'verbose' in config_data: + VERBOSE = config_data['verbose'] + if 'only-in-tree' in config_data: + ONLY_IN_TREE = config_data['only-in-tree'] + if 'remote' in config_data: + if 'github' in config_data['remote']: + if 'rss-get' in config_data['remote']['github']: + GITHUB_RSS_GET = config_data['remote']['github']['rss-get'] + + +def get_verbose(): + """Verbose output""" + return VERBOSE + + +def set_verbose(v): + """Verbose output setter""" + global VERBOSE + VERBOSE = v + return VERBOSE + + +def get_only_in_tree(): + """Only in tree""" + return ONLY_IN_TREE + + +def set_only_in_tree(v): + """ONLY IN TREE set""" + global ONLY_IN_TREE + ONLY_IN_TREE = v + return ONLY_IN_TREE def get_gh_rss_get(): diff --git a/src/pkgupdates/version.py b/src/pkgupdates/version.py index c58607d..1d243f1 100644 --- a/src/pkgupdates/version.py +++ b/src/pkgupdates/version.py @@ -3,8 +3,9 @@ import re from enum import Enum -rex_date = re.compile("([0-9]{4})([0-9]{2})([0-9]{2})") +rex_date = re.compile("(20[0-9]{2})([0-9]{2})([0-9]{2})") rex_semantic = re.compile("([0-9]+)[.]([0-9]+)(?:[.]([0-9]+))?") +rex_numeric = re.compile("([0-9]+)$") rex_revision = re.compile("-r([0-9]+)$") @@ -14,6 +15,7 @@ class EVersion(Enum): UNKNOWN = 0 SEMANTIC = 1 DATE = 2 + COUNTER = 3 VCS = 9999 @@ -42,7 +44,7 @@ class Version: @property def revision(self): - """Get Gentoo ebuild revision""" + """Get ebuild revision""" return self.__revision @@ -127,6 +129,18 @@ class DateVersion(Version): return self.__day +class CounterVersion(Version): + """Versions that are just a single incrementing counter""" + + TYPE = EVersion.COUNTER + + +class VCSVersion(Version): + """Not a version but an indicator to install the newest commit""" + + TYPE = EVersion.VCS + + def parse_version(version): """Parse version and return appropriate version class""" m = rex_semantic.search(version) @@ -137,4 +151,11 @@ def parse_version(version): if m is not None: return DateVersion(version, *m.groups()) + m = rex_numeric.search(version) + if m is not None: + if int(m[1]) == 9999: + return VCSVersion(version) + + return CounterVersion(version) + return Version(version)