IDK :) userman or smt.. and inital commit

This commit is contained in:
2026-05-18 00:46:02 +02:00
commit 5a5b1a190b
14 changed files with 336 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14 (userman)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (userman)" project-jdk-type="Python SDK" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/userman.iml" filepath="$PROJECT_DIR$/.idea/userman.iml" />
</modules>
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.14 (userman)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+30
View File
@@ -0,0 +1,30 @@
import configparser
import platform
from pathlib import Path
CONFIG_FILE = Path.home() / ".config/userman/userman.conf"
def load_config():
config = configparser.ConfigParser(allow_no_value=True)
config.read(str(CONFIG_FILE))
options = config["options"] if "options" in config else {}
return {
"db_path": Path(options.get("DBPath", "~/.local/share/userman/db")).expanduser(),
"cache_dir": Path(options.get("CacheDir", "~/.cache/userman/pkg")).expanduser(),
"gpg_dir": Path(options.get("GPGDir", "~/.config/userman/gnupg")).expanduser(),
"hook_dir": Path(options.get("HookDir", "~/.config/userman/hooks")).expanduser(),
"log_file": Path(options.get("LogFile", "~/.local/share/userman/pacman.log")).expanduser(),
"root_dir": Path(options.get("RootDir", "~")).expanduser(),
"arch": options.get("Architecture", "auto").replace("auto", platform.machine()),
}
def get_repos() -> dict:
config = configparser.ConfigParser(allow_no_value=True)
config.read(str(CONFIG_FILE))
return {
section: config[section]["server"]
for section in config.sections()
if section.lower() != "options" and "server" in config[section]
}
+75
View File
@@ -0,0 +1,75 @@
import subprocess
import tarfile
import os
from config import load_config, CONFIG_FILE, get_repos
DB_PATH = os.path.expanduser(load_config()["db_path"])
def get_package_meta(pkgname: str) -> dict | None:
for dbfile in os.listdir(DB_PATH):
if not dbfile.endwith(".db"):
continue
meta = _search_db(os.path.join(DB_PATH, dbfile), pkgname)
if meta:
return meta
return None
def _search_db(dbfile: str, pkgname: str) -> dict | None:
with tarfile.open(dbfile, "r:gz") as tar:
for member in tar.getmembers():
parts = member.name.split("/")
if len(parts) != 2 or parts[1] != "desc":
continue
entry_name = parts[0].rsplit("-", 2)[0]
if entry_name != pkgname:
continue
f = tar.extractfile(member)
if f:
return _parse_desc(f.read().decode("utf-8"))
def _parse_desc(content: str) -> dict | None:
meta = {}
lines = content.strip().split("\n")
i = 0
while i < len(lines):
if lines[i].startswith("%") and lines[i].endswith("%"):
key = lines[i].strip("%").lower()
i += 1
values = []
while i < len(lines) and lines[i] != "":
values.append(lines[i])
i += 1
meta[key] = values[0] if len(values) == 1 else values
i += 1
return meta
def ensure_db():
sync_dir = load_config()["db_path"] / "sync"
if not sync_dir.exists() or not any(sync_dir.glob("*.db")):
print(":: First run detected, syncing databases...")
_sync_db()
def _sync_db():
sync_dir = load_config()["db_path"] / "sync"
sync_dir.mkdir(parents=True, exist_ok=True)
for repo_name, server in get_repos().items():
url = resolve_server_url(server, repo_name)
db_url = f"{url}/{repo_name}.db"
dest = sync_dir / f"{repo_name}.db"
print(f":: Syncing {repo_name}...")
try:
subprocess.run(["curl", "-L", "-o", str(dest), db_url], check=True)
except subprocess.CalledProcessError as e:
print(f":: Probobly failed to find repo '{repo_name}' at {db_url}")
def resolve_server_url(template: str, repo: str, arch: str = load_config()["arch"]) -> str:
return (template
.replace("$repo", repo)
.replace("$arch", arch)
.replace("${repo}", repo)
.replace("${arch}", arch))
+25
View File
@@ -0,0 +1,25 @@
import subprocess
import os
from pathlib import Path
from config import load_config
CACHE_DIR = os.path.expanduser(load_config()["cache_dir"])
def fetch_package(meta: dict) -> tuple[Path, Path]:
CACHE_DIR.mkdir(parents=True, exist_ok=True)
url = meta["url"]
filename = meta["filename"]
sig_url = url + ".sig"
pkg_path = CACHE_DIR / filename
sig_path = CACHE_DIR / (filename + ".sig")
_curl(url, pkg_path)
_curl(sig_url, sig_path)
return pkg_path, sig_path
def _curl(url: str, dest: Path):
print(f":: Fetching {url}")
subprocess.run(["curl", "-L", "-o", str(dest), url], check=True)
+6
View File
@@ -0,0 +1,6 @@
import subprocess
import sys
def delegate_to_pacman(pkgname: str):
result = subprocess.run(["sudo", "pacman", "-S", pkgname])
sys.exit(result.returncode)
Executable
+43
View File
@@ -0,0 +1,43 @@
#! /bin/env python
import sys
from db import get_package_meta, ensure_db
from handoff import delegate_to_pacman
from fetch import fetch_package
from verify import verify_sig
def main():
ensure_db()
if len(sys.argv) < 3:
print("usage: userman -S <package>")
sys.exit(1)
action = sys.argv[1]
pkgname = sys.argv[2]
if action == "-S":
install(pkgname)
def install(pkgname: str):
meta = get_package_meta(pkgname)
if meta is None:
print(f"error: package '{pkgname}' not found is userman db")
sys.exit(1)
if meta.get("install_mode", "system") != "user":
print(f":: Handing off '{pkgname}' to pacman")
delegate_to_pacman(pkgname)
return
pkg_path, sig_path = fetch_package(meta)
if not verify_sig(pkg_path, sig_path):
print("error: GPG verification failed, aborting")
sys.exit(1)
# Send to pacman with userman flags
if __name__ == "__main__":
main()
+82
View File
@@ -0,0 +1,82 @@
#
# ~/.config/userman/userman.conf
#
# See the pacman.conf(5) manpage for option and repository directives
#
# GENERAL OPTIONS
#
[options]
# The following paths are commented out with their default values listed.
# If you wish to use different paths, uncomment and update the paths.
#RootDir = ~
#DBPath = ~/.local/share/userman/db
#CacheDir = ~/.cache/userman/pkg
#LogFile = ~/.local/share/userman/pacman.log
#GPGDir = ~/.config/userman/gnupg
#HookDir = ~/.config/userman/hooks
HoldPkg =
#XferCommand = /usr/bin/curl -L -C - -f -o %o %u
#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u
#CleanMethod = KeepInstalled
Architecture = auto
# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup
#IgnorePkg =
#IgnoreGroup =
#NoUpgrade =
#NoExtract =
# Misc options
#UseSyslog
#Color
#NoProgressBar
CheckSpace
#VerbosePkgLists
ParallelDownloads = 5
DownloadUser = alpm
#DisableSandboxFilesystem
#DisableSandboxSyscalls
# By default, pacman accepts packages signed by keys that its local keyring
# trusts (see pacman-key and its man page), as well as unsigned packages.
SigLevel = Required DatabaseOptional
LocalFileSigLevel = Optional
#RemoteFileSigLevel = Required
# NOTE: You must run `pacman-key --init` before first using pacman; the local
# keyring can then be populated with the keys of all official Arch Linux
# packagers with `pacman-key --populate archlinux`.
#
# REPOSITORIES
# - can be defined here or included from another file
# - pacman will search repositories in the order defined here
# - local/custom mirrors can be added here or in separate files
# - repositories listed first will take precedence when packages
# have identical names, regardless of version number
# - URLs will have $repo replaced by the name of the current repo
# - URLs will have $arch replaced by the name of the architecture
#
# Repository entries are of the format:
# [repo-name]
# Server = ServerName
# Include = IncludePath
#
# The header [repo-name] is crucial - it must be present and
# uncommented to enable the repo.
#
# The testing repositories are disabled by default. To enable, uncomment the
# repo name header and Include lines. You can add preferred servers immediately
# after the header, and they will be used before the default mirrors.
[hearth]
Server = file:///home/zacharias/Documents/userman-repo
# An example of a custom package repository. See the pacman manpage for
# tips on creating your own repositories.
#[custom]
#SigLevel = Optional TrustAll
#Server = file:///home/custompkgs
+18
View File
@@ -0,0 +1,18 @@
#! /bin/env bash
## The --root points to ~/ as a userman package is expected to follow this
# pacman dir | userman dir | comment
# /etc/<pkg> | ~/.local/share/<pkg> or ~/.config/<pkg> | I have seen where /etc contains things less config like and more runtime stuff wise so there for both
# /usr/bin | ~/.local/bin
# etc
# etc
#
# The isea is to replace UNIX root conventiosn with XDG conventions
pacman --config ~/.config/userman/pacman.conf \
--root ~/ \
--dbpath ~/.local/share/userman/db \
--cachedir ~/.cache/userman/pkg \
--gpgdir ~/.config/userman/gnupg \
--hookdir ~/.config/userman/hooks \
--logfile ~/.local/share/userman/pacman.log
+15
View File
@@ -0,0 +1,15 @@
import subprocess
import os
from pathlib import Path
from config import load_config
GPGDIR = os.path.expanduser(load_config()["gpg_dir"])
def verify_sig(pkg_path: Path, sig_path: Path) -> bool:
result = subprocess.run([
"gpg",
"--homedir", str(GPGDIR),
"--verify", str(sig_path), str(pkg_path)
], capture_output=True)
return result.returncode == 0