karnel-termux 4.18.2 → 4.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://github.com/karnel-termuxOFC/karnel-termux">
11
- <img src="https://img.shields.io/badge/version-4.18.2-0078D4?style=for-the-badge" alt="Version">
11
+ <img src="https://img.shields.io/badge/version-4.18.3-0078D4?style=for-the-badge" alt="Version">
12
12
  </a>
13
13
  <a href="https://www.npmjs.com/package/karnel-termux">
14
14
  <img src="https://img.shields.io/npm/v/karnel-termux?style=for-the-badge&logo=npm&color=cb3837" alt="npm">
@@ -78,7 +78,7 @@ Why it's great:
78
78
  ### Via checksummed GitHub release (recommended)
79
79
 
80
80
  ```bash
81
- version=4.18.2
81
+ version=4.18.3
82
82
  tmpdir=$(mktemp -d) && trap 'rm -rf "$tmpdir"' EXIT
83
83
  base="https://github.com/karnel-termuxOFC/karnel-termux/releases/download/v$version"
84
84
  curl -fsSL "$base/karnel-termux-install.sh" -o "$tmpdir/karnel-termux-install.sh"
package/docs/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ layout: base
6
6
 
7
7
  # Documentation Changelog
8
8
 
9
+ ## 4.18.3
10
+
11
+ - **Every command that installs something now adapts what it put on PATH.** The dispatcher brackets `install`, `update`, `upgrade`, `reinstall`, `restore`, `plugin`, `supabase`, `deploy`, `voice`, `robin`, `pg`, `open` and `init` with a snapshot of the writable parts of PATH (`path|size|mtime` per entry) and afterwards adapts exactly the entries that appeared or changed. That closes the gap the registries could never close: an installer that never calls the compatibility layer, one that names a command its registry does not mention, and a file restored from an archive with its original timestamp all end up wrapped anyway. Sweeping the prefix instead was measured on this device at 51 seconds of `readelf` over 800 entries (and 875 seconds with `readelf -l`); diffing a snapshot takes 60 ms and touches nothing the command did not write. The exit code of the command is taken before adaptation and returned unchanged.
12
+ - **The AI reinstall fallback adapts what it installed.** `_run_ai_tool_action` called `$install_fn` and returned without passing through its adapt call, so those binaries stayed unwrapped.
13
+ - **An install can no longer report success for a command that does not start.** A device audit of all 169 catalogued tools found 21 installed and 147 absent — and exactly one of them dead: `$PREFIX/bin/freebuff` had been left as npm's shim, which exits 1 on Termux with "Unsupported platform: android arm64", while `karnel install ai --freebuff` still reported "already installed". Both paths of `install_freebuff` now prove the entry runs before returning, the same way `_fix_freebuff_runtime` already proves the runtime does. The audit is green afterwards: freebuff `0.2.22`, bun `1.4.2`, supabase `2.20.8`, zero broken.
14
+ - **The compatibility suite is part of the test run.** `tests/android-compat.sh` had never been listed in `tests/run.sh`, so its cases only ever ran by hand; it now runs in `npm test` and in CI, and every other file under `tests/` was checked against the list while adding it.
15
+ - **Tests.** Five new cases in `tests/android-compat.sh` (a binary created during a command, a binary rewritten in place, only the entries that changed, an exit code that cannot change, and the dispatcher wiring itself) plus two in `tests/ai-installer-hardening.sh` (a dead freebuff entry fails, a live one is accepted), taking the suites to 28 and 11 cases. Every branch was proven to fail first: dropping the adapt call fails 1 case, dropping the snapshot fails 1, making `compat_adapt_since` a no-op fails 2, removing the AI fallback's adapt fails 1, removing the already-installed verification fails 1, and turning that verification into a no-op fails both new freebuff cases.
16
+
9
17
  ## 4.18.2
10
18
 
11
19
  - **Wrappers no longer hand a preload to glibc.** `login(1)` exports `LD_PRELOAD=$PREFIX/lib/libtermux-exec-ld-preload.so`, a bionic object; glibc's `ld.so` cannot load it, so every wrapped tool died before `main()` with `.../glibc/lib/libc.so: invalid ELF header`. Tier 3's own preload was fatal in the other direction: exporting the glibc `libtermux-exec.so` shim left every bionic child of the tool failing with `library "libc.so.6" not found`. Every wrapper now starts with `unset LD_PRELOAD`, and tier 3 exports `PATH` only.
@@ -1 +1 @@
1
- fff0d351f344943ef88e0745cc0f7f56c7aaf970
1
+ 4f1840c76ffba58dde286b506142fa1f9e5ee1d8
@@ -2,6 +2,7 @@
2
2
 
3
3
  import "@/utils/log"
4
4
  import "@/utils/colors"
5
+ import "@/utils/compat"
5
6
 
6
7
  : "${KARNEL_VERSION:=unknown}"
7
8
 
@@ -67,8 +68,23 @@ _karnel_dispatch() {
67
68
 
68
69
  if [[ -f "$command_file" ]]; then
69
70
  import "@/cli/commands/$cmd"
71
+ # Every command that can put a binary on PATH is bracketed by a snapshot
72
+ # of PATH, and whatever changed inside it is adapted afterwards. That is
73
+ # what makes the layer cover *all* tools: an installer that never calls
74
+ # compat itself, one that names a command its registry does not mention,
75
+ # and one that restores a file from an archive all end up here anyway.
76
+ local compat_snap=""
77
+ case "$cmd" in
78
+ install | update | upgrade | reinstall | restore | plugin | supabase | deploy | voice | robin | pg | open | init)
79
+ compat_snap="$(compat_path_snapshot 2>/dev/null)" || compat_snap=""
80
+ ;;
81
+ esac
70
82
  "${cmd}_main" "$@"
71
- return $?
83
+ local cmd_rc=$?
84
+ if [[ -n "$compat_snap" ]]; then
85
+ compat_adapt_since "$compat_snap" || true
86
+ fi
87
+ return "$cmd_rc"
72
88
  fi
73
89
 
74
90
  import "@/tools/plugins/install"
@@ -230,6 +230,13 @@ _run_ai_tool_action() {
230
230
  return "$uninstall_rc"
231
231
  fi
232
232
  "$install_fn"
233
+ local install_rc=$?
234
+ # The fallback path never passes through _run_ai_tool_action's adapt call,
235
+ # so the binaries this install just wrote would stay unwrapped.
236
+ if ((install_rc == 0)); then
237
+ _ai_tool_compat_adapt "$id"
238
+ fi
239
+ return "$install_rc"
233
240
  }
234
241
 
235
242
  _all_ai_tools_action() {
@@ -23,6 +23,7 @@ install_freebuff() {
23
23
  log_info "Freebuff is already installed"
24
24
  _fix_freebuff_runtime || true
25
25
  _install_freebuff_entry || true
26
+ _verify_freebuff_entry || return 1
26
27
  return 2
27
28
  fi
28
29
 
@@ -42,6 +43,7 @@ install_freebuff() {
42
43
  _fix_freebuff_shebang || return 1
43
44
  _fix_freebuff_runtime || return 1
44
45
  _install_freebuff_entry || return 1
46
+ _verify_freebuff_entry || return 1
45
47
  log_success "Freebuff installed"
46
48
  }
47
49
 
@@ -96,6 +98,25 @@ _freebuff_binary_runs() {
96
98
  ((rc == 0))
97
99
  }
98
100
 
101
+ # npm recreates its shim at $PREFIX/bin/freebuff on every global operation, and
102
+ # that shim cannot start on Android - so an install can look successful while
103
+ # the command itself is dead. Every path therefore proves the entry runs before
104
+ # reporting success, the same way _fix_freebuff_runtime proves the runtime does.
105
+ _verify_freebuff_entry() {
106
+ local entry rc=0
107
+ entry="$(command -v freebuff 2>/dev/null)"
108
+ if [[ -z "$entry" || ! -e "$entry" ]]; then
109
+ log_error "Freebuff is not on PATH after install"
110
+ return 1
111
+ fi
112
+ { timeout 30 "$entry" --version >/dev/null 2>&1; rc=$?; } 2>/dev/null
113
+ if ((rc != 0)); then
114
+ log_error "Freebuff does not run after install (exit $rc)"
115
+ return 1
116
+ fi
117
+ return 0
118
+ }
119
+
99
120
  _freebuff_target_key() {
100
121
  case "$(uname -m)" in
101
122
  aarch64 | arm64) printf 'linux-arm64\n' ;;
@@ -534,13 +534,11 @@ compat_fix_shebangs() {
534
534
  printf '%s\n' "$fixed"
535
535
  }
536
536
 
537
- # Adapts whatever a tool has just put on PATH. Wraps a glibc ELF so the
538
- # kernel can start it; leaves native binaries, scripts and text files alone.
539
- # Always returns 0: adaptation is an optimisation, never a reason to fail an
540
- # install or an update.
541
- compat_adapt_installed() {
542
- local tool="$1" path real
543
- path="$(command -v -- "$tool" 2>/dev/null)" || return 0
537
+ # Adapts one resolved path. Wraps a glibc ELF so the kernel can start it;
538
+ # leaves native binaries, scripts and text files alone. Always returns 0:
539
+ # adaptation is an optimisation, never a reason to fail an install.
540
+ _compat_adapt_path() {
541
+ local path="$1" real
544
542
  [[ -n "$path" ]] || return 0
545
543
  real="$(readlink -f -- "$path" 2>/dev/null || printf '%s' "$path")"
546
544
  [[ -f "$real" && -x "$real" ]] || return 0
@@ -560,6 +558,57 @@ compat_adapt_installed() {
560
558
  return 0
561
559
  }
562
560
 
561
+ # Adapts whatever a tool has just put on PATH.
562
+ compat_adapt_installed() {
563
+ local tool="$1" path
564
+ path="$(command -v -- "$tool" 2>/dev/null)" || return 0
565
+ [[ -n "$path" ]] || return 0
566
+ _compat_adapt_path "$path"
567
+ }
568
+
569
+ # Records every direct entry of the writable parts of PATH as
570
+ # "path|size|mtime", printed to a temporary file. Taken before a command runs
571
+ # so the layer can afterwards adapt exactly what that command touched.
572
+ #
573
+ # Sweeping the whole prefix instead would need readelf on every ELF, which
574
+ # measures 60+ seconds over a Termux bin directory of 800 entries; diffing a
575
+ # snapshot adapts only the handful of files that actually changed. It also
576
+ # means no installer has to declare its binary names: a tool that installs a
577
+ # command its registry does not mention is still covered, and a file restored
578
+ # from an archive with its original mtime is caught by being a new path.
579
+ compat_path_snapshot() {
580
+ local dir tmp
581
+ tmp="$(mktemp "${TMPDIR:-${KARNEL_CACHE:-/tmp}}/karnel-compat-snap.XXXXXX" 2>/dev/null)" || return 1
582
+ local IFS=':'
583
+ for dir in $PATH; do
584
+ [[ -n "$dir" && -d "$dir" && -w "$dir" ]] || continue
585
+ find "$dir" -maxdepth 1 \( -type f -o -type l \) -printf '%p|%s|%T@\n' 2>/dev/null || true
586
+ done | sort >"$tmp" || true
587
+ printf '%s\n' "$tmp"
588
+ }
589
+
590
+ # Adapts every PATH entry that appeared or changed since a snapshot, then
591
+ # removes both files. Returns 0 unconditionally.
592
+ compat_adapt_since() {
593
+ local snap="$1" tmp dir path
594
+ [[ -f "$snap" ]] || return 0
595
+ tmp="$(mktemp "${TMPDIR:-${KARNEL_CACHE:-/tmp}}/karnel-compat-now.XXXXXX" 2>/dev/null)" || {
596
+ rm -f -- "$snap"
597
+ return 0
598
+ }
599
+ local IFS=':'
600
+ for dir in $PATH; do
601
+ [[ -n "$dir" && -d "$dir" && -w "$dir" ]] || continue
602
+ find "$dir" -maxdepth 1 \( -type f -o -type l \) -printf '%p|%s|%T@\n' 2>/dev/null || true
603
+ done | sort >"$tmp" || true
604
+ while IFS= read -r path; do
605
+ [[ -n "$path" ]] || continue
606
+ _compat_adapt_path "$path" || true
607
+ done < <(comm -13 -- "$snap" "$tmp" 2>/dev/null | cut -d'|' -f1)
608
+ rm -f -- "$snap" "$tmp"
609
+ return 0
610
+ }
611
+
563
612
  # Human-readable one-liner for doctor output.
564
613
  compat_describe() {
565
614
  local file="$1" kind interp real
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karnel-termux",
3
- "version": "4.18.2",
3
+ "version": "4.18.3",
4
4
  "description": "Modular Dev Environment for Termux — install languages, databases, AI agents, editors, and more with one command",
5
5
  "bin": {
6
6
  "karnel": "karnel/bin/karnel"