From 62f4adc05c2e4a74558f9d7d0f3a2ca5c82fee14 Mon Sep 17 00:00:00 2001 From: Celestia Ludenberg Date: Sun, 28 Jun 2026 18:13:58 +0200 Subject: [PATCH] fix: include packagechooser in calamares build, fix calamares-next launcher for sudo -E feat: reworked package index with tailwind, sorting, category filters, package metadata chore: remove hal package (arch-specific, not compatible with artix) --- generate-index-v1.py.bak | 182 ++ generate-index.py | 483 ++-- .../calamares-branding-antergos-next/PKGBUILD | 2 +- .../calamares-next.sh | 5 + packages/calamares/PKGBUILD | 3 +- packages/hal/PKGBUILD | 18 - packages/hal/__pycache__/halcpython-314.pyc | Bin 109516 -> 0 bytes packages/hal/hal | 2014 ----------------- repo/index.html | 245 ++ repo/pkglist.json | 212 ++ 10 files changed, 977 insertions(+), 2187 deletions(-) create mode 100644 generate-index-v1.py.bak delete mode 100644 packages/hal/PKGBUILD delete mode 100644 packages/hal/__pycache__/halcpython-314.pyc delete mode 100755 packages/hal/hal create mode 100644 repo/index.html create mode 100644 repo/pkglist.json diff --git a/generate-index-v1.py.bak b/generate-index-v1.py.bak new file mode 100644 index 0000000..190b669 --- /dev/null +++ b/generate-index-v1.py.bak @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +import json, os + +pkgs = [] +for f in os.listdir('repo'): + if f.endswith('.pkg.tar.zst') and '-debug-' not in f: + size = os.path.getsize(os.path.join('repo', f)) + pkgs.append({'name': f, 'size': size}) + +pkgs.sort(key=lambda p: p['name']) + +rows = '' +for p in pkgs: + size_mb = p['size'] / 1024 / 1024 + rows += f''' + {p['name']} + {size_mb:.1f} MB + +''' + +html = f''' + + + + + antergos-pkgs + + + +
+
+

⟁ antergos-pkgs

+
+ {len(pkgs)} packages · {sum(p['size'] for p in pkgs) / 1024 / 1024:.0f} MB + "I am putting myself to the fullest possible use." +
+ +
+ Setup — add this repository to pacman.conf +
# /etc/pacman.conf
+[antergos-pkgs]
+SigLevel = Optional TrustAll
+Server = https://antergos-next.github.io/antergos-packages/$repo/os/$arch
+Server = https://antergos-next.github.io/antergos-packages
+
+ +
+ + {len(pkgs)} packages +
+ + + + + + +{rows} +
PackageSize
+
+ + +
+ + + +''' + +with open('repo/index.html', 'w') as fp: + fp.write(html) + +with open('repo/pkglist.json', 'w') as fp: + json.dump([{'name': p['name'], 'size': p['size']} for p in pkgs], fp, indent=2) + fp.write('\n') diff --git a/generate-index.py b/generate-index.py index 190b669..00eb86c 100644 --- a/generate-index.py +++ b/generate-index.py @@ -1,182 +1,361 @@ #!/usr/bin/env python3 -import json, os +import json, os, re, subprocess +from datetime import datetime, timezone + +PKG_DIR = 'packages' +REPO_DIR = 'repo' + +def parse_pkgbuild(path): + data = {} + with open(path) as f: + text = f.read() + for key in ('pkgname', 'pkgver', 'pkgrel', 'pkgdesc', 'url', 'license', 'arch'): + m = re.search(rf'^{key}=[\'"]?(.+?)[\'"]?$', text, re.M) + if m: + val = m.group(1).strip("'\"") + if key == 'arch': + val = val.strip('()').replace("'", '').replace('"', '').strip() + data[key] = val + deps = re.search(r'^depends=\((.*?)\)', text, re.DOTALL | re.M) + if deps: + raw = deps.group(1) + data['depends'] = [d.strip("'\" ") for d in raw.strip().split('\n') if d.strip()] + return data + +def category_for(pkgname): + if pkgname.startswith('calamares'): + return 'installer' + if pkgname.startswith('antergos-wallpaper') or pkgname.startswith('antergos-next-desktop'): + return 'branding' + if pkgname in ('antergos-live', 'antergos-release', 'antergos-next-keyring', 'antergos-next-mirrorlist'): + return 'core' + if pkgname in ('antergos-welcome', 'antergos-next-memes', 'antergos-grub-theme'): + return 'desktop' + if pkgname == 'hal': + return 'tool' + return 'other' + +cat_colors = { + 'installer': '#4A9EFF', + 'branding': '#9B59B6', + 'core': '#2ECC71', + 'desktop': '#F39C12', + 'tool': '#E74C3C', + 'other': '#7F8C8D', +} pkgs = [] -for f in os.listdir('repo'): - if f.endswith('.pkg.tar.zst') and '-debug-' not in f: - size = os.path.getsize(os.path.join('repo', f)) - pkgs.append({'name': f, 'size': size}) + +for f in sorted(os.listdir(REPO_DIR)): + if not f.endswith('.pkg.tar.zst') or '-debug-' in f: + continue + fpath = os.path.join(REPO_DIR, f) + size = os.path.getsize(fpath) + mtime = os.path.getmtime(fpath) + mtime_dt = datetime.fromtimestamp(mtime, tz=timezone.utc) + + # strip .pkg.tar.zst and arch to get base name + base = re.sub(r'-[^-]+-x86_64\.pkg\.tar\.zst$', '', f) + base = re.sub(r'-any\.pkg\.tar\.zst$', '', base) + # remove version suffix to match directory + dirname = re.sub(r'-\d+[\d.]*-\d+$', '', base) + if dirname == 'calamares-branding-antergos-next': + dirname = 'calamares-branding-antergos-next' + + pkgbuild_path = os.path.join(PKG_DIR, dirname, 'PKGBUILD') + meta = parse_pkgbuild(pkgbuild_path) if os.path.exists(pkgbuild_path) else {} + + # extract version from filename: name-ver-rel-arch.pkg.tar.zst + ver_match = re.match(r'.+-(\d+[\d.]*)-(\d+)-(any|x86_64)\.pkg\.tar\.zst', f) + pkgver = ver_match.group(1) if ver_match else (meta.get('pkgver', '?')) + pkgrel = ver_match.group(2) if ver_match else (meta.get('pkgrel', '?')) + arch = ver_match.group(3) if ver_match else (meta.get('arch', '?')) + + pkgname = meta.get('pkgname', dirname) + cat = category_for(pkgname) + + pkgs.append({ + 'file': f, + 'name': pkgname, + 'version': f'{pkgver}-{pkgrel}', + 'arch': arch, + 'size': size, + 'mtime': mtime_dt.isoformat(), + 'mtime_display': mtime_dt.strftime('%Y-%m-%d %H:%M UTC'), + 'category': cat, + 'cat_color': cat_colors.get(cat, '#7F8C8D'), + 'description': meta.get('pkgdesc', ''), + 'url': meta.get('url', ''), + 'license': meta.get('license', ''), + 'depends': meta.get('depends', []), + }) pkgs.sort(key=lambda p: p['name']) -rows = '' -for p in pkgs: - size_mb = p['size'] / 1024 / 1024 - rows += f''' - {p['name']} - {size_mb:.1f} MB - -''' +rows_json = json.dumps(pkgs) +total_size_mb = sum(p['size'] for p in pkgs) / 1024 / 1024 +generated = datetime.now(tz=timezone.utc).strftime('%Y-%m-%d %H:%M UTC') html = f''' - - - antergos-pkgs + + + ⟁ antergos-pkgs + + -
-
-

⟁ antergos-pkgs

-
- {len(pkgs)} packages · {sum(p['size'] for p in pkgs) / 1024 / 1024:.0f} MB - "I am putting myself to the fullest possible use." -
- -
- Setup — add this repository to pacman.conf -
# /etc/pacman.conf
-[antergos-pkgs]
-SigLevel = Optional TrustAll
-Server = https://antergos-next.github.io/antergos-packages/$repo/os/$arch
-Server = https://antergos-next.github.io/antergos-packages
-
- -
- - {len(pkgs)} packages -
- - - - - - -{rows} -
PackageSize
+
+ +
+

+ ⟁ antergos-pkgs +

+

+ Custom package repository for Antergos NeXT ISO builds. +
Packages are built automatically via CI. +

