pkget

Binary package manager for CRUX
git clone git://git.emmett1.my/pkget.git
Log | Files | Refs | README

pkget (17144B)


      1 #!/bin/sh
      2 # pkget - binary package fetcher and installer
      3 # Fetches packages from a binary repo, resolves deps, verifies, installs via pkgadd
      4 # Usage: pkget [options] <package> [package ...]
      5 
      6 REPO_URL="${REPO_URL:-}"
      7 CACHEDIR="${CACHEDIR:-/var/cache/pkget}"
      8 DBCACHE="${DBCACHE:-$CACHEDIR/repo.db}"
      9 SUMCACHE="${SUMCACHE:-$CACHEDIR/repo.sha256}"
     10 PKGADD="${PKGADD:-pkgadd}"
     11 VERBOSE=0
     12 DRY_RUN=0
     13 NO_DEPS=0
     14 FORCE=0
     15 UPGRADE=0
     16 FULL_UPGRADE=0
     17 DO_OUTDATED=0
     18 OUTDATED_NAMES=""
     19 
     20 usage() {
     21     cat <<USAGE
     22 Usage: pkget [options] <package> [package ...]
     23 
     24 Options:
     25   -u <url>    Repository base URL (required, or set REPO_URL)
     26   -c <dir>    Cache directory (default: /var/cache/pkget)
     27   -s          Sync repo DB from server
     28   -n          Dry run: resolve and print, no install
     29   -N          Skip dependency resolution
     30   -f          Force reinstall even if already installed
     31   -U          Upgrade mode (passes -u to pkgadd)
     32   -o          Show outdated packages (compare installed vs repo)
     33   -v          Verbose output
     34   -h          Show this help
     35 
     36 Environment:
     37   REPO_URL    Repository base URL (e.g. https://pkg.example.com)
     38   CACHEDIR    Local cache dir
     39 
     40 Examples:
     41   pkget -u https://pkg.example.com -s          # sync DB
     42   pkget -u https://pkg.example.com wget        # install wget + deps
     43   pkget -n wget                                # dry run
     44   pkget -U -u https://pkg.example.com curl     # upgrade curl
     45   pkget -o -u https://pkg.example.com            # list outdated packages
     46 USAGE
     47     exit 0
     48 }
     49 
     50 log()  { [ "$VERBOSE" -eq 1 ] && printf '[v] %s\n' "$*" >&2; }
     51 info() { printf '==> %s\n' "$*" >&2; }
     52 warn() { printf 'pkget: warning: %s\n' "$*" >&2; }
     53 err()  { printf 'pkget: error: %s\n'   "$*" >&2; }
     54 die()  { err "$*"; exit 1; }
     55 
     56 # ---------------------------------------------------------------------------
     57 # fetch <url> <dest>
     58 # ---------------------------------------------------------------------------
     59 fetch() {
     60     _url="$1"; _dest="$2"
     61     if command -v curl >/dev/null 2>&1; then
     62         curl -fsSL -o "$_dest" "$_url"
     63     elif command -v wget >/dev/null 2>&1; then
     64         wget -q -O "$_dest" "$_url"
     65     else
     66         die "neither curl nor wget found"
     67     fi
     68 }
     69 
     70 # ---------------------------------------------------------------------------
     71 # _mktemp -- portable temp file creation (GNU + BSD)
     72 # ---------------------------------------------------------------------------
     73 _mktemp() {
     74     mktemp 2>/dev/null || mktemp -t pkget 2>/dev/null || \
     75         die "mktemp failed"
     76 }
     77 
     78 # ---------------------------------------------------------------------------
     79 # _sha256 <file> -- print SHA256 hash, portable across implementations
     80 # ---------------------------------------------------------------------------
     81 _sha256() {
     82     if command -v sha256sum >/dev/null 2>&1; then
     83         sha256sum "$1" | awk '{print $1}'
     84     elif command -v shasum >/dev/null 2>&1; then
     85         shasum -a 256 "$1" | awk '{print $1}'
     86     elif command -v sha256 >/dev/null 2>&1; then
     87         sha256 -q "$1"
     88     else
     89         die "no sha256 tool found (need sha256sum, shasum, or sha256)"
     90     fi
     91 }
     92 
     93 # ---------------------------------------------------------------------------
     94 # DB helpers
     95 # ---------------------------------------------------------------------------
     96 db_has() {
     97     # db_has <dbfile> <pkgname>
     98     grep -Fx "name:${2}" "$1" >/dev/null 2>&1
     99 }
    100 
    101 db_get() {
    102     # db_get <dbfile> <pkgname> <field>  -> prints value
    103     awk -v pkg="$2" -v field="$3" '
    104         /^$/ { in_pkg=0 }
    105         /^name:/ { in_pkg=(substr($0,6)==pkg) }
    106         in_pkg && $0 ~ "^" field ":" { print substr($0, length(field)+2); exit }
    107     ' "$1"
    108 }
    109 
    110 # ---------------------------------------------------------------------------
    111 # Sync repo DB + checksums from server
    112 # ---------------------------------------------------------------------------
    113 sync_db() {
    114     [ -z "$REPO_URL" ] && die "REPO_URL not set (use -u)"
    115     info "syncing repo DB from $REPO_URL"
    116     mkdir -p "$CACHEDIR"
    117     fetch "$REPO_URL/repo.db"     "$DBCACHE"  || die "failed to fetch repo.db"
    118     fetch "$REPO_URL/repo.sha256" "$SUMCACHE" || die "failed to fetch repo.sha256"
    119     _count=$(grep -c '^name:' "$DBCACHE" 2>/dev/null || printf '0')
    120     info "synced: $_count packages available"
    121 }
    122 
    123 # ---------------------------------------------------------------------------
    124 # verify_pkg <path>  -- verify against repo.sha256
    125 # ---------------------------------------------------------------------------
    126 verify_pkg() {
    127     _pkgpath="$1"
    128     _basename="${_pkgpath##*/}"
    129 
    130     _expected=$(awk -v f="$_basename" '$2==f{print $1}' "$SUMCACHE")
    131     if [ -z "$_expected" ]; then
    132         warn "no checksum entry for $_basename"
    133         return 1
    134     fi
    135 
    136     _actual=$(_sha256 "$_pkgpath")
    137     if [ "$_actual" = "$_expected" ]; then
    138         log "checksum OK: $_basename"
    139         return 0
    140     else
    141         err "checksum MISMATCH: $_basename"
    142         err "  expected: $_expected"
    143         err "  actual:   $_actual"
    144         return 1
    145     fi
    146 }
    147 
    148 # ---------------------------------------------------------------------------
    149 # run_script <pkgname> <pre-install|post-install>
    150 # ---------------------------------------------------------------------------
    151 run_script() {
    152     _pkg="$1"; _type="$2"
    153     _script="$CACHEDIR/${_pkg}.${_type}"
    154 
    155     if [ ! -f "$_script" ]; then
    156         _val=$(db_get "$DBCACHE" "$_pkg" "$_type")
    157         case "$_val" in
    158             b64:*)
    159                 # Embedded base64 script in DB (current format)
    160                 printf '%s' "${_val#b64:}" | base64 -d > "$_script" 2>/dev/null || {
    161                     rm -f "$_script"; return 0
    162                 }
    163                 ;;
    164             1)
    165                 # Legacy: fetch separate script file from repo
    166                 fetch "$REPO_URL/${_pkg}.${_type}" "$_script" 2>/dev/null || {
    167                     rm -f "$_script"; return 0
    168                 }
    169                 ;;
    170             *)  return 0 ;;
    171         esac
    172     fi
    173 
    174     [ -f "$_script" ] || return 0
    175 
    176     info "running ${_type} for ${_pkg}"
    177     chmod +x "$_script"
    178     sh "$_script" || warn "${_type} script for ${_pkg} exited non-zero"
    179 }
    180 
    181 # ---------------------------------------------------------------------------
    182 # url_encode_path <string>  -- encode characters unsafe in URL paths
    183 # Only encodes # (fragment marker); extend if needed.
    184 # ---------------------------------------------------------------------------
    185 url_encode_path() {
    186     # Encode chars unsafe in URL path: % first to avoid double-encoding
    187     printf '%s' "$1" | sed \
    188         -e 's/%/%25/g' \
    189         -e 's/#/%23/g' \
    190         -e 's/ /%20/g' \
    191         -e 's/?/%3F/g' \
    192         -e 's/&/%26/g'
    193 }
    194 
    195 # ---------------------------------------------------------------------------
    196 # fetch_pkg <pkgname>  -- fetch to cache, verify; prints local path
    197 # ---------------------------------------------------------------------------
    198 fetch_pkg() {
    199     _pkg="$1"
    200     _file=$(db_get "$DBCACHE" "$_pkg" "file")
    201     [ -z "$_file" ] && die "no file entry for $_pkg in DB"
    202 
    203     _dest="$CACHEDIR/$_file"
    204     # Encode # in filename for use in HTTP URL (curl/wget treat # as fragment)
    205     _urlfile=$(url_encode_path "$_file")
    206 
    207     if [ -f "$_dest" ]; then
    208         log "cached: $_file"
    209         verify_pkg "$_dest" || {
    210             info "re-fetching (bad cache): $_file"
    211             rm -f "$_dest"
    212         }
    213     fi
    214 
    215     if [ ! -f "$_dest" ]; then
    216         info "fetching: $_file"
    217         fetch "$REPO_URL/$_urlfile" "$_dest" || die "fetch failed: $_file"
    218         verify_pkg "$_dest" || die "checksum failed: $_file"
    219     fi
    220 
    221     printf '%s' "$_dest"
    222 }
    223 
    224 # ---------------------------------------------------------------------------
    225 # is_installed <pkgname>
    226 # ---------------------------------------------------------------------------
    227 is_installed() {
    228     if command -v pkginfo >/dev/null 2>&1; then
    229         pkginfo -i | awk '{print $1}' | grep -Fx "$1" >/dev/null 2>&1
    230     else
    231         [ -f /var/lib/pkg/db ] && grep -Fx "$1" /var/lib/pkg/db >/dev/null 2>&1
    232     fi
    233 }
    234 
    235 # ---------------------------------------------------------------------------
    236 # _list_outdated <outfile>
    237 # Writes "name inst_verrel repo_verrel" for each outdated package to <outfile>.
    238 # ---------------------------------------------------------------------------
    239 _list_outdated() {
    240     _out="$1"
    241     _repo_tmp=$(_mktemp)
    242     awk '
    243       /^name:/    { name=substr($0,6) }
    244       /^version:/ { ver=substr($0,9) }
    245       /^release:/ { rel=substr($0,9) }
    246       /^$/        { if(name) { printf "%s %s %s\n", name, ver, rel }
    247                     name=""; ver=""; rel="" }
    248       END         { if(name) printf "%s %s %s\n", name, ver, rel }
    249     ' "$DBCACHE" | LC_ALL=C sort > "$_repo_tmp"
    250 
    251     _inst_tmp=$(_mktemp)
    252     if command -v pkginfo >/dev/null 2>&1; then
    253         pkginfo -i 2>/dev/null | awk '{print $1, $2}' | LC_ALL=C sort > "$_inst_tmp"
    254     else
    255         while IFS= read -r _pkg; do
    256             [ -z "$_pkg" ] && continue
    257             _v=$(cat "/var/lib/pkg/$_pkg/version" 2>/dev/null)
    258             _r=$(cat "/var/lib/pkg/$_pkg/release" 2>/dev/null)
    259             [ -n "$_v" ] && printf '%s %s-%s\n' "$_pkg" "$_v" "$_r"
    260         done < /var/lib/pkg/db | LC_ALL=C sort > "$_inst_tmp"
    261     fi
    262 
    263     _joined=$(_mktemp)
    264     LC_ALL=C join "$_repo_tmp" "$_inst_tmp" > "$_joined"
    265 
    266     while IFS=' ' read -r _name _repo_ver _repo_rel _inst_verrel; do
    267         [ -z "$_name" ] && continue
    268         _repo_verrel="${_repo_ver}-${_repo_rel}"
    269         [ "$_inst_verrel" != "$_repo_verrel" ] && \
    270             printf '%s %s %s\n' "$_name" "$_inst_verrel" "$_repo_verrel"
    271     done < "$_joined" > "$_out"
    272 
    273     rm -f "$_repo_tmp" "$_inst_tmp" "$_joined"
    274 }
    275 
    276 # ---------------------------------------------------------------------------
    277 # install_pkg <pkgname> <local_path>
    278 # ---------------------------------------------------------------------------
    279 install_pkg() {
    280     _pkg="$1"; _path="$2"
    281 
    282     _has_pre=$(db_get  "$DBCACHE" "$_pkg" "pre-install")
    283     _has_post=$(db_get "$DBCACHE" "$_pkg" "post-install")
    284 
    285     [ -n "$_has_pre"  ] && run_script "$_pkg" "pre-install"
    286 
    287     _flags=""
    288     [ "$UPGRADE" -eq 1 ] && _flags="$_flags -u"
    289     [ "$FORCE"   -eq 1 ] && _flags="$_flags -f"
    290 
    291     info "installing: $_pkg"
    292     # shellcheck disable=SC2086
    293     "$PKGADD" $_flags "$_path" || die "pkgadd failed: $_pkg"
    294 
    295     [ -n "$_has_post" ] && run_script "$_pkg" "post-install"
    296 }
    297 
    298 # ---------------------------------------------------------------------------
    299 # main
    300 # ---------------------------------------------------------------------------
    301 DO_SYNC=0
    302 
    303 while getopts 'u:c:snNfUovh' _opt; do
    304     case "$_opt" in
    305         u) REPO_URL="$OPTARG" ;;
    306         c) CACHEDIR="$OPTARG"
    307            DBCACHE="$CACHEDIR/repo.db"
    308            SUMCACHE="$CACHEDIR/repo.sha256" ;;
    309         s) DO_SYNC=1 ;;
    310         n) DRY_RUN=1 ;;
    311         N) NO_DEPS=1 ;;
    312         f) FORCE=1 ;;
    313         U) UPGRADE=1 ;;
    314         o) DO_OUTDATED=1 ;;
    315         v) VERBOSE=1 ;;
    316         h) usage ;;
    317         *) usage ;;
    318     esac
    319 done
    320 shift $((OPTIND - 1))
    321 
    322 [ "$DO_SYNC" -eq 1 ] && sync_db
    323 [ $# -eq 0 ] && [ "$DO_SYNC" -eq 1 ] && exit 0
    324 if [ $# -eq 0 ]; then
    325     [ "$DO_OUTDATED" -ne 1 ] && [ "$UPGRADE" -ne 1 ] && usage
    326 fi
    327 
    328 [ -z "$REPO_URL" ] && die "REPO_URL not set (use -u or export REPO_URL)"
    329 [ -f "$DBCACHE"  ] || die "repo DB not found at $DBCACHE  -- run: pkget -s"
    330 [ -f "$SUMCACHE" ] || die "checksum file not found at $SUMCACHE  -- run: pkget -s"
    331 
    332 # -------------------------------------------------------------------
    333 # Outdated mode: compare installed versions against repo
    334 # -------------------------------------------------------------------
    335 if [ "$DO_OUTDATED" -eq 1 ]; then
    336     _outdated_raw=$(_mktemp)
    337     _list_outdated "$_outdated_raw"
    338 
    339     # If named packages, filter to just those
    340     if [ $# -gt 0 ]; then
    341         _filtered=$(_mktemp)
    342         for _pkg in "$@"; do
    343             grep "^${_pkg} " "$_outdated_raw" >> "$_filtered" 2>/dev/null || \
    344                 warn "'$_pkg' is not installed, not in repo, or up to date"
    345         done
    346         LC_ALL=C sort -u "$_filtered" > "$_outdated_raw"
    347         rm -f "$_filtered"
    348     fi
    349 
    350     _outdated=$(wc -l < "$_outdated_raw" | tr -d ' ')
    351     if [ "$_outdated" -eq 0 ]; then
    352         info "all packages up to date"
    353         rm -f "$_outdated_raw"
    354         exit 0
    355     fi
    356 
    357     info "outdated packages ($_outdated):"
    358     while IFS=' ' read -r _name _inst_verrel _repo_verrel; do
    359         printf '  %-24s %s -> %s\n' "$_name" "$_inst_verrel" "$_repo_verrel"
    360     done < "$_outdated_raw"
    361 
    362     rm -f "$_outdated_raw"
    363     exit 0
    364 fi
    365 
    366 # -------------------------------------------------------------------
    367 # Full upgrade: -U with no package args → upgrade all outdated
    368 # -------------------------------------------------------------------
    369 if [ "$UPGRADE" -eq 1 ] && [ $# -eq 0 ]; then
    370     _upgrade_raw=$(_mktemp)
    371     _list_outdated "$_upgrade_raw"
    372 
    373     _count=$(wc -l < "$_upgrade_raw" | tr -d ' ')
    374     if [ "$_count" -eq 0 ]; then
    375         info "all packages up to date"
    376         rm -f "$_upgrade_raw"
    377         exit 0
    378     fi
    379 
    380     info "outdated packages ($_count):"
    381     while IFS=' ' read -r _name _inst_verrel _repo_verrel; do
    382         printf '  %-24s %s -> %s\n' "$_name" "$_inst_verrel" "$_repo_verrel"
    383     done < "$_upgrade_raw"
    384 
    385     OUTDATED_NAMES=$(_mktemp)
    386     awk '{print $1}' "$_upgrade_raw" > "$OUTDATED_NAMES"
    387     FULL_UPGRADE=1
    388 
    389     # Replace positional args with outdated package names (dep resolver runs below)
    390     set --
    391     while IFS=' ' read -r _name _inst_verrel _repo_verrel; do
    392         [ -n "$_name" ] && set -- "$@" "$_name"
    393     done < "$_upgrade_raw"
    394     rm -f "$_upgrade_raw"
    395 fi
    396 
    397 mkdir -p "$CACHEDIR"
    398 
    399 _raw_list=$(_mktemp)
    400 
    401 if [ "$NO_DEPS" -eq 1 ]; then
    402     for _pkg in "$@"; do
    403         db_has "$DBCACHE" "$_pkg" || die "package not found in repo: $_pkg"
    404         printf '%s\n' "$_pkg"
    405     done >> "$_raw_list"
    406 else
    407     # Pre-extract deps from DB
    408     _deps_cache=$(_mktemp)
    409     awk '
    410       /^name:/  { name=substr($0,6) }
    411       /^deps:/  { deps=substr($0,6)
    412                   if (name) { printf "%s:%s\n", name, deps; name="" } }
    413       /^$/      { name="" }
    414     ' "$DBCACHE" > "$_deps_cache"
    415 
    416     # Validate roots exist in the cache
    417     for _pkg in "$@"; do
    418         grep "^${_pkg}:" "$_deps_cache" >/dev/null 2>&1 || \
    419             die "package not found in repo: $_pkg"
    420     done
    421 
    422     printf '%s\n' "$@" | awk -v depsfile="$_deps_cache" '
    423     BEGIN {
    424         while ((getline < depsfile) > 0) {
    425             col = index($0, ":")
    426             name = substr($0, 1, col-1)
    427             deps[name] = substr($0, col+1)
    428         }
    429         close(depsfile)
    430     }
    431     { roots[++nr] = $0 }
    432     END {
    433         for (i = 1; i <= nr; i++) visit(roots[i])
    434         for (i = 1; i <= oi; i++) print order[i]
    435     }
    436     function visit(pkg,    j, n, a) {
    437         if (pkg in visited) return
    438         if (pkg in stack) {
    439             printf "pkget: warning: circular dependency: %s\n", pkg > "/dev/stderr"
    440             return
    441         }
    442         if (pkg in deps) {
    443             stack[pkg] = 1
    444             n = split(deps[pkg], a, " ")
    445             for (j = 1; j <= n; j++) {
    446                 if (a[j] != "") visit(a[j])
    447             }
    448             delete stack[pkg]
    449         }
    450         visited[pkg] = 1
    451         order[++oi] = pkg
    452     }
    453     ' >> "$_raw_list"
    454 
    455     rm -f "$_deps_cache"
    456 fi
    457 
    458 _full_list=$(_mktemp)
    459 awk '!seen[$0]++' "$_raw_list" > "$_full_list"
    460 rm -f "$_raw_list"
    461 
    462 _to_install=$(_mktemp)
    463 while IFS= read -r _pkg; do
    464     [ -z "$_pkg" ] && continue
    465     if is_installed "$_pkg" && [ "$FORCE" -eq 0 ] && [ "$UPGRADE" -eq 0 ] && [ "$FULL_UPGRADE" -eq 0 ]; then
    466         log "already installed: $_pkg"
    467     elif [ "$FULL_UPGRADE" -eq 1 ] && is_installed "$_pkg"; then
    468         if grep -Fx "$_pkg" "$OUTDATED_NAMES" >/dev/null 2>&1; then
    469             printf '%s\n' "$_pkg" >> "$_to_install"
    470         else
    471             log "up to date: $_pkg"
    472         fi
    473     else
    474         printf '%s\n' "$_pkg" >> "$_to_install"
    475     fi
    476 done < "$_full_list"
    477 rm -f "$_full_list"
    478 
    479 _count=$(wc -l < "$_to_install" | tr -d ' ')
    480 if [ "$_count" -eq 0 ]; then
    481     info "nothing to do (all packages already installed)"
    482     rm -f "$_to_install"
    483     [ -n "$OUTDATED_NAMES" ] && rm -f "$OUTDATED_NAMES"
    484     exit 0
    485 fi
    486 
    487 info "packages to install ($_count):"
    488 while IFS= read -r _pkg; do
    489     _ver=$(db_get "$DBCACHE" "$_pkg" "version")
    490     _rel=$(db_get "$DBCACHE" "$_pkg" "release")
    491     printf '  %-20s %s-%s\n' "$_pkg" "$_ver" "$_rel"
    492 done < "$_to_install"
    493 
    494 [ "$DRY_RUN" -eq 1 ] && { info "dry run, not installing"; rm -f "$_to_install"; exit 0; }
    495 
    496 printf 'Proceed? [y/N] '
    497 read -r _ans
    498 case "$_ans" in
    499     [Yy]|[Yy][Ee][Ss]) ;;
    500     *) info "aborted"; rm -f "$_to_install"; exit 0 ;;
    501 esac
    502 
    503 info "fetching packages..."
    504 _fetch_map=$(_mktemp)
    505 
    506 while IFS= read -r _pkg; do
    507     [ -z "$_pkg" ] && continue
    508     _path=$(fetch_pkg "$_pkg") || {
    509         rm -f "$_to_install" "$_fetch_map"
    510         die "fetch failed, aborting"
    511     }
    512     printf '%s\t%s\n' "$_pkg" "$_path" >> "$_fetch_map"
    513 done < "$_to_install"
    514 
    515 # Install phase
    516 info "installing..."
    517 while IFS= read -r _pkg; do
    518     [ -z "$_pkg" ] && continue
    519     _path=$(awk -v p="$_pkg" -F'\t' '$1==p{print $2}' "$_fetch_map")
    520     install_pkg "$_pkg" "$_path"
    521 done < "$_to_install"
    522 
    523 rm -f "$_to_install" "$_fetch_map"
    524 [ -n "$OUTDATED_NAMES" ] && rm -f "$OUTDATED_NAMES"
    525 info "done"