diff options
Diffstat (limited to 'apkg-bin')
| -rwxr-xr-x | apkg-bin | 591 |
1 files changed, 591 insertions, 0 deletions
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 |