feat(termux): pinned termux toolchain via pm (linux-arm64-bionic)

A seventh pm target (linux-arm64-bionic) stages the TUR python3.11
.deb, the termux nodejs/uv .debs, and their runtime-lib deps into the
payload -- same pm-consumer shape as the desktop legs: pm owns the pin,
the hardened download (redirect-safe, retry-wrapped), the ar+tar
DebPackage extraction, and file-evidence verify for binaries the
staging host cannot execute.
This commit is contained in:
ethernet8023
2026-09-05 20:00:00 -04:00
parent e84e625d87
commit 0ae85925f3
14 changed files with 744 additions and 13 deletions

View File

@@ -11,6 +11,7 @@ from pathlib import Path
from typing import Optional
from pm.package import (
DebPackage,
InstallError,
Package,
StatePackage,
@@ -115,17 +116,64 @@ class BinaryPackage(Package):
return env_for(*self.deps)
class _BionicDebArm:
"""Shared bionic-arm behavior for the termux tool packages: extract as a
.deb on the bionic target (DebPackage's hardened ar+tar), as the binary
package otherwise, and never host-exec-probe a bionic binary (it cannot
run on the staging host)."""
def unpack(self, archive: Path, staged: Path, target: str) -> None:
if target == "linux-arm64-bionic":
DebPackage.unpack(self, archive, staged, target)
else:
BinaryPackage.unpack(self, archive, staged, target)
def binary(self, entry: Path, target: str) -> Optional[Path]:
if target == "linux-arm64-bionic":
return None # bionic binaries cannot exec on the staging host
return BinaryPackage.binary(self, entry, target)
def verify(self, entry: Path, target: str) -> str:
# MRO order puts BinaryPackage.verify (exec-probe semantics) ahead of
# DebPackage.verify (file-evidence semantics); bionic needs the
# latter -- one dispatch here replaces the per-class copies.
if target == "linux-arm64-bionic":
return DebPackage.verify(self, entry, target)
return BinaryPackage.verify(self, entry, target)
@register
class Uv(BinaryPackage):
class Uv(_BionicDebArm, BinaryPackage, DebPackage):
"""astral's prebuilt tarballs for glibc/mac/win; the Termux main-repo
uv .deb for bionic (termux builds uv from source -- no astral bionic
artifact exists). The bionic arm is a runtime tool on the phone (lazy
plugin installs) and the wheelhouse's resolver in the build container."""
name = "uv"
internal = True
binary_rel = {"win32": "uv.exe", "posix": "uv"}
# The staged .deb's main binary: DebPackage.verify checks it.
main_bin_rel = "bin/uv"
def main_rel(self, target: str) -> str:
return self.main_bin_rel
deb_package = "uv"
def fetch_url(self, version: str, target: str) -> str:
if target == "linux-arm64-bionic":
return f"https://packages.termux.dev/apt/termux-main/pool/main/u/uv/uv_{version}_aarch64.deb"
triple = _RUST_TRIPLE[target]
ext = "zip" if target.startswith("win32") else "tar.gz"
return f"https://github.com/astral-sh/uv/releases/download/{version}/uv-{triple}.{ext}"
def env(self, entry: Path, target: str) -> dict:
"""The bionic arm exposes uv through PATH composition -- the same
mechanism every other pm package uses. No system PATH install."""
if target == "linux-arm64-bionic":
return {"PATH": str(entry / self.prefix_rel / "bin")}
return BinaryPackage.env(self, entry, target)
def latest_versions(self, target: str, locked=None) -> list[str]:
return github_release_tags("astral-sh/uv")
@@ -179,7 +227,7 @@ def _macos_sign_managed_python(python: Path) -> bool:
@register
class Python(BinaryPackage):
class Python(_BionicDebArm, BinaryPackage, DebPackage):
"""The payload interpreter (python-build-standalone install_only).
Optional: dev installs use their own venv's python; bundles stage this
and point the relocatable venv's pyvenv.cfg at it (pm adopt)."""
@@ -188,6 +236,13 @@ class Python(BinaryPackage):
optional = True
probe_version = False
binary_rel = {"win32": "python.exe", "posix": "bin/python3"}
# The staged .deb's main binary: DebPackage.verify checks it.
main_bin_rel = "bin/python3.11"
def main_rel(self, target: str) -> str:
return self.main_bin_rel
deb_package = "python3.11"
def stage(self, store: Store, staged: Path, version: str, target: str) -> None:
super().stage(store, staged, version, target)
@@ -196,6 +251,9 @@ class Python(BinaryPackage):
_macos_sign_managed_python(binary)
def fetch_url(self, version: str, target: str) -> str:
if target == "linux-arm64-bionic":
pyver = version.partition("+")[0]
return f"https://tur.kcubeterm.com/pool/tur/python3.11_{pyver}_aarch64.deb"
# lock version is "<python>+<release tag>", e.g. "3.11.13+202****0807"
pyver, _, tag = version.partition("+")
if not tag:
@@ -482,15 +540,58 @@ class Venv(StatePackage):
@register
class Nodejs(BinaryPackage):
class Nodejs(_BionicDebArm, BinaryPackage, DebPackage):
"""nodejs.org tarballs for glibc/mac/win; the Termux main-repo nodejs
.deb for bionic (same major line, termux-built)."""
name = "node"
binary_rel = {"win32": "node.exe", "posix": "bin/node"}
# The staged .deb's main binary: DebPackage.verify checks it.
main_bin_rel = "bin/node"
def main_rel(self, target: str) -> str:
return self.main_bin_rel
deb_package = "nodejs"
def fetch_url(self, version: str, target: str) -> str:
if target == "linux-arm64-bionic":
# termux's deb carries a -1 revision after the upstream version
return f"https://packages.termux.dev/apt/termux-main/pool/main/n/nodejs/nodejs_{version}-1_aarch64.deb"
plat = _NODE_PLAT[target]
ext = "zip" if target.startswith("win32") else "tar.xz"
return f"https://nodejs.org/dist/v{version}/node-v{version}-{plat}.{ext}"
@register
class TermuxDocker(Package):
"""The termux/termux-docker container image, pinned by registry digest.
The image is never downloaded or unpacked by pm -- docker pulls it by
digest reference at build time. The lock row exists so the digest is
pinned in the single pin authority beside every other third-party
artifact: consumers read the digest string from the lock's url field
(termux/termux-docker@sha256:...). verify() is presence-shaped: this
package stages nothing.
"""
name = "termux-docker"
optional = True
# Pure pin: no bytes are staged, so stage_only()/install skip the store
# entirely -- the digest's consumers (docker pull) verify it.
pin_only = True
def missing_reason(self, target: str) -> Optional[str]:
return None if target == "linux-arm64-bionic" else "docker image target is linux-arm64-bionic"
def fetch_url(self, version: str, target: str) -> str:
return f"docker://termux/termux-docker@{version}"
def unpack(self, archive: Path, staged: Path, target: str) -> None:
raise InstallError(self.name, "a docker image digest is a pin, not a downloadable artifact")
def verify(self, entry: Path, target: str) -> str:
return ""
def latest_versions(self, target: str, locked=None) -> list[str]:
return node_latest_versions()
@@ -704,7 +805,6 @@ class Ffmpeg(BinaryPackage):
)
@register
class Ripgrep(BinaryPackage):
name = "ripgrep"