forked from admin/terminal_worse
287 lines
12 KiB
Python
287 lines
12 KiB
Python
#!/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())
|