Files

317 lines
9.7 KiB
Bash
Executable File

#!/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 <command>"
printf '%s\n' ""
printf '%s\n' " update Refresh package indexes"
printf '%s\n' " search <text> Search package names/descriptions"
printf '%s\n' " show <package> Show package details"
printf '%s\n' " install <package|deb> Install a package (dependencies are resolved locally)"
printf '%s\n' " remove|purge <package> 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 <name> <version> <payload-dir> [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