forked from admin/terminal_worse
Terminal 1.0: a-Shell 2.3.2 full + worse_query + native plugin environment
This commit is contained in:
commit
598ef7b1bf
43 files changed
+5118
No files matched your search
@@ -0,0 +1,316 @@
|
||||
#!/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
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
# apt-get compatibility wrapper for the native Terminal package manager.
|
||||
sh "$APPDIR/bin/apt" "$@"
|
||||
exit $?
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal ar archive packer/extractor for .deb files on a-Shell.
|
||||
|
||||
Usage:
|
||||
ardeb pack <out.ar> <file> [file ...]
|
||||
ardeb extract <in.ar> <dest-dir>
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def pack(out_path, files):
|
||||
with open(out_path, "wb") as out:
|
||||
out.write(b"!<arch>\n")
|
||||
for path in files:
|
||||
data = open(path, "rb").read()
|
||||
name = os.path.basename(path)
|
||||
if len(name) > 15:
|
||||
raise SystemExit("ardeb: member name too long: " + name)
|
||||
header = (
|
||||
name.ljust(16)
|
||||
+ "0".ljust(12)
|
||||
+ "0".ljust(6)
|
||||
+ "0".ljust(6)
|
||||
+ "100644".ljust(8)
|
||||
+ str(len(data)).ljust(10)
|
||||
+ "`\n"
|
||||
).encode("ascii")
|
||||
out.write(header)
|
||||
out.write(data)
|
||||
if len(data) & 1:
|
||||
out.write(b"\n")
|
||||
|
||||
|
||||
def extract(archive, dest):
|
||||
data = open(archive, "rb").read()
|
||||
if not data.startswith(b"!<arch>\n"):
|
||||
raise SystemExit("ardeb: not an ar archive")
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
pos = 8
|
||||
while pos + 60 <= len(data):
|
||||
header = data[pos:pos + 60]
|
||||
pos += 60
|
||||
name = header[:16].decode("ascii", "ignore").strip().rstrip("/")
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
size = int(header[48:58].decode("ascii", "ignore").strip() or "0")
|
||||
except ValueError:
|
||||
size = 0
|
||||
body = data[pos:pos + size]
|
||||
pos += size + (size & 1)
|
||||
if name in (".", ".."):
|
||||
continue
|
||||
open(os.path.join(dest, name), "wb").write(body)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4:
|
||||
raise SystemExit(__doc__)
|
||||
if sys.argv[1] == "pack":
|
||||
pack(sys.argv[2], sys.argv[3:])
|
||||
elif sys.argv[1] == "extract":
|
||||
extract(sys.argv[2], sys.argv[3])
|
||||
else:
|
||||
raise SystemExit(__doc__)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
# bash-compat - a-Shell POSIX sh compatibility launcher.
|
||||
#
|
||||
# GNU bash is not bundled with Terminal. This launcher provides the common
|
||||
# `bash script` / `bash -c 'command'` interface on top of a-Shell's sh so
|
||||
# simple plugin scripts work. If a real bash is installed at
|
||||
# $PREFIX/bin/bash, the prefix PATH takes precedence over this launcher.
|
||||
|
||||
if [ "${1:-}" = "--version" ] || [ "${1:-}" = "-version" ]; then
|
||||
echo "bash-compat 1.0 (a-Shell POSIX sh; GNU bash is not bundled)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "-c" ]; then
|
||||
shift
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "bash-compat: -c requires a command" >&2
|
||||
exit 2
|
||||
fi
|
||||
command="$1"
|
||||
shift
|
||||
sh -c "$command" "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ $# -gt 0 ] && [ -f "$1" ]; then
|
||||
sh "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "bash-compat: a-Shell POSIX sh. GNU bash-specific syntax is not supported." >&2
|
||||
sh -s
|
||||
exit $?
|
||||
fi
|
||||
|
||||
sh "$@"
|
||||
exit $?
|
||||
@@ -0,0 +1,391 @@
|
||||
#!/bin/sh
|
||||
# dpkg - native Terminal package manager (Debian .deb format subset)
|
||||
#
|
||||
# Packages must be built for the native Terminal/iOS environment (scripts or
|
||||
# iOS/Darwin arm64 binaries). Debian Linux arm64/amd64 packages are rejected
|
||||
# by default because their ELF binaries cannot run on iOS without an emulator.
|
||||
|
||||
ROOT="${TERM_ROOT:-$HOME/Documents/.terminal}"
|
||||
STATUS="$ROOT/var/lib/dpkg/status"
|
||||
INFO="$ROOT/var/lib/dpkg/info"
|
||||
FORCE_ARCH=0
|
||||
|
||||
usage() {
|
||||
printf '%s\n' "Usage: dpkg <action>"
|
||||
printf '%s\n' ""
|
||||
printf '%s\n' " -i, --install <file.deb> Install a Terminal-compatible .deb"
|
||||
printf '%s\n' " -r, --remove <package> Remove a package"
|
||||
printf '%s\n' " -P, --purge <package> Remove a package (same as -r here)"
|
||||
printf '%s\n' " -l, --list List installed packages"
|
||||
printf '%s\n' " -s, --status <package> Show package status"
|
||||
printf '%s\n' " -L, --listfiles <package> List files installed by a package"
|
||||
printf '%s\n' " -x, --extract <deb> <dir> Extract a .deb data archive"
|
||||
printf '%s\n' " --contents <deb> List the contents of a .deb"
|
||||
printf '%s\n' " --print-architecture Print the native architecture"
|
||||
printf '%s\n' " --force-architecture Allow non-native architecture packages"
|
||||
printf '%s\n' " --version Show version"
|
||||
}
|
||||
ensure() {
|
||||
mkdir -p "$ROOT" "$INFO" "$ROOT/tmp" "$ROOT/var/lib/dpkg"
|
||||
[ -f "$STATUS" ] || : > "$STATUS"
|
||||
}
|
||||
|
||||
field() {
|
||||
key="$1"; file="$2"
|
||||
awk -v key="$key" 'index($0,key": ")==1 { sub("^" key ": ",""); print; exit }' "$file"
|
||||
}
|
||||
|
||||
stanza_field() {
|
||||
key="$1"; stanza="$2"
|
||||
printf '%s\n' "$stanza" | awk -v key="$key" 'index($0,key": ")==1 { sub("^" key ": ",""); print; exit }'
|
||||
}
|
||||
|
||||
stanza_for() {
|
||||
pkg="$1"
|
||||
awk -v pkg="$pkg" 'BEGIN{RS=""; ORS="\n\n"} $0 ~ "(^|\\n)Package: " pkg "(\\n|$)" { print; exit }' "$STATUS"
|
||||
}
|
||||
|
||||
remove_status() {
|
||||
pkg="$1"
|
||||
TMP="$ROOT/tmp/status.$$"
|
||||
awk -v pkg="$pkg" 'BEGIN{RS=""; ORS="\n\n"} $0 !~ "(^|\\n)Package: " pkg "(\\n|$)" { print }' "$STATUS" > "$TMP"
|
||||
mv "$TMP" "$STATUS"
|
||||
}
|
||||
|
||||
append_status() {
|
||||
{
|
||||
echo "Package: $PKG"
|
||||
echo "Status: install ok installed"
|
||||
echo "Priority: ${PRIORITY:-optional}"
|
||||
echo "Section: ${SECTION:-plugins}"
|
||||
echo "Installed-Size: ${INSTALLED_SIZE:-0}"
|
||||
echo "Maintainer: ${MAINTAINER:-unknown}"
|
||||
echo "Architecture: ${ARCH:-iphoneos-arm64}"
|
||||
echo "Version: $VERSION"
|
||||
echo "Description: $DESCRIPTION"
|
||||
echo ""
|
||||
} >> "$STATUS"
|
||||
}
|
||||
|
||||
ar_extract() {
|
||||
archive="$1"
|
||||
dest="$2"
|
||||
tar -xf "$archive" -C "$dest" >/dev/null 2>&1
|
||||
if [ ! -f "$dest/debian-binary" ]; then
|
||||
if python3 -c 'pass' >/dev/null 2>&1 && [ -f "$APPDIR/bin/ardeb" ]; then
|
||||
python3 "$APPDIR/bin/ardeb" extract "$archive" "$dest"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
install_deb() {
|
||||
DEB="$1"
|
||||
if [ ! -f "$DEB" ]; then
|
||||
echo "dpkg: cannot access archive '$DEB': No such file" >&2
|
||||
return 1
|
||||
fi
|
||||
if python3 -c 'pass' >/dev/null 2>&1 && [ -f "$APPDIR/bin/pkgtool" ]; then
|
||||
python3 "$APPDIR/bin/pkgtool" install "$DEB"
|
||||
return $?
|
||||
fi
|
||||
ensure
|
||||
echo "dpkg: unpacking $DEB"
|
||||
TMP="$ROOT/tmp/dpkg.$$"
|
||||
rm -rf "$TMP"
|
||||
mkdir -p "$TMP"
|
||||
cp "$DEB" "$TMP/pkg.deb" || { rm -rf "$TMP"; return 1; }
|
||||
ar_extract "$TMP/pkg.deb" "$TMP"
|
||||
echo "dpkg: ar extracted"
|
||||
if [ ! -f "$TMP/debian-binary" ]; then
|
||||
echo "dpkg: '$DEB' is not a Debian-format archive" >&2
|
||||
rm -rf "$TMP"
|
||||
return 1
|
||||
fi
|
||||
|
||||
CTRL_ARCHIVE=""
|
||||
for f in "$TMP"/control.tar.*; do [ -f "$f" ] && { CTRL_ARCHIVE="$f"; break; }; done
|
||||
DATA_ARCHIVE=""
|
||||
for f in "$TMP"/data.tar.*; do [ -f "$f" ] && { DATA_ARCHIVE="$f"; break; }; done
|
||||
if [ -z "$CTRL_ARCHIVE" ] || [ -z "$DATA_ARCHIVE" ]; then
|
||||
echo "dpkg: missing control or data archive" >&2
|
||||
rm -rf "$TMP"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$TMP/control"
|
||||
case "$CTRL_ARCHIVE" in
|
||||
*.gz) tar -xzf "$CTRL_ARCHIVE" -C "$TMP/control" 2>/dev/null ;;
|
||||
*.xz) tar -xJf "$CTRL_ARCHIVE" -C "$TMP/control" 2>/dev/null ;;
|
||||
*) tar -xf "$CTRL_ARCHIVE" -C "$TMP/control" 2>/dev/null ;;
|
||||
esac
|
||||
CONTROL=""
|
||||
if [ -f "$TMP/control/control" ]; then
|
||||
CONTROL="$TMP/control/control"
|
||||
elif [ -f "$TMP/control/DEBIAN/control" ]; then
|
||||
CONTROL="$TMP/control/DEBIAN/control"
|
||||
fi
|
||||
if [ -z "$CONTROL" ]; then
|
||||
echo "dpkg: no control file in package" >&2
|
||||
rm -rf "$TMP"
|
||||
return 1
|
||||
fi
|
||||
|
||||
PKG=$(field Package "$CONTROL")
|
||||
VERSION=$(field Version "$CONTROL")
|
||||
ARCH=$(field Architecture "$CONTROL")
|
||||
PRIORITY=$(field Priority "$CONTROL")
|
||||
SECTION=$(field Section "$CONTROL")
|
||||
MAINTAINER=$(field Maintainer "$CONTROL")
|
||||
DESCRIPTION=$(field Description "$CONTROL")
|
||||
[ -n "$PKG" ] && [ -n "$VERSION" ] || {
|
||||
echo "dpkg: invalid control file (Package/Version missing)" >&2
|
||||
rm -rf "$TMP"
|
||||
return 1
|
||||
}
|
||||
case "$PKG" in
|
||||
*[!A-Za-z0-9+.-]*|"") echo "dpkg: invalid package name: $PKG" >&2; rm -rf "$TMP"; return 1 ;;
|
||||
esac
|
||||
|
||||
case "$ARCH" in
|
||||
""|all|iphoneos-arm64|darwin-arm64|a-shell) ;;
|
||||
*)
|
||||
if [ "$FORCE_ARCH" != "1" ]; then
|
||||
echo "dpkg: architecture '$ARCH' is not native to iOS/Terminal." >&2
|
||||
echo " Debian Linux binaries cannot run without an emulator." >&2
|
||||
echo " Use --force-architecture only for script-only packages." >&2
|
||||
rm -rf "$TMP"
|
||||
return 1
|
||||
fi
|
||||
echo "dpkg: warning: forcing architecture '$ARCH'" >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Selecting previously unselected package $PKG."
|
||||
echo "Unpacking $PKG ($VERSION) ..."
|
||||
|
||||
if [ -f "$TMP/control/preinst" ]; then
|
||||
TERM_ROOT="$ROOT" PREFIX="$ROOT" sh "$TMP/control/preinst" install || {
|
||||
echo "dpkg: preinst script failed" >&2
|
||||
rm -rf "$TMP"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
echo "dpkg: extracting data archive..."
|
||||
case "$DATA_ARCHIVE" in
|
||||
*.gz) tar -tzf "$DATA_ARCHIVE" 2>/dev/null > "$TMP/filelist" ;;
|
||||
*.xz) tar -tJf "$DATA_ARCHIVE" 2>/dev/null > "$TMP/filelist" ;;
|
||||
*) tar -tf "$DATA_ARCHIVE" 2>/dev/null > "$TMP/filelist" ;;
|
||||
esac
|
||||
awk '{ sub(/^\.\//,""); if ($0 != "") print }' "$TMP/filelist" > "$INFO/$PKG.list"
|
||||
DATA_OK=0
|
||||
case "$DATA_ARCHIVE" in
|
||||
*.gz) tar -xzf "$DATA_ARCHIVE" -C "$ROOT" --no-same-owner 2>/dev/null && DATA_OK=1 ;;
|
||||
*.xz) tar -xJf "$DATA_ARCHIVE" -C "$ROOT" --no-same-owner 2>/dev/null && DATA_OK=1 ;;
|
||||
*) tar -xf "$DATA_ARCHIVE" -C "$ROOT" --no-same-owner 2>/dev/null && DATA_OK=1 ;;
|
||||
esac
|
||||
if [ $DATA_OK -ne 1 ]; then
|
||||
case "$DATA_ARCHIVE" in
|
||||
*.gz) tar -xzf "$DATA_ARCHIVE" -C "$ROOT" 2>/dev/null && DATA_OK=1 ;;
|
||||
*.xz) tar -xJf "$DATA_ARCHIVE" -C "$ROOT" 2>/dev/null && DATA_OK=1 ;;
|
||||
*) tar -xf "$DATA_ARCHIVE" -C "$ROOT" 2>/dev/null && DATA_OK=1 ;;
|
||||
esac
|
||||
fi
|
||||
if [ $DATA_OK -ne 1 ]; then
|
||||
echo "dpkg: failed to extract data archive" >&2
|
||||
rm -rf "$TMP"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Warn about native Mach-O payloads: a-Shell cannot execute them.
|
||||
if [ -f "$INFO/$PKG.list" ]; then
|
||||
for f in $(cat "$INFO/$PKG.list"); do
|
||||
case "$f" in
|
||||
bin/*|sbin/*|usr/bin/*|usr/sbin/*|usr/libexec/*)
|
||||
if file "$ROOT/$f" 2>/dev/null | grep -q 'Mach-O'; then
|
||||
echo "dpkg: note: $f is a native Mach-O executable; a-Shell cannot run it." >&2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
remove_status "$PKG"
|
||||
INSTALLED_SIZE=$(du -sk "$ROOT" 2>/dev/null)
|
||||
INSTALLED_SIZE=${INSTALLED_SIZE%%[!0-9]*}
|
||||
append_status
|
||||
|
||||
for script in preinst postinst prerm postrm; do
|
||||
if [ -f "$TMP/control/$script" ]; then
|
||||
cp "$TMP/control/$script" "$INFO/$PKG.$script"
|
||||
chmod +x "$INFO/$PKG.$script"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -f "$TMP/control/postinst" ]; then
|
||||
TERM_ROOT="$ROOT" PREFIX="$ROOT" sh "$TMP/control/postinst" configure || {
|
||||
echo "dpkg: postinst script failed" >&2
|
||||
rm -rf "$TMP"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
echo "Setting up $PKG ($VERSION) ..."
|
||||
rm -rf "$TMP"
|
||||
return 0
|
||||
}
|
||||
|
||||
remove_pkg() {
|
||||
PKG="$1"
|
||||
[ -n "$PKG" ] || { usage; return 2; }
|
||||
ensure
|
||||
STANZA=$(stanza_for "$PKG")
|
||||
if [ -z "$STANZA" ]; then
|
||||
echo "dpkg: package '$PKG' is not installed" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ -f "$INFO/$PKG.prerm" ]; then
|
||||
( TERM_ROOT="$ROOT" PREFIX="$ROOT" sh "$INFO/$PKG.prerm" remove ) || true
|
||||
fi
|
||||
if [ -f "$INFO/$PKG.list" ]; then
|
||||
for f in $(cat "$INFO/$PKG.list"); do
|
||||
case "$f" in
|
||||
""|/*|../*|*/../*) continue ;;
|
||||
esac
|
||||
[ -d "$ROOT/$f" ] && continue
|
||||
rm -f "$ROOT/$f"
|
||||
done
|
||||
fi
|
||||
if [ -f "$INFO/$PKG.postrm" ]; then
|
||||
( TERM_ROOT="$ROOT" PREFIX="$ROOT" sh "$INFO/$PKG.postrm" remove ) || true
|
||||
fi
|
||||
remove_status "$PKG"
|
||||
rm -f "$INFO/$PKG.list" "$INFO/$PKG.preinst" "$INFO/$PKG.postinst" \
|
||||
"$INFO/$PKG.prerm" "$INFO/$PKG.postrm"
|
||||
echo "Removing $PKG ..."
|
||||
echo "Purging configuration files for $PKG ..."
|
||||
return 0
|
||||
}
|
||||
|
||||
list_packages() {
|
||||
ensure
|
||||
echo "Desired=Unknown/Install/Remove/Purge/Hold"
|
||||
echo "| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend"
|
||||
echo "|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)"
|
||||
echo "||/ Name Version Architecture Description"
|
||||
echo "+++-====================-================-================-================================="
|
||||
awk 'BEGIN{RS="";FS="\n"} {
|
||||
p="";v="";a="";d="";s="";
|
||||
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 ($i ~ /^Description: /) d=substr($i,14);
|
||||
if ($i ~ /^Status: /) s=substr($i,9);
|
||||
}
|
||||
if (s ~ /installed/) printf "ii %-20s %-16s %-16s %s\n", p, v, a, d;
|
||||
}' "$STATUS"
|
||||
}
|
||||
|
||||
status_pkg() {
|
||||
PKG="$1"
|
||||
STANZA=$(stanza_for "$PKG")
|
||||
if [ -z "$STANZA" ]; then
|
||||
echo "dpkg-query: package '$PKG' is not installed" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$STANZA"
|
||||
}
|
||||
|
||||
list_files() {
|
||||
PKG="$1"
|
||||
if [ ! -f "$INFO/$PKG.list" ]; then
|
||||
echo "dpkg-query: package '$PKG' is not installed" >&2
|
||||
return 1
|
||||
fi
|
||||
sed "s#^#/#" "$INFO/$PKG.list"
|
||||
}
|
||||
|
||||
extract_deb() {
|
||||
DEB="$1"; DEST="$2"
|
||||
[ -f "$DEB" ] || { echo "dpkg: cannot access '$DEB'" >&2; return 1; }
|
||||
[ -n "$DEST" ] || { usage; return 2; }
|
||||
TMP="$ROOT/tmp/dpkg-x.$$"
|
||||
rm -rf "$TMP"; mkdir -p "$TMP" "$DEST"
|
||||
cp "$DEB" "$TMP/pkg.deb"
|
||||
ar_extract "$TMP/pkg.deb" "$TMP"
|
||||
DATA=""
|
||||
for f in "$TMP"/data.tar.*; do [ -f "$f" ] && { DATA="$f"; break; }; done
|
||||
[ -n "$DATA" ] || { echo "dpkg: no data archive" >&2; rm -rf "$TMP"; return 1; }
|
||||
tar -xf "$DATA" -C "$DEST" --no-same-owner 2>/dev/null || tar -xf "$DATA" -C "$DEST" 2>/dev/null
|
||||
rm -rf "$TMP"
|
||||
}
|
||||
|
||||
contents_deb() {
|
||||
DEB="$1"
|
||||
[ -f "$DEB" ] || { echo "dpkg: cannot access '$DEB'" >&2; return 1; }
|
||||
TMP="$ROOT/tmp/dpkg-c.$$"
|
||||
rm -rf "$TMP"; mkdir -p "$TMP"
|
||||
cp "$DEB" "$TMP/pkg.deb"
|
||||
ar_extract "$TMP/pkg.deb" "$TMP"
|
||||
DATA=""
|
||||
for f in "$TMP"/data.tar.*; do [ -f "$f" ] && { DATA="$f"; break; }; done
|
||||
[ -n "$DATA" ] || { echo "dpkg: no data archive" >&2; rm -rf "$TMP"; return 1; }
|
||||
case "$DATA" in
|
||||
*.gz) tar -tzf "$DATA" 2>/dev/null > "$TMP/contents" ;;
|
||||
*.xz) tar -tJf "$DATA" 2>/dev/null > "$TMP/contents" ;;
|
||||
*) tar -tf "$DATA" 2>/dev/null > "$TMP/contents" ;;
|
||||
esac
|
||||
awk '{ sub(/^\.\//,""); if ($0 != "") print }' "$TMP/contents"
|
||||
rm -rf "$TMP"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-i|--install)
|
||||
shift
|
||||
if [ "${1:-}" = "--force-architecture" ]; then FORCE_ARCH=1; shift; fi
|
||||
[ -n "${1:-}" ] || { usage; exit 2; }
|
||||
install_deb "$1"
|
||||
;;
|
||||
-r|--remove|-P|--purge)
|
||||
shift
|
||||
remove_pkg "${1:-}"
|
||||
;;
|
||||
-l|--list)
|
||||
list_packages
|
||||
;;
|
||||
-s|--status)
|
||||
shift
|
||||
status_pkg "${1:-}"
|
||||
;;
|
||||
-L|--listfiles)
|
||||
shift
|
||||
list_files "${1:-}"
|
||||
;;
|
||||
-x|--extract)
|
||||
shift
|
||||
extract_deb "${1:-}" "${2:-}"
|
||||
;;
|
||||
--contents)
|
||||
shift
|
||||
contents_deb "${1:-}"
|
||||
;;
|
||||
--print-architecture)
|
||||
echo "iphoneos-arm64"
|
||||
;;
|
||||
--force-architecture)
|
||||
FORCE_ARCH=1
|
||||
shift
|
||||
[ -n "${1:-}" ] && install_deb "$1"
|
||||
;;
|
||||
--version|-V)
|
||||
echo "dpkg 1.0 (Terminal native package manager)"
|
||||
;;
|
||||
""|-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# dpkg-deb compatibility wrapper (subset: -x, -c, --contents)
|
||||
case "${1:-}" in
|
||||
-x|--extract)
|
||||
shift
|
||||
sh "$APPDIR/bin/dpkg" -x "$1" "$2"
|
||||
exit $?
|
||||
;;
|
||||
-c|--contents)
|
||||
shift
|
||||
sh "$APPDIR/bin/dpkg" --contents "$1"
|
||||
exit $?
|
||||
;;
|
||||
-b|--build)
|
||||
echo "dpkg-deb: use mkdeb <name> <version> <payload-dir> [arch]" >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
echo "usage: dpkg-deb -x <deb> <dir> | -c <deb> | (use mkdeb to build)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/bin/sh
|
||||
# mkdeb - build a Terminal-compatible .deb package and add it to the local repo
|
||||
# Usage: mkdeb <name> <version> <payload-dir> [architecture]
|
||||
# The payload directory is copied to the prefix root; put files under
|
||||
# bin/, usr/bin/, lib/, share/, etc. Maintainer scripts may be placed in
|
||||
# <payload-dir>/DEBIAN/{preinst,postinst,prerm,postrm}.
|
||||
|
||||
ROOT="${TERM_ROOT:-$HOME/Documents/.terminal}"
|
||||
NAME="${1:-}"
|
||||
VERSION="${2:-}"
|
||||
PAYLOAD="${3:-}"
|
||||
ARCH="${4:-iphoneos-arm64}"
|
||||
MAINTAINER="${MKDEB_MAINTAINER:-Terminal User <mobile@localhost>}"
|
||||
DESCRIPTION="${MKDEB_DESCRIPTION:-$NAME plugin for Terminal}"
|
||||
|
||||
usage() {
|
||||
echo "usage: mkdeb <name> <version> <payload-dir> [architecture]" >&2
|
||||
echo " architecture: iphoneos-arm64 (default), all, darwin-arm64" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ -n "$NAME" ] && [ -n "$VERSION" ] && [ -n "$PAYLOAD" ] || usage
|
||||
[ -d "$PAYLOAD" ] || { echo "mkdeb: payload directory not found: $PAYLOAD" >&2; exit 1; }
|
||||
case "$NAME" in
|
||||
*[!A-Za-z0-9+.-]*|"") echo "mkdeb: invalid package name: $NAME" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "mkdeb: start $NAME $VERSION"
|
||||
mkdir -p "$ROOT/repo/pool" "$ROOT/tmp" "$ROOT/var/lib/dpkg/info"
|
||||
DEB="$ROOT/repo/pool/${NAME}_${VERSION}_${ARCH}.deb"
|
||||
if python3 -c 'pass' >/dev/null 2>&1 && [ -f "$APPDIR/bin/pkgtool" ]; then
|
||||
MKDEB_MAINTAINER="$MAINTAINER" MKDEB_DESCRIPTION="$DESCRIPTION" \
|
||||
python3 "$APPDIR/bin/pkgtool" build "$DEB" "$NAME" "$VERSION" "$ARCH" "$PAYLOAD" "${MKDEB_DEPENDS:-}"
|
||||
exit $?
|
||||
fi
|
||||
TMP="$ROOT/tmp/mkdeb.$$"
|
||||
rm -rf "$TMP"
|
||||
mkdir -p "$TMP/DEBIAN"
|
||||
|
||||
# Control file.
|
||||
{
|
||||
echo "Package: $NAME"
|
||||
echo "Version: $VERSION"
|
||||
echo "Architecture: $ARCH"
|
||||
echo "Maintainer: $MAINTAINER"
|
||||
echo "Section: plugins"
|
||||
echo "Priority: optional"
|
||||
echo "Description: $DESCRIPTION"
|
||||
[ -n "${MKDEB_DEPENDS:-}" ] && echo "Depends: $MKDEB_DEPENDS"
|
||||
} > "$TMP/DEBIAN/control"
|
||||
|
||||
# Optional maintainer scripts from the payload's DEBIAN directory.
|
||||
for script in preinst postinst prerm postrm; do
|
||||
if [ -f "$PAYLOAD/DEBIAN/$script" ]; then
|
||||
cp "$PAYLOAD/DEBIAN/$script" "$TMP/DEBIAN/$script"
|
||||
chmod +x "$TMP/DEBIAN/$script"
|
||||
fi
|
||||
done
|
||||
|
||||
# Build the two tar archives. Copy the payload first (no shell pipeline;
|
||||
# nested tar pipelines can stall inside a-Shell's in-process command model).
|
||||
DATA_TMP="$TMP/data"
|
||||
mkdir -p "$DATA_TMP"
|
||||
echo "mkdeb: copying payload..."
|
||||
cp -R "$PAYLOAD"/. "$DATA_TMP"/ 2>/dev/null
|
||||
rm -rf "$DATA_TMP/DEBIAN"
|
||||
echo "mkdeb: writing data archive..."
|
||||
tar -czf "$TMP/data.tar.gz" -C "$DATA_TMP" .
|
||||
echo "mkdeb: writing control archive..."
|
||||
tar -czf "$TMP/control.tar.gz" -C "$TMP" DEBIAN
|
||||
printf '2.0\n' > "$TMP/debian-binary"
|
||||
|
||||
DEB="$ROOT/repo/pool/${NAME}_${VERSION}_${ARCH}.deb"
|
||||
rm -f "$DEB"
|
||||
echo "mkdeb: writing ar archive..."
|
||||
rm -f "$DEB"
|
||||
tar --format=ar -cf "$DEB" -C "$TMP" debian-binary control.tar.gz data.tar.gz >/dev/null 2>&1
|
||||
if [ ! -s "$DEB" ]; then
|
||||
echo "mkdeb: tar ar writer unavailable; trying Python fallback..." >&2
|
||||
if python3 -c 'pass' >/dev/null 2>&1 && [ -f "$APPDIR/bin/ardeb" ]; then
|
||||
python3 "$APPDIR/bin/ardeb" pack "$DEB" "$TMP/debian-binary" "$TMP/control.tar.gz" "$TMP/data.tar.gz"
|
||||
fi
|
||||
fi
|
||||
if [ ! -s "$DEB" ]; then
|
||||
echo "mkdeb: failed to create $DEB" >&2
|
||||
rm -rf "$TMP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update Packages: append the new package stanza.
|
||||
SIZE=$(wc -c < "$DEB" | tr -d ' ')
|
||||
PACKAGES="$ROOT/repo/Packages"
|
||||
touch "$PACKAGES"
|
||||
{
|
||||
echo "Package: $NAME"
|
||||
echo "Version: $VERSION"
|
||||
echo "Architecture: $ARCH"
|
||||
echo "Maintainer: $MAINTAINER"
|
||||
echo "Section: plugins"
|
||||
echo "Priority: optional"
|
||||
echo "Filename: pool/$(basename "$DEB")"
|
||||
echo "Size: $SIZE"
|
||||
echo "Description: $DESCRIPTION"
|
||||
[ -n "${MKDEB_DEPENDS:-}" ] && echo "Depends: $MKDEB_DEPENDS"
|
||||
echo ""
|
||||
} >> "$PACKAGES"
|
||||
|
||||
rm -rf "$TMP"
|
||||
echo "$DEB"
|
||||
Vendored
+286
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pkgtool - Python package backend for Terminal's native apt/dpkg.
|
||||
|
||||
This avoids a-Shell's shell/tar/ar quirks for package construction and
|
||||
extraction. It is used when Python 3 is available (the full a-Shell app has
|
||||
it). Native Mach-O payloads are installed but flagged as non-runnable.
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
ROOT = os.environ.get("TERM_ROOT") or os.path.join(os.path.expanduser("~"), "Documents", ".terminal")
|
||||
INFO = os.path.join(ROOT, "var", "lib", "dpkg", "info")
|
||||
STATUS = os.path.join(ROOT, "var", "lib", "dpkg", "status")
|
||||
PACKAGES = os.path.join(ROOT, "repo", "Packages")
|
||||
|
||||
|
||||
def ar_pack(out_path, members):
|
||||
with open(out_path, "wb") as out:
|
||||
out.write(b"!<arch>\n")
|
||||
for path in members:
|
||||
data = open(path, "rb").read()
|
||||
name = os.path.basename(path)
|
||||
if len(name) > 15:
|
||||
raise SystemExit("pkgtool: ar member name too long: " + name)
|
||||
header = (
|
||||
name.ljust(16) + "0".ljust(12) + "0".ljust(6) + "0".ljust(6)
|
||||
+ "100644".ljust(8) + str(len(data)).ljust(10) + "`\n"
|
||||
).encode("ascii")
|
||||
out.write(header)
|
||||
out.write(data)
|
||||
if len(data) & 1:
|
||||
out.write(b"\n")
|
||||
|
||||
|
||||
def ar_unpack(archive, dest):
|
||||
data = open(archive, "rb").read()
|
||||
if not data.startswith(b"!<arch>\n"):
|
||||
raise SystemExit("pkgtool: not a Debian ar archive")
|
||||
pos = 8
|
||||
while pos + 60 <= len(data):
|
||||
header = data[pos:pos + 60]
|
||||
pos += 60
|
||||
name = header[:16].decode("ascii", "ignore").strip().rstrip("/")
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
size = int(header[48:58].decode("ascii", "ignore").strip() or "0")
|
||||
except ValueError:
|
||||
size = 0
|
||||
body = data[pos:pos + size]
|
||||
pos += size + (size & 1)
|
||||
if name in (".", ".."):
|
||||
continue
|
||||
open(os.path.join(dest, name), "wb").write(body)
|
||||
|
||||
|
||||
def safe_extract(tar_path, dest):
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
with tarfile.open(tar_path, "r:*") as tar:
|
||||
for member in tar.getmembers():
|
||||
name = member.name.lstrip("./")
|
||||
if not name or name.startswith("/") or ".." in name.split("/"):
|
||||
continue
|
||||
target = os.path.join(dest, name)
|
||||
if member.isdir():
|
||||
os.makedirs(target, exist_ok=True)
|
||||
elif member.isfile():
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
src = tar.extractfile(member)
|
||||
if src is None:
|
||||
continue
|
||||
with open(target, "wb") as out:
|
||||
shutil.copyfileobj(src, out)
|
||||
os.chmod(target, member.mode & 0o777)
|
||||
elif member.issym():
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
# Symlinks are installed relative to the package root.
|
||||
try:
|
||||
os.symlink(member.linkname, target)
|
||||
except FileExistsError:
|
||||
os.unlink(target)
|
||||
os.symlink(member.linkname, target)
|
||||
|
||||
|
||||
def parse_control(control_path):
|
||||
fields = {}
|
||||
with open(control_path, "r", errors="replace") as f:
|
||||
for line in f:
|
||||
if line.startswith(" ") or line.startswith("\t"):
|
||||
continue
|
||||
if ":" in line:
|
||||
k, v = line.split(":", 1)
|
||||
fields[k.strip()] = v.strip()
|
||||
return fields
|
||||
|
||||
|
||||
def append_packages(fields, filename):
|
||||
os.makedirs(os.path.dirname(PACKAGES), exist_ok=True)
|
||||
with open(PACKAGES, "a") as f:
|
||||
f.write("Package: %s\n" % fields.get("Package", ""))
|
||||
f.write("Version: %s\n" % fields.get("Version", ""))
|
||||
f.write("Architecture: %s\n" % fields.get("Architecture", "iphoneos-arm64"))
|
||||
f.write("Maintainer: %s\n" % fields.get("Maintainer", "unknown"))
|
||||
f.write("Section: %s\n" % fields.get("Section", "plugins"))
|
||||
f.write("Priority: %s\n" % fields.get("Priority", "optional"))
|
||||
f.write("Filename: pool/%s\n" % os.path.basename(filename))
|
||||
f.write("Size: %d\n" % os.path.getsize(filename))
|
||||
if fields.get("Depends"):
|
||||
f.write("Depends: %s\n" % fields["Depends"])
|
||||
f.write("Description: %s\n\n" % fields.get("Description", ""))
|
||||
|
||||
|
||||
def build(out_path, name, version, arch, payload, depends=""):
|
||||
os.makedirs(os.path.dirname(packages_dir()), exist_ok=True)
|
||||
tmp = tempfile.mkdtemp(prefix="mkdeb.", dir=os.path.join(ROOT, "tmp"))
|
||||
try:
|
||||
control_dir = os.path.join(tmp, "DEBIAN")
|
||||
os.makedirs(control_dir, exist_ok=True)
|
||||
control = (
|
||||
"Package: %s\nVersion: %s\nArchitecture: %s\n"
|
||||
"Maintainer: %s\nSection: plugins\nPriority: optional\n"
|
||||
"Description: %s\n"
|
||||
% (name, version, arch,
|
||||
os.environ.get("MKDEB_MAINTAINER", "Terminal User <mobile@localhost>"),
|
||||
os.environ.get("MKDEB_DESCRIPTION", name + " plugin for Terminal"))
|
||||
)
|
||||
if depends:
|
||||
control += "Depends: %s\n" % depends
|
||||
open(os.path.join(control_dir, "control"), "w").write(control)
|
||||
|
||||
payload_debian = os.path.join(payload, "DEBIAN")
|
||||
if os.path.isdir(payload_debian):
|
||||
for script in ("preinst", "postinst", "prerm", "postrm"):
|
||||
src = os.path.join(payload_debian, script)
|
||||
if os.path.isfile(src):
|
||||
shutil.copy(src, os.path.join(control_dir, script))
|
||||
|
||||
control_tar = os.path.join(tmp, "control.tar.gz")
|
||||
with tarfile.open(control_tar, "w:gz") as tar:
|
||||
for root, dirs, files in os.walk(control_dir):
|
||||
for fn in files:
|
||||
full = os.path.join(root, fn)
|
||||
arc = os.path.relpath(full, tmp)
|
||||
tar.add(full, arcname=arc)
|
||||
|
||||
data_tar = os.path.join(tmp, "data.tar.gz")
|
||||
with tarfile.open(data_tar, "w:gz") as tar:
|
||||
for root, dirs, files in os.walk(payload):
|
||||
dirs[:] = [d for d in dirs if not (root == payload and d == "DEBIAN")]
|
||||
for fn in files:
|
||||
full = os.path.join(root, fn)
|
||||
arc = os.path.relpath(full, payload)
|
||||
tar.add(full, arcname="./" + arc)
|
||||
open(os.path.join(tmp, "debian-binary"), "w").write("2.0\n")
|
||||
ar_pack(out_path, [os.path.join(tmp, "debian-binary"), control_tar, data_tar])
|
||||
|
||||
fields = parse_control(os.path.join(control_dir, "control"))
|
||||
append_packages(fields, out_path)
|
||||
print(out_path)
|
||||
return 0
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def packages_dir():
|
||||
return os.path.join(ROOT, "repo", "pool")
|
||||
|
||||
|
||||
def install(deb):
|
||||
if not os.path.isfile(deb):
|
||||
print("pkgtool: cannot access archive %s" % deb, file=sys.stderr)
|
||||
return 1
|
||||
os.makedirs(INFO, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(STATUS), exist_ok=True)
|
||||
tmp = tempfile.mkdtemp(prefix="dpkg.", dir=os.path.join(ROOT, "tmp"))
|
||||
try:
|
||||
ar_unpack(deb, tmp)
|
||||
control_tar = data_tar = None
|
||||
for fn in os.listdir(tmp):
|
||||
if fn.startswith("control.tar"):
|
||||
control_tar = os.path.join(tmp, fn)
|
||||
elif fn.startswith("data.tar"):
|
||||
data_tar = os.path.join(tmp, fn)
|
||||
if not control_tar or not data_tar:
|
||||
print("pkgtool: missing control or data archive", file=sys.stderr)
|
||||
return 1
|
||||
control_dir = os.path.join(tmp, "control")
|
||||
safe_extract(control_tar, control_dir)
|
||||
control_path = os.path.join(control_dir, "control")
|
||||
if not os.path.isfile(control_path):
|
||||
control_path = os.path.join(control_dir, "DEBIAN", "control")
|
||||
if not os.path.isfile(control_path):
|
||||
print("pkgtool: control file not found", file=sys.stderr)
|
||||
return 1
|
||||
fields = parse_control(control_path)
|
||||
pkg = fields.get("Package", "")
|
||||
version = fields.get("Version", "")
|
||||
arch = fields.get("Architecture", "iphoneos-arm64")
|
||||
if not pkg or not version:
|
||||
print("pkgtool: invalid control (Package/Version missing)", file=sys.stderr)
|
||||
return 1
|
||||
if arch not in ("", "all", "iphoneos-arm64", "darwin-arm64", "a-shell"):
|
||||
print("pkgtool: architecture '%s' is not native to iOS/Terminal" % arch, file=sys.stderr)
|
||||
print("pkgtool: use the shell dpkg --force-architecture only for script packages", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("Selecting previously unselected package %s." % pkg)
|
||||
print("Unpacking %s (%s) ..." % (pkg, version))
|
||||
safe_extract(data_tar, ROOT)
|
||||
|
||||
file_list = []
|
||||
with tarfile.open(data_tar, "r:*") as tar:
|
||||
for member in tar.getmembers():
|
||||
name = member.name.lstrip("./")
|
||||
if name and not name.startswith("/") and ".." not in name.split("/"):
|
||||
file_list.append(name)
|
||||
if member.isfile() and any(part in ("bin", "sbin", "libexec") for part in name.split("/")):
|
||||
full = os.path.join(ROOT, name)
|
||||
try:
|
||||
magic = open(full, "rb").read(4)
|
||||
except OSError:
|
||||
magic = b""
|
||||
if magic in (b"\xcf\xfa\xed\xfe", b"\xfe\xed\xfa\xcf"):
|
||||
print("pkgtool: note: %s is a native Mach-O binary; a-Shell cannot run it." % name, file=sys.stderr)
|
||||
open(os.path.join(INFO, pkg + ".list"), "w").write("\n".join(file_list) + "\n")
|
||||
|
||||
# Maintainer scripts are kept for inspection/removal, but not executed:
|
||||
# a-Shell has no fork/exec for arbitrary shell children in the Python
|
||||
# backend. Script-only packages should use dpkg's shell path if needed.
|
||||
for script in ("preinst", "postinst", "prerm", "postrm"):
|
||||
src = os.path.join(control_dir, "DEBIAN", script)
|
||||
if not os.path.isfile(src):
|
||||
src = os.path.join(control_dir, script)
|
||||
if os.path.isfile(src):
|
||||
shutil.copy(src, os.path.join(INFO, pkg + "." + script))
|
||||
|
||||
# Replace the package stanza in the status database.
|
||||
stanzas = []
|
||||
if os.path.isfile(STATUS):
|
||||
with open(STATUS, "r", errors="replace") as f:
|
||||
current = []
|
||||
for line in f:
|
||||
if line.strip() == "":
|
||||
if current:
|
||||
stanzas.append("".join(current))
|
||||
current = []
|
||||
else:
|
||||
current.append(line)
|
||||
if current:
|
||||
stanzas.append("".join(current))
|
||||
kept = [s for s in stanzas if not s.startswith("Package: %s\n" % pkg)]
|
||||
kept.append(
|
||||
"Package: %s\nStatus: install ok installed\nPriority: %s\nSection: %s\n"
|
||||
"Maintainer: %s\nArchitecture: %s\nVersion: %s\nDescription: %s\n\n"
|
||||
% (pkg, fields.get("Priority", "optional"), fields.get("Section", "plugins"),
|
||||
fields.get("Maintainer", "unknown"), arch, version,
|
||||
fields.get("Description", ""))
|
||||
)
|
||||
open(STATUS, "w").write("\n".join(kept))
|
||||
print("Setting up %s (%s) ..." % (pkg, version))
|
||||
return 0
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "build" and len(sys.argv) >= 7:
|
||||
return build(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6],
|
||||
sys.argv[7] if len(sys.argv) > 7 else "")
|
||||
if cmd == "install" and len(sys.argv) >= 3:
|
||||
return install(sys.argv[2])
|
||||
print(__doc__)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
# plugin-run - run a native Terminal plugin safely.
|
||||
# a-Shell's in-process command loader refuses standalone Mach-O binaries;
|
||||
# script plugins and WebAssembly plugins can run. This wrapper gives a clear
|
||||
# error instead of "command not found" for native packages.
|
||||
|
||||
ROOT="${TERM_ROOT:-$HOME/Documents/.terminal}"
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "usage: plugin-run <command> [args...]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
CMD="$1"
|
||||
shift
|
||||
FOUND=$(which "$CMD" 2>/dev/null)
|
||||
if [ -z "$FOUND" ]; then
|
||||
echo "plugin-run: '$CMD' not found in PATH" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
MAGIC=$(dd if="$FOUND" bs=4 count=1 2>/dev/null | od -An -tx1 2>/dev/null | tr -d ' \n')
|
||||
case "$MAGIC" in
|
||||
cffaedfe|feedfacf|cafebabe|bebafeca)
|
||||
echo "plugin-run: '$CMD' is a native Mach-O executable." >&2
|
||||
echo "a-Shell does not execute standalone Mach-O binaries in its sandbox." >&2
|
||||
echo "Native Procursus/Havoc CLI packages require a jailbreak or a Linux emulator." >&2
|
||||
echo "Script plugins (#!/bin/sh, python, perl, lua, wasm) can run normally." >&2
|
||||
exit 126
|
||||
;;
|
||||
esac
|
||||
|
||||
"$FOUND" "$@"
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
#!/bin/sh
|
||||
# termenv - independent native plugin environment for Terminal
|
||||
# This is a POSIX sh environment, not a Linux userland. Packages are
|
||||
# Terminal-compatible .deb files (scripts or iOS/Darwin arm64 binaries).
|
||||
|
||||
ROOT="${TERM_ROOT:-$HOME/Documents/.terminal}"
|
||||
APPDIR="${APPDIR:-/Applications/Terminal.app}"
|
||||
PROFILE="$ROOT/etc/profile"
|
||||
SOURCES="$ROOT/etc/apt/sources.list"
|
||||
|
||||
usage() {
|
||||
printf '%s\n' "Usage: termenv <command>"
|
||||
printf '%s\n' ""
|
||||
printf '%s\n' " init Create the independent prefix and the local apt repository"
|
||||
printf '%s\n' " status Show prefix, PATH, apt/dpkg and installed packages"
|
||||
printf '%s\n' " enter Print the command that activates the environment"
|
||||
printf '%s\n' " run <cmd...> Run a command inside the environment"
|
||||
printf '%s\n' " on Add activation to ~/Documents/.profile"
|
||||
printf '%s\n' " off Remove activation from ~/Documents/.profile"
|
||||
printf '%s\n' " selftest init + apt update + install the sample \"hello\" package"
|
||||
printf '%s\n' " clean Remove apt caches and temporary files"
|
||||
printf '%s\n' ""
|
||||
printf '%s\n' "Environment:"
|
||||
printf '%s\n' " TERM_ROOT=~/Documents/.terminal"
|
||||
printf '%s\n' " PREFIX=$TERM_ROOT"
|
||||
}
|
||||
ensure_dirs() {
|
||||
mkdir -p "$ROOT/bin" "$ROOT/sbin" "$ROOT/lib" "$ROOT/include" \
|
||||
"$ROOT/share/man" "$ROOT/etc/apt" \
|
||||
"$ROOT/var/lib/dpkg/info" "$ROOT/var/lib/apt/lists" \
|
||||
"$ROOT/var/cache/apt/archives" "$ROOT/var/log" \
|
||||
"$ROOT/tmp" "$ROOT/repo/pool" "$ROOT/home"
|
||||
[ -f "$ROOT/var/lib/dpkg/status" ] || : > "$ROOT/var/lib/dpkg/status"
|
||||
}
|
||||
|
||||
write_profile() {
|
||||
if [ ! -f "$PROFILE" ]; then
|
||||
cat > "$PROFILE" <<EOF
|
||||
# Terminal plugin environment (generated by termenv init)
|
||||
TERM_ROOT="\$HOME/Documents/.terminal"
|
||||
export TERM_ROOT
|
||||
PREFIX="\$TERM_ROOT"
|
||||
export PREFIX
|
||||
TERM_PLUGIN_ENV=1
|
||||
export TERM_PLUGIN_ENV
|
||||
PATH="\$TERM_ROOT/bin:\$TERM_ROOT/sbin:\$TERM_ROOT/usr/bin:\$PATH"
|
||||
export PATH
|
||||
LD_LIBRARY_PATH="\$TERM_ROOT/lib:\$TERM_ROOT/usr/lib:\$LD_LIBRARY_PATH"
|
||||
export LD_LIBRARY_PATH
|
||||
CPATH="\$TERM_ROOT/include:\$CPATH"
|
||||
export CPATH
|
||||
LIBRARY_PATH="\$TERM_ROOT/lib:\$TERM_ROOT/usr/lib:\$LIBRARY_PATH"
|
||||
export LIBRARY_PATH
|
||||
PKG_CONFIG_PATH="\$TERM_ROOT/lib/pkgconfig:\$PKG_CONFIG_PATH"
|
||||
export PKG_CONFIG_PATH
|
||||
MANPATH="\$TERM_ROOT/share/man:\$MANPATH"
|
||||
export MANPATH
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
write_sources() {
|
||||
LOCAL_LINE="deb [trusted=yes] file://$ROOT/repo ./"
|
||||
PROCURUS_LINE="deb [trusted=yes] https://repo.procurs.us/ ./"
|
||||
HAVOC_LINE="deb [trusted=yes] https://repo.havoc.app/ ./"
|
||||
if [ ! -f "$SOURCES" ]; then
|
||||
{
|
||||
printf '%s\n' "$LOCAL_LINE"
|
||||
printf '%s\n' "$PROCURUS_LINE"
|
||||
printf '%s\n' "$HAVOC_LINE"
|
||||
} > "$SOURCES"
|
||||
return
|
||||
fi
|
||||
TMP="$ROOT/tmp/sources.$$"
|
||||
awk -v desired="$LOCAL_LINE" '
|
||||
/^deb .*file:\/\/.*\/\.terminal\/repo/ { print desired; found=1; next }
|
||||
{ print }
|
||||
END { if (!found) print desired }
|
||||
' "$SOURCES" > "$TMP"
|
||||
mv "$TMP" "$SOURCES"
|
||||
if ! grep -qF "https://repo.procurs.us/" "$SOURCES"; then
|
||||
printf '%s\n' "$PROCURUS_LINE" >> "$SOURCES"
|
||||
fi
|
||||
if ! grep -qF "https://repo.havoc.app/" "$SOURCES"; then
|
||||
printf '%s\n' "$HAVOC_LINE" >> "$SOURCES"
|
||||
fi
|
||||
}
|
||||
|
||||
make_sample() {
|
||||
if grep -q '^Package: hello$' "$ROOT/repo/Packages" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
SAMPLE="$ROOT/share/termenv/samples/hello"
|
||||
mkdir -p "$SAMPLE/bin"
|
||||
cat > "$SAMPLE/bin/hello" << 'EOF'
|
||||
#!/bin/sh
|
||||
echo "Hello from the Terminal plugin environment."
|
||||
echo "PREFIX=$PREFIX"
|
||||
echo "TERM_ROOT=$TERM_ROOT"
|
||||
if [ -d "$HOME/Documents/_sandbox" ]; then
|
||||
echo "Sandbox escape: available (see $HOME/Documents/_sandbox)"
|
||||
fi
|
||||
EOF
|
||||
chmod +x "$SAMPLE/bin/hello"
|
||||
if [ -f "$APPDIR/bin/mkdeb" ]; then
|
||||
sh "$APPDIR/bin/mkdeb" hello 1.0.0 "$SAMPLE" iphoneos-arm64
|
||||
echo "termenv: mkdeb exit=$?"
|
||||
fi
|
||||
}
|
||||
|
||||
do_init() {
|
||||
echo "termenv: creating directories..."
|
||||
ensure_dirs
|
||||
echo "termenv: writing profile/sources..."
|
||||
write_profile
|
||||
write_sources
|
||||
[ -f "$ROOT/repo/Packages" ] || : > "$ROOT/repo/Packages"
|
||||
echo "termenv: creating sample package..."
|
||||
make_sample
|
||||
echo "termenv: sample ready"
|
||||
echo "Prefix: $ROOT"
|
||||
echo "Profile: $PROFILE"
|
||||
echo "Run: source \"$PROFILE\" (or: termenv on)"
|
||||
}
|
||||
|
||||
do_status() {
|
||||
ensure_dirs
|
||||
echo "Terminal native plugin environment"
|
||||
echo " prefix: $ROOT"
|
||||
echo " profile: $PROFILE"
|
||||
echo " apt: $APPDIR/bin/apt"
|
||||
echo " dpkg: $APPDIR/bin/dpkg"
|
||||
echo " sources: $SOURCES"
|
||||
if [ -f "$SOURCES" ]; then
|
||||
sed 's/^/ /' "$SOURCES"
|
||||
fi
|
||||
echo " packages:"
|
||||
if [ -s "$ROOT/var/lib/dpkg/status" ]; then
|
||||
awk '/^Package: /{printf " %s", substr($0,10)} /^Version: /{printf " %s", substr($0,10)} /^Status: /{status=substr($0,9)} /^$/{if(status ~ /installed/) print ""}' "$ROOT/var/lib/dpkg/status"
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
echo " sandbox: $HOME/Documents/_sandbox"
|
||||
}
|
||||
|
||||
do_run() {
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "usage: termenv run <command...>" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -f "$PROFILE" ]; then
|
||||
. "$PROFILE"
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
do_on() {
|
||||
do_init
|
||||
PROFILE_FILE="$HOME/Documents/.profile"
|
||||
MARK_BEGIN="# >>> Terminal plugin environment >>>"
|
||||
MARK_END="# <<< Terminal plugin environment <<<"
|
||||
if grep -qF "$MARK_BEGIN" "$PROFILE_FILE" 2>/dev/null; then
|
||||
echo "Activation already present in $PROFILE_FILE"
|
||||
return 0
|
||||
fi
|
||||
{
|
||||
echo "$MARK_BEGIN"
|
||||
echo "source \"\$HOME/Documents/.terminal/etc/profile\""
|
||||
echo "$MARK_END"
|
||||
} >> "$PROFILE_FILE"
|
||||
echo "Activation added to $PROFILE_FILE"
|
||||
}
|
||||
|
||||
do_off() {
|
||||
PROFILE_FILE="$HOME/Documents/.profile"
|
||||
if [ ! -f "$PROFILE_FILE" ]; then
|
||||
echo "No $PROFILE_FILE"
|
||||
return 0
|
||||
fi
|
||||
TMP="$ROOT/tmp/profile.$$"
|
||||
awk '
|
||||
/# >>> Terminal plugin environment >>>/ {skip=1; next}
|
||||
/# <<< Terminal plugin environment <<</ {skip=0; next}
|
||||
!skip {print}
|
||||
' "$PROFILE_FILE" > "$TMP"
|
||||
mv "$TMP" "$PROFILE_FILE"
|
||||
echo "Activation removed from $PROFILE_FILE"
|
||||
}
|
||||
|
||||
do_clean() {
|
||||
rm -rf "$ROOT/tmp"/*
|
||||
rm -f "$ROOT/var/cache/apt/archives/"*.deb
|
||||
echo "Cleaned caches"
|
||||
}
|
||||
|
||||
do_selftest() {
|
||||
do_init
|
||||
. "$PROFILE"
|
||||
echo "=== apt update ==="
|
||||
APT_SKIP_REMOTE=1 sh "$APPDIR/bin/apt" update
|
||||
echo "=== apt install hello ==="
|
||||
sh "$APPDIR/bin/apt" install hello
|
||||
echo "=== hello ==="
|
||||
hello
|
||||
echo "=== installed packages ==="
|
||||
sh "$APPDIR/bin/apt" list --installed
|
||||
echo "=== dpkg -l ==="
|
||||
sh "$APPDIR/bin/dpkg" -l
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
init) do_init ;;
|
||||
status) do_status ;;
|
||||
enter) echo "source \"$PROFILE\"" ;;
|
||||
run) shift; do_run "$@" ;;
|
||||
on) do_on ;;
|
||||
off) do_off ;;
|
||||
clean) do_clean ;;
|
||||
selftest) do_selftest ;;
|
||||
""|-h|--help|help) usage ;;
|
||||
*) usage; exit 2 ;;
|
||||
esac
|
||||
Reference in new issue
Block a user