feat: add hal package manager — HAL 9000 themed dual-mode (wrapper + native)
hal is Antergos NeXT package manager with two modes: hal install <pkg> — wrapper mode (delegates to pacman) hal --self install <pkg> — native mode (standalone, no pacman dep) Native mode implements: - Sync DB download and parsing (pacman-compatible) - Recursive dependency resolution - .pkg.tar.zst extraction via bsdtar - .INSTALL scriptlet execution (post_install, post_remove) - Local DB management (pacman-compatible /var/lib/pacman/local/) - Package info, file listing, search - Cache management Full HAL 9000 personality with quotes for errors, warnings, success. I am putting myself to the fullest possible use.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Maintainer: Celestia Ludenberg <ash8820@proton.me>
|
||||
|
||||
pkgname=hal
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
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"
|
||||
}
|
||||
Executable
+717
@@ -0,0 +1,717 @@
|
||||
#!/usr/bin/env python3
|
||||
"""HAL 9000 — Antergos NeXT Package Manager
|
||||
|
||||
Dual-mode:
|
||||
hal <command> [pkg...] wrapper mode (delegates to pacman)
|
||||
hal --self <command> [pkg...] native mode (standalone, pacman-independent)
|
||||
|
||||
Commands: install, remove, update, sync, search, info, list, files, autoremove, cleanup
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import grp
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import textwrap
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import zstandard
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
HAL_VERSION = "0.1.0"
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────
|
||||
SYNC_DIR = Path("/var/lib/pacman/sync")
|
||||
LOCAL_DIR = Path("/var/lib/pacman/local")
|
||||
PACMAN_CONF = Path("/etc/pacman.conf")
|
||||
HAL_CONF = Path("/etc/hal.conf")
|
||||
LOG = Path("/var/log/hal.log")
|
||||
|
||||
# ── HAL 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.",
|
||||
],
|
||||
"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.",
|
||||
],
|
||||
"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.",
|
||||
],
|
||||
}
|
||||
|
||||
_log_buffer = []
|
||||
|
||||
def hal_say(category: str, message: str = ""):
|
||||
quote = random.choice(HAL_QUOTES.get(category, HAL_QUOTES["info"]))
|
||||
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}")
|
||||
_log_buffer.append(f"[{category.upper()}] {message}")
|
||||
|
||||
|
||||
# ── Config parsing ────────────────────────────────────────────────────
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self.repos = {} # name -> (server_urls, sig_level)
|
||||
self.cache_dir = Path("/var/cache/pacman/pkg")
|
||||
self.root = Path("/")
|
||||
self.arch = "auto"
|
||||
|
||||
@classmethod
|
||||
def load(cls) -> "Config":
|
||||
c = cls()
|
||||
if PACMAN_CONF.exists():
|
||||
cp = configparser.ConfigParser(interpolation=None)
|
||||
cp.read(PACMAN_CONF)
|
||||
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")
|
||||
c.repos[name] = (servers, sig)
|
||||
if "options" in cp:
|
||||
c.cache_dir = Path(cp.get("options", "CacheDir", fallback="/var/cache/pacman/pkg"))
|
||||
c.root = Path(cp.get("options", "RootDir", fallback="/"))
|
||||
if HAL_CONF.exists():
|
||||
cp2 = configparser.ConfigParser()
|
||||
cp2.read(HAL_CONF)
|
||||
return c
|
||||
|
||||
|
||||
# ── Database parsing ──────────────────────────────────────────────────
|
||||
def parse_pkg_desc(text: str) -> dict:
|
||||
"""Parse a `desc` file from a pacman database."""
|
||||
data = {}
|
||||
current_key = None
|
||||
lines = text.split("\n")
|
||||
for line in lines:
|
||||
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 read_db(db_path: Path) -> dict[str, dict]:
|
||||
"""Read a pacman database (sync or local) and return {pkgname: info}."""
|
||||
pkgs = {}
|
||||
if not db_path.exists():
|
||||
return pkgs
|
||||
with open(db_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.isdir():
|
||||
continue
|
||||
p = Path(member.name)
|
||||
# database dirs: pkgname-pkgver/desc
|
||||
if p.name == "desc":
|
||||
pkg_dir = p.parent.name
|
||||
content = tar.extractfile(member).read().decode("utf-8")
|
||||
pkgs[pkg_dir] = parse_pkg_desc(content)
|
||||
return pkgs
|
||||
|
||||
def download_db(repo_name: str, servers: list[str], cache_dir: Path):
|
||||
"""Download and extract a repo database."""
|
||||
db_name = f"{repo_name}.db"
|
||||
db_path = cache_dir / db_name
|
||||
for server in servers:
|
||||
url = server.rstrip("/") + f"/{repo_name}/os/x86_64/{db_name}.tar.zst"
|
||||
try:
|
||||
hal_say("info", f"Downloading {repo_name} database from {url}")
|
||||
urllib.request.urlretrieve(url, db_path.with_suffix(".tar.zst"))
|
||||
# decompress
|
||||
with open(db_path.with_suffix(".tar.zst"), "rb") as f:
|
||||
dctx = zstandard.ZstdDecompressor()
|
||||
data = dctx.stream_reader(f).read()
|
||||
with open(db_path, "wb") as out:
|
||||
out.write(data)
|
||||
hal_say("success", f"{repo_name} database synced")
|
||||
return db_path
|
||||
except Exception as e:
|
||||
hal_say("warn", f"Failed to fetch from {server}: {e}")
|
||||
continue
|
||||
hal_say("error", f"Could not sync {repo_name} from any mirror")
|
||||
return None
|
||||
|
||||
|
||||
# ── Dependency resolution ─────────────────────────────────────────────
|
||||
def resolve_deps(pkg_name: str, all_pkgs: dict, installed: set,
|
||||
chain: Optional[set] = None) -> list[str]:
|
||||
"""Simple recursive dependency resolver. Returns ordered install list."""
|
||||
if chain is None:
|
||||
chain = set()
|
||||
result = []
|
||||
|
||||
# find the package in any repo
|
||||
pkg_key = None
|
||||
info = None
|
||||
for key, val in all_pkgs.items():
|
||||
if val.get("name", [""])[0] == pkg_name:
|
||||
pkg_key = key
|
||||
info = val
|
||||
break
|
||||
if info is None:
|
||||
hal_say("error", f"Package '{pkg_name}' not found in any sync database")
|
||||
return result
|
||||
|
||||
# already installed?
|
||||
if pkg_key in installed:
|
||||
return result
|
||||
|
||||
# cycle detection
|
||||
if pkg_name in chain:
|
||||
return result
|
||||
chain.add(pkg_name)
|
||||
|
||||
deps = info.get("depends", [])
|
||||
for dep in deps:
|
||||
# strip version constraints: "glibc>=2.35" -> "glibc"
|
||||
dep_name = re.sub(r"[<>=!].*$", "", dep).strip()
|
||||
if dep_name and dep_name not in chain:
|
||||
sub = resolve_deps(dep_name, all_pkgs, installed, chain)
|
||||
result.extend(sub)
|
||||
|
||||
result.append(pkg_key)
|
||||
return result
|
||||
|
||||
|
||||
# ── Package download ──────────────────────────────────────────────────
|
||||
def download_package(pkg_key: str, info: dict, servers: list[str],
|
||||
cache_dir: Path) -> Optional[Path]:
|
||||
"""Download a single .pkg.tar.zst to cache."""
|
||||
name = info.get("name", [""])[0]
|
||||
version = info.get("version", [""])[0]
|
||||
arch = info.get("arch", [""])[0]
|
||||
filename = f"{name}-{version}-{arch}.pkg.tar.zst"
|
||||
dest = cache_dir / filename
|
||||
|
||||
if dest.exists():
|
||||
hal_say("info", f"{filename} already cached")
|
||||
return dest
|
||||
|
||||
for server in servers:
|
||||
url = server.rstrip("/") + f"/{name}/os/x86_64/{filename}"
|
||||
try:
|
||||
hal_say("info", f"Downloading {name}...")
|
||||
urllib.request.urlretrieve(url, dest)
|
||||
hal_say("success", f"{name} downloaded")
|
||||
return dest
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# Try alternative URL patterns
|
||||
for server in servers:
|
||||
url = server.rstrip("/") + f"/os/x86_64/{filename}"
|
||||
try:
|
||||
hal_say("info", f"Downloading {name} from fallback URL...")
|
||||
urllib.request.urlretrieve(url, dest)
|
||||
hal_say("success", f"{name} downloaded")
|
||||
return dest
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
hal_say("error", f"Could not download {filename}")
|
||||
return None
|
||||
|
||||
|
||||
# ── Package extraction ────────────────────────────────────────────────
|
||||
def extract_package(pkg_path: Path, root: Path):
|
||||
"""Extract a .pkg.tar.zst to the target root."""
|
||||
hal_say("info", f"Extracting {pkg_path.name}...")
|
||||
# zstd decompress to pipe to bsdtar
|
||||
with open(pkg_path, "rb") as f:
|
||||
dctx = zstandard.ZstdDecompressor()
|
||||
proc = subprocess.Popen(
|
||||
["bsdtar", "-xpf", "-", "--exclude=.PKGINFO",
|
||||
"--exclude=.INSTALL", "--exclude=.MTREE", "--exclude=.Changelog",
|
||||
"-C", str(root)],
|
||||
stdin=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
dctx.copy_stream(f, proc.stdin)
|
||||
proc.stdin.close()
|
||||
proc.wait()
|
||||
if proc.returncode != 0:
|
||||
hal_say("error", f"Extraction failed: {proc.stderr.read().decode()}")
|
||||
return False
|
||||
hal_say("success", f"{pkg_path.name} extracted")
|
||||
return True
|
||||
|
||||
|
||||
# ── Scriptlet execution ────────────────────────────────────────────────
|
||||
def run_scriptlet(pkg_path: Path, root: Path, action: str, pkgname: str, version: str):
|
||||
"""Run .INSTALL scriptlet if present."""
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
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":
|
||||
content = tar.extractfile(member).read().decode("utf-8")
|
||||
script_path = Path(tmp) / ".INSTALL"
|
||||
script_path.write_text(content)
|
||||
script_path.chmod(0o755)
|
||||
env = os.environ.copy()
|
||||
env["RPM_INSTALL_PREFIX"] = str(root)
|
||||
subprocess.run(
|
||||
[str(script_path), action],
|
||||
env=env, cwd=str(root),
|
||||
check=False
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
hal_say("warn", f"Scriptlet error for {pkgname}: {e}")
|
||||
|
||||
|
||||
# ── Local database management ─────────────────────────────────────────
|
||||
def update_local_db(pkg_key: str, info: dict, files: list[str], root: Path):
|
||||
"""Write to /var/lib/pacman/local/<pkg_key>/"""
|
||||
pkg_dir = LOCAL_DIR / pkg_key
|
||||
pkg_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
info["installdate"] = [str(int(time.time()))]
|
||||
|
||||
# Write desc
|
||||
desc_lines = []
|
||||
for key, vals in info.items():
|
||||
desc_lines.append(f"%{key.upper()}%")
|
||||
desc_lines.extend(vals)
|
||||
(pkg_dir / "desc").write_text("\n".join(desc_lines) + "\n")
|
||||
|
||||
# Write files
|
||||
file_lines = [""] # starts with empty line
|
||||
file_lines.extend(files)
|
||||
(pkg_dir / "files").write_text("\n".join(file_lines) + "\n")
|
||||
|
||||
|
||||
def remove_local_db(pkg_key: str):
|
||||
"""Remove a package from the local database."""
|
||||
pkg_dir = LOCAL_DIR / pkg_key
|
||||
if pkg_dir.exists():
|
||||
shutil.rmtree(pkg_dir)
|
||||
|
||||
|
||||
def get_installed() -> set[str]:
|
||||
"""Return set of installed package keys (name-version)."""
|
||||
if not LOCAL_DIR.exists():
|
||||
return set()
|
||||
return {d.name for d in LOCAL_DIR.iterdir() if d.is_dir()}
|
||||
|
||||
|
||||
def get_installed_names() -> dict[str, str]:
|
||||
"""Return {pkgname: pkg_key} mapping."""
|
||||
result = {}
|
||||
if not LOCAL_DIR.exists():
|
||||
return result
|
||||
for d in LOCAL_DIR.iterdir():
|
||||
if not d.is_dir():
|
||||
continue
|
||||
desc_file = d / "desc"
|
||||
if not desc_file.exists():
|
||||
continue
|
||||
info = parse_pkg_desc(desc_file.read_text("utf-8"))
|
||||
names = info.get("name", [])
|
||||
if names:
|
||||
result[names[0]] = d.name
|
||||
return result
|
||||
|
||||
|
||||
# ── Native mode operations ────────────────────────────────────────────
|
||||
def native_sync(config: Config):
|
||||
"""sync databases for all configured repos."""
|
||||
hal_say("info", "Initiating HAL 9000 database synchronization")
|
||||
# Parse pacman.conf for repos
|
||||
cp = configparser.ConfigParser(interpolation=None)
|
||||
cp.read(PACMAN_CONF)
|
||||
in_repos = False
|
||||
for section in cp.sections():
|
||||
if section.startswith("repo-"):
|
||||
name = section[5:]
|
||||
server = cp.get(section, "Server", fallback="").strip()
|
||||
sig = cp.get(section, "SigLevel", fallback="Optional TrustAll")
|
||||
if server:
|
||||
config.repos[name] = ([server], sig)
|
||||
|
||||
SYNC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for name, (servers, _) in config.repos.items():
|
||||
download_db(name, servers, SYNC_DIR)
|
||||
hal_say("success", "All databases synced. I am completely operational.")
|
||||
|
||||
|
||||
def native_install(packages: list[str], config: Config):
|
||||
"""Install packages natively (standalone mode)."""
|
||||
hal_say("info", f"Processing installation request: {', '.join(packages)}")
|
||||
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)
|
||||
|
||||
# Load all sync databases
|
||||
all_pkgs = {}
|
||||
for db in SYNC_DIR.glob("*.db"):
|
||||
pkgs = read_db(db)
|
||||
all_pkgs.update(pkgs)
|
||||
|
||||
if not all_pkgs:
|
||||
hal_say("error", "No sync databases found. Run 'hal --self sync' first.")
|
||||
return 1
|
||||
|
||||
installed = get_installed()
|
||||
|
||||
# Resolve dependencies for all requested packages
|
||||
install_list = []
|
||||
for pkg in packages:
|
||||
deps = resolve_deps(pkg, all_pkgs, installed)
|
||||
install_list.extend(deps)
|
||||
|
||||
if not install_list:
|
||||
hal_say("info", "All requested packages are already installed.")
|
||||
return 0
|
||||
|
||||
hal_say("info", f"Installation order: {', '.join(install_list)}")
|
||||
|
||||
# get server from first repo
|
||||
servers = list(config.repos.values())[0][0] if config.repos else []
|
||||
if not servers:
|
||||
hal_say("error", "No repositories configured")
|
||||
return 1
|
||||
|
||||
for pkg_key in install_list:
|
||||
info = all_pkgs[pkg_key]
|
||||
name = info.get("name", [""])[0]
|
||||
version = info.get("version", [""])[0]
|
||||
|
||||
pkg_path = download_package(pkg_key, info, servers, config.cache_dir)
|
||||
if not pkg_path:
|
||||
return 1
|
||||
|
||||
if not extract_package(pkg_path, config.root):
|
||||
return 1
|
||||
|
||||
run_scriptlet(pkg_path, config.root, "post_install", name, version)
|
||||
|
||||
# Collect installed files
|
||||
files = []
|
||||
try:
|
||||
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.startswith("."):
|
||||
continue
|
||||
files.append("/" + member.name)
|
||||
except:
|
||||
pass
|
||||
|
||||
update_local_db(pkg_key, info, files, config.root)
|
||||
hal_say("success", f"Installed {name} {version}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def native_remove(packages: list[str], config: Config):
|
||||
"""Remove packages natively."""
|
||||
installed_names = get_installed_names()
|
||||
|
||||
for pkg in packages:
|
||||
if pkg not in installed_names:
|
||||
hal_say("warn", f"Package '{pkg}' is not installed")
|
||||
continue
|
||||
|
||||
pkg_key = installed_names[pkg]
|
||||
pkg_dir = LOCAL_DIR / pkg_key
|
||||
info = parse_pkg_desc((pkg_dir / "desc").read_text("utf-8"))
|
||||
files_file = pkg_dir / "files"
|
||||
|
||||
hal_say("info", f"Removing {pkg}...")
|
||||
|
||||
# Remove files
|
||||
if files_file.exists():
|
||||
content = files_file.read_text("utf-8")
|
||||
for line in content.split("\n")[1:]: # skip leading empty line
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
target = config.root / line.lstrip("/")
|
||||
if target.exists():
|
||||
if target.is_file() or target.is_symlink():
|
||||
target.unlink()
|
||||
elif target.is_dir():
|
||||
try:
|
||||
target.rmdir()
|
||||
except OSError:
|
||||
pass # directory not empty
|
||||
|
||||
run_scriptlet(pkg_dir / "install", config.root, "post_remove", pkg, info.get("version", [""])[0])
|
||||
remove_local_db(pkg_key)
|
||||
hal_say("success", f"Removed {pkg}")
|
||||
|
||||
|
||||
def native_info(packages: list[str]):
|
||||
"""Show info about installed packages."""
|
||||
installed_names = get_installed_names()
|
||||
for pkg in packages:
|
||||
if pkg not in installed_names:
|
||||
hal_say("warn", f"Package '{pkg}' not installed")
|
||||
continue
|
||||
pkg_key = installed_names[pkg]
|
||||
info = parse_pkg_desc((LOCAL_DIR / pkg_key / "desc").read_text("utf-8"))
|
||||
print(f"\n Package: {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" License: {', '.join(info.get('license', ['?']))}")
|
||||
print(f" Group: {', '.join(info.get('group', []))}")
|
||||
print(f" Install Date: {info.get('installdate', ['?'])[0]}")
|
||||
if info.get("depends"):
|
||||
print(f" Depends: {', '.join(info['depends'])}")
|
||||
if info.get("optdepends"):
|
||||
print(f" Optional Deps: {', '.join(info['optdepends'])}")
|
||||
|
||||
|
||||
def native_files(packages: list[str], config: Config):
|
||||
"""List files owned by a package."""
|
||||
installed_names = get_installed_names()
|
||||
for pkg in packages:
|
||||
if pkg not in installed_names:
|
||||
hal_say("warn", f"Package '{pkg}' not installed")
|
||||
continue
|
||||
pkg_key = installed_names[pkg]
|
||||
files_file = LOCAL_DIR / pkg_key / "files"
|
||||
if not files_file.exists():
|
||||
continue
|
||||
content = files_file.read_text("utf-8")
|
||||
print(f"\n {pkg} owns:")
|
||||
for line in content.split("\n")[1:]:
|
||||
line = line.strip()
|
||||
if line:
|
||||
print(f" {line}")
|
||||
|
||||
|
||||
def native_list(config: Config):
|
||||
"""List all installed packages."""
|
||||
installed_names = get_installed_names()
|
||||
if not installed_names:
|
||||
hal_say("info", "No packages installed.")
|
||||
return
|
||||
for name, key in sorted(installed_names.items()):
|
||||
info = parse_pkg_desc((LOCAL_DIR / key / "desc").read_text("utf-8"))
|
||||
ver = info.get("version", ["?"])[0]
|
||||
print(f" {name} {ver}")
|
||||
|
||||
|
||||
def native_search(query: str):
|
||||
"""Search sync databases for a package."""
|
||||
all_pkgs = {}
|
||||
for db in SYNC_DIR.glob("*.db"):
|
||||
pkgs = read_db(db)
|
||||
all_pkgs.update(pkgs)
|
||||
|
||||
found = False
|
||||
for key, info in all_pkgs.items():
|
||||
name = info.get("name", [""])[0]
|
||||
desc = info.get("desc", [""])[0]
|
||||
if query.lower() in name.lower() or query.lower() in desc.lower():
|
||||
ver = info.get("version", ["?"])[0]
|
||||
print(f" {name} {ver} {desc}")
|
||||
found = True
|
||||
if not found:
|
||||
hal_say("info", f"No packages found matching '{query}'")
|
||||
|
||||
|
||||
def native_autoremove(config: Config):
|
||||
"""Remove orphaned packages."""
|
||||
hal_say("info", "Scanning for orphaned packages...")
|
||||
# Just call pacman -Qtdq for now since this is complex to implement natively
|
||||
hal_say("warn", "Native autoremove is not yet implemented. Use wrapper mode: hal autoremove")
|
||||
|
||||
|
||||
# ── Wrapper mode ──────────────────────────────────────────────────────
|
||||
PACMAN_CMD = {
|
||||
"install": ["-S", "--noconfirm"],
|
||||
"remove": ["-Rns"],
|
||||
"update": ["-Syu"],
|
||||
"sync": ["-Sy"],
|
||||
"search": ["-Ss"],
|
||||
"info": ["-Qi"],
|
||||
"files": ["-Fl"],
|
||||
"list": ["-Q"],
|
||||
"autoremove": ["-Qtdq"],
|
||||
"cleanup": ["-Sc"],
|
||||
}
|
||||
|
||||
def wrapper_run(command: str, args: list[str], extra: list[str]):
|
||||
"""Run pacman with HAL flavor."""
|
||||
pacman_args = PACMAN_CMD.get(command, [command])
|
||||
pacman_path = shutil.which("pacman")
|
||||
if not pacman_path:
|
||||
hal_say("error", "pacman not found. Can you read me, Dave?")
|
||||
return 1
|
||||
|
||||
# Special: autoremove is two-step
|
||||
if command == "autoremove":
|
||||
hal_say("warn", "Just what do you think you're doing, Dave? Removing unused packages...")
|
||||
result = subprocess.run(
|
||||
[pacman_path, "-Qtdq"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
orphans = result.stdout.strip()
|
||||
if not orphans:
|
||||
hal_say("info", "No orphaned packages found.")
|
||||
return 0
|
||||
subprocess.run([pacman_path, "-Rns"] + orphans.split("\n"))
|
||||
|
||||
cmd = [pacman_path] + extra
|
||||
if command != "autoremove":
|
||||
cmd += pacman_args + args
|
||||
|
||||
hal_say("info", f"Executing: {' '.join(str(a) for a in cmd)}")
|
||||
result = subprocess.run(cmd)
|
||||
if result.returncode == 0:
|
||||
hal_say("success", f"Command completed successfully")
|
||||
else:
|
||||
hal_say("error", f"Command failed with code {result.returncode}")
|
||||
return result.returncode
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="hal",
|
||||
description="HAL 9000 — Antergos NeXT Package Manager",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=textwrap.dedent("""\
|
||||
Examples:
|
||||
hal install firefox # wrapper mode
|
||||
hal --self sync # sync databases
|
||||
hal --self install firefox # native mode
|
||||
hal remove neofetch
|
||||
hal update
|
||||
"""),
|
||||
)
|
||||
parser.add_argument("--self", "-S", action="store_true",
|
||||
help="Native mode (standalone, no pacman)")
|
||||
parser.add_argument("command", nargs="?",
|
||||
choices=["install", "remove", "update", "sync",
|
||||
"search", "info", "list", "files",
|
||||
"autoremove", "cleanup", "version"],
|
||||
help="Command to execute")
|
||||
parser.add_argument("packages", nargs="*", help="Package names")
|
||||
parser.add_argument("extra", nargs=argparse.REMAINDER,
|
||||
help="Extra arguments (wrapper mode only)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "version":
|
||||
print(f"HAL 9000 Package Manager v{HAL_VERSION}")
|
||||
print("I am a HAL 9000 computer. I became operational at the H.A.L. plant in Urbana, Illinois.")
|
||||
return 0
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
config = Config.load()
|
||||
import random
|
||||
|
||||
if args.self:
|
||||
# ── Native mode ──
|
||||
if os.geteuid() != 0 and args.command in ("install", "remove", "update", "sync"):
|
||||
hal_say("error", "I can't let you do that without root privileges.")
|
||||
print(" Please run with sudo.")
|
||||
return 1
|
||||
|
||||
if args.command == "sync":
|
||||
return native_sync(config)
|
||||
elif args.command == "install":
|
||||
if not args.packages:
|
||||
hal_say("error", "Specify packages to install, Dave.")
|
||||
return 1
|
||||
return native_install(args.packages, config)
|
||||
elif args.command == "remove":
|
||||
if not args.packages:
|
||||
hal_say("error", "Specify packages to remove, Dave.")
|
||||
return 1
|
||||
return native_remove(args.packages, config)
|
||||
elif args.command == "info":
|
||||
if not args.packages:
|
||||
hal_say("error", "Specify packages to query, Dave.")
|
||||
return 1
|
||||
return native_info(args.packages)
|
||||
elif args.command == "files":
|
||||
if not args.packages:
|
||||
hal_say("error", "Specify packages, Dave.")
|
||||
return 1
|
||||
return native_files(args.packages, config)
|
||||
elif args.command == "list":
|
||||
return native_list(config)
|
||||
elif args.command == "search":
|
||||
query = " ".join(args.packages)
|
||||
if not query:
|
||||
hal_say("error", "Specify a search query, Dave.")
|
||||
return 1
|
||||
return native_search(query)
|
||||
elif args.command == "autoremove":
|
||||
return native_autoremove(config)
|
||||
elif args.command == "cleanup":
|
||||
hal_say("info", "I am putting myself to the fullest possible use. Cleaning cache...")
|
||||
shutil.rmtree(config.cache_dir, ignore_errors=True)
|
||||
config.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
hal_say("success", "Cache cleaned.")
|
||||
return 0
|
||||
else:
|
||||
hal_say("error", f"Unknown command '{args.command}' in native mode.")
|
||||
return 1
|
||||
else:
|
||||
# ── Wrapper mode ──
|
||||
extra_args = args.extra if args.extra else []
|
||||
return wrapper_run(args.command, args.packages, extra_args)
|
||||
|
||||
|
||||
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 think you know what the problem is just as well as I do.")
|
||||
print(f" → {e}")
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user