aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoremmett1 <me@emmett1.my>2026-06-20 16:59:18 +0800
committeremmett1 <me@emmett1.my>2026-06-20 16:59:18 +0800
commit1e19e28d455b893171a5c91ead9aefbfe7c96b86 (patch)
tree3b9f4c29c19ae7ed5b0f4c1c4fa61b53732fed78
parentd215a2198e396698c72bfc2e4166d8f6b5269c2b (diff)
downloadautils-1e19e28d455b893171a5c91ead9aefbfe7c96b86.tar.gz
autils-1e19e28d455b893171a5c91ead9aefbfe7c96b86.zip
re-added apkg-bin
-rw-r--r--Makefile1
-rwxr-xr-xapkg-bin591
-rw-r--r--man/apkg-bin.8226
3 files changed, 818 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index 303714f..db25fb7 100644
--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,7 @@ BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/share/man/man8
SCRIPTS = apkg \
+ apkg-bin \
apkg-chroot \
apkg-clean \
apkg-deps \
diff --git a/apkg-bin b/apkg-bin
new file mode 100755
index 0000000..5d8de7f
--- /dev/null
+++ b/apkg-bin
@@ -0,0 +1,591 @@
+#!/bin/sh
+#
+# APKG-BIN - Alice Binary Package Manager (C) 2023-2026 Emmett1
+#
+# Fetches pre-built .spm packages from a binary repo, resolves dependencies,
+# and installs via spm. Alternative to source-based 'apkg'.
+
+msg() {
+ printf "%s\n" "[apkg-bin] $*" >&2
+}
+
+die() {
+ [ "$1" ] && printf "%s\n" "error: $1" >&2
+ exit 1
+}
+
+needroot() {
+ [ "$(id -u)" = 0 ] || die "This operation requires root access"
+}
+
+prompt_user() {
+ [ "$APKG_NOPROMPT" ] && return
+ msg "Press ENTER to continue. Press Ctrl+C to abort."
+ read -r _null
+}
+
+needbinsrc() {
+ [ "$APKGBIN_REPO" ] || die "APKGBIN_REPO is not set"
+}
+
+needcachedb() {
+ [ -f "$APKGBIN_CACHE_DIR/APKGBINDB" ] || die "No cached APKGBINDB found. Run 'apkg-bin -S' first."
+ [ -r "$APKGBIN_CACHE_DIR/APKGBINDB" ] || die "No read permission on $APKGBIN_CACHE_DIR/APKGBINDB"
+}
+
+pkg_is_installed() {
+ [ -s "$SPM_PKGDB/$1" ]
+}
+
+pkg_installed_ver() {
+ head -n1 "$SPM_PKGDB/$1" 2>/dev/null
+}
+
+# get field from a APKGBINDB line: bin_fetch_field <line> <fieldnum>
+# 1=namever, 2=size, 3=sha3sum, 4=deps, 5=preinstall, 6=postinstall, 7=desc
+bin_fetch_field() {
+ _line=$1
+ _field=$2
+ _rest=$_line
+ _i=1
+ while [ "$_i" -lt "$_field" ]; do
+ _rest=${_rest#*|}
+ _i=$((_i + 1))
+ done
+ case $_field in
+ 7) printf "%s\n" "${_rest#*|}" ;;
+ *) printf "%s\n" "${_rest%%|*}" ;;
+ esac
+}
+
+# find a package name in APKGBINDB, output the full line
+bin_find_pkg() {
+ _pkg=$1
+ _line=$(grep "^$_pkg#" "$APKGBIN_CACHE_DIR/APKGBINDB" 2>/dev/null | head -n1)
+ [ "$_line" ] && printf "%s\n" "$_line"
+}
+
+# get full name#version-release for a package name
+bin_get_pkgid() {
+ _line=$(bin_find_pkg "$1") || return 1
+ bin_fetch_field "$_line" 1
+}
+
+# get deps for a package (space-separated names) from APKGBINDB only
+bin_get_deps() {
+ _line=$(bin_find_pkg "$1" 2>/dev/null)
+ if [ "$_line" ]; then
+ _deps=$(bin_fetch_field "$_line" 4)
+ if [ "$_deps" ]; then
+ printf "%s\n" "$_deps" | tr ',' ' '
+ fi
+ fi
+}
+
+# get description for a package
+bin_get_desc() {
+ _line=$(bin_find_pkg "$1") || return 0
+ bin_fetch_field "$_line" 7
+}
+
+# get preinstall script (base64) for a package
+bin_get_preinstall() {
+ _line=$(bin_find_pkg "$1" 2>/dev/null) || return 0
+ bin_fetch_field "$_line" 5
+}
+
+# get postinstall script (base64) for a package
+bin_get_postinstall() {
+ _line=$(bin_find_pkg "$1" 2>/dev/null) || return 0
+ bin_fetch_field "$_line" 6
+}
+
+# run a pre/post install script from base64
+bin_run_script() {
+ _name=$1
+ _b64=$2
+ _tag=$3 # "pre" or "post"
+ [ "$_b64" ] || return 0
+ msg "Running ${_tag}install script for $_name ..."
+ printf "%s\n" "$_b64" | base64 -d | sh -e || {
+ msg "Warning: ${_tag}install script for $_name failed"
+ }
+}
+
+solve_alias() {
+ [ "$APKG_ALIAS" ] || {
+ printf "%s\n" "$@"
+ return
+ }
+ for _a in "$@"; do
+ _d=$(printf "%s\n" $APKG_ALIAS | tr ' ' '\n' | grep "^$_a:" | head -n1 | awk -F : '{print $2}')
+ printf "%s\n" "${_d:-$_a}"
+ done
+}
+
+bin_checkdep() {
+ # skip packages not in APKGBINDB
+ if ! bin_find_pkg "$1" >/dev/null 2>&1; then
+ [ "$_process" ] && msg "Warning: '$1' not found in APKGBINDB, skipping"
+ return
+ fi
+ _process="$_process $1"
+ for _d in $(solve_alias $(bin_get_deps "$1")); do
+ [ "$_d" = "$1" ] && continue
+ if [ "$_skip_installed" ]; then
+ pkg_is_installed "$_d" && continue
+ fi
+ # cycle detection
+ printf "%s\n" $_process | tr ' ' '\n' | grep -Fxq "$_d" && continue
+ # already resolved
+ printf "%s\n" $DEPS | tr ' ' '\n' | grep -Fxq "$_d" && continue
+ bin_checkdep "$_d"
+ done
+ printf "%s\n" $DEPS | tr ' ' '\n' | grep -Fxq "$1" || DEPS="$DEPS $1"
+}
+
+bin_deplist() {
+ DEPS=""
+ _process=""
+ _skip_installed=""
+ for _p in "$@"; do
+ printf "%s\n" $DEPS | tr ' ' '\n' | grep -Fxq "$_p" || bin_checkdep "$_p"
+ done
+ printf "%s\n" $DEPS | tr ' ' '\n' | grep -v ^$
+}
+
+bin_sync() {
+ needbinsrc
+ mkdir -p "$APKGBIN_CACHE_DIR" 2>/dev/null
+ [ -w "$APKGBIN_CACHE_DIR" ] || die "No write permission on $APKGBIN_CACHE_DIR"
+ case $APKGBIN_REPO in
+ http://*|https://*|ftp://*)
+ msg "Syncing $APKGBIN_REPO ..."
+ curl -fL -o "$APKGBIN_CACHE_DIR/APKGBINDB.tmp" "$APKGBIN_REPO/APKGBINDB" || {
+ rm -f "$APKGBIN_CACHE_DIR/APKGBINDB.tmp"
+ die "Failed to fetch APKGBINDB from $APKGBIN_REPO"
+ }
+ mv "$APKGBIN_CACHE_DIR/APKGBINDB.tmp" "$APKGBIN_CACHE_DIR/APKGBINDB"
+ ;;
+ *)
+ [ -f "$APKGBIN_REPO/APKGBINDB" ] || die "No APKGBINDB found at $APKGBIN_REPO"
+ msg "Syncing $APKGBIN_REPO (local) ..."
+ cp "$APKGBIN_REPO/APKGBINDB" "$APKGBIN_CACHE_DIR/APKGBINDB"
+ ;;
+ esac
+ msg "Sync complete."
+}
+
+bin_generate() {
+ [ "$APKG_REPO" ] || die "APKG_REPO is not set (needed to read abuild metadata)"
+ [ -d "$APKG_PACKAGE_DIR" ] || die "APKG_PACKAGE_DIR ($APKG_PACKAGE_DIR) not found"
+ [ -r "$APKG_PACKAGE_DIR" ] || die "No read permission on $APKG_PACKAGE_DIR"
+ [ -w "$APKG_PACKAGE_DIR" ] || die "No write permission on $APKG_PACKAGE_DIR"
+ mkdir -p "$APKGBIN_CACHE_DIR" 2>/dev/null
+ [ -w "$APKGBIN_CACHE_DIR" ] || die "No write permission on $APKGBIN_CACHE_DIR"
+
+ _output="${APKG_PACKAGE_DIR}/APKGBINDB"
+ > "$_output"
+
+ _count=0
+ for _spm in "$APKG_PACKAGE_DIR"/*.spm; do
+ [ -f "$_spm" ] || continue
+ _filename=${_spm##*/}
+ _namever=${_filename%.spm}
+ _name=${_namever%%#*}
+ _size=$(stat -c%s "$_spm" 2>/dev/null || wc -c < "$_spm")
+ _sha3=$(sha3sum "$_spm" 2>/dev/null | awk '{print $1}')
+
+ _deps=""
+ for _r in $APKG_REPO; do
+ if [ -f "$_r/$_name/depends" ]; then
+ for _d in $(grep -Ev '^(#|$)' "$_r/$_name/depends" | awk '{print $1}'); do
+ for _rr in $APKG_REPO; do
+ [ -d "$_rr/$_d" ] && { _deps="$_deps$_d,"; break; }
+ done
+ done
+ _deps=${_deps%,}
+ break
+ fi
+ done
+
+ _desc=""
+ _pre=""
+ _post=""
+ for _r in $APKG_REPO; do
+ [ -f "$_r/$_name/info" ] && [ ! "$_desc" ] && \
+ _desc=$(grep '^description:' "$_r/$_name/info" | sed 's/^description:[[:space:]]*//')
+ [ -f "$_r/$_name/preinstall" ] && [ ! "$_pre" ] && \
+ _pre=$(base64 "$_r/$_name/preinstall" 2>/dev/null | tr -d '\n')
+ [ -f "$_r/$_name/postinstall" ] && [ ! "$_post" ] && \
+ _post=$(base64 "$_r/$_name/postinstall" 2>/dev/null | tr -d '\n')
+ done
+
+ printf "%s|%s|%s|%s|%s|%s|%s\n" "$_namever" "$_size" "$_sha3" "$_deps" "$_pre" "$_post" "$_desc" >> "$_output"
+ _count=$((_count + 1))
+ done
+
+ # also cache locally so -l/-s/-i work immediately
+ mkdir -p "$APKGBIN_CACHE_DIR"
+ [ "$_output" = "$APKGBIN_CACHE_DIR/APKGBINDB" ] || cp "$_output" "$APKGBIN_CACHE_DIR/APKGBINDB"
+ msg "Wrote APKGBINDB with $_count entries to $_output"
+}
+
+bin_download_pkg() {
+ _pkgid=$1
+ _name=${_pkgid%%#*}
+ _pkgfile="$_pkgid.spm"
+
+ mkdir -p "$APKGBIN_CACHE_DIR" 2>/dev/null
+ [ -w "$APKGBIN_CACHE_DIR" ] || die "No write permission on $APKGBIN_CACHE_DIR"
+
+ case $APKGBIN_REPO in
+ http://*|https://*|ftp://*)
+ _urlfile=$(printf "%s\n" "$_pkgfile" | sed 's/#/%23/g')
+ _target="$APKGBIN_CACHE_DIR/$_pkgfile"
+ if [ "$force" ] || [ ! -f "$_target" ]; then
+ msg "Downloading $APKGBIN_REPO/$_pkgfile"
+ curl -fL -o "$_target.tmp" "$APKGBIN_REPO/$_urlfile" || {
+ rm -f "$_target.tmp"
+ die "Failed to download $_pkgfile from $APKGBIN_REPO"
+ }
+ mv "$_target.tmp" "$_target"
+ fi
+ ;;
+ *)
+ _target="$APKGBIN_REPO/$_pkgfile"
+ [ -f "$_target" ] || die "Package file not found: $_target"
+ ;;
+ esac
+
+ # verify sha3sum if available in APKGBINDB
+ _line=$(bin_find_pkg "$_name")
+ _expected=$(bin_fetch_field "$_line" 3)
+ if [ "$_expected" ]; then
+ _actual=$(sha3sum "$_target" | awk '{print $1}')
+ [ "$_actual" = "$_expected" ] || die "sha3sum mismatch for $_pkgfile"
+ fi
+
+ printf "%s\n" "$_target"
+}
+
+bin_install() {
+ _resolve=${1:-1}; shift # 1=resolve deps, 0=no deps
+ needroot
+ needcachedb
+ [ "$1" ] || die "No packages specified"
+
+ _todo=""
+ for _p in "$@"; do
+ if pkg_is_installed "$_p"; then
+ msg "Package '$_p' is already installed. Skipping."
+ else
+ _todo="$_todo $_p"
+ fi
+ done
+ [ "$_todo" ] || { msg "Nothing to install."; exit 0; }
+
+ if [ "$_resolve" = 1 ]; then
+ msg "Solving dependencies..."
+ _deps=$(_skip_installed=1 bin_deplist $_todo)
+ else
+ _deps=$_todo
+ fi
+ [ "$_deps" ] || { msg "Nothing to install."; exit 0; }
+
+ _total=$(printf "%s\n" "$_deps" | wc -l)
+ msg "Installing $_total package(s): $(printf "%s\n" "$_deps" | tr '\n' ' ')"
+
+ for _d in $_deps; do
+ _pkgid=$(bin_get_pkgid "$_d") || continue
+ msg " $_pkgid"
+ done
+
+ prompt_user
+
+ for _d in $_deps; do
+ _pkgid=$(bin_get_pkgid "$_d") || continue
+ _pkgpath=$(bin_download_pkg "$_pkgid") || continue
+ bin_run_script "$_d" "$(bin_get_preinstall "$_d")" pre
+ msg "Installing $_pkgid ..."
+ SPM_ROOT=${APKG_ROOT%/} spm -i "$_pkgpath" || die "Failed to install $_pkgid"
+ bin_run_script "$_d" "$(bin_get_postinstall "$_d")" post
+ done
+}
+
+bin_upgrade() {
+ needroot
+ needcachedb
+ [ "$1" ] || die "No packages specified"
+
+ for _p in "$@"; do
+ if ! pkg_is_installed "$_p"; then
+ msg "Package '$_p' not installed. Skipping."
+ continue
+ fi
+ _pkgid=$(bin_get_pkgid "$_p") || {
+ msg "Package '$_p' not found in APKGBINDB. Skipping."
+ continue
+ }
+ _pkgpath=$(bin_download_pkg "$_pkgid") || continue
+ _installed=$(pkg_installed_ver "$_p")
+ _available=${_pkgid#*#}
+ [ "$_installed" = "$_available" ] && msg "Package '$_p' is up to date, reinstalling."
+ bin_run_script "$_p" "$(bin_get_preinstall "$_p")" pre
+ msg "Upgrading $_pkgid ..."
+ SPM_ROOT=${APKG_ROOT%/} spm -u "$_pkgpath" || die "Failed to upgrade $_pkgid"
+ bin_run_script "$_p" "$(bin_get_postinstall "$_p")" post
+ done
+}
+
+bin_sysupgrade() {
+ needroot
+ needcachedb
+
+ msg "Checking for outdated packages..."
+ _outdated=$(_apply_mask=1 bin_list_outdated | awk '{print $1}')
+ [ "$_outdated" ] || { msg "No outdated packages."; exit 0; }
+
+ msg "Resolving dependencies..."
+ _skip_installed=""
+ DEPS=""
+ for _name in $_outdated; do
+ printf "%s\n" $DEPS | tr ' ' '\n' | grep -Fxq "$_name" || {
+ _process=""
+ bin_checkdep "$_name"
+ }
+ done
+ _all=$(printf "%s\n" $DEPS | tr ' ' '\n' | grep -v ^$ | sort -u)
+
+ _newpkgs=""
+ _upgradepkgs=""
+ _seen_new=""
+ _seen_up=""
+ for _name in $_all; do
+ if pkg_is_installed "$_name"; then
+ printf "%s\n" $_seen_up | tr ' ' '\n' | grep -Fxq "$_name" && continue
+ _seen_up="$_seen_up $_name"
+ _pkgid=$(bin_get_pkgid "$_name") || continue
+ _installed=$(pkg_installed_ver "$_name")
+ _available=${_pkgid#*#}
+ [ "$_installed" = "$_available" ] && continue
+ _upgradepkgs="$_upgradepkgs $_name"
+ else
+ printf "%s\n" $_seen_new | tr ' ' '\n' | grep -Fxq "$_name" && continue
+ _seen_new="$_seen_new $_name"
+ _newpkgs="$_newpkgs $_name"
+ fi
+ done
+
+ [ "$_newpkgs" ] && msg "Installing new package(s): $_newpkgs"
+ [ "$_upgradepkgs" ] || { msg "All packages up to date."; exit 0; }
+ _ncount=$(printf "%s\n" $_upgradepkgs | wc -l)
+ msg "Upgrading $_ncount package(s): $(printf "%s\n" $_upgradepkgs | tr '\n' ' ')"
+ prompt_user
+
+ if [ "$_newpkgs" ]; then
+ for _name in $_newpkgs; do
+ _pkgid=$(bin_get_pkgid "$_name") || continue
+ _pkgpath=$(bin_download_pkg "$_pkgid") || continue
+ bin_run_script "$_name" "$(bin_get_preinstall "$_name")" pre
+ msg "Installing $_pkgid ..."
+ SPM_ROOT=${APKG_ROOT%/} spm -i "$_pkgpath" || die "Failed to install $_pkgid"
+ bin_run_script "$_name" "$(bin_get_postinstall "$_name")" post
+ done
+ fi
+ for _name in $_upgradepkgs; do
+ _pkgid=$(bin_get_pkgid "$_name") || continue
+ _pkgpath=$(bin_download_pkg "$_pkgid") || continue
+ bin_run_script "$_name" "$(bin_get_preinstall "$_name")" pre
+ msg "Upgrading $_pkgid ..."
+ SPM_ROOT=${APKG_ROOT%/} spm -u "$_pkgpath" || die "Failed to upgrade $_pkgid"
+ bin_run_script "$_name" "$(bin_get_postinstall "$_name")" post
+ done
+
+ msg "System upgrade complete."
+}
+
+bin_list_outdated() {
+ needcachedb
+ [ -d "$SPM_PKGDB" ] || return 0
+ for _db in "$SPM_PKGDB"/*; do
+ [ -f "$_db" ] || continue
+ _name=${_db##*/}
+ if [ "$APKG_MASK" ] && [ "$_apply_mask" ]; then
+ printf "%s\n" "$APKG_MASK" | tr ' ' '\n' | grep -Fxq "$_name" && continue
+ fi
+ _installed=$(head -n1 "$_db")
+ _line=$(bin_find_pkg "$_name") || continue
+ _namever=$(bin_fetch_field "$_line" 1)
+ _available=${_namever#*#}
+ [ "$_installed" = "$_available" ] || printf "%s\n" "$_name $_installed -> $_available"
+ done
+}
+
+bin_search() {
+ needcachedb
+ _pattern=$1
+ while IFS= read -r _line; do
+ [ "$_line" ] || continue
+ _namever=$(bin_fetch_field "$_line" 1)
+ _name=${_namever%%#*}
+ _display=$(printf "%s\n" "$_namever" | tr '#' ' ')
+ if [ "$_pattern" ]; then
+ printf "%s\n" "$_name" | grep -q "$_pattern" || continue
+ fi
+ if [ "$verbose" ]; then
+ _desc=$(bin_fetch_field "$_line" 7)
+ printf "%s %s\n" "$_display" "$_desc"
+ else
+ printf "%s\n" "$_name"
+ fi
+ done < "$APKGBIN_CACHE_DIR/APKGBINDB"
+}
+
+bin_list_installed() {
+ [ -d "$SPM_PKGDB" ] || return 0
+ for _db in "$SPM_PKGDB"/*; do
+ [ -f "$_db" ] || continue
+ _name=${_db##*/}
+ if [ "$verbose" ]; then
+ _ver=$(head -n1 "$_db")
+ printf "%s %s\n" "$_name" "$_ver"
+ else
+ printf "%s\n" "$_name"
+ fi
+ done
+}
+
+bin_clean() {
+ needcachedb
+ _dldir="$APKGBIN_CACHE_DIR"
+ [ -d "$_dldir" ] || { msg "No downloaded packages to clean."; exit 0; }
+ _count=0
+ for _spm in "$_dldir"/*.spm; do
+ [ -f "$_spm" ] || continue
+ _filename=${_spm##*/}
+ _name=${_filename%%#*}
+ _line=$(bin_find_pkg "$_name" 2>/dev/null)
+ [ "$_line" ] || continue
+ _expected=$(bin_fetch_field "$_line" 3)
+ [ "$_expected" ] || continue
+ _actual=$(sha3sum "$_spm" | awk '{print $1}')
+ if [ "$_actual" != "$_expected" ]; then
+ msg "Removing $_filename (sha3sum mismatch)"
+ rm -f "$_spm"
+ _count=$((_count + 1))
+ fi
+ done
+ msg "Removed $_count package(s) with sha3sum mismatch."
+}
+
+bin_show_deps() {
+ needcachedb
+ [ "$1" ] || die "No package specified"
+ _deps=$(bin_get_deps "$1")
+ [ "$_deps" ] && printf "%s\n" $_deps
+}
+
+bin_show_deptree() {
+ needcachedb
+ [ "$1" ] || die "No package specified"
+ [ "$(bin_find_pkg "$1")" ] || exit 0
+ _deps=$(bin_deplist "$1")
+ printf "%s\n" "$_deps" | grep -Fxq "$1" || _deps="$_deps
+$1"
+ printf "%s\n" "$_deps" | grep -v ^$
+}
+
+bin_help() {
+ cat << 'EOF'
+usage: apkg-bin <option> [arg(s)]
+
+options:
+ -g generate APKGBINDB from APKG_PACKAGE_DIR and APKG_REPO
+ -S sync APKGBINDB from APKGBIN_REPO
+ -s [pattern] search available binary packages (optional filter)
+ -i <pkg(s)> install package(s) without dependency resolution
+ -I <pkg(s)> install package(s) with dependency resolution
+ -u <pkg(s)> upgrade package(s)
+ -U full system upgrade (all outdated packages)
+ -o <pkg(s)> download package(s) only (no install)
+ -c clean downloaded packages with sha3sum mismatch
+ -d <pkg> show direct dependencies for a package
+ -D <pkg(s)> show full dependency tree for package(s)
+ -l list outdated packages
+ -a list all installed packages
+ -f force re-download of cached packages
+ -v verbose output
+ -h print this help message
+
+environment variables:
+ APKGBIN_REPO binary repo URL or path
+ APKGBIN_CACHE_DIR cache directory (required)
+ APKG_REPO source repo directories (needed for -g)
+ APKG_PACKAGE_DIR package directory (default: $PWD)
+ APKG_ROOT alternative install root
+ APKG_MASK packages to skip during -U (system upgrade)
+ APKG_ALIAS dependency substitution (real:alias pairs)
+ APKG_NOPROMPT skip confirmation prompts
+EOF
+}
+
+_filter_flags() {
+ _args=""
+ for _a in "$@"; do
+ case $_a in
+ -f) force=1 ;;
+ -v) verbose=1 ;;
+ *) _args="$_args $_a" ;;
+ esac
+ done
+}
+
+parseopts() {
+ while [ "$1" ]; do
+ case $1 in
+ -g) bin_generate; exit 0;;
+ -S) bin_sync; exit 0;;
+ -s) _do_search=1; if [ "$2" ] && [ "${2#-}" = "$2" ]; then search_pattern="$2"; shift; fi;;
+ -i) shift; _filter_flags "$@"; bin_install 0 $_args; exit 0;;
+ -I) shift; _filter_flags "$@"; bin_install 1 $_args; exit 0;;
+ -u) shift; _filter_flags "$@"; bin_upgrade $_args; exit 0;;
+ -U) bin_sysupgrade; exit 0;;
+ -o) shift; _filter_flags "$@"; needcachedb; for _p in $_args; do _pkgid=$(bin_get_pkgid "$_p") || die "Package '$_p' not found in APKGBINDB"; bin_download_pkg "$_pkgid"; done; exit 0;;
+ -c) bin_clean; exit 0;;
+ -d) bin_show_deps "$2"; exit 0;;
+ -D) shift; _filter_flags "$@"; needcachedb; _valid=""; for _p in $_args; do [ "$(bin_find_pkg "$_p")" ] && _valid="$_valid $_p"; done; [ "$_valid" ] || exit 0; _out=$(bin_deplist $_valid); for _p in $_valid; do printf "%s\n" "$_out" | grep -Fxq "$_p" || _out="$_out
+$_p"; done; printf "%s\n" "$_out" | grep -v ^$; exit 0;;
+ -l) bin_list_outdated; exit 0;;
+ -a) bin_list_installed; exit 0;;
+ -f) force=1;;
+ -v) verbose=1;;
+ -h) bin_help; exit 0;;
+ -*) die "invalid option '$1'";;
+ *) pkg="$pkg $1";;
+ esac
+ shift
+ done
+}
+
+main() {
+ parseopts "$@"
+
+ if [ "$_do_search" ]; then
+ bin_search "$search_pattern"
+ exit 0
+ fi
+
+ bin_help
+ exit 0
+}
+
+umask 022
+
+SPM_PKGDB="${APKG_ROOT%/}/var/lib/spm/db"
+APKG_PACKAGE_DIR="${APKG_PACKAGE_DIR:-$PWD}"
+[ "$APKGBIN_CACHE_DIR" ] || die "APKGBIN_CACHE_DIR is not set"
+
+main "$@"
+
+exit 0
diff --git a/man/apkg-bin.8 b/man/apkg-bin.8
new file mode 100644
index 0000000..f872623
--- /dev/null
+++ b/man/apkg-bin.8
@@ -0,0 +1,226 @@
+.\" -*- mode: troff; coding: utf-8 -*-
+.TH APKG\-BIN 8
+.SH NAME
+apkg\-bin \- Alice Linux binary package manager
+.SH DESCRIPTION
+.LP
+\fBapkg\-bin\fR is the binary package manager for Alice Linux. It fetches
+pre-built \fI.spm\fR package archives from a binary repository, resolves
+dependencies, and installs or upgrades packages via the \fBspm\fR(8) backend.
+It is the binary counterpart to the source-based \fBapkg\fR(8).
+
+Before use, run \fBapkg\-bin \-S\fR to sync the package index from
+\fBAPKGBIN_REPO\fR. The index is cached locally at \fBAPKGBIN_CACHE_DIR\fR;
+subsequent operations operate against the cache.
+
+.SH OPTIONS
+.TP
+\fB\-h\fR
+Print help and exit.
+.TP
+\fB\-g\fR
+Generate a binary repository index (\fIAPKGBINDB\fR) from \fI.spm\fR files found
+in \fBAPKG_PACKAGE_DIR\fR. Reads \fIdepends\fR, \fIinfo\fR, \fIpreinstall\fR,
+and \fIpostinstall\fR files from \fBAPKG_REPO\fR directories. The index is
+written to \fBAPKG_PACKAGE_DIR\fR and copied to \fBAPKGBIN_CACHE_DIR\fR for
+immediate local use.
+.TP
+\fB\-S\fR
+Sync the binary repository index from \fBAPKGBIN_REPO\fR. For remote URLs (http,
+https, ftp), downloads \fIAPKGBINDB\fR via \fBcurl\fR(1). For local paths, copies
+the file directly. Cached at \fI$APKGBIN_CACHE_DIR/APKGBINDB\fR.
+.TP
+\fB\-s\fR [\fIpattern\fR]
+Search available binary packages by name pattern. Without a pattern, lists all
+packages. With \fB\-v\fR, prints name, version-release, and description.
+Without \fB\-v\fR, prints package name only.
+.TP
+\fB\-i\fR \fI<pkg...>\fR
+Install package(s) without dependency resolution. Skips packages that are
+already installed. Requires root.
+.TP
+\fB\-I\fR \fI<pkg...>\fR
+Install package(s) with full recursive dependency resolution. Skips packages
+that are already installed. Prompts for confirmation before proceeding unless
+\fBAPKG_NOPROMPT\fR is set. Requires root.
+.TP
+\fB\-u\fR \fI<pkg...>\fR
+Upgrade or reinstall specific package(s). No dependency resolution, no
+confirmation prompt. Reinstalls even if already up to date. Skips packages
+not currently installed. Requires root.
+.TP
+\fB\-U\fR
+Full system upgrade. Compares all installed packages against the cached index,
+resolves the full dependency closure for outdated packages, installs any new
+dependencies, and upgrades all outdated packages. Prompts for confirmation
+unless \fBAPKG_NOPROMPT\fR is set. Respects \fBAPKG_MASK\fR. Requires root.
+.TP
+\fB\-d\fR \fI<pkg>\fR
+Show direct dependencies for a package. Prints one dependency name per line.
+.TP
+\fB\-D\fR \fI<pkg...>\fR
+Show full dependency tree for package(s) (recursive, all transitive dependencies).
+Prints one dependency name per line, with the input package last.
+.TP
+\fB\-l\fR
+List installed packages that have newer versions available in the binary index.
+.TP
+\fB\-a\fR
+List all installed packages. With \fB\-v\fR, also prints the installed version.
+.TP
+\fB\-o\fR \fI<pkg...>\fR
+Download package(s) only, without installing. Resolves each package name in the
+binary index and downloads the \fI.spm\fR to \fI$APKGBIN_CACHE_DIR\fR.
+.TP
+\fB\-c\fR
+Clean downloaded packages with sha3sum mismatch. Scans \fI$APKGBIN_CACHE_DIR/\fR,
+compares each \fI.spm\fR against the expected sha3sum in APKGBINDB, and removes
+any with a mismatch.
+.TP
+\fB\-f\fR
+Force re-download of cached \fI.spm\fR files. Usable with \fB\-i\fR, \fB\-I\fR,
+\fB\-u\fR, and \fB\-o\fR.
+.TP
+\fB\-v\fR
+Verbose output. Affects \fB\-s\fR (show descriptions) and \fB\-a\fR (show versions).
+
+.SH BINARY REPOSITORY FORMAT
+.LP
+A binary repository is a directory (local or remote) containing:
+.TP
+\fIAPKGBINDB\fR
+The package index file. Each line is a pipe-delimited record:
+.RS
+.IP
+\fIname\fB#\fIversion-release\fB|\fIsize\fB|\fIsha3sum\fB|\fIdep1,dep2,...\fB|\fIpreinstall_b64\fB|\fIpostinstall_b64\fB|\fIdescription\fR
+.RE
+.IP
+Fields: package identifier, file size in bytes, sha3sum hash, comma-separated
+dependency names (empty if none), base64-encoded preinstall script (empty if
+none), base64-encoded postinstall script (empty if none), and a human-readable
+description. Use \fBapkg\-bin \-g\fR to generate.
+.TP
+\fI<name>#<version-release>.spm\fR
+Pre-built package archives in \fBspm\fR(8) format. The \fB#\fR in filenames
+is URL-encoded as \fB%23\fR when served over HTTP.
+
+.SH ENVIRONMENT
+.TP
+\fBAPKGBIN_REPO\fR
+Binary repository URL or local path. Required for \fB\-S\fR, \fB\-i\fR,
+\fB\-I\fR, \fB\-u\fR, \fB\-U\fR, and \fB\-o\fR.
+.TP
+\fBAPKGBIN_CACHE_DIR\fR
+Directory where the synced \fIAPKGBINDB\fR and downloaded \fI.spm\fR packages
+are cached. Required.
+.TP
+\fBAPKG_REPO\fR
+Space-separated list of source repository directories. Required for \fB\-g\fR
+(to read \fIdepends\fR, \fIinfo\fR, \fIpreinstall\fR, and \fIpostinstall\fR
+files).
+.TP
+\fBAPKG_PACKAGE_DIR\fR
+Directory for package storage. Scanned by \fB\-g\fR for \fI.spm\fR files.
+Default: current directory.
+.TP
+\fBAPKG_ROOT\fR
+Alternative root directory for installation. Sets \fBSPM_ROOT\fR. Default: \fI/\fR.
+.TP
+\fBAPKG_MASK\fR
+Space-separated list of packages to skip during \fB\-U\fR (system upgrade).
+.TP
+\fBAPKG_ALIAS\fR
+Space-separated list of \fIreal:alias\fR pairs for dependency substitution
+(e.g. \fIopenssl:libressl\fR).
+.TP
+\fBAPKG_NOPROMPT\fR
+If set, skip the confirmation prompt in \fB\-I\fR and \fB\-U\fR operations.
+
+.SH FILES
+.TP
+\fI$APKGBIN_CACHE_DIR/APKGBINDB\fR
+Cached binary repository index.
+.TP
+\fI$APKGBIN_CACHE_DIR/\fR
+Downloaded \fI.spm\fR package files.
+.TP
+\fI/var/lib/spm/db/\fR
+SPM package database. Used to check installed packages and versions.
+
+.SH EXAMPLES
+.LP
+Sync the binary repository index:
+.RS
+\f(CRAPKGBIN_REPO=https://example.com/alice/main apkg\-bin \-S\fR
+.RE
+.LP
+Search for packages:
+.RS
+\f(CRapkg\-bin \-s browser\fR
+.RE
+.LP
+Install a package without dependency resolution:
+.RS
+\f(CRapkg\-bin \-i firefox\fR
+.RE
+.LP
+Install a package with all dependencies:
+.RS
+\f(CRapkg\-bin \-I firefox\fR
+.RE
+.LP
+Show dependencies of a package:
+.RS
+\f(CRapkg\-bin \-d firefox\fR
+.RE
+.LP
+Reinstall a package (force re-download):
+.RS
+\f(CRapkg\-bin \-u \-f opus\fR
+.RE
+.LP
+List outdated packages:
+.RS
+\f(CRapkg\-bin \-l\fR
+.RE
+.LP
+Full system upgrade:
+.RS
+\f(CRapkg\-bin \-U\fR
+.RE
+.LP
+Generate a binary index (for repo maintainers):
+.RS
+\f(CRapkg\-bin \-g\fR
+.RE
+.LP
+Clean mismatched downloads:
+.RS
+\f(CRapkg\-bin \-c\fR
+.RE
+
+.SH SEE ALSO
+.BR apkg (8),
+.BR apkg\-chroot (8),
+.BR apkg\-clean (8),
+.BR apkg\-deps (8),
+.BR apkg\-foreign (8),
+.BR apkg\-genabuild (8),
+.BR apkg\-orphan (8),
+.BR apkg\-purge (8),
+.BR apkg\-redundantdeps (8),
+.BR reposync (8),
+.BR revdep (8),
+.BR updateconf (8),
+.BR spm (8)
+
+.SH AUTHORS
+.LP
+emmett1 \c
+.MT me@emmett1.my
+.ME
+
+.SH REPORTING BUGS
+.LP
+.UR https://codeberg.org/emmett1/autils/issues
+.UE