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"