python/checkpkg.py
2025-04-09 19:35:28 +02:00

219 lines
7.2 KiB
Python
Executable file

#!/usr/bin/env python3
import argparse
import urllib.request
import json
import signal
import re
import shutil
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.error import HTTPError, URLError
# Updated color codes with simpler separator
COLORS = {
'reset': '\033[0m',
'bold': '\033[1m',
'red': '\033[91m',
'green': '\033[92m',
'yellow': '\033[93m',
'header': '\033[94m' # Simpler header color
}
def colorize(text, color):
if not hasattr(colorize, 'is_tty'):
colorize.is_tty = __import__('sys').stdout.isatty()
return f"{COLORS[color]}{text}{COLORS['reset']}" if colorize.is_tty else text
signal.signal(signal.SIGINT, lambda s, f: exit(1))
REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) checkpkg/1.0',
'Accept': 'application/json'
}
PM_MAPPINGS = {
'apt': ['debian_', 'ubuntu_'],
'dnf': ['fedora_'],
'pacman': ['arch'],
'aur': ['aur'],
'apk': ['alpine_'],
'zypper': ['opensuse_']
}
REPO_FORMATS = {
'debian_': "Debian {}",
'ubuntu_': "Ubuntu {}",
'fedora_': "Fedora {}",
'arch': "Arch",
'aur': "AUR",
'alpine_': "Alpine {}",
'opensuse_': "openSUSE {}"
}
def version_key(version):
"""Create a sorting key for version comparison"""
return [
(0, int(part)) if part.isdigit() else (1, part.lower())
for part in re.findall(r'(\d+|\D+)', version)
]
def get_package_manager(repo):
for pm, patterns in PM_MAPPINGS.items():
if any(repo.startswith(p) for p in patterns):
return pm
return None
def format_repository(repo):
for pattern, fmt in REPO_FORMATS.items():
if repo.startswith(pattern):
parts = repo.split('_')
return fmt.format(parts[1] if len(parts) > 1 else '')
return repo
def fetch_package_data(package):
try:
req = urllib.request.Request(
f'https://repology.org/api/v1/project/{package}',
headers=REQUEST_HEADERS
)
with urllib.request.urlopen(req, timeout=10) as response:
return json.load(response)
except HTTPError as e:
if e.code == 403:
print(colorize(f"Error: Repology blocked the request for {package} (try again later)", 'red'))
return None
except Exception:
return None
def main():
parser = argparse.ArgumentParser(description='Package search tool')
parser.add_argument('--all', action='store_true')
parser.add_argument('--apt', action='store_true')
parser.add_argument('--dnf', action='store_true')
parser.add_argument('--pacman', action='store_true')
parser.add_argument('--apk', action='store_true')
parser.add_argument('--zypper', action='store_true')
parser.add_argument('--aur', action='store_true')
parser.add_argument('packages', nargs='+')
args = parser.parse_args()
selected_pms = [pm for pm, flag in [
('apt', args.apt or args.all),
('dnf', args.dnf or args.all),
('pacman', args.pacman or args.all),
('apk', args.apk or args.all),
('zypper', args.zypper or args.all),
('aur', args.aur or args.all)
] if flag]
if not selected_pms:
print(colorize("Error: No package managers selected", 'red'))
return
results = {}
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(fetch_package_data, pkg): pkg for pkg in args.packages}
for future in as_completed(futures):
pkg = futures[future]
try:
data = future.result()
results[pkg] = data or []
except Exception as e:
print(colorize(f"Error processing {pkg}: {str(e)}", 'red'))
results[pkg] = []
output = {}
for pkg, entries in results.items():
pm_versions = {pm: {'version': '', 'repos': set(), 'key': []} for pm in selected_pms}
for entry in entries:
repo = entry.get('repo', '')
version = entry.get('version', 'N/A')
pm = get_package_manager(repo)
if pm in selected_pms and version != 'N/A':
repo_fmt = format_repository(repo)
current_key = version_key(version)
stored = pm_versions[pm]
if not stored['key'] or current_key > stored['key']:
stored['version'] = version
stored['repos'] = {repo_fmt}
stored['key'] = current_key
elif current_key == stored['key']:
stored['repos'].add(repo_fmt)
output[pkg] = {}
for pm in selected_pms:
data = pm_versions[pm]
if data['version']:
repos = ', '.join(sorted(data['repos']))
output[pkg][pm] = f"{data['version']} ({repos})"
else:
output[pkg][pm] = 'Not found'
headers = ['Package'] + selected_pms
terminal_width = get_terminal_width()
# Calculate initial column widths
col_widths = [len(h) for h in headers]
content_widths = {0: max(len(pkg) for pkg in args.packages)}
for pkg in args.packages:
versions = output.get(pkg, {})
for i, pm in enumerate(selected_pms, 1):
content_widths[i] = max(content_widths.get(i, 0), len(versions.get(pm, '')))
# Set initial widths
for i in range(len(headers)):
col_widths[i] = max(len(headers[i]), content_widths.get(i, 0))
# Adjust for terminal width
total_width = sum(col_widths) + 3 * (len(headers) - 1) # 3 chars per separator
max_reductions = 5 # Max characters to reduce per column
if total_width > terminal_width:
excess = total_width - terminal_width
reducible = [i for i in range(1, len(headers)) if col_widths[i] > 10]
while excess > 0 and reducible:
per_col = max(1, min(max_reductions, excess // len(reducible)))
for i in reducible:
reduction = min(per_col, col_widths[i] - 10, excess)
col_widths[i] -= reduction
excess -= reduction
if excess <= 0:
break
# Truncate text with ellipsis if needed
def truncate(text, width):
if len(text) > width and width > 3:
return text[:width-3] + '...'
return text.ljust(width)
# Print header
header_line = ' | '.join(
truncate(colorize(h, 'header'), w)
for h, w in zip(headers, col_widths)
)
print(header_line)
# Separator line
print('-' * min(sum(col_widths) + 3*(len(headers)-1), terminal_width))
# Print rows
for pkg in args.packages:
row = [truncate(colorize(pkg, 'bold'), col_widths[0])]
versions = output.get(pkg, {pm: 'Not found' for pm in selected_pms})
for i, pm in enumerate(selected_pms, 1):
version = versions.get(pm, 'Not found')
color = 'green' if version != 'Not found' else 'red'
if 'AUR' in version:
color = 'yellow'
# Fixed line with proper parenthesis closure
row.append(truncate(colorize(version, color), col_widths[i]))
print(' | '.join(row))
if __name__ == '__main__':
main()