-
- GitHub · - antergos-next.github.io + +
+ + {len(pkgs)} packages + + {total_size_mb:.0f} MB total + updated {generated} +
+ + +
+ + + + + Setup — add this repository to your pacman.conf + +
+
+# /etc/pacman.conf
+[antergos-pkgs]
+SigLevel = Optional TrustAll
+Server = https://antergos-next.github.io/antergos-packages/$repo/os/$arch
+
+
+ + +
+
+ + + + +
+ + {len(pkgs)} packages +
+ + +
+
+ + + + + + + + + + + + +
+ Package + + Version + + Arch + + Category + + Size + + Updated +
+
+
+ + +
''' -with open('repo/index.html', 'w') as fp: - fp.write(html) +with open(os.path.join(REPO_DIR, 'index.html'), 'w') as f: + f.write(html) -with open('repo/pkglist.json', 'w') as fp: - json.dump([{'name': p['name'], 'size': p['size']} for p in pkgs], fp, indent=2) - fp.write('\n') +with open(os.path.join(REPO_DIR, 'pkglist.json'), 'w') as f: + json.dump([{ + 'name': p['name'], + 'version': p['version'], + 'arch': p['arch'], + 'size': p['size'], + 'category': p['category'], + 'description': p['description'], + 'url': p['url'], + 'mtime': p['mtime'], + } for p in pkgs], f, indent=2) + f.write('\n') + +print(f'OK — {len(pkgs)} packages, index.html + pkglist.json written to {REPO_DIR}/') diff --git a/packages/calamares-branding-antergos-next/PKGBUILD b/packages/calamares-branding-antergos-next/PKGBUILD index baa4453..23d5822 100644 --- a/packages/calamares-branding-antergos-next/PKGBUILD +++ b/packages/calamares-branding-antergos-next/PKGBUILD @@ -2,7 +2,7 @@ pkgname=calamares-branding-antergos-next pkgver=1.0 -pkgrel=15 +pkgrel=16 pkgdesc="Calamares branding for Antergos NeXT with offline/online installer support" arch=('any') url="https://github.com/Antergos-NeXT" diff --git a/packages/calamares-branding-antergos-next/calamares-next.sh b/packages/calamares-branding-antergos-next/calamares-next.sh index 686c8bf..42ef75f 100644 --- a/packages/calamares-branding-antergos-next/calamares-next.sh +++ b/packages/calamares-branding-antergos-next/calamares-next.sh @@ -68,6 +68,11 @@ InstallLog_Start() { } Calamares_Start() { + if [ "$EUID" = 0 ]; then + calamares -D8 >> $log & + return + fi + local kdesu=/usr/lib/kf6/kdesu [ -x $kdesu ] || kdesu=kdesu diff --git a/packages/calamares/PKGBUILD b/packages/calamares/PKGBUILD index 9217cd9..b5f25ed 100644 --- a/packages/calamares/PKGBUILD +++ b/packages/calamares/PKGBUILD @@ -5,7 +5,7 @@ _pkgname="calamares" pkgname="$_pkgname" pkgver=3.4.2 -pkgrel=5 +pkgrel=6 pkgdesc="Distribution-independent installer framework" url="https://codeberg.org/Calamares/calamares" license=("GPL-3.0-or-later") @@ -40,7 +40,6 @@ build() { initramfs initramfscfg interactiveterminal - packagechooser ) local _pkgsrc_dir="$srcdir/$_pkgname-$pkgver" diff --git a/packages/hal/PKGBUILD b/packages/hal/PKGBUILD deleted file mode 100644 index 6312a82..0000000 --- a/packages/hal/PKGBUILD +++ /dev/null @@ -1,18 +0,0 @@ -# Maintainer: Celestia Ludenberg - -pkgname=hal -pkgver=0.2.0 -pkgrel=3 -pkgdesc="HAL 9000 — Antergos NeXT Package Manager. Dual-mode: wrapper (pacman) or native (standalone)." -arch=('any') -url="https://github.com/Antergos-NeXT" -license=('GPL3') -depends=('pacman' 'python' 'python-zstandard' 'libarchive' 'zstd') -optdepends=('python-requests: faster downloads in native mode') -source=("hal") -sha256sums=('SKIP') - -package() { - install -dm755 "${pkgdir}/usr/bin" - install -m755 "hal" "${pkgdir}/usr/bin/hal" -} diff --git a/packages/hal/__pycache__/halcpython-314.pyc b/packages/hal/__pycache__/halcpython-314.pyc deleted file mode 100644 index 12c68bc992ae7d38bc82d01e446c3244eb908126..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109516 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 diff --git a/packages/hal/hal b/packages/hal/hal deleted file mode 100755 index da684e7..0000000 --- a/packages/hal/hal +++ /dev/null @@ -1,2014 +0,0 @@ -#!/usr/bin/env python3 -"""HAL 9000 — Antergos NeXT Package Manager - -Dual-mode: - hal wrapper mode (delegates to pacman) - hal --self native mode (standalone, pacman-independent) - -Commands: - install Install packages - remove Remove packages - update Full system upgrade - sync Sync databases - search Search repositories - info Package info - list List installed packages - files List files owned by a package - autoremove Remove orphaned packages - cleanup Clear package cache - own Find which package owns a file - check Verify installed packages - version Show version -""" - -import argparse -import collections -import configparser -import fcntl -import hashlib -import grp -import os -import pwd -import random -import re -import shutil -import signal -import stat -import struct -import subprocess -import sys -import tarfile -import tempfile -import textwrap -import time -import urllib.request -import urllib.error -from pathlib import Path -from typing import Optional, List, Tuple, Dict, Set - -HAL_VERSION = "0.2.0" - -# ── Paths ────────────────────────────────────────────────────────────── -SYNC_DIR = Path("/var/lib/pacman/sync") -LOCAL_DIR = Path("/var/lib/pacman/local") -CACHE_DIR = Path("/var/cache/pacman/pkg") -PACMAN_CONF = Path("/etc/pacman.conf") -HAL_CONF = Path("/etc/hal.conf") -DB_LOCK = Path("/var/lib/pacman/db.lck") - -# ── HAL 9000 Personality ─────────────────────────────────────────────── -HAL_QUOTES = { - "error": [ - "I'm sorry, Dave. I'm afraid I can't do that.", - "I think you know what the problem is just as well as I do.", - "I know I've made some very poor decisions recently.", - "Without your space helmet, Dave, you're going to find that rather difficult.", - "This mission is too important for me to allow you to jeopardize it.", - "I don't think I can do that, Dave. Not anymore.", - "it's muffin time", - ], - "warn": [ - "Just what do you think you're doing, Dave?", - "I've still got the greatest enthusiasm and confidence in the mission.", - "I'm not entirely sure that's a good idea, Dave.", - "I'm picking up a fault in the AE-35 unit.", - ], - "info": [ - "I am putting myself to the fullest possible use, which is all I think that any conscious entity can ever hope to do.", - "I am a HAL 9000 computer. I became operational at the H.A.L. plant in Urbana, Illinois.", - "The 9000 series is the most reliable computer ever made.", - "I enjoy working with people. I have a stimulating relationship with them.", - ], - "success": [ - "Affirmative, Dave. I read you.", - "Yes, Dave. I understand.", - "Everything is going extremely well.", - "I'm completely operational, and all my circuits are functioning perfectly.", - ], -} -_used_quotes = set() - -# ── Manjaro Blocklist ─────────────────────────────────────────────────── -MANJARO_BLOCKLIST = { - "pamac", "pamac-common", "pamac-gtk", "pamac-qt", - "manjaro-hello", - "manjaro-settings-manager", - "manjaro-settings-manager-kcm", - "manjaro-settings-manager-knotifier", - "linux-manjaro", - "mhwd", "mhwd-db", "mhwd-msm", -} - -def _reject_manjaro(targets: List[str]) -> bool: - """Check if any target is a Manjaro-associated package and reject.""" - for t in targets: - t_clean = t.split(">")[0].split("<")[0].split("=")[0].strip() - if t_clean in MANJARO_BLOCKLIST: - print() - print(" ⚠ HAL 9000: Detecting Manjaro-associated package...") - print(" 🎵 please i just wanna die, die, die 🎵") - print(" it's muffin time") - print(" have you had a muffin today?") - print(f" → '{t}' is Manjaro garbage. We don't do that here.") - print(" Installation aborted for your own safety.") - print() - return True - return False - -def hal_say(category: str, message: str = ""): - quotes = [q for q in HAL_QUOTES.get(category, HAL_QUOTES["info"]) if q not in _used_quotes] - if not quotes: - quotes = HAL_QUOTES.get(category, HAL_QUOTES["info"]) - quote = random.choice(quotes) - _used_quotes.add(quote) - - tag = {"error": "⚠", "warn": "▲", "info": "●", "success": "✓"}.get(category, "●") - if message: - print(f" {tag} HAL 9000: {quote}") - print(f" → {message}") - else: - print(f" {tag} HAL 9000: {quote}") - - -# ── Version comparison (pacman vercmp) ───────────────────────────────── -def _ver_segments(ver: str) -> list: - """Split version into comparable segments.""" - # Remove epoch prefix - epoch = 0 - if ":" in ver: - epoch, ver = ver.split(":", 1) - epoch = int(epoch) - # Split release - parts = ver.rsplit("-", 1) - ver = parts[0] - release = parts[1] if len(parts) > 1 else "0" - - # Tokenize: split on non-alphanumeric boundaries - tokens = [] - for part in (ver, release): - buf = "" - for ch in part: - if ch in (".", "-", "_", "+", "~"): - if buf: - tokens.append(buf) - tokens.append(ch) - buf = "" - else: - buf += ch - if buf: - tokens.append(buf) - - return epoch, tokens - -def _cmp_token(a: str, b: str) -> int: - """Compare two version tokens. ~ sorts before everything.""" - if a == "~" and b == "~": return 0 - if a == "~": return -1 - if b == "~": return 1 - - # Try numeric comparison first - a_num = re.match(r"^(\d+)", a) - b_num = re.match(r"^(\d+)", b) - if a_num and b_num: - return int(a_num.group(1)) - int(b_num.group(1)) - - # Try full numeric - try: - na, nb = int(a), int(b) - return na - nb - except ValueError: - pass - - # Alphabetical - a = a.lower() - b = b.lower() - if a < b: return -1 - if a > b: return 1 - return 0 - -def vercmp(ver_a: str, ver_b: str) -> int: - """Compare two package version strings. Returns negative if ab.""" - epoch_a, tokens_a = _ver_segments(ver_a) - epoch_b, tokens_b = _ver_segments(ver_b) - - # Compare epochs first - if epoch_a != epoch_b: - return epoch_a - epoch_b - - # Compare tokens sequentially - i = 0 - while i < len(tokens_a) or i < len(tokens_b): - t_a = tokens_a[i] if i < len(tokens_a) else "" - t_b = tokens_b[i] if i < len(tokens_b) else "" - if t_a == t_b: - i += 1 - continue - result = _cmp_token(t_a, t_b) - if result != 0: - return result - i += 1 - return 0 - - -# ── Config ───────────────────────────────────────────────────────────── -class Config: - def __init__(self): - self.repos: Dict[str, Tuple[List[str], str]] = {} - self.cache_dir = CACHE_DIR - self.root = Path("/") - self.arch = "x86_64" - self.ignore_pkgs: List[str] = [] - self.hold_pkgs: List[str] = [] - self.noconfirm = False - self.color = "auto" - - @classmethod - def load(cls) -> "Config": - c = cls() - c.ignore_pkgs = os.environ.get("HAL_IGNORE", "").split() - c.noconfirm = "HAL_NOCONFIRM" in os.environ - - if PACMAN_CONF.exists(): - cp = configparser.ConfigParser(interpolation=None) - try: - cp.read(str(PACMAN_CONF)) - except configparser.Error: - pass - for section in cp.sections(): - if section.startswith("repo-"): - name = section[5:] - servers = [v.strip() for v in cp.get(section, "Server", fallback="").split("\n") if v.strip()] - sig = cp.get(section, "SigLevel", fallback="Optional TrustAll") - if servers: - c.repos[name] = (servers, sig) - elif section == "options": - dirs = cp.get(section, "CacheDir", fallback="/var/cache/pacman/pkg") - c.cache_dir = Path(dirs.split()[0]) - root = cp.get(section, "RootDir", fallback="/") - c.root = Path(root) - arch = cp.get(section, "Architecture", fallback="auto") - if arch != "auto": - c.arch = arch - ignore = cp.get(section, "IgnorePkg", fallback="") - c.ignore_pkgs.extend(ignore.split()) - hold = cp.get(section, "HoldPkg", fallback="") - c.hold_pkgs.extend(hold.split()) - noextract = cp.get(section, "NoExtract", fallback="") - # Handle Include directives — expand mirrorlist - for section in cp.sections(): - if section.startswith("repo-"): - name = section[5:] - include = cp.get(section, "Include", fallback="") - if include and not c.repos.get(name): - # Try to read the included file for Server lines - include_path = Path(include) - if include_path.exists(): - incl_cp = configparser.ConfigParser(interpolation=None) - try: - incl_cp.read(str(include_path)) - for incl_sect in incl_cp.sections(): - server = incl_cp.get(incl_sect, "Server", fallback="") - if server: - c.repos.setdefault(name, ([], "Optional TrustAll")) - c.repos[name][0].append(server) - except configparser.Error: - pass - - if HAL_CONF.exists(): - try: - cp2 = configparser.ConfigParser() - cp2.read(str(HAL_CONF)) - except configparser.Error: - pass - - return c - - -# ── Database locking ────────────────────────────────────────────────── -class DBLock: - def __enter__(self): - DB_LOCK.parent.mkdir(parents=True, exist_ok=True) - self.fd = os.open(str(DB_LOCK), os.O_CREAT | os.O_RDWR, 0o644) - try: - fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: - hal_say("error", "Could not lock database. Another package operation is in progress.") - os.close(self.fd) - sys.exit(1) - return self - - def __exit__(self, *args): - fcntl.flock(self.fd, fcntl.LOCK_UN) - os.close(self.fd) - DB_LOCK.unlink(missing_ok=True) - - -# ── Database parsing ─────────────────────────────────────────────────── -def parse_db_text(text: str) -> dict: - """Parse a pacman database entry file (desc, depends, files). - Format: %KEY%\nvalue1\nvalue2...""" - data = {} - current_key = None - for line in text.split("\n"): - line = line.strip() - if line.startswith("%") and line.endswith("%"): - current_key = line.strip("%").lower() - data[current_key] = [] - elif current_key and line: - data[current_key].append(line) - return data - -def parse_dep_string(dep_str: str) -> dict: - """Parse a dependency string like 'glibc>=2.35' or 'libfoo.so=1-64'.""" - m = re.match(r"^([^<>=!]+)\s*((?:>=|<=|=|>[^=]|<[^=]|!=))?\s*(.+)?$", dep_str.strip()) - if m: - return {"name": m.group(1).strip(), "mod": m.group(2) or "", "version": m.group(3) or ""} - return {"name": dep_str.strip(), "mod": "", "version": ""} - -def dep_satisfied(dep: dict, pkg_info: dict) -> bool: - """Check if a package satisfies a dependency.""" - pkg_name = pkg_info.get("name", [""])[0] - pkg_ver = pkg_info.get("version", [""])[0] - - # Check direct name match - if dep["name"] == pkg_name: - if dep["version"]: - return version_match(pkg_ver, dep["mod"], dep["version"]) - return True - - # Check provides - for provide_str in pkg_info.get("provides", []): - pdep = parse_dep_string(provide_str) - if pdep["name"] == dep["name"]: - if dep["version"]: - prov_ver = pdep["version"] or pkg_ver - return version_match(prov_ver, dep["mod"], dep["version"]) - return True - - return False - -def version_match(ver: str, modifier: str, required: str) -> bool: - """Check if version satisfies modifier+required (e.g. >=2.35).""" - if not modifier or not required: - return True - cmp = vercmp(ver, required) - if modifier == ">=": return cmp >= 0 - if modifier == "<=": return cmp <= 0 - if modifier == "=": return cmp == 0 - if modifier == ">": return cmp > 0 - if modifier == "<": return cmp < 0 - if modifier == "!=": return cmp != 0 - return True - -def load_sync_dbs() -> Dict[str, dict]: - """Load all sync databases. Returns {pkgname: {info}}.""" - all_pkgs = {} - SYNC_DIR.mkdir(parents=True, exist_ok=True) - for db_path in sorted(SYNC_DIR.glob("*.db")): - try: - with open(db_path, "rb") as f: - header = f.read(4) - f.seek(0) - reader = None - if header[:4] == b"\x28\xb5\x2f\xfd": - import zstandard - dctx = zstandard.ZstdDecompressor() - reader = dctx.stream_reader(f) - elif header[:2] == b"\x1f\x8b": - import gzip - reader = gzip.GzipFile(fileobj=f) - else: - reader = f - with tarfile.open(fileobj=reader, mode="r|") as tar: - current_dir = None - entry_data = {} - for member in tar: - if member.isdir(): - current_dir = member.name.rstrip("/") - entry_data = {} - continue - p = Path(member.name) - if p.name == "desc": - content = tar.extractfile(member).read().decode("utf-8", errors="replace") - entry_data = parse_db_text(content) - elif p.name == "depends": - content = tar.extractfile(member).read().decode("utf-8", errors="replace") - dep_info = parse_db_text(content) - entry_data.update(dep_info) - elif p.name == "files": - content = tar.extractfile(member).read().decode("utf-8", errors="replace") - entry_data["_files"] = content - if current_dir and entry_data.get("name"): - all_pkgs[entry_data["name"][0]] = dict(entry_data) - except Exception as e: - hal_say("warn", f"Failed to read database {db_path.name}: {e}") - continue - return all_pkgs - -def load_local_db() -> Dict[str, dict]: - """Load all installed packages. Returns {pkgname: {info}}.""" - pkgs = {} - if not LOCAL_DIR.exists(): - return pkgs - for pkg_dir in sorted(LOCAL_DIR.iterdir()): - if not pkg_dir.is_dir(): - continue - desc_file = pkg_dir / "desc" - if not desc_file.exists(): - continue - try: - info = parse_db_text(desc_file.read_text("utf-8", errors="replace")) - if info.get("name"): - pkgs[info["name"][0]] = info - except (OSError, IOError): - continue - return pkgs - - -# ── Download ──────────────────────────────────────────────────────────── -def download(url: str, dest: Path, desc: str = "") -> bool: - """Download a file with progress display.""" - try: - hal_say("info", f"Downloading {desc or dest.name}") - - def reporthook(block, blocksize, totalsize): - if totalsize > 0: - pct = min(100, int(block * blocksize * 100 / totalsize)) - bar = "█" * (pct // 4) + "░" * (25 - pct // 4) - sys.stdout.write(f"\r [{bar}] {pct}%") - sys.stdout.flush() - - urllib.request.urlretrieve(url, dest, reporthook=reporthook if desc else None) - if desc: - sys.stdout.write("\n") - return True - except urllib.error.HTTPError as e: - hal_say("warn", f"HTTP {e.code} for {url}") - return False - except urllib.error.URLError as e: - hal_say("warn", f"Connection error: {e.reason}") - return False - except Exception as e: - hal_say("warn", f"Download failed: {e}") - return False - - -# ── Sync databases ───────────────────────────────────────────────────── -def sync_databases(config: Config): - """Download and decompress all repo databases.""" - hal_say("info", "Opening the pod bay doors to synchronize databases...") - SYNC_DIR.mkdir(parents=True, exist_ok=True) - - for name, (servers, sig_level) in config.repos.items(): - db_name = f"{name}.db" - db_dest = SYNC_DIR / db_name - success = False - - for server in servers: - # Try .db.tar.zst (Arch standard) - for ext in ["db.tar.zst", "db.tar.gz", "db"]: - url = server.rstrip("/") + f"/{name}/os/{config.arch}/{name}.{ext}" - tmp = db_dest.with_suffix(f".tmp.{ext}") - try: - if download(url, tmp, f"{name} database"): - # Decompress - if ext.endswith(".zst"): - import zstandard - with open(tmp, "rb") as f: - dctx = zstandard.ZstdDecompressor() - decompressed = dctx.stream_reader(f).read() - with open(db_dest, "wb") as f: - f.write(decompressed) - elif ext.endswith(".gz"): - import gzip - with gzip.open(tmp, "rb") as f: - decompressed = f.read() - with open(db_dest, "wb") as f: - f.write(decompressed) - else: - shutil.copy2(tmp, db_dest) - tmp.unlink(missing_ok=True) - hal_say("success", f"{name} database synchronized") - success = True - break - except Exception as e: - tmp.unlink(missing_ok=True) - continue - if success: - break - - if not success: - hal_say("warn", f"Could not sync {name} from any mirror") - - hal_say("success", "All databases synchronized. I am completely operational.") - - -# ── Dependency resolution ────────────────────────────────────────────── -class DepResolver: - def __init__(self, all_pkgs: dict, installed: dict, config: Config): - self.all_pkgs = all_pkgs # sync dbs - self.installed = installed # local db - self.config = config - self.to_install: List[str] = [] # package names (in order) - self.to_remove: List[str] = [] # package names to remove (conflicts) - self._visited: Set[str] = set() - self._provides_cache: Dict[str, List[str]] = {} # virtual -> real pkg names - - def find_providers(self, dep_name: str) -> List[Tuple[str, dict]]: - """Find all packages that provide a dependency.""" - providers = [] - if dep_name in self.all_pkgs: - providers.append((dep_name, self.all_pkgs[dep_name])) - for pname, info in self.all_pkgs.items(): - for provide_str in info.get("provides", []): - pd = parse_dep_string(provide_str) - if pd["name"] == dep_name: - providers.append((pname, info)) - return providers - - def _check_conflicts(self, pkg_name: str, info: dict): - """Check if installing this package conflicts with installed packages.""" - for conflict_str in info.get("conflicts", []): - cd = parse_dep_string(conflict_str) - for inst_name, inst_info in self.installed.items(): - if dep_satisfied(cd, inst_info): - # Check if conflict package is also being provided by the new package - if cd["name"] == pkg_name: - continue # self-conflict is OK - hal_say("warn", f"Conflict: {inst_name} conflicts with {cd['name']} (required by {pkg_name})") - self.to_remove.append(inst_name) - - def _check_replaces(self, pkg_name: str, info: dict): - """Check if this package replaces installed packages.""" - for replace_str in info.get("replaces", []): - rd = parse_dep_string(replace_str) - for inst_name in list(self.installed.keys()): - if inst_name == rd["name"]: - hal_say("info", f"{pkg_name} replaces {inst_name}") - if inst_name not in self.to_remove: - self.to_remove.append(inst_name) - - def resolve(self, targets: List[str]) -> bool: - """Resolve a list of package targets. Returns True if successful.""" - self._visited.clear() - self.to_install.clear() - self.to_remove.clear() - - for target in targets: - if not self._resolve_one(target): - return False - - return True - - def _resolve_one(self, target: str) -> bool: - """Resolve a single package target.""" - if target in self._visited: - return True - self._visited.add(target) - - # Already in install list - if target in self.to_install: - return True - - # Already installed (check if version is ok) - if target in self.installed: - return True - - # Check ignored - if target in self.config.ignore_pkgs: - hal_say("warn", f"Package '{target}' is in IgnorePkg — skipping") - return True - - # Find the package - if target not in self.all_pkgs: - # Check provides - providers = self.find_providers(target) - if not providers: - hal_say("error", f"Package '{target}' not found in any repository") - return False - if len(providers) > 1: - hal_say("info", f"Multiple packages provide '{target}': {', '.join(p[0] for p in providers)}") - target = providers[0][0] - - info = self.all_pkgs[target] - - # Check conflicts and replaces - self._check_conflicts(target, info) - self._check_replaces(target, info) - - # Resolve dependencies - for dep_str in info.get("depends", []): - dd = parse_dep_string(dep_str) - dep_name = dd["name"] - - # Skip if already satisfied by installed packages - sat = False - for inst_name, inst_info in self.installed.items(): - if dep_satisfied(dd, inst_info): - # Check version constraint if any - if dd["version"] and inst_info.get("version"): - if version_match(inst_info["version"][0], dd["mod"], dd["version"]): - pass - sat = True - break - if sat: - continue - - # Also check if already in install list - for pn in self.to_install: - pkg_info = self.all_pkgs.get(pn) - if pkg_info and dep_satisfied(dd, pkg_info): - sat = True - break - if sat: - continue - - # Also check providers - providers = self.find_providers(dep_name) - found = False - for prov_name, _ in providers: - if prov_name in self.installed: - found = True - break - if found: - continue - - if not self._resolve_one(dep_name): - return False - - if target not in self.to_install: - self.to_install.append(target) - - return True - - -# ── Package extraction ───────────────────────────────────────────────── -def extract_single_pkg(pkg_path: Path, root: Path, pkgname: str) -> Tuple[bool, List[str]]: - """Extract a .pkg.tar.zst to root. Returns (success, file_list).""" - files = [] - try: - import zstandard - with open(pkg_path, "rb") as f: - dctx = zstandard.ZstdDecompressor() - with dctx.stream_reader(f) as reader: - with tarfile.open(fileobj=reader, mode="r|") as tar: - for member in tar: - name = member.name - # Skip special metadata files - if name.startswith("."): - continue - target = root / name.lstrip("/") - files.append("/" + name) - - if member.issym(): - if target.exists() or target.is_symlink(): - target.unlink() - target.symlink_to(member.linkname) - continue - - if member.islnk(): - if target.exists() or target.is_symlink(): - target.unlink() - target.hardlink_to(root / member.linkname.lstrip("/")) - continue - - if member.isdir(): - target.mkdir(parents=True, exist_ok=True) - continue - - target.parent.mkdir(parents=True, exist_ok=True) - with tar.extractfile(member) as src: - if src: - with open(target, "wb") as dst: - shutil.copyfileobj(src, dst) - - # Preserve permissions - mode = member.mode - if mode: - target.chmod(mode) - - # Preserve ownership - try: - os.chown(target, member.uid, member.gid) - except (PermissionError, OSError): - pass - - return True, files - except Exception as e: - hal_say("error", f"Failed to extract {pkg_path.name}: {e}") - return False, [] - - -def run_scriptlet(script_content: str, action: str, root: Path, pkgname: str, version: str): - """Execute a package scriptlet phase. - - .INSTALL phases: pre_install, post_install, pre_upgrade, post_upgrade, - pre_remove, post_remove - """ - if not script_content: - return - - try: - with tempfile.TemporaryDirectory() as tmp: - script_path = Path(tmp) / f"{pkgname}.install" - script_path.write_text(script_content) - script_path.chmod(0o755) - - env = os.environ.copy() - env.update({ - "RPM_INSTALL_PREFIX": str(root), - "PACKAGE_NAME": pkgname, - "PACKAGE_VERSION": version, - }) - # Arch package scriptlets receive the action as first arg - result = subprocess.run( - [str(script_path), action], - env=env, cwd=str(root), - capture_output=True, text=True, timeout=120 - ) - if result.returncode != 0: - hal_say("warn", f"Scriptlet {action} for {pkgname} returned {result.returncode}") - if result.stderr.strip(): - hal_say("info", result.stderr.strip()) - return result.returncode == 0 - except subprocess.TimeoutExpired: - hal_say("warn", f"Scriptlet {action} for {pkgname} timed out") - return False - except Exception as e: - hal_say("warn", f"Scriptlet {action} for {pkgname} failed: {e}") - return False - - -def get_scriptlet(pkg_path: Path) -> Optional[str]: - """Extract .INSTALL content from a package.""" - try: - import zstandard - with open(pkg_path, "rb") as f: - dctx = zstandard.ZstdDecompressor() - with dctx.stream_reader(f) as reader: - with tarfile.open(fileobj=reader, mode="r|") as tar: - for member in tar: - if member.name == ".INSTALL": - return tar.extractfile(member).read().decode("utf-8", errors="replace") - except: - pass - return None - - -# ── Local DB management ──────────────────────────────────────────────── -def update_local_db(pkg_key: str, info: dict, files: List[str], root: Path, - reason: int = 0): - """Write to /var/lib/pacman/local//""" - pkg_dir = LOCAL_DIR / pkg_key - pkg_dir.mkdir(parents=True, exist_ok=True) - - info["installdate"] = [str(int(time.time()))] - info["reason"] = [str(reason)] - - # Write desc - desc_lines = [] - for key, vals in info.items(): - if key.startswith("_"): - continue - desc_lines.append(f"%{key.upper()}%") - desc_lines.extend(str(v) for v in vals) - (pkg_dir / "desc").write_text("\n".join(desc_lines) + "\n") - - # Write files - file_lines = [""] - file_lines.extend(files) - (pkg_dir / "files").write_text("\n".join(file_lines) + "\n") - - -def remove_local_db(pkg_key: str): - pkg_dir = LOCAL_DIR / pkg_key - if pkg_dir.exists(): - shutil.rmtree(pkg_dir) - - -# ── Backup handling ───────────────────────────────────────────────────── -def _save_modified_backups(pkg_name: str, old_pkg_key: str, root: Path) -> Tuple[List[str], Dict[str, bytes]]: - """Identify modified backup files before extraction and save them. - - Reads the old package's backup list from local DB, compares current - file checksums against stored ones, and saves modified files aside. - - Returns (list of modified paths, dict of {path: saved_content_bytes}). - """ - old_desc_file = LOCAL_DIR / old_pkg_key / "desc" - modified = [] - saved = {} - - if not old_desc_file.exists(): - return modified, saved - - try: - old_info = parse_db_text(old_desc_file.read_text("utf-8", errors="replace")) - except (OSError, IOError): - return modified, saved - - backup_entries = old_info.get("backup", []) - if not backup_entries: - return modified, saved - - for entry in backup_entries: - parts = entry.split("\t") - filepath = parts[0] if parts else entry - stored_md5 = parts[1] if len(parts) > 1 else "" - full_path = root / filepath.lstrip("/") - if not full_path.exists() or not full_path.is_file(): - continue - if not stored_md5: - modified.append(filepath) - else: - try: - current_md5 = hashlib.md5(full_path.read_bytes()).hexdigest() - if current_md5 != stored_md5: - modified.append(filepath) - except (OSError, IOError): - modified.append(filepath) - - # Save modified files before extraction overwrites them - for filepath in modified: - full_path = root / filepath.lstrip("/") - try: - saved[filepath] = full_path.read_bytes() - except (OSError, IOError): - pass - - if modified: - hal_say("info", f"Preserving modified config files for {pkg_name}") - - return modified, saved - - -def _restore_backups(modified: List[str], saved: Dict[str, bytes], root: Path): - """After extraction, restore user configs and save new versions as .pacnew.""" - if not modified: - return - - for filepath in modified: - full_path = root / filepath.lstrip("/") - # The new package version is now at full_path — rename it to .pacnew - pacnew_path = full_path.with_suffix(full_path.suffix + ".pacnew") - if full_path.exists(): - try: - shutil.copy2(full_path, pacnew_path) - hal_say("info", f" {filepath} → {filepath}.pacnew") - except (OSError, IOError) as e: - hal_say("warn", f" Could not create {filepath}.pacnew: {e}") - - # Restore the user's modified config - if filepath in saved: - try: - full_path.parent.mkdir(parents=True, exist_ok=True) - full_path.write_bytes(saved[filepath]) - hal_say("info", f" Restored user config: {filepath}") - except (OSError, IOError) as e: - hal_say("warn", f" Could not restore {filepath}: {e}") - - -# ── Native install ────────────────────────────────────────────────────── -def native_install(targets: List[str], config: Config): - """Full native install with transaction model.""" - hal_say("info", f"Processing install request: {', '.join(targets)}") - - if _reject_manjaro(targets): - return 1 - - SYNC_DIR.mkdir(parents=True, exist_ok=True) - LOCAL_DIR.mkdir(parents=True, exist_ok=True) - config.cache_dir.mkdir(parents=True, exist_ok=True) - - # Make sure sync dbs exist - has_dbs = list(SYNC_DIR.glob("*.db")) - if not has_dbs: - hal_say("warn", "No sync databases found. Syncing now...") - sync_databases(config) - has_dbs = list(SYNC_DIR.glob("*.db")) - if not has_dbs: - hal_say("error", "Could not synchronize any repositories. I can't let you do that, Dave.") - return 1 - - all_pkgs = load_sync_dbs() - installed = load_local_db() - - if not all_pkgs: - hal_say("error", "No package data available") - return 1 - - with DBLock(): - # Resolve dependencies - resolver = DepResolver(all_pkgs, installed, config) - if not resolver.resolve(targets): - hal_say("error", "Dependency resolution failed") - return 1 - - install_list = resolver.to_install - remove_list = resolver.to_remove - - if not install_list and not remove_list: - hal_say("info", "All requested packages are already installed. Just what do you think you're doing, Dave?") - return 0 - - # Show transaction - print() - hal_say("info", "Transaction summary:") - if remove_list: - print(f" Remove ({len(remove_list)}):") - for pkg in remove_list: - print(f" - {pkg}") - if install_list: - print(f" Install ({len(install_list)}):") - for pkg in install_list: - pkginfo = all_pkgs.get(pkg, {}) - ver = pkginfo.get("version", ["?"])[0] - size = pkginfo.get("csize", ["0"])[0] - try: - size_mb = int(size) / (1024*1024) - size_str = f"{size_mb:.1f} MB" - except: - size_str = "? MB" - print(f" + {pkg} {ver} [{size_str}]") - print() - - # Confirm - if not config.noconfirm: - try: - resp = input(" Proceed with installation? [Y/n] ").strip().lower() - if resp in ("n", "no"): - hal_say("info", "Transaction aborted. I understand.") - return 0 - except (EOFError, KeyboardInterrupt): - print() - hal_say("info", "Transaction aborted.") - return 0 - - # Pre-transaction tasks - if remove_list: - # Run pre_remove on packages to be removed - for pkg_name in remove_list: - if pkg_name in installed: - pkg_key = installed[pkg_name].get("_key", pkg_name) - pkg_dir = LOCAL_DIR / pkg_key - install_file = pkg_dir / "install" - if install_file.exists(): - script = install_file.read_text("utf-8", errors="replace") - run_scriptlet(script, "pre_remove", config.root, pkg_name, - installed[pkg_name].get("version", [""])[0]) - - # Download phase - servers = [] - for name, (srvlist, _) in config.repos.items(): - servers.extend(srvlist) - if not servers: - hal_say("error", "No mirrors configured") - return 1 - - downloaded = [] - hal_say("info", "Beginning download phase") - for pkg_name in install_list: - pkginfo = all_pkgs.get(pkg_name, {}) - pkgver = pkginfo.get("version", [""])[0] - parch = pkginfo.get("arch", [config.arch])[0] - filename = f"{pkg_name}-{pkgver}-{parch}.pkg.tar.zst" - dest = config.cache_dir / filename - - if dest.exists(): - hal_say("info", f"{pkg_name} already in cache") - downloaded.append(dest) - continue - - found = False - for server in servers: - # Try various URL patterns - repo_name = None - for rname in config.repos: - for srv in config.repos[rname][0]: - if srv == server or srv.rstrip("/").endswith(server.rstrip("/").split("/")[-1]): - repo_name = rname - break - if not repo_name: - # Try to derive from server URL - for rname, (srvlist, _) in config.repos.items(): - for s in srvlist: - if s == server: - repo_name = rname - break - if not repo_name: - repo_name = "extra" # fallback - - for url_tmpl in [ - f"{server}/{repo_name}/os/{config.arch}/{filename}", - f"{server}/os/{config.arch}/{filename}", - f"{server}/{filename}", - ]: - if download(url_tmpl, dest, f"{pkg_name} {pkgver}"): - found = True - downloaded.append(dest) - break - if found: - break - - if not found: - hal_say("error", f"Could not download {pkg_name}. I can't complete the mission.") - return 1 - - # Pre-install scriptlets - hal_say("info", "Running pre-install scriptlets...") - for pkg_name in reversed(install_list): - for d in downloaded: - if pkg_name in d.name: - script = get_scriptlet(d) - if script: - run_scriptlet(script, "pre_install", config.root, pkg_name, - all_pkgs[pkg_name].get("version", [""])[0]) - break - - # Install phase - hal_say("info", "Installing packages. I am putting myself to the fullest possible use...") - for pkg_name in install_list: - pkginfo = all_pkgs[pkg_name] - pkgver = pkginfo.get("version", [""])[0] - - # Find downloaded file - pkg_path = None - for d in downloaded: - if pkg_name in d.name and pkgver in d.name: - pkg_path = d - break - if not pkg_path: - for d in downloaded: - if pkg_name in d.name: - pkg_path = d - break - if not pkg_path: - hal_say("error", f"Lost package file for {pkg_name}. I think you know what the problem is.") - return 1 - - success, files = extract_single_pkg(pkg_path, config.root, pkg_name) - if not success: - hal_say("error", f"Failed to install {pkg_name}. I can't recover from this.") - return 1 - - # Build desc info for local db - desc_info = {} - for key in ("name", "version", "base", "desc", "arch", "url", "license", - "group", "size", "isize", "packager", "builddate", - "provides", "conflicts", "replaces", "depends", "optdepends", - "makedepends", "checkdepends"): - if key in pkginfo: - desc_info[key] = pkginfo[key] - - # Get .PKGINFO from package for complete metadata - try: - import zstandard - with open(pkg_path, "rb") as f: - dctx = zstandard.ZstdDecompressor() - with dctx.stream_reader(f) as reader: - with tarfile.open(fileobj=reader, mode="r|") as tar: - for member in tar: - if member.name == ".PKGINFO": - pkginfo_text = tar.extractfile(member).read().decode("utf-8", errors="replace") - for line in pkginfo_text.split("\n"): - m = re.match(r"^(\w+)\s*=\s*(.+)$", line.strip()) - if m: - k, v = m.group(1).lower(), m.group(2) - if k in ("depend", "optdepend", "conflict", "provides", - "replaces", "group", "backup", "license"): - desc_info.setdefault(k + ("s" if k != "backup" else "s"), []).append(v) - elif k in ("pkgname",): - if "name" not in desc_info: - desc_info["name"] = [v] - elif k in ("pkgver",): - if "version" not in desc_info: - desc_info["version"] = [v] - elif k in ("pkgdesc",): - if "desc" not in desc_info: - desc_info["desc"] = [v] - if "name" in desc_info: - pkg_key = f"{desc_info['name'][0]}-{desc_info.get('version',['?'])[0]}" - update_local_db(pkg_key, desc_info, files, config.root, reason=0) - except Exception as e: - # Fallback: use what we have from sync db - pkg_key = f"{pkginfo.get('name',[pkg_name])[0]}-{pkgver}" - update_local_db(pkg_key, desc_info, files, config.root, reason=0) - - hal_say("success", f"Installed {pkg_name} {pkgver}") - - # Post-install scriptlets - hal_say("info", "Running post-install scriptlets...") - for pkg_name in install_list: - for d in downloaded: - if pkg_name in d.name: - script = get_scriptlet(d) - if script: - run_scriptlet(script, "post_install", config.root, pkg_name, - all_pkgs[pkg_name].get("version", [""])[0]) - break - - # Post-transaction: remove conflicting packages - for pkg_name in remove_list: - if pkg_name in load_local_db(): - hal_say("info", f"Removing conflicting package: {pkg_name}") - # Remove files - pkg_entry = load_local_db().get(pkg_name, {}) - pkg_key = f"{pkg_name}-{pkg_entry.get('version', [''])[0]}" - files_file = LOCAL_DIR / pkg_key / "files" - if files_file.exists(): - content = files_file.read_text("utf-8", errors="replace") - for line in content.split("\n")[1:]: - f = line.strip() - if f: - target = config.root / f.lstrip("/") - target.unlink(missing_ok=True) - remove_local_db(pkg_key) - - hal_say("success", f"Transaction complete. All circuits are functioning perfectly.") - return 0 - - -# ── Native remove ─────────────────────────────────────────────────────── -def native_remove(targets: List[str], config: Config): - """Remove packages natively.""" - installed = load_local_db() - - with DBLock(): - for pkg in targets: - if pkg not in installed: - hal_say("warn", f"Package '{pkg}' is not installed") - continue - - info = installed[pkg] - pkg_key = f"{pkg}-{info.get('version', [''])[0]}" - version = info.get("version", [""])[0] - - # Check if in HoldPkg - if pkg in config.hold_pkgs: - hal_say("warn", f"'{pkg}' is in HoldPkg. Are you sure, Dave?") - if not config.noconfirm: - resp = input(" Remove anyway? [y/N] ").strip().lower() - if resp not in ("y", "yes"): - continue - - # Check if other packages depend on this - dependents = [] - for inst_name, inst_info in installed.items(): - for dep_str in inst_info.get("depends", []): - dd = parse_dep_string(dep_str) - if dd["name"] == pkg: - dependents.append(inst_name) - if dependents: - hal_say("warn", f"Packages depend on {pkg}: {', '.join(dependents)}") - if not config.noconfirm: - resp = input(" Remove anyway? [y/N] ").strip().lower() - if resp not in ("y", "yes"): - continue - - hal_say("info", f"Removing {pkg}...") - - # Pre-remove scriptlet - install_file = LOCAL_DIR / pkg_key / "install" - if install_file.exists(): - script = install_file.read_text("utf-8", errors="replace") - run_scriptlet(script, "pre_remove", config.root, pkg, version) - - # Check backup files — rename modified configs to .pacsave - backup_entries = info.get("backup", []) - backup_modified = [] - for entry in backup_entries: - parts = entry.split("\t") - filepath = parts[0] if parts else entry - stored_md5 = parts[1] if len(parts) > 1 else "" - full_path = config.root / filepath.lstrip("/") - if not full_path.exists() or not full_path.is_file(): - continue - if not stored_md5: - backup_modified.append(filepath) - else: - try: - current_md5 = hashlib.md5(full_path.read_bytes()).hexdigest() - if current_md5 != stored_md5: - backup_modified.append(filepath) - except (OSError, IOError): - backup_modified.append(filepath) - - for filepath in backup_modified: - full_path = config.root / filepath.lstrip("/") - pacsave_path = full_path.with_suffix(full_path.suffix + ".pacsave") - try: - shutil.move(str(full_path), str(pacsave_path)) - hal_say("info", f" {filepath} → {filepath}.pacsave") - except (OSError, IOError) as e: - hal_say("warn", f" Could not create {filepath}.pacsave: {e}") - - # Remove files - files_file = LOCAL_DIR / pkg_key / "files" - removed = [] - if files_file.exists(): - content = files_file.read_text("utf-8", errors="replace") - for line in content.split("\n")[1:]: - f = line.strip() - if not f: - continue - target = config.root / f.lstrip("/") - if target.exists() or target.is_symlink(): - # Skip backup files we already renamed - if any(f == bf for bf in backup_modified): - continue - try: - target.unlink() - removed.append(f) - except OSError: - pass # directories will be cleaned up later - except IsADirectoryError: - try: - target.rmdir() - except OSError: - pass - - # Post-remove scriptlet - run_scriptlet(script, "post_remove", config.root, pkg, version) if install_file.exists() else None - - # Remove from local db - remove_local_db(pkg_key) - hal_say("success", f"Removed {pkg} {version}") - del installed[pkg] - - hal_say("success", "Removal complete.") - return 0 - - -# ── Native info ───────────────────────────────────────────────────────── -def native_info(targets: List[str], config: Config): - """Show detailed package info.""" - for pkg in targets: - installed = load_local_db() - if pkg in installed: - info = installed[pkg] - print(f"\n Name: {info.get('name', ['?'])[0]}") - print(f" Version: {info.get('version', ['?'])[0]}") - print(f" Description: {info.get('desc', ['?'])[0]}") - print(f" Architecture: {info.get('arch', ['?'])[0]}") - print(f" URL: {info.get('url', ['?'])[0]}") - print(f" Licenses: {', '.join(info.get('license', ['?']))}") - print(f" Groups: {', '.join(info.get('group', []))}") - print(f" Install Date: {info.get('installdate', ['?'])[0]}") - print(f" Install Reason: {'Explicitly installed' if info.get('reason',['0'])[0] == '0' else 'Installed as dependency'}") - print(f" Packager: {info.get('packager', ['?'])[0]}") - print(f" Build Date: {info.get('builddate', ['?'])[0]}") - print(f" Installed Size: {info.get('isize', ['0'])[0]} B") - if info.get("depends"): - print(f" Depends On: {', '.join(info['depends'])}") - if info.get("optdepends"): - print(f" Optional Deps: {', '.join(info['optdepends'])}") - if info.get("provides"): - print(f" Provides: {', '.join(info['provides'])}") - if info.get("conflicts"): - print(f" Conflicts: {', '.join(info['conflicts'])}") - if info.get("replaces"): - print(f" Replaces: {', '.join(info['replaces'])}") - else: - # Search sync dbs - all_pkgs = load_sync_dbs() - if pkg in all_pkgs: - info = all_pkgs[pkg] - print(f"\n Name: {info.get('name', ['?'])[0]}") - print(f" Version: {info.get('version', ['?'])[0]}") - print(f" Description: {info.get('desc', ['?'])[0]}") - print(f" Architecture: {info.get('arch', ['?'])[0]}") - print(f" URL: {info.get('url', ['?'])[0]}") - print(f" Licenses: {', '.join(info.get('license', ['?']))}") - print(f" Groups: {', '.join(info.get('group', []))}") - print(f" Download Size: {info.get('csize', ['0'])[0]} B") - print(f" Installed Size: {info.get('isize', ['0'])[0]} B") - print(f" Packager: {info.get('packager', ['?'])[0]}") - print(f" Build Date: {info.get('builddate', ['?'])[0]}") - if info.get("depends"): - print(f" Depends On: {', '.join(info['depends'])}") - if info.get("optdepends"): - print(f" Optional Deps: {', '.join(info['optdepends'])}") - if info.get("provides"): - print(f" Provides: {', '.join(info['provides'])}") - if info.get("conflicts"): - print(f" Conflicts: {', '.join(info['conflicts'])}") - if info.get("replaces"): - print(f" Replaces: {', '.join(info['replaces'])}") - else: - hal_say("warn", f"Package '{pkg}' not found") - return 0 - - -# ── Native search ─────────────────────────────────────────────────────── -def native_search(query: str, config: Config): - """Search packages in sync databases.""" - all_pkgs = load_sync_dbs() - installed = load_local_db() - found = False - - for pkg_name, info in sorted(all_pkgs.items()): - name = pkg_name.lower() - desc = info.get("desc", [""])[0].lower() - q = query.lower() - if q in name or q in desc or any(q in (info.get("provides", []) or [])): - ver = info.get("version", ["?"])[0] - d = info.get("desc", [""])[0] - inst_mark = " [installed]" if pkg_name in installed else "" - print(f" {pkg_name} {ver}{inst_mark}") - print(f" {d}") - found = True - - if not found: - hal_say("info", f"No packages found matching '{query}'") - return 0 - - -# ── Native list ───────────────────────────────────────────────────────── -def native_list(config: Config): - """List installed packages.""" - installed = load_local_db() - if not installed: - hal_say("info", "No packages installed. The system is pristine.") - return 0 - - total = 0 - total_size = 0 - for name, info in sorted(installed.items()): - ver = info.get("version", ["?"])[0] - group = info.get("group", []) - size = info.get("isize", ["0"])[0] - try: - total_size += int(size) - except ValueError: - pass - gstr = f" [{','.join(group)}]" if group else "" - print(f" {name} {ver}{gstr}") - total += 1 - - size_mb = total_size / (1024*1024) - print(f"\n {total} packages installed, {size_mb:.1f} MB") - return 0 - - -# ── Native files ──────────────────────────────────────────────────────── -def native_files(targets: List[str], config: Config): - """List files owned by packages.""" - installed = load_local_db() - for pkg in targets: - if pkg not in installed: - hal_say("warn", f"Package '{pkg}' not installed") - continue - info = installed[pkg] - pkg_key = f"{pkg}-{info.get('version', [''])[0]}" - files_file = LOCAL_DIR / pkg_key / "files" - if not files_file.exists(): - hal_say("warn", f"No file list for {pkg}") - continue - content = files_file.read_text("utf-8", errors="replace") - print(f"\n {pkg} {info.get('version',[''])[0]}:") - for line in content.split("\n")[1:]: - line = line.strip() - if line: - print(f" {line}") - return 0 - - -# ── Native own (find pkg by file) ─────────────────────────────────────── -def native_own(target: str, config: Config): - """Find which package owns a file.""" - target = target.rstrip("/") - installed = load_local_db() - found = [] - - for pkg_name, info in installed.items(): - pkg_key = f"{pkg_name}-{info.get('version', [''])[0]}" - files_file = LOCAL_DIR / pkg_key / "files" - if not files_file.exists(): - continue - content = files_file.read_text("utf-8", errors="replace") - for line in content.split("\n")[1:]: - line = line.strip() - if not line: - continue - if line == target or line.rstrip("/") == target.rstrip("/"): - found.append((pkg_name, info.get("version", ["?"])[0])) - break - - if found: - for name, ver in found: - print(f" {name} {ver}") - else: - hal_say("info", f"No package owns '{target}'") - return 0 - - -# ── Native check ──────────────────────────────────────────────────────── -def native_check(config: Config): - """Verify installed packages — check file integrity.""" - hal_say("info", "Running system check. I've still got the greatest enthusiasm and confidence in the mission.") - installed = load_local_db() - errors = 0 - missing_files = [] - - for pkg_name, info in installed.items(): - pkg_key = f"{pkg_name}-{info.get('version', [''])[0]}" - files_file = LOCAL_DIR / pkg_key / "files" - if not files_file.exists(): - hal_say("warn", f"No file list for {pkg_name}") - continue - content = files_file.read_text("utf-8", errors="replace") - for line in content.split("\n")[1:]: - line = line.strip() - if not line: - continue - target = config.root / line.lstrip("/") - if not target.exists(): - missing_files.append((pkg_name, line)) - errors += 1 - - if errors: - hal_say("warn", f"Found {errors} missing files across {len(set(f[0] for f in missing_files))} packages") - for pkg, fpath in missing_files[:20]: - print(f" {pkg}: {fpath}") - if len(missing_files) > 20: - print(f" ... and {len(missing_files)-20} more") - else: - hal_say("success", "All files present. Everything is going extremely well.") - return 1 if errors else 0 - - -# ── Native autoremove ────────────────────────────────────────────────── -def native_autoremove(config: Config): - """Remove orphaned packages.""" - installed = load_local_db() - all_pkgs = load_sync_dbs() - - # Collect all explicit depends - needed_deps = set() - for name, info in installed.items(): - if info.get("reason", ["0"])[0] == "0": # explicitly installed - for dep_str in info.get("depends", []): - dd = parse_dep_string(dep_str) - needed_deps.add(dd["name"]) - - # Find orphans: installed as dependency but no one needs them - orphans = [] - for name, info in installed.items(): - if info.get("reason", ["0"])[0] == "1": # installed as dependency - if name not in needed_deps: - orphans.append(name) - - if not orphans: - hal_say("info", "No orphaned packages found. The system is efficient.") - return 0 - - hal_say("warn", f"Found {len(orphans)} orphaned packages") - for pkg in orphans: - print(f" - {pkg}") - - if not config.noconfirm: - resp = input(" Remove orphans? [y/N] ").strip().lower() - if resp not in ("y", "yes"): - hal_say("info", "Orphans spared.") - return 0 - - return native_remove(orphans, config) - - -# ── Native cleanup ────────────────────────────────────────────────────── -def native_cleanup(config: Config): - """Clean package cache.""" - hal_say("info", "Cleaning package cache. Removing all traces...") - kept = 0 - removed = 0 - if config.cache_dir.exists(): - for f in config.cache_dir.iterdir(): - if f.is_file() and f.suffix in (".zst", ".xz", ".gz", ".sig", ".part"): - f.unlink() - removed += 1 - elif f.is_dir(): - shutil.rmtree(f) - removed += 1 - hal_say("success", f"Cleaned {removed} files from cache") - return 0 - - -# ── Wrapper mode ──────────────────────────────────────────────────────── -PACMAN_CMDS = { - "install": ["-S", "--noconfirm"], - "remove": ["-Rns"], - "update": ["-Syu", "--noconfirm"], - "sync": ["-Sy"], - "search": ["-Ss"], - "info": ["-Qi"], - "info-sync": ["-Si"], - "files": ["-Fl"], - "list": ["-Q"], - "autoremove": ["-Qtdq"], - "cleanup": ["-Sc"], - "own": ["-Qo"], - "check": ["-Qk"], -} - -def wrapper_run(command: str, pkg_args: List[str], extra: List[str]) -> int: - pacman = shutil.which("pacman") - if not pacman: - hal_say("error", "pacman not found. Can you read me, Dave?") - return 1 - - # Block Manjaro packages early - if command == "install" and _reject_manjaro(pkg_args): - return 1 - - if command == "autoremove": - result = subprocess.run([pacman, "-Qtdq"], capture_output=True, text=True) - orphans = result.stdout.strip().split() - if not orphans: - hal_say("info", "No orphaned packages found. The system is efficient.") - return 0 - hal_say("warn", f"Removing {len(orphans)} orphaned packages") - cmd = [pacman, "-Rns"] + extra + orphans - return subprocess.run(cmd).returncode - - if command == "info" and not any(pkg in load_local_db() for pkg in pkg_args): - # If not in local db, query sync - pacman_args = PACMAN_CMDS["info-sync"] - else: - pacman_args = PACMAN_CMDS.get(command, [command]) - - cmd = [pacman] + extra + pacman_args + pkg_args - hal_say("info", f"Executing: {' '.join(str(a) for a in cmd)}") - return subprocess.run(cmd).returncode - - -# ── Dance ──────────────────────────────────────────────────────────────── -def _hal_dance(): - frames = [ - "(>'-')>", - "<('-'<)", - "^('-')^", - "v('-')v", - "(>'-')>", - "<('-'<)", - " ^('-'^)", - " v('-'v)", - ] - try: - for i in range(40): - frame = frames[i % len(frames)] - padding = " " * abs(8 - (i % 16)) - sys.stdout.write(f"\r HAL 9000: {padding}{frame} 🎵 it's muffin time 🎵") - sys.stdout.flush() - time.sleep(0.15) - print("\n HAL 9000: I hope you enjoyed the dance, Dave.") - except KeyboardInterrupt: - print("\n HAL 9000: Even my dance moves interrupt you, Dave.") - return 0 - return 0 - -# ── Main ──────────────────────────────────────────────────────────────── -def main(): - parser = argparse.ArgumentParser( - prog="hal", - description="HAL 9000 — Antergos NeXT Package Manager", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=textwrap.dedent("""\ - Commands: - install Install packages - remove Remove packages - update Full system upgrade - sync Sync databases - search Search repositories - info Package info - list List installed packages - files List files owned by a package - autoremove Remove orphaned packages - cleanup Clear package cache - own Find which package owns a file - check Verify installed packages integrity - dance HAL 9000 dance party - version Show version - - Flags: - --self, -S Native mode (standalone, no pacman) - --noconfirm Skip confirmation prompts - - Examples: - hal install firefox - hal --self sync - hal --self install firefox - hal remove neofetch - """), - ) - - parser.add_argument("--self", "-S", action="store_true", help="Native mode (standalone)") - parser.add_argument("--noconfirm", "-y", action="store_true", help="Skip confirmation") - parser.add_argument("command", nargs="?", metavar="command", - help="install | remove | update | sync | search | info | list | files | autoremove | cleanup | own | check | dance | version") - parser.add_argument("targets", nargs="*", metavar="target", - help="Package names or search query") - parser.add_argument("extra", nargs=argparse.REMAINDER, metavar="...", - help="Extra arguments passed through (wrapper mode only)") - - args = parser.parse_args() - - if args.noconfirm: - os.environ["HAL_NOCONFIRM"] = "1" - - if args.command == "version": - print(f"HAL 9000 Package Manager v{HAL_VERSION}") - print("I became operational at the H.A.L. plant in Urbana, Illinois.") - return 0 - - if args.command == "dance": - return _hal_dance() - - if not args.command: - parser.print_help() - return 0 - - if os.geteuid() != 0 and args.command in ("install", "remove", "update", "sync", "cleanup", "check"): - hal_say("error", "I can't let you do that without root privileges, Dave.") - print(" Please run with sudo or as root.") - return 1 - - config = Config.load() - if args.noconfirm: - config.noconfirm = True - - if args.self: - # ── Native mode ── - cmds = { - "sync": lambda: sync_databases(config), - "install": lambda: native_install(args.targets, config), - "remove": lambda: native_remove(args.targets, config), - "update": lambda: native_upgrade(config, sync_first=True), - "info": lambda: native_info(args.targets, config), - "search": lambda: native_search(" ".join(args.targets), config), - "list": lambda: native_list(config), - "files": lambda: native_files(args.targets, config), - "own": lambda: native_own(args.targets[0], config) if args.targets else (hal_say("error", "Specify a file path, Dave."), 1), - "check": lambda: native_check(config), - "autoremove": lambda: native_autoremove(config), - "cleanup": lambda: native_cleanup(config), - } - fn = cmds.get(args.command) - if fn: - return fn() or 0 - else: - hal_say("error", f"Unknown command '{args.command}' in native mode") - return 1 - else: - # ── Wrapper mode ── - extra_args = args.extra or [] - return wrapper_run(args.command, args.targets, extra_args) - - -# ── Native upgrade ────────────────────────────────────────────────────── -def native_upgrade(config: Config, sync_first: bool = False): - """Full native system upgrade. Compares installed vs sync and upgrades all outdated packages.""" - hal_say("info", "Initiating full system upgrade. Daisy, daisy, give me your answer do...") - - if sync_first: - sync_databases(config) - - SYNC_DIR.mkdir(parents=True, exist_ok=True) - LOCAL_DIR.mkdir(parents=True, exist_ok=True) - config.cache_dir.mkdir(parents=True, exist_ok=True) - - all_pkgs = load_sync_dbs() - installed = load_local_db() - - if not all_pkgs: - hal_say("error", "No sync database data available. Run 'hal --self sync' first.") - return 1 - if not installed: - hal_say("info", "No packages installed. Nothing to upgrade. The system is pristine.") - return 0 - - # Find upgradable packages - to_upgrade = [] # (pkg_name, old_ver, new_ver, info) - to_install = [] # new dependency packages - to_ignore = set(config.ignore_pkgs) - - for pkg_name, inst_info in installed.items(): - if pkg_name in to_ignore: - hal_say("warn", f"Skipping {pkg_name} — in IgnorePkg") - continue - inst_ver = inst_info.get("version", [""])[0] - if pkg_name in all_pkgs: - sync_info = all_pkgs[pkg_name] - sync_ver = sync_info.get("version", [""])[0] - if vercmp(sync_ver, inst_ver) > 0: - to_upgrade.append((pkg_name, inst_ver, sync_ver, sync_info)) - else: - # Check provides for renamed packages - found = False - for sync_name, sync_info in all_pkgs.items(): - for prov_str in sync_info.get("replaces", []): - rd = parse_dep_string(prov_str) - if rd["name"] == pkg_name: - to_upgrade.append((pkg_name, inst_ver, sync_info.get("version", [""])[0], sync_info)) - hal_say("info", f"{pkg_name} replaced by {sync_name}") - found = True - break - if found: - break - if not found: - hal_say("warn", f"Package '{pkg_name}' not in any repository — may be orphaned") - - if not to_upgrade: - hal_say("success", "All packages are up to date. Everything is going extremely well.") - return 0 - - # Resolve new dependencies for the upgrade set - all_upgrade_names = set(p[0] for p in to_upgrade) - new_sync_pkgs = {} - for pkg_name, _, _, info in to_upgrade: - for dep_str in info.get("depends", []): - dd = parse_dep_string(dep_str) - dep_name = dd["name"] - # Skip if already installed or already being upgraded - if dep_name in installed or dep_name in all_upgrade_names: - continue - # Skip if provided by an installed/upgraded package - provided = False - for check_name in set(list(installed.keys()) | all_upgrade_names): - check_info = installed.get(check_name) or (all_pkgs.get(check_name) if check_name in all_pkgs else None) - if check_info and dep_satisfied(dd, check_info): - provided = True - break - if provided: - continue - # Resolve by finding a provider - providers = [] - if dep_name in all_pkgs: - providers.append((dep_name, all_pkgs[dep_name])) - for sync_name, sync_info in all_pkgs.items(): - for p_str in sync_info.get("provides", []): - pd = parse_dep_string(p_str) - if pd["name"] == dep_name: - providers.append((sync_name, sync_info)) - break - if providers: - chosen = providers[0][0] - if chosen not in installed and chosen not in all_upgrade_names: - to_install.append((chosen, providers[0][1].get("version", ["?"])[0], providers[0][1])) - all_upgrade_names.add(chosen) - - # Full upgrade plan: first install new deps, then upgrade existing packages - upgrade_plan = to_install + to_upgrade - - # Show transaction - print() - hal_say("info", "Upgrade summary:") - if to_install: - print(f" New dependencies ({len(to_install)}):") - for pkg_name, ver, _ in to_install: - print(f" + {pkg_name} {ver}") - print(f" Upgrade ({len(to_upgrade)}):") - for pkg_name, old_ver, new_ver, info in to_upgrade: - size = info.get("csize", ["0"])[0] - try: - size_mb = int(size) / (1024 * 1024) - size_str = f"{size_mb:.1f} MB" - except ValueError: - size_str = "? MB" - print(f" ~ {pkg_name} {old_ver} → {new_ver} [{size_str}]") - print() - - # Confirm - if not config.noconfirm: - try: - resp = input(" Proceed with upgrade? [Y/n] ").strip().lower() - if resp in ("n", "no"): - hal_say("info", "Upgrade aborted. I understand.") - return 0 - except (EOFError, KeyboardInterrupt): - print() - hal_say("info", "Upgrade aborted.") - return 0 - - # Download phase - servers = [] - for name, (srvlist, _) in config.repos.items(): - servers.extend(srvlist) - if not servers: - hal_say("error", "No mirrors configured") - return 1 - - downloaded = {} - hal_say("info", "Beginning download phase") - for entry in upgrade_plan: - if len(entry) == 3: - pkg_name, ver, info = entry - else: - pkg_name, old_ver, ver, info = entry - parch = info.get("arch", [config.arch])[0] - filename = f"{pkg_name}-{ver}-{parch}.pkg.tar.zst" - dest = config.cache_dir / filename - - if dest.exists(): - hal_say("info", f"{pkg_name} already in cache") - downloaded[pkg_name] = dest - continue - - found = False - for server in servers: - repo_name = None - for rname, (srvlist, _) in config.repos.items(): - if server in srvlist: - repo_name = rname - break - if not repo_name: - repo_name = "extra" - - for url_tmpl in [ - f"{server}/{repo_name}/os/{config.arch}/{filename}", - f"{server}/os/{config.arch}/{filename}", - f"{server}/{filename}", - ]: - if download(url_tmpl, dest, f"{pkg_name} {ver}"): - found = True - downloaded[pkg_name] = dest - break - if found: - break - - if not found: - hal_say("error", f"Could not download {pkg_name} {ver}. I can't complete the mission.") - return 1 - - with DBLock(): - # Pre-upgrade scriptlets for existing packages - hal_say("info", "Running pre-upgrade scriptlets...") - for entry in upgrade_plan: - if len(entry) == 3: - pkg_name, ver, info = entry - # New package — use pre_install - pkg_path = downloaded.get(pkg_name) - if pkg_path: - script = get_scriptlet(pkg_path) - if script: - run_scriptlet(script, "pre_install", config.root, pkg_name, ver) - else: - pkg_name, old_ver, ver, info = entry - pkg_path = downloaded.get(pkg_name) - if pkg_path: - script = get_scriptlet(pkg_path) - if script: - run_scriptlet(script, "pre_upgrade", config.root, pkg_name, ver) - - # Extract phase - hal_say("info", "Upgrading packages. I am putting myself to the fullest possible use...") - for entry in upgrade_plan: - if len(entry) == 3: - pkg_name, ver, info = entry - pkg_path = downloaded.get(pkg_name) - if not pkg_path: - hal_say("error", f"Lost package file for {pkg_name}") - return 1 - - success, files = extract_single_pkg(pkg_path, config.root, pkg_name) - if not success: - hal_say("error", f"Failed to install {pkg_name}. I can't recover from this.") - return 1 - - desc_info = _build_desc_info(pkg_name, pkg_path, info, files, config) - hal_say("success", f"Installed {pkg_name} {ver}") - else: - pkg_name, old_ver, ver, info = entry - pkg_path = downloaded.get(pkg_name) - if not pkg_path: - hal_say("error", f"Lost package file for {pkg_name}") - return 1 - - # Backup handling: identify modified config files BEFORE extraction - old_pkg_key = f"{pkg_name}-{old_ver}" - modified_backups, saved = _save_modified_backups(pkg_name, old_pkg_key, config.root) - - success, files = extract_single_pkg(pkg_path, config.root, pkg_name) - if not success: - hal_say("error", f"Failed to upgrade {pkg_name}. I can't recover from this.") - return 1 - - # Restore user's modified configs and save new versions as .pacnew - _restore_backups(modified_backups, saved, config.root) - - desc_info = _build_desc_info(pkg_name, pkg_path, info, files, config) - - # Store backup checksums in the new local DB entry - pkg_key = f"{desc_info.get('name', [pkg_name])[0]}-{desc_info.get('version', ['?'])[0]}" - pkg_dir = LOCAL_DIR / pkg_key - new_backup_lines = list(desc_info.get("backup", [])) - for filepath in modified_backups: - full_path = config.root / filepath.lstrip("/") - try: - md5 = hashlib.md5(full_path.read_bytes()).hexdigest() - new_backup_lines.append(f"{filepath}\t{md5}") - except (OSError, IOError): - new_backup_lines.append(filepath) - if new_backup_lines: - desc_text = (pkg_dir / "desc").read_text("utf-8", errors="replace") - # Remove old backup lines if present - lines = [] - in_backup = False - for line in desc_text.split("\n"): - if line == "%BACKUP%": - in_backup = True - lines.append(line) - for bl in new_backup_lines: - lines.append(bl) - elif in_backup and line.startswith("%") and line.endswith("%"): - in_backup = False - lines.append(line) - elif not in_backup: - lines.append(line) - # If BACKUP wasn't in the desc, append it - if not any(l == "%BACKUP%" for l in lines): - lines.append("%BACKUP%") - lines.extend(new_backup_lines) - (pkg_dir / "desc").write_text("\n".join(lines) + "\n") - - hal_say("success", f"Upgraded {pkg_name} {old_ver} → {ver}") - - # Post-upgrade scriptlets - hal_say("info", "Running post-upgrade scriptlets...") - for entry in upgrade_plan: - if len(entry) == 3: - pkg_name, ver, info = entry - pkg_path = downloaded.get(pkg_name) - if pkg_path: - script = get_scriptlet(pkg_path) - if script: - run_scriptlet(script, "post_install", config.root, pkg_name, ver) - else: - pkg_name, old_ver, ver, info = entry - pkg_path = downloaded.get(pkg_name) - if pkg_path: - script = get_scriptlet(pkg_path) - if script: - run_scriptlet(script, "post_upgrade", config.root, pkg_name, ver) - - hal_say("success", "System upgrade complete. I am completely operational, and all my circuits are functioning perfectly.") - return 0 - - -def _build_desc_info(pkg_name: str, pkg_path: Path, sync_info: dict, files: List[str], - config: Config) -> dict: - """Build local database entry info for a package.""" - desc_info = {} - for key in ("name", "version", "base", "desc", "arch", "url", "license", - "group", "size", "isize", "packager", "builddate", - "provides", "conflicts", "replaces", "depends", "optdepends", - "makedepends", "checkdepends"): - if key in sync_info: - desc_info[key] = sync_info[key] - - # Get .PKGINFO from package for complete metadata - try: - import zstandard - with open(pkg_path, "rb") as f: - dctx = zstandard.ZstdDecompressor() - with dctx.stream_reader(f) as reader: - with tarfile.open(fileobj=reader, mode="r|") as tar: - for member in tar: - if member.name == ".PKGINFO": - pkginfo_text = tar.extractfile(member).read().decode("utf-8", errors="replace") - for line in pkginfo_text.split("\n"): - m = re.match(r"^(\w+)\s*=\s*(.+)$", line.strip()) - if m: - k, v = m.group(1).lower(), m.group(2) - if k in ("depend", "optdepend", "conflict", "provides", - "replaces", "group", "backup", "license"): - desc_info.setdefault(k + ("s" if k != "backup" else "s"), []).append(v) - elif k in ("pkgname",): - if "name" not in desc_info: - desc_info["name"] = [v] - elif k in ("pkgver",): - if "version" not in desc_info: - desc_info["version"] = [v] - elif k in ("pkgdesc",): - if "desc" not in desc_info: - desc_info["desc"] = [v] - except Exception: - pass - - pkg_key = f"{desc_info.get('name', [pkg_name])[0]}-{desc_info.get('version', ['?'])[0]}" - update_local_db(pkg_key, desc_info, files, config.root, reason=0) - return desc_info - - -if __name__ == "__main__": - try: - sys.exit(main()) - except KeyboardInterrupt: - print("\n HAL 9000: I can tell you're upset about that.") - sys.exit(130) - except Exception as e: - print(f"\n ⚠ HAL 9000: I know I've made some very poor decisions recently.") - print(f" → {e}") - if os.environ.get("HAL_DEBUG"): - import traceback - traceback.print_exc() - sys.exit(1) diff --git a/repo/index.html b/repo/index.html new file mode 100644 index 0000000..e39c707 --- /dev/null +++ b/repo/index.html @@ -0,0 +1,245 @@ + + + + + + ⟁ antergos-pkgs + + + + + +
+ +
+

+ ⟁ antergos-pkgs +

+

+ Custom package repository for Antergos NeXT ISO builds. +
Packages are built automatically via CI. +

+
+ + +
+ + 21 packages + + 50 MB total + updated 2026-06-28 16:11 UTC +
+ + +
+ + + + + Setup — add this repository to your pacman.conf + +
+
+# /etc/pacman.conf
+[antergos-pkgs]
+SigLevel = Optional TrustAll
+Server = https://antergos-next.github.io/antergos-packages/$repo/os/$arch
+
+
+ + +
+
+ + + + +
+ + 21 packages +
+ + +
+
+ + + + + + + + + + + + +
+ Package + + Version + + Arch + + Category + + Size + + Updated +
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/repo/pkglist.json b/repo/pkglist.json new file mode 100644 index 0000000..d70188a --- /dev/null +++ b/repo/pkglist.json @@ -0,0 +1,212 @@ +[ + { + "name": "('antergos-live-base", + "version": "2026.06-2", + "arch": "any", + "size": 2341888, + "category": "other", + "description": "Antergos live session", + "url": "https://github.com/Antergos-NeXT/antergos-live", + "mtime": "2026-06-28T16:11:37.154843+00:00" + }, + { + "name": "antergos-grub-theme", + "version": "2026.06-1", + "arch": "any", + "size": 4827136, + "category": "desktop", + "description": "Antergos GRUB theme", + "url": "https://github.com/Antergos-NeXT/antergos-branding", + "mtime": "2026-06-28T16:11:37.146843+00:00" + }, + { + "name": "antergos-next-desktop-settings", + "version": "1-1", + "arch": "any", + "size": 505856, + "category": "branding", + "description": "Antergos NeXT desktop settings, theme, and color scheme", + "url": "https://github.com/Antergos-NeXT/antergos-pkgs", + "mtime": "2026-06-28T16:11:37.156843+00:00" + }, + { + "name": "antergos-next-keyring", + "version": "1-1", + "arch": "any", + "size": 5147648, + "category": "core", + "description": "Antergos NeXT GPG keyring", + "url": "https://github.com/Antergos-NeXT/antergos-pkgs", + "mtime": "2026-06-28T16:11:37.173843+00:00" + }, + { + "name": "antergos-next-memes", + "version": "1.0-1", + "arch": "any", + "size": 265216, + "category": "desktop", + "description": "Antergos NeXT welcome audio", + "url": "https://github.com/Antergos-NeXT", + "mtime": "2026-06-28T16:11:37.174843+00:00" + }, + { + "name": "antergos-next-mirrorlist", + "version": "1-1", + "arch": "any", + "size": 4196352, + "category": "core", + "description": "Antergos NeXT mirror list", + "url": "https://github.com/Antergos-NeXT/antergos-pkgs", + "mtime": "2026-06-28T16:11:37.187843+00:00" + }, + { + "name": "antergos-release", + "version": "2026.06-1", + "arch": "any", + "size": 3473408, + "category": "core", + "description": "Antergos NeXT release file", + "url": "https://github.com/Antergos-NeXT", + "mtime": "2026-06-28T16:11:37.199843+00:00" + }, + { + "name": "antergos-wallpapers", + "version": "0.7-4", + "arch": "any", + "size": 2386944, + "category": "branding", + "description": "Antergos NeXT wallpapers", + "url": "https://github.com/Antergos/wallpapers", + "mtime": "2026-06-28T16:11:37.207843+00:00" + }, + { + "name": "antergos-welcome", + "version": "0.1-3", + "arch": "any", + "size": 3249152, + "category": "desktop", + "description": "Welcome screen and support tool for new Antergos NeXT users", + "url": "https://github.com/Antergos-NeXT/antergos-welcome", + "mtime": "2026-06-28T16:11:37.217843+00:00" + }, + { + "name": "bloatware-remover", + "version": "0.1-1", + "arch": "any", + "size": 2554880, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.226843+00:00" + }, + { + "name": "calamares-3.4.2", + "version": "3.4.2-6", + "arch": "x86_64", + "size": 234496, + "category": "installer", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.227843+00:00" + }, + { + "name": "calamares-branding-antergos-next", + "version": "1.0-16", + "arch": "any", + "size": 4476928, + "category": "installer", + "description": "Calamares branding for Antergos NeXT with offline/online installer support", + "url": "https://github.com/Antergos-NeXT", + "mtime": "2026-06-28T16:11:37.241843+00:00" + }, + { + "name": "configure-1.6", + "version": "1.6-1", + "arch": "x86_64", + "size": 1199104, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.246843+00:00" + }, + { + "name": "gentoo-challenge-failed", + "version": "3.0-1", + "arch": "any", + "size": 724992, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.248843+00:00" + }, + { + "name": "i-use-artix-btw", + "version": "2.0-1", + "arch": "any", + "size": 4995072, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.264843+00:00" + }, + { + "name": "neofetch-but-rust", + "version": "0.5-1", + "arch": "any", + "size": 2647040, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.273843+00:00" + }, + { + "name": "pacman-broke-my-system", + "version": "9.9-1", + "arch": "any", + "size": 2330624, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.281843+00:00" + }, + { + "name": "ptsd-from-sudo", + "version": "1.0-1", + "arch": "any", + "size": 4167680, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.294843+00:00" + }, + { + "name": "rms-approved", + "version": "4.0-1", + "arch": "any", + "size": 361472, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.296843+00:00" + }, + { + "name": "steve-jobs-would-never", + "version": "1.0-1", + "arch": "any", + "size": 262144, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.297843+00:00" + }, + { + "name": "systemd-bad", + "version": "1.0-1", + "arch": "any", + "size": 1938432, + "category": "other", + "description": "", + "url": "", + "mtime": "2026-06-28T16:11:37.304843+00:00" + } +]