#!/bin/sh # apt - native Terminal package front-end (Terminal-compatible .deb packages) # This is not Debian apt: it manages packages built for iOS/Terminal. ROOT="${TERM_ROOT:-$HOME/Documents/.terminal}" LISTS="$ROOT/var/lib/apt/lists" INDEX="$LISTS/Packages" CACHE="$ROOT/var/cache/apt/archives" SOURCES="$ROOT/etc/apt/sources.list" VISITED="" DPKG="${APPDIR:-}/bin/dpkg" [ -f "$DPKG" ] || DPKG="dpkg" usage() { printf '%s\n' "Usage: apt " printf '%s\n' "" printf '%s\n' " update Refresh package indexes" printf '%s\n' " search Search package names/descriptions" printf '%s\n' " show Show package details" printf '%s\n' " install Install a package (dependencies are resolved locally)" printf '%s\n' " remove|purge Remove a package" printf '%s\n' " list --installed List installed packages" printf '%s\n' " list List packages in the current index" printf '%s\n' " clean Clear downloaded archives" printf '%s\n' " --version Show version" printf '%s\n' "" printf '%s\n' "Default repository: file://$ROOT/repo" printf '%s\n' "Create packages with: mkdeb [arch]" } ensure() { mkdir -p "$ROOT" "$LISTS" "$CACHE" "$ROOT/var/lib/dpkg" "$ROOT/tmp" [ -f "$ROOT/var/lib/dpkg/status" ] || : > "$ROOT/var/lib/dpkg/status" } field() { key="$1"; stanza="$2" tmp="$ROOT/tmp/field.$$" printf '%s\n' "$stanza" > "$tmp" awk -v key="$key" 'index($0,key": ")==1 { sub("^" key ": ",""); print; exit }' "$tmp" rm -f "$tmp" } find_stanza() { local pkg="$1" [ -f "$INDEX" ] || return 0 awk -v pkg="$pkg" 'BEGIN{RS=""; ORS="\n\n"} $0 ~ "(^|\\n)Package: " pkg "(\\n|$)" { print; exit }' "$INDEX" } is_installed() { out="$ROOT/tmp/dpkg-status.$$" sh "$DPKG" -s "$1" > "$out" 2>/dev/null grep -q '^Status: install ok installed' "$out" result=$? rm -f "$out" return $result } decompress_index() { file="$1" out="$2" case "$file" in *.xz) xz -d -c "$file" >> "$out" 2>/dev/null || python3 -c 'import lzma,sys; sys.stdout.buffer.write(lzma.open(sys.argv[1],"rb").read())' "$file" >> "$out" 2>/dev/null ;; *.gz) gzip -dc "$file" >> "$out" 2>/dev/null || python3 -c 'import gzip,sys; sys.stdout.buffer.write(gzip.open(sys.argv[1],"rb").read())' "$file" >> "$out" 2>/dev/null ;; *) cat "$file" >> "$out" ;; esac } fetch_index() { URI="$1" TMPIDX="$ROOT/tmp/index.$$" rm -f "$TMPIDX" "$TMPIDX.xz" "$TMPIDX.gz" case "$URI" in file://*) DIR="${URI#file://}" for name in Packages Packages.xz Packages.gz; do if [ -f "$DIR/$name" ]; then decompress_index "$DIR/$name" "$INDEX" return 0 fi done ;; http://*|https://*) if curl -L --fail --silent --connect-timeout 10 --max-time 120 "$URI/Packages.xz" -o "$TMPIDX.xz" 2>/dev/null; then decompress_index "$TMPIDX.xz" "$INDEX" rm -f "$TMPIDX.xz" return 0 fi if curl -L --fail --silent --connect-timeout 10 --max-time 120 "$URI/Packages.gz" -o "$TMPIDX.gz" 2>/dev/null; then decompress_index "$TMPIDX.gz" "$INDEX" rm -f "$TMPIDX.gz" return 0 fi if curl -L --fail --silent --connect-timeout 10 --max-time 120 "$URI/Packages" -o "$TMPIDX" 2>/dev/null; then cat "$TMPIDX" >> "$INDEX" rm -f "$TMPIDX" return 0 fi ;; esac return 1 } update_lists() { ensure echo "apt: updating package lists..." : > "$INDEX" if [ ! -f "$SOURCES" ]; then mkdir -p "$(dirname "$SOURCES")" printf 'deb [trusted=yes] file://%s/repo ./\n' "$ROOT" > "$SOURCES" fi n=1 while [ $n -le 8 ]; do LINE=$(awk -v want="$n" '/^deb /{ count++; if (count == want) { print; exit } }' "$SOURCES") [ -n "$LINE" ] || break set -- $LINE if [ "${2:-}" = "[trusted=yes]" ]; then URI="${3:-}"; SUITE="${4:-./}" else URI="${2:-}"; SUITE="${3:-./}" fi if [ -n "$URI" ]; then case "$URI" in http://*|https://*) if [ "${APT_SKIP_REMOTE:-0}" = "1" ]; then echo "Ign:$n $URI (remote skipped)" n=$((n + 1)) continue fi ;; esac if fetch_index "$URI"; then echo "Get:$n $URI $SUITE" else echo "Err:$n $URI (no index / unreachable)" >&2 fi fi n=$((n + 1)) done echo "Reading package lists... Done" } search_packages() { text="${1:-}" ensure [ -f "$INDEX" ] || { echo "E: Run 'apt update' first" >&2; return 1; } awk -v pat="$text" 'BEGIN{RS="";FS="\n"} { if (pat == "" || tolower($0) ~ tolower(pat)) { p=""; v=""; d=""; for (i=1;i<=NF;i++) { if ($i ~ /^Package: /) p=substr($i,10); if ($i ~ /^Version: /) v=substr($i,10); if ($i ~ /^Description: /) d=substr($i,14); } if (p != "") printf "%-28s %-16s %s\n", p, v, d; } }' "$INDEX" } show_package() { pkg="${1:-}" ensure stanza=$(find_stanza "$pkg") if [ -z "$stanza" ]; then echo "E: Unable to locate package $pkg" >&2 return 1 fi printf '%s\n' "$stanza" } install_one() { local pkg="$1" local stanza version depends dep filename local_archive base oldifs [ -n "$pkg" ] || return 0 case " $VISITED " in *" $pkg "*) return 0 ;; esac VISITED="$VISITED $pkg" if is_installed "$pkg"; then echo "$pkg is already the newest version." return 0 fi stanza=$(find_stanza "$pkg") if [ -z "$stanza" ]; then for localdeb in "$ROOT/repo/pool/${pkg}"_*.deb; do if [ -f "$localdeb" ]; then echo "Installing $pkg from local pool..." sh "$DPKG" -i "$localdeb" || return 1 return 0 fi done echo "E: Unable to locate package $pkg" >&2 return 1 fi version=$(field Version "$stanza") depends=$(field Depends "$stanza") if [ -n "$depends" ]; then oldifs="$IFS" IFS=',' for dep in $depends; do IFS="$oldifs" dep=${dep%%|*} dep=${dep%%(*} dep=$(echo "$dep" | tr -d ' ') dep=${dep%:any} [ -n "$dep" ] || continue if ! is_installed "$dep"; then if [ -n "$(find_stanza "$dep")" ]; then echo "Installing dependency: $dep" install_one "$dep" || return 1 else echo "W: dependency '$dep' is not in the index" >&2 fi fi IFS=',' done IFS="$oldifs" fi filename=$(field Filename "$stanza") if [ -z "$filename" ]; then echo "E: package $pkg has no Filename field" >&2 return 1 fi filename="${filename#./}" local_archive="$CACHE/${pkg}_${version}.deb" if [ -f "$ROOT/repo/$filename" ]; then cp "$ROOT/repo/$filename" "$local_archive" || return 1 else fetched=0 BASES=$(awk '/^deb /{for(i=1;i<=NF;i++) if($i ~ /^https?:/) print $i}' "$SOURCES") for base in $BASES; do if curl -L --fail --silent --connect-timeout 10 --max-time 180 "$base/$filename" -o "$local_archive" 2>/dev/null; then fetched=1 break fi done if [ $fetched -ne 1 ]; then echo "E: cannot fetch $filename" >&2 return 1 fi fi echo "Installing $pkg ($version)..." echo "apt: invoking dpkg..." sh "$DPKG" -i "$local_archive" || return 1 return 0 } install_packages() { ensure echo "apt: resolving packages..." [ -f "$INDEX" ] || { echo "E: Run 'apt update' first" >&2; return 1; } status=0 for arg in "$@"; do case "$arg" in *.deb) sh "$DPKG" -i "$arg" || status=1 ;; *) install_one "$arg" || status=1 ;; esac done echo "Done." return $status } remove_packages() { status=0 for pkg in "$@"; do sh "$DPKG" -r "$pkg" || status=1 done return $status } list_installed() { sh "$DPKG" -l } list_all() { ensure [ -f "$INDEX" ] || { echo "E: Run 'apt update' first" >&2; return 1; } awk 'BEGIN{RS="";FS="\n"} { p="";v="";a=""; for(i=1;i<=NF;i++){ if($i~/^Package: /)p=substr($i,10); if($i~/^Version: /)v=substr($i,10); if($i~/^Architecture: /)a=substr($i,14); } if(p!="") printf "%-28s %-16s %s\n", p, v, a; }' "$INDEX" } case "${1:-}" in update) update_lists ;; search) shift; search_packages "${1:-}" ;; show) shift; show_package "${1:-}" ;; install) shift; install_packages "$@" ;; remove|uninstall) shift; remove_packages "$@" ;; purge) shift; remove_packages "$@" ;; list) shift if [ "${1:-}" = "--installed" ]; then list_installed; else list_all; fi ;; clean) rm -f "$CACHE"/*.deb echo "Cleaned package archives" ;; --version|-v) echo "apt 1.0 (Terminal native package front-end)" ;; ""|-h|--help|help) usage ;; *) usage; exit 2 ;; esac