karnel-termux 4.17.45 → 4.18.0

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.17.45-0078D4?style=for-the-badge" alt="Version">
11
+ <img src="https://img.shields.io/badge/version-4.18.0-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.17.45
81
+ version=4.18.0
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.0
10
+
11
+ - **Three-tier compatibility layer:** a foreign glibc binary is no longer handled by one fixed trick. `compat_adapt` wraps, probes and climbs to the cheapest tier that actually starts the tool — the glibc loader alone (free, installs nothing), the glibc userland (`$PREFIX/glibc/bin`, 383 glibc tools, prepended to `PATH` together with the glibc exec shim so child processes resolve the same way), or proot over a synthetic `/lib`, `/usr` and `/etc` assembled from the sysroot already on the device. That last tier replaces `proot-distro install ubuntu` for binaries that merely hardcode an absolute path, and it downloads nothing. A tier that cannot be reached is refused before the wrapper is written, so `proot -r ''` can never ship, and every step unwraps back to `<path>.karnel-real`.
12
+ - **Shebang repair that survives symlinks:** `compat_fix_shebang` edits the target of a link rather than the link itself. `sed -i` unlinks its argument, so repairing `$PREFIX/bin/npm` used to replace that symlink with a detached copy of `npm-cli.js` whose relative `require('../lib/cli.js')` then resolved from `$PREFIX/bin` — one broken `npm` traded for another. The same bug was live in `karnel doctor`, whose `_fix_npm_shebangs` ran `sed -i` over every entry in `$PREFIX/bin`.
13
+ - **`npm` and the CLIs on top of it were dead on arrival:** Android has no `/usr`, so `#!/usr/bin/env node` exits 126 with `bad interpreter` even though node is installed, and none of the three `termux-exec` preloads intercepts the kernel's shebang resolution. `npm`, `npx`, `kc`, `kcode`, `keel`, `snyk`, `httptmuxd`, `gdbus-codegen`, `glib-genmarshal` and `glib-mkenums` all now resolve to their Termux interpreter; `$PREFIX/bin` reports 0 unresolvable shebangs.
14
+ - **Doctor:** the check asks whether the interpreter path exists instead of grepping for the literal `#!/usr/bin/env`, so a stale absolute path is caught and a resolvable `env` line is left alone; the fix delegates to the compatibility layer; a new *Android Compatibility Layer* section reports the glibc sysroot, how many tools are riding a wrapper, whether proot is reachable for the FHS tier, and any shebang that still does not resolve.
15
+ - **Tests:** `tests/android-compat.sh` covers classification, wrapping, re-tiering without disturbing the parked original, escalation, falling back when no tier helps, building the FHS root exactly once, refusing tier 4 without a root, shebang repair through a symlink, and agreement with `readelf` (17 cases).
16
+
9
17
  ## 4.17.45
10
18
 
11
19
  - **npm on a real Termux host:** every npm call goes through `karnel_npm()`, which runs npm through `node` when its `#!/usr/bin/env node` shebang cannot be executed (Termux ships no `/usr`, so the kernel answered `bad interpreter`) and retries with `--force` only on `EBADPLATFORM`. The remaining bare `npm install/uninstall/update/view/ls` calls in the installers were converted; guests inside proot keep plain npm.
@@ -1 +1 @@
1
- 29fb2632bc7558b002e10d68272418bcb4450cb1
1
+ a6c1aeb11507e48d4e14c3f6c2eb9bd9e9f969da
@@ -4,6 +4,7 @@
4
4
  import "@/utils/log"
5
5
  import "@/utils/colors"
6
6
  import "@/utils/install"
7
+ import "@/utils/compat"
7
8
 
8
9
  _fix_storage() {
9
10
  termux-setup-storage 2>/dev/null
@@ -219,19 +220,13 @@ _fix_pip_check() {
219
220
  }
220
221
 
221
222
  _fix_npm_shebangs() {
222
- local fixed=0
223
- for f in "$PREFIX/bin/"*; do
224
- [[ -f "$f" ]] || continue
225
- local shebang
226
- shebang=$(head -1 "$f" 2>/dev/null | tr -d '\0\r' | xargs)
227
- if [[ "$shebang" == "#!/usr/bin/env node" ]]; then
228
- sed -i "1s|^.*$|#!$PREFIX/bin/node|" "$f" && ((fixed++))
229
- elif [[ "$shebang" == "#!/usr/bin/env bash" ]] || [[ "$shebang" == "#!/usr/bin/env sh" ]]; then
230
- sed -i "1s|^.*$|#!$PREFIX/bin/bash|" "$f" && ((fixed++))
231
- elif [[ "$shebang" == "#!/usr/bin/env python3" ]] || [[ "$shebang" == "#!/usr/bin/env python" ]]; then
232
- sed -i "1s|^.*$|#!$PREFIX/bin/python3|" "$f" && ((fixed++))
233
- fi
234
- done
223
+ # Delegates to the compatibility layer instead of sed-ing $PREFIX/bin
224
+ # itself: most of those entries are symlinks into node_modules, and
225
+ # `sed -i` unlinks its argument, which would replace npm's symlink with a
226
+ # detached copy whose relative require() resolves from the wrong directory.
227
+ # compat_fix_shebang edits the target and copes with any interpreter, not
228
+ # just node/bash/python3.
229
+ compat_fix_shebangs "$PREFIX/bin" >/dev/null 2>&1
235
230
  return 0
236
231
  }
237
232
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import "@/utils/log"
4
4
  import "@/utils/colors"
5
+ import "@/utils/compat"
5
6
 
6
7
  doctor_main() {
7
8
  local subcommand="${1:-termux}"
@@ -769,19 +770,31 @@ doctor_termux() {
769
770
  separator_section "Binary & Shebang Health"
770
771
  echo
771
772
 
772
- # Check for bad shebangs (#!/usr/bin/env) in PREFIX/bin (fast grep)
773
+ # Interpreters that do not exist on this device. Android has no /usr, so
774
+ # every "#!/usr/bin/env node" the kernel meets answers ENOENT and the shell
775
+ # reports 126, even though node itself is installed. The check is on the
776
+ # interpreter path rather than on the literal "#!/usr/bin/env" prefix, so a
777
+ # shebang pointing at a stale absolute path is caught too.
773
778
  local bad_shebangs=0
774
- local bad_files
775
- bad_files=$(timeout 10 rg -l "^#!/usr/bin/env" "$PREFIX/bin/" 2>/dev/null || true)
776
- if [[ -n "$bad_files" ]]; then
777
- bad_shebangs=$(echo "$bad_files" | wc -l)
778
- log_warn "$bad_shebangs binary(s) with #!/usr/bin/env — will fail on Termux"
779
- echo "$bad_files" | while IFS= read -r f; do
780
- list_item "$(basename "$f")"
779
+ bad_shebangs="$(timeout 20 compat_count_broken_shebangs "$PREFIX/bin" 2>/dev/null)"
780
+ bad_shebangs="${bad_shebangs:-0}"
781
+ if [[ "$bad_shebangs" =~ ^[0-9]+$ ]] && ((bad_shebangs > 0)); then
782
+ log_warn "$bad_shebangs binary(s) point at an interpreter that does not exist"
783
+ local sb_file sb_name
784
+ for sb_file in "$PREFIX/bin"/*; do
785
+ [[ -f "$sb_file" && -r "$sb_file" ]] || continue
786
+ IFS= read -r sb_head <"$sb_file" 2>/dev/null || continue
787
+ [[ "$sb_head" == '#!'* ]] || continue
788
+ sb_name="${sb_head#\#!}"
789
+ sb_name="${sb_name# }"
790
+ sb_name="${sb_name%% *}"
791
+ if [[ "$sb_name" == /* && ! -x "$sb_name" ]]; then
792
+ list_item "$(basename "$sb_file") ($sb_name)"
793
+ fi
781
794
  done
782
795
  ((warnings++))
783
- fix_commands+=("karnel install npm 2>/dev/null || true")
784
- fix_descriptions+=("Reinstall npm tools to fix shebangs")
796
+ fix_commands+=("karnel doctor --fix")
797
+ fix_descriptions+=("Repoint $bad_shebangs shebang(s) at their Termux interpreter")
785
798
  fix_callbacks+=("_fix_npm_shebangs")
786
799
  else
787
800
  log_success "All binaries have valid shebangs"
@@ -963,6 +976,53 @@ doctor_termux() {
963
976
  log_info "proot not installed (optional)"
964
977
  fi
965
978
 
979
+ # ===== ANDROID COMPATIBILITY LAYER =====
980
+ echo
981
+ separator_section "Android Compatibility Layer"
982
+ echo
983
+ # A foreign (glibc) binary cannot be started by the Android kernel at all;
984
+ # karnel hands it to the loader from the glibc sysroot instead. This block
985
+ # reports which of the three tiers are reachable and how many tools are
986
+ # currently riding them.
987
+ local compat_root compat_wrapped=0 compat_entry compat_fixable
988
+ if compat_glibc_ready; then
989
+ compat_root="$(compat_glibc_root)"
990
+ log_success "glibc sysroot: available ($compat_root)"
991
+ else
992
+ log_info "glibc sysroot: not installed (required to run glibc-only tools)"
993
+ fix_commands+=("pkg install -y glibc-repo glibc")
994
+ fix_descriptions+=("Install the glibc sysroot so foreign binaries can run")
995
+ fix_callbacks+=("_fix_pkg_install")
996
+ fi
997
+
998
+ for compat_entry in "$PREFIX/bin"/*; do
999
+ [[ -f "$compat_entry" ]] || continue
1000
+ if _compat_is_wrapper "$compat_entry"; then
1001
+ compat_wrapped=$((compat_wrapped + 1))
1002
+ fi
1003
+ done
1004
+ if ((compat_wrapped > 0)); then
1005
+ log_success "$compat_wrapped tool(s) running through the compatibility wrapper"
1006
+ else
1007
+ log_info "no tool on PATH currently needs a compatibility wrapper"
1008
+ fi
1009
+
1010
+ if compat_proot_available; then
1011
+ log_success "proot: available (tier 4 for binaries that hardcode /usr or /etc)"
1012
+ elif command -v proot >/dev/null 2>&1; then
1013
+ log_info "proot: installed, but the FHS root could not be assembled"
1014
+ else
1015
+ log_info "proot: not installed (optional; only needed for absolute-path tools)"
1016
+ fi
1017
+
1018
+ compat_fixable="$(timeout 20 compat_count_broken_shebangs "$PREFIX/bin" 2>/dev/null)"
1019
+ compat_fixable="${compat_fixable:-0}"
1020
+ if [[ "$compat_fixable" =~ ^[0-9]+$ ]] && ((compat_fixable > 0)); then
1021
+ log_warn "$compat_fixable shebang(s) still point at a missing interpreter"
1022
+ else
1023
+ log_success "every shebang in $PREFIX/bin resolves"
1024
+ fi
1025
+
966
1026
  # ===== TERMUX:API =====
967
1027
  echo
968
1028
  separator_section "Termux:API"
@@ -3,6 +3,7 @@
3
3
  import "@/utils/log"
4
4
  import "@/utils/colors"
5
5
  import "@/utils/tools"
6
+ import "@/utils/compat"
6
7
 
7
8
  install_main() {
8
9
 
@@ -244,6 +245,10 @@ _install_specific_tools() {
244
245
  fi
245
246
  done
246
247
  if [ -n "$found_bin" ]; then
248
+ for _bin in "${_bin_list[@]}"; do
249
+ declare -f compat_adapt_installed >/dev/null 2>&1 &&
250
+ compat_adapt_installed "$_bin" || true
251
+ done
247
252
  ((installed_count++))
248
253
  else
249
254
  log_warn "$tool: binary not found in PATH after installation"
@@ -3,6 +3,7 @@
3
3
  import "@/utils/log"
4
4
  import "@/utils/colors"
5
5
  import "@/utils/tools"
6
+ import "@/utils/compat"
6
7
 
7
8
  reinstall_main() {
8
9
 
@@ -4,6 +4,7 @@ import "@/utils/npm"
4
4
  import "@/utils/log"
5
5
  import "@/utils/colors"
6
6
  import "@/utils/tools"
7
+ import "@/utils/compat"
7
8
 
8
9
  update_main() {
9
10
 
@@ -136,6 +136,25 @@ _validate_tool_installed() {
136
136
 
137
137
  # ---- FUNÇÕES DE LOTE ----
138
138
 
139
+ # Runs the Android compatibility layer over the binaries an AI tool puts on
140
+ # PATH. glibc-only binaries cannot be exec'd here - their PT_INTERP points at
141
+ # /lib/ld-linux-*.so.1, which Android does not have - until compat wraps them.
142
+ _ai_tool_compat_adapt() {
143
+ local id="$1" entry entry_id name bins
144
+ local -a bin_list=()
145
+ declare -f compat_adapt_installed >/dev/null 2>&1 || return 0
146
+ for entry in "${AI_TOOLS_REGISTRY[@]}"; do
147
+ IFS=':' read -r entry_id name bins <<<"$entry"
148
+ [[ "$entry_id" == "$id" ]] || continue
149
+ IFS=',' read -ra bin_list <<<"$bins"
150
+ for entry_id in "${bin_list[@]}"; do
151
+ compat_adapt_installed "$entry_id" || true
152
+ done
153
+ break
154
+ done
155
+ return 0
156
+ }
157
+
139
158
  _ai_tool_registered() {
140
159
  local wanted="$1"
141
160
  local entry id
@@ -184,13 +203,21 @@ _run_ai_tool_action() {
184
203
 
185
204
  if [[ "$action" != "reinstall" ]]; then
186
205
  declare -f "$func_name" &>/dev/null || return 127
187
- "$func_name"
188
- return $?
206
+ local rc=0
207
+ "$func_name" || rc=$?
208
+ if (( rc == 0 || rc == 2 )) && [[ "$action" == "install" || "$action" == "update" ]]; then
209
+ _ai_tool_compat_adapt "$id"
210
+ fi
211
+ return "$rc"
189
212
  fi
190
213
 
191
214
  if declare -f "$func_name" &>/dev/null; then
192
- "$func_name"
193
- return $?
215
+ local rc=0
216
+ "$func_name" || rc=$?
217
+ if (( rc == 0 )); then
218
+ _ai_tool_compat_adapt "$id"
219
+ fi
220
+ return "$rc"
194
221
  fi
195
222
 
196
223
  local uninstall_fn="uninstall_${id//-/_}"
@@ -1,13 +1,28 @@
1
1
  #!/usr/bin/env bash
2
2
  import "@/utils/npm"
3
3
  import "@/utils/npm-shebang"
4
+ import "@/utils/compat"
4
5
 
5
6
  import "@/utils/log"
7
+ import "@/utils/install"
6
8
  import "@/utils/version"
7
9
 
10
+ # Freebuff ships its launcher on npm and fetches the real runtime (a Bun
11
+ # standalone, ~50 MB) on first use. That runtime is a glibc ELF whose
12
+ # PT_INTERP, /lib/ld-linux-aarch64.so.1, Android does not have, so the kernel
13
+ # refuses to start it with ENOENT ("bad interpreter"). The fix is to hand it
14
+ # to the glibc loader instead - and never to patchelf it, which grows the image
15
+ # by a PT_LOAD segment and makes it SIGSEGV.
16
+ FREEBUFF_ORIGIN="${FREEBUFF_ORIGIN:-https://codebuff.com}"
17
+ FREEBUFF_DATA_DIR="${FREEBUFF_DATA_DIR:-$HOME/.config/manicode}"
18
+ # Line 2 of the generated npm entry wrapper, used to recognise our own copy.
19
+ FREEBUFF_ENTRY_MARKER="# karnel-freebuff-entry"
20
+
8
21
  install_freebuff() {
9
22
  if command -v freebuff &>/dev/null; then
10
23
  log_info "Freebuff is already installed"
24
+ _fix_freebuff_runtime || true
25
+ _install_freebuff_entry || true
11
26
  return 2
12
27
  fi
13
28
 
@@ -25,21 +40,224 @@ install_freebuff() {
25
40
  }
26
41
  _fix_npm_shebang "freebuff" || return 1
27
42
  _fix_freebuff_shebang || return 1
43
+ _fix_freebuff_runtime || return 1
44
+ _install_freebuff_entry || return 1
28
45
  log_success "Freebuff installed"
29
46
  }
30
47
 
31
48
  _fix_freebuff_shebang() {
32
- local binary
49
+ local binary real
33
50
  binary="$(command -v freebuff 2>/dev/null)"
34
51
  if [[ -z "$binary" || ! -f "$binary" ]]; then
35
52
  log_error "Freebuff binary was not found after npm operation"
36
53
  return 1
37
54
  fi
38
- if command -v termux-fix-shebang &>/dev/null; then
39
- termux-fix-shebang "$binary" &>/dev/null || return 1
55
+ real="$(readlink -f -- "$binary" 2>/dev/null || printf '%s' "$binary")"
56
+ [[ -f "$real" ]] || return 0
57
+ # 2 = already runnable, 1 = nothing to resolve; neither is fatal here,
58
+ # _fix_npm_shebang has already normalised the npm shim.
59
+ compat_fix_shebang "$real" || true
60
+ return 0
61
+ }
62
+
63
+ # Prepares the runtime: fetch it when it is missing, wrap it so the glibc
64
+ # loader can start it, and replace it when the copy on disk no longer runs.
65
+ _fix_freebuff_runtime() {
66
+ local data_bin="$FREEBUFF_DATA_DIR/freebuff"
67
+ if [[ ! -e "$data_bin" ]]; then
68
+ if ! _fetch_freebuff_binary "$data_bin"; then
69
+ log_warn "Freebuff runtime was not fetched (offline?); it will be prepared on the next install"
70
+ return 0
71
+ fi
72
+ fi
73
+ compat_adapt "$data_bin"
74
+ if _freebuff_binary_runs "$data_bin"; then
75
+ return 0
76
+ fi
77
+ log_info "Freebuff runtime does not run; downloading it again"
78
+ if ! _replace_freebuff_binary "$data_bin"; then
79
+ log_error "Freebuff runtime could not be repaired"
80
+ return 1
81
+ fi
82
+ if ! _freebuff_binary_runs "$data_bin"; then
83
+ log_error "Freebuff runtime still does not run after repair"
84
+ return 1
85
+ fi
86
+ return 0
87
+ }
88
+
89
+ _freebuff_binary_runs() {
90
+ local bin="$1" rc=0
91
+ [[ -e "$bin" ]] || return 1
92
+ # The braces redirect the shell itself: bash prints "Segmentation fault" to
93
+ # its own stderr, not to the child's, so `2>/dev/null` on the command alone
94
+ # would still leak the crash of a damaged runtime into the install output.
95
+ { timeout 30 "$bin" --version >/dev/null 2>&1; rc=$?; } 2>/dev/null
96
+ ((rc == 0))
97
+ }
98
+
99
+ _freebuff_target_key() {
100
+ case "$(uname -m)" in
101
+ aarch64 | arm64) printf 'linux-arm64\n' ;;
102
+ x86_64 | amd64) printf 'linux-x64\n' ;;
103
+ *) return 1 ;;
104
+ esac
105
+ }
106
+
107
+ _freebuff_package_json() {
108
+ local bin candidate
109
+ if bin="$(command -v freebuff 2>/dev/null)" && [[ -n "$bin" ]]; then
110
+ bin="$(readlink -f -- "$bin" 2>/dev/null || printf '%s' "$bin")"
111
+ candidate="$(dirname -- "$bin")/package.json"
112
+ if [[ -f "$candidate" ]]; then
113
+ printf '%s\n' "$candidate"
114
+ return 0
115
+ fi
116
+ fi
117
+ candidate="$(karnel_npm root -g 2>/dev/null | tail -n 1)/freebuff/package.json"
118
+ if [[ -f "$candidate" ]]; then
119
+ printf '%s\n' "$candidate"
120
+ return 0
121
+ fi
122
+ return 1
123
+ }
124
+
125
+ # Downloads and checksum-verifies the published runtime, then moves it into
126
+ # place. The archive digest comes from the npm package's own binaryChecksums
127
+ # map, so a substituted download is rejected before anything is executed.
128
+ _fetch_freebuff_binary() {
129
+ local dest="$1"
130
+ local pkg_json version key expected url archive stage
131
+ pkg_json="$(_freebuff_package_json)" || {
132
+ log_error "freebuff package metadata not found"
133
+ return 1
134
+ }
135
+ version="$(node -p "require('$pkg_json').version" 2>/dev/null)"
136
+ [[ -n "$version" ]] || {
137
+ log_error "Could not read the freebuff version"
138
+ return 1
139
+ }
140
+ key="$(_freebuff_target_key)" || {
141
+ log_error "Freebuff has no runtime for $(uname -m)"
142
+ return 1
143
+ }
144
+ expected="$(node -p "(require('$pkg_json').binaryChecksums||{})['$key']||''" 2>/dev/null)"
145
+ [[ "$expected" =~ ^[0-9a-f]{64}$ ]] || {
146
+ log_error "No published checksum for freebuff $key"
147
+ return 1
148
+ }
149
+ url="$FREEBUFF_ORIGIN/api/releases/download/$version/freebuff-$key.tar.gz"
150
+ archive="$(mktemp "${TMPDIR:-$PREFIX/tmp}/freebuff.XXXXXX")" || return 1
151
+ if ! curl --fail --silent --show-error --location --retry 3 "$url" \
152
+ -o "$archive" &>>"$LOG_FILE"; then
153
+ rm -f -- "$archive"
154
+ log_error "Failed to download the Freebuff runtime"
155
+ return 1
156
+ fi
157
+ if ! verify_sha256 "$archive" "$expected"; then
158
+ rm -f -- "$archive"
159
+ return 1
160
+ fi
161
+ stage="$(mktemp -d "${TMPDIR:-$PREFIX/tmp}/freebuff-stage.XXXXXX")" || {
162
+ rm -f -- "$archive"
163
+ return 1
164
+ }
165
+ if ! safe_extract_tar "$archive" "$stage"; then
166
+ rm -rf -- "$stage" "$archive"
167
+ return 1
168
+ fi
169
+ rm -f -- "$archive"
170
+ if [[ ! -f "$stage/freebuff" ]]; then
171
+ rm -rf -- "$stage"
172
+ log_error "Freebuff runtime missing from the archive"
173
+ return 1
174
+ fi
175
+ chmod 755 -- "$stage/freebuff" || {
176
+ rm -rf -- "$stage"
177
+ return 1
178
+ }
179
+ if [[ -e "$dest" || -L "$dest" ]]; then
180
+ rm -f -- "$dest" "$dest.karnel-real" || {
181
+ rm -rf -- "$stage"
182
+ return 1
183
+ }
184
+ fi
185
+ mv -f -- "$stage/freebuff" "$dest" || {
186
+ rm -rf -- "$stage"
187
+ return 1
188
+ }
189
+ rm -rf -- "$stage"
190
+ return 0
191
+ }
192
+
193
+ # Swaps a runtime that no longer runs for a freshly verified copy.
194
+ _replace_freebuff_binary() {
195
+ local data_bin="$1"
196
+ local staging
197
+ staging="$(mktemp -d "${TMPDIR:-$PREFIX/tmp}/freebuff-repair.XXXXXX")" || return 1
198
+ if ! _fetch_freebuff_binary "$staging/freebuff"; then
199
+ rm -rf -- "$staging"
200
+ return 1
201
+ fi
202
+ rm -f -- "$data_bin" "$data_bin.karnel-real" || {
203
+ rm -rf -- "$staging"
204
+ return 1
205
+ }
206
+ mv -f -- "$staging/freebuff" "$data_bin" || {
207
+ rm -rf -- "$staging"
208
+ return 1
209
+ }
210
+ rm -rf -- "$staging"
211
+ compat_adapt "$data_bin"
212
+ return 0
213
+ }
214
+
215
+ # npm links $PREFIX/bin/freebuff to a launcher that derives its release
216
+ # target from `process.platform`, which Node reports as "android" here - so it
217
+ # refuses every download with `Unsupported platform: android arm64` before the
218
+ # runtime is ever spawned. The launcher already honours FREEBUFF_BINARY_TARGET,
219
+ # so the entry is replaced by a wrapper that exports the supported key; no
220
+ # launcher code is patched and the real entry point keeps working.
221
+ _install_freebuff_entry() {
222
+ local entry="$PREFIX/bin/freebuff" key real tmp
223
+ key="$(_freebuff_target_key)" || {
224
+ log_error "Freebuff has no runtime for $(uname -m)"
225
+ return 1
226
+ }
227
+ if [[ -L "$entry" ]]; then
228
+ real="$(readlink -f -- "$entry")"
229
+ elif [[ -f "$entry" ]] && head -n 4 -- "$entry" | grep -qF "$FREEBUFF_ENTRY_MARKER"; then
230
+ real="$(sed -n 's/^# real: //p' -- "$entry" | head -n 1)"
40
231
  else
41
- sed -i '1s|#!/usr/bin/env|#!/data/data/com.termux/files/usr/bin/env|' "$binary" || return 1
232
+ real="$(karnel_npm root -g 2>/dev/null | tail -n 1)/freebuff/index.js"
42
233
  fi
234
+ if [[ -z "$real" || ! -f "$real" ]]; then
235
+ log_error "The freebuff launcher was not found"
236
+ return 1
237
+ fi
238
+ tmp="$entry.tmp.$$"
239
+ {
240
+ printf '%s\n' "#!$PREFIX/bin/bash"
241
+ printf '%s\n' "$FREEBUFF_ENTRY_MARKER"
242
+ printf '%s\n' "# real: $real"
243
+ printf '%s\n' "# Node reports process.platform \"android\" on Termux, so the launcher"
244
+ printf '%s\n' "# resolves its release target to android-arm64 and no build is published"
245
+ printf '%s\n' "# for it. The launcher honours this override, so no code is patched."
246
+ printf 'export FREEBUFF_BINARY_TARGET=%q\n' "$key"
247
+ printf 'exec %q %q "$@"\n' "$PREFIX/bin/node" "$real"
248
+ } >"$tmp" || {
249
+ rm -f -- "$tmp"
250
+ return 1
251
+ }
252
+ chmod 755 -- "$tmp" || {
253
+ rm -f -- "$tmp"
254
+ return 1
255
+ }
256
+ mv -f -- "$tmp" "$entry" || {
257
+ rm -f -- "$tmp"
258
+ return 1
259
+ }
260
+ return 0
43
261
  }
44
262
 
45
263
  uninstall_freebuff() {
@@ -69,6 +287,8 @@ _do_update_freebuff() {
69
287
  return 1
70
288
  }
71
289
  _fix_freebuff_shebang
290
+ _fix_freebuff_runtime
291
+ _install_freebuff_entry
72
292
  }
73
293
 
74
294
  reinstall_freebuff() {
@@ -0,0 +1,543 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Android <-> glibc compatibility layer.
4
+ #
5
+ # Termux runs bionic, has no /usr and cannot start a stock Linux ELF: the
6
+ # PT_INTERP recorded in the binary (/lib/ld-linux-aarch64.so.1) does not
7
+ # exist, execve() answers ENOENT and the shell reports 127. The glibc-repo
8
+ # package ships a full aarch64 glibc under $PREFIX/glibc whose loader is an
9
+ # ordinary executable, so the very same binary starts natively when it is
10
+ # handed to that loader instead of to the kernel:
11
+ #
12
+ # ld-linux-aarch64.so.1 --library-path <sysroot>/lib:<dir of binary> <binary> [args]
13
+ #
14
+ # That form is preferred over `patchelf --set-interpreter`: patchelf grows Bun
15
+ # standalone builds by a PT_LOAD segment and they then SIGSEGV, while the
16
+ # loader form leaves the file byte-identical, needs no re-patching on upgrade
17
+ # and is reversible. Everything below is idempotent so installers can call it
18
+ # after install, after update and from doctor without bookkeeping.
19
+ #
20
+ # A bare loader is not always enough, so a wrapper can be escalated through
21
+ # three tiers and the cheapest one that actually starts the binary wins:
22
+ #
23
+ # 2 loader hand the ELF to the glibc loader. No userland, no
24
+ # proot, no download. Covers most standalone binaries.
25
+ # 3 glibc userland same, plus $PREFIX/glibc/bin (383 glibc tools) in
26
+ # PATH and the glibc termux-exec preload so that child
27
+ # processes the tool spawns resolve the same way.
28
+ # 4 proot a synthetic FHS root assembled from the sysroot that
29
+ # is already installed, bound at /lib, /usr and /etc.
30
+ # This is what a tool needs when it hardcodes
31
+ # /usr/share or /etc/... -- and unlike
32
+ # `proot-distro install ubuntu` it downloads nothing.
33
+ #
34
+ # compat_adapt probes after wrapping and climbs the ladder; every step is
35
+ # reversible through compat_unwrap.
36
+
37
+ # Marker written on line 2 of every generated wrapper, tier on line 3.
38
+ COMPAT_WRAPPER_MARKER="# karnel-compat-wrapper"
39
+ COMPAT_WRAPPER_TIER_MARKER="# karnel-compat-tier:"
40
+ COMPAT_TIER_LOADER=2
41
+ COMPAT_TIER_USERLAND=3
42
+ COMPAT_TIER_PROOT=4
43
+
44
+ compat_glibc_root() { printf '%s\n' "${KARNEL_GLIBC_ROOT:-$PREFIX/glibc}"; }
45
+
46
+ compat_glibc_libdir() { printf '%s\n' "$(compat_glibc_root)/lib"; }
47
+
48
+ compat_glibc_bindir() { printf '%s\n' "$(compat_glibc_root)/bin"; }
49
+
50
+ # Prints the path of a usable glibc dynamic loader, if one is installed.
51
+ compat_glibc_loader() {
52
+ local libdir candidate
53
+ libdir="$(compat_glibc_libdir)"
54
+ [[ -d "$libdir" ]] || return 1
55
+ for candidate in "$libdir"/ld-*.so*; do
56
+ [[ -f "$candidate" && -x "$candidate" ]] || continue
57
+ printf '%s\n' "$candidate"
58
+ return 0
59
+ done
60
+ return 1
61
+ }
62
+
63
+ compat_glibc_ready() {
64
+ compat_glibc_loader >/dev/null || return 1
65
+ [[ -e "$(compat_glibc_libdir)/libc.so.6" ]]
66
+ }
67
+
68
+ # Installs the glibc sysroot on demand. Safe to call repeatedly.
69
+ compat_glibc_ensure() {
70
+ compat_glibc_ready && return 0
71
+ if ! command -v pkg >/dev/null 2>&1; then
72
+ log_error "pkg is required to install the glibc compatibility layer"
73
+ return 1
74
+ fi
75
+ if [[ ! -f "$PREFIX/etc/apt/sources.list.d/glibc.list" ]]; then
76
+ if ! yes | pkg install glibc-repo &>>"${LOG_FILE:-/dev/null}"; then
77
+ log_error "Failed to install glibc-repo"
78
+ return 1
79
+ fi
80
+ fi
81
+ if [[ ! -e "$(compat_glibc_libdir)/libc.so.6" ]]; then
82
+ if ! yes | pkg install glibc &>>"${LOG_FILE:-/dev/null}"; then
83
+ log_error "Failed to install glibc"
84
+ return 1
85
+ fi
86
+ fi
87
+ if ! compat_glibc_ready; then
88
+ log_error "glibc sysroot is still missing after installation"
89
+ return 1
90
+ fi
91
+ return 0
92
+ }
93
+
94
+ compat_is_elf() {
95
+ local file="$1" magic
96
+ [[ -f "$file" && -r "$file" ]] || return 1
97
+ magic="$(head -c 4 -- "$file" 2>/dev/null)" || return 1
98
+ [[ "$magic" == $'\x7fELF' ]]
99
+ }
100
+
101
+ # Prints the PT_INTERP of an ELF, or nothing when it is statically linked.
102
+ # readelf (binutils) rejects a bare `--`, so the path is normalised instead.
103
+ compat_elf_interp() {
104
+ local file="$1" real
105
+ command -v readelf >/dev/null 2>&1 || return 1
106
+ real="$(readlink -f -- "$file" 2>/dev/null || printf '%s' "$file")"
107
+ readelf -l "$real" 2>/dev/null |
108
+ awk '/Requesting program interpreter/ { gsub(/\].*/, "", $NF); print $NF; exit }'
109
+ }
110
+
111
+ # glibc binaries link libc.so.6; bionic links libc.so. A fully static ELF has
112
+ # neither and runs anywhere, so it is not classified as glibc.
113
+ compat_is_glibc_elf() {
114
+ local file="$1" real
115
+ compat_is_elf "$file" || return 1
116
+ command -v readelf >/dev/null 2>&1 || return 1
117
+ real="$(readlink -f -- "$file" 2>/dev/null || printf '%s' "$file")"
118
+ readelf -d "$real" 2>/dev/null | grep -q 'Shared library: \[libc\.so\.6\]'
119
+ }
120
+
121
+ # native | glibc | script | missing | unknown
122
+ compat_classify() {
123
+ local file="$1" first
124
+ [[ -e "$file" ]] || { printf 'missing\n'; return 0; }
125
+ if compat_is_elf "$file"; then
126
+ if compat_is_glibc_elf "$file"; then
127
+ printf 'glibc\n'
128
+ else
129
+ printf 'native\n'
130
+ fi
131
+ return 0
132
+ fi
133
+ if [[ -f "$file" && -r "$file" ]]; then
134
+ IFS= read -r first <"$file" || true
135
+ if [[ "$first" == '#!'* ]]; then
136
+ printf 'script\n'
137
+ return 0
138
+ fi
139
+ fi
140
+ printf 'unknown\n'
141
+ }
142
+
143
+ # Search path handed to the loader: the glibc sysroot first (proven to work),
144
+ # then the binary's own directory so bundled shared objects still resolve. The
145
+ # directory is skipped when it ships its own libc.so.6, which would be a bionic
146
+ # libc and would take the glibc loader down with it.
147
+ compat_library_path() {
148
+ local real="$1" sysroot dir out
149
+ sysroot="$(compat_glibc_libdir)"
150
+ dir="$(dirname -- "$real")"
151
+ out="$sysroot"
152
+ if [[ "$dir" != "$sysroot" && ! -e "$dir/libc.so.6" ]]; then
153
+ out="$out:$dir"
154
+ fi
155
+ printf '%s\n' "$out"
156
+ }
157
+
158
+ # Runs a tool through whichever layer can actually execute it. Used by the
159
+ # generated wrappers and by doctor probes; never mutates the file.
160
+ compat_exec() {
161
+ local file="$1"
162
+ shift
163
+ local kind real loader
164
+ kind="$(compat_classify "$file")"
165
+ case "$kind" in
166
+ missing)
167
+ log_error "compat: $file does not exist"
168
+ return 127
169
+ ;;
170
+ glibc)
171
+ loader="$(compat_glibc_loader)" || {
172
+ log_error "compat: no glibc loader; run 'pkg install glibc-repo glibc'"
173
+ return 127
174
+ }
175
+ real="$(readlink -f -- "$file" 2>/dev/null || printf '%s' "$file")"
176
+ exec "$loader" --library-path "$(compat_library_path "$real")" "$real" "$@"
177
+ ;;
178
+ *)
179
+ exec "$file" "$@"
180
+ ;;
181
+ esac
182
+ }
183
+
184
+ _compat_is_wrapper() {
185
+ local file="$1"
186
+ [[ -f "$file" ]] || return 1
187
+ head -n 2 -- "$file" 2>/dev/null | grep -qF "$COMPAT_WRAPPER_MARKER"
188
+ }
189
+
190
+ # The tier recorded on line 3 of a wrapper, or 0 when it predates tiers.
191
+ compat_wrapper_tier() {
192
+ local file="$1" line
193
+ _compat_is_wrapper "$file" || { printf '0\n'; return 1; }
194
+ line="$(sed -n '3p' -- "$file" 2>/dev/null || true)"
195
+ case "$line" in
196
+ "$COMPAT_WRAPPER_TIER_MARKER"*)
197
+ line="${line#*:}"
198
+ # ${line#*:} keeps the space after the colon; strip it or the caller
199
+ # compares " 2" against 2 and the tiers never match.
200
+ line="${line#"${line%%[![:space:]]*}"}"
201
+ printf '%s\n' "$line"
202
+ ;;
203
+ *)
204
+ printf '2\n'
205
+ ;;
206
+ esac
207
+ }
208
+
209
+ # Builds (once) a synthetic filesystem root out of the glibc sysroot that is
210
+ # already on the device. proot then maps /lib, /usr and /etc onto it, which is
211
+ # everything a binary that hardcodes those paths needs -- for a few kilobytes
212
+ # of symlinks instead of the hundreds of megabytes of a distro image.
213
+ compat_proot_root() {
214
+ local root glibc lib
215
+ [[ -n "${KARNEL_DATA:-}" || -n "${KARNEL_COMPAT_ROOT:-}" ]] || return 1
216
+ root="${KARNEL_COMPAT_ROOT:-$KARNEL_DATA/compat-root}"
217
+ glibc="$(compat_glibc_root)"
218
+ lib="$(compat_glibc_libdir)"
219
+ [[ -d "$lib" && -d "$glibc" ]] || return 1
220
+ if [[ ! -f "$root/.ready" ]]; then
221
+ # Plain directories, not symlinks: every one of them is a bind target and
222
+ # proot needs the mount point to exist before it can cover it.
223
+ mkdir -p -- "$root/lib" "$root/lib64" "$root/usr/lib" "$root/usr/bin" \
224
+ "$root/usr/share" "$root/etc" "$root/tmp" "$root/home" "$root/proc" \
225
+ "$root/dev" || return 1
226
+ # Overlay the sysroot onto every path a stock Linux ELF is likely to open.
227
+ # proot resolves these as binds, so the host directories stay
228
+ # authoritative and nothing is copied.
229
+ printf '%s\n' "$glibc" >"$root/.ready" || return 1
230
+ fi
231
+ printf '%s\n' "$root"
232
+ }
233
+
234
+ compat_proot_available() {
235
+ command -v proot >/dev/null 2>&1 || return 1
236
+ compat_proot_root >/dev/null || return 1
237
+ return 0
238
+ }
239
+
240
+ # 0 when the binary starts, 1 when it demonstrably does not.
241
+ # A timeout counts as success: the process was created, it simply did not
242
+ # exit, which is exactly what an interactive tool does with --version.
243
+ compat_probe() {
244
+ local wrapper="$1" out rc
245
+ [[ -x "$wrapper" ]] || return 1
246
+ out="$(timeout "${COMPAT_PROBE_TIMEOUT:-15}" "$wrapper" --version 2>&1)"
247
+ rc=$?
248
+ if ((rc == 124)); then
249
+ return 0
250
+ fi
251
+ if ((rc == 125 || rc == 126 || rc == 127 || rc >= 128)); then
252
+ return 1
253
+ fi
254
+ if printf '%s' "$out" | grep -qiE \
255
+ 'bad interpreter|error while loading shared libraries|cannot execute binary|exec format error'; then
256
+ return 1
257
+ fi
258
+ return 0
259
+ }
260
+
261
+ _compat_write_wrapper() {
262
+ local wrapper="$1" real="$2" loader="$3" tier="${4:-$COMPAT_TIER_LOADER}"
263
+ local libpath bash_bin root="" proot_bin=""
264
+ libpath="$(compat_library_path "$real")"
265
+ bash_bin="$(command -v bash 2>/dev/null)" || bash_bin="$PREFIX/bin/bash"
266
+ # Resolve every tier-4 dependency before the redirect opens, otherwise a
267
+ # refusal below would leave a half-written file behind at $wrapper.
268
+ if [[ "$tier" == "$COMPAT_TIER_PROOT" ]]; then
269
+ root="$(compat_proot_root)" || return 1
270
+ proot_bin="$(command -v proot 2>/dev/null)" || return 1
271
+ fi
272
+ {
273
+ printf '%s\n' "#!$bash_bin"
274
+ printf '%s\n' "$COMPAT_WRAPPER_MARKER"
275
+ printf '%s\n' "$COMPAT_WRAPPER_TIER_MARKER $tier"
276
+ case "$tier" in
277
+ "$COMPAT_TIER_USERLAND")
278
+ # The $root/$PATH/$LD_PRELOAD below must reach the wrapper verbatim.
279
+ # shellcheck disable=SC2016
280
+ printf '%s\n' "# Tier 3: the glibc loader plus the glibc userland, so the child"
281
+ printf '%s\n' "# processes this tool spawns find the same coreutils and exec shim."
282
+ # Everything below is emitted literally and expanded by the wrapper at
283
+ # runtime, where "root" is the assignment written on the previous line.
284
+ printf 'root=%q\n' "$(compat_glibc_root)"
285
+ # shellcheck disable=SC2016 # expands in the wrapper
286
+ printf 'export PATH="$root/bin:$PATH"\n'
287
+ # The glibc termux-exec shim is optional; a preload pointing nowhere
288
+ # makes ld.so complain on every start of the tool.
289
+ # shellcheck disable=SC2016 # expands in the wrapper
290
+ printf 'if [ -f "$root/lib/libtermux-exec.so" ]; then\n'
291
+ # shellcheck disable=SC2016 # expands in the wrapper
292
+ printf ' export LD_PRELOAD="$root/lib/libtermux-exec.so${LD_PRELOAD:+:$LD_PRELOAD}"\n'
293
+ printf 'fi\n'
294
+ printf 'exec %q --library-path %q %q "$@"\n' "$loader" "$libpath" "$real"
295
+ ;;
296
+ "$COMPAT_TIER_PROOT")
297
+ printf '%s\n' "# Tier 4: a synthetic FHS root built from the installed glibc sysroot,"
298
+ printf '%s\n' "# for binaries that open /usr/... or /etc/... by absolute path."
299
+ printf 'exec %q -r %q \\
300
+ -b %q:/lib -b %q:/lib64 -b %q:/usr/lib -b %q:/usr/bin \\
301
+ -b %q:/usr/share -b %q:/etc -b %q -b /dev -b /proc \\
302
+ %q "$@"\n' \
303
+ "$proot_bin" "$root" \
304
+ "$(compat_glibc_libdir)" "$(compat_glibc_libdir)" "$(compat_glibc_libdir)" \
305
+ "$(compat_glibc_bindir)" "$(compat_glibc_root)/share" "$(compat_glibc_root)/etc" \
306
+ "$PREFIX" "$real"
307
+ ;;
308
+ *)
309
+ printf '%s\n' "# Tier 2: runs a glibc ELF through the Termux glibc loader. Android has"
310
+ printf '%s\n' "# no /lib/ld-linux-*.so.1, so the kernel cannot start this binary itself."
311
+ printf 'exec %q --library-path %q %q "$@"\n' "$loader" "$libpath" "$real"
312
+ ;;
313
+ esac
314
+ } >"$wrapper" || return 1
315
+ chmod 755 -- "$wrapper" 2>/dev/null || true
316
+ return 0
317
+ }
318
+
319
+ # Replaces a glibc ELF with a launcher that hands it to the glibc loader.
320
+ # The original is kept beside it as <path>.karnel-real, so the change is
321
+ # reversible and survives `head`/`file` style checks. Returns 1 when the file
322
+ # is not a glibc ELF (nothing to adapt) and 0 when it is already adapted.
323
+ compat_wrap() {
324
+ local target="$1" tier="${2:-$COMPAT_TIER_LOADER}"
325
+ local real stored loader current
326
+ [[ -e "$target" || -L "$target" ]] || return 1
327
+ real="$(readlink -f -- "$target" 2>/dev/null || printf '%s' "$target")"
328
+ [[ -f "$real" ]] || return 1
329
+ if _compat_is_wrapper "$real"; then
330
+ current="$(compat_wrapper_tier "$real")"
331
+ [[ "$current" == "$tier" ]] && return 0
332
+ # Re-tier an existing wrapper: the original is already parked next to it,
333
+ # so only the launcher changes.
334
+ stored="$real.karnel-real"
335
+ [[ -e "$stored" ]] || return 1
336
+ loader="$(compat_glibc_loader)" || loader="/lib/ld-linux-aarch64.so.1"
337
+ _compat_write_wrapper "$real" "$stored" "$loader" "$tier" || return 1
338
+ log_info "compat: $real retiered to level $tier"
339
+ return 0
340
+ fi
341
+ compat_is_glibc_elf "$real" || return 1
342
+ loader="$(compat_glibc_loader)" || return 1
343
+ stored="$real.karnel-real"
344
+ if [[ -e "$stored" ]]; then
345
+ log_error "compat: refusing to overwrite existing $stored"
346
+ return 1
347
+ fi
348
+ if ! mv -- "$real" "$stored"; then
349
+ log_error "compat: could not move $real aside"
350
+ return 1
351
+ fi
352
+ if ! _compat_write_wrapper "$real" "$stored" "$loader" "$tier"; then
353
+ mv -- "$stored" "$real" 2>/dev/null || true
354
+ log_error "compat: could not write wrapper for $real"
355
+ return 1
356
+ fi
357
+ log_info "compat: $real now runs through the glibc loader"
358
+ return 0
359
+ }
360
+
361
+ compat_unwrap() {
362
+ local target="$1" real stored
363
+ real="$(readlink -f -- "$target" 2>/dev/null || printf '%s' "$target")"
364
+ _compat_is_wrapper "$real" || return 1
365
+ stored="$real.karnel-real"
366
+ [[ -e "$stored" ]] || return 1
367
+ rm -f -- "$real" || return 1
368
+ mv -- "$stored" "$real" || return 1
369
+ log_info "compat: restored $real"
370
+ return 0
371
+ }
372
+
373
+ # One-shot adaptation used after every install: wraps glibc binaries, leaves
374
+ # native binaries, scripts and text files untouched, and never fails an
375
+ # install over a tool that does not need adapting.
376
+ compat_adapt() {
377
+ local target="$1"
378
+ [[ -e "$target" ]] || return 0
379
+ compat_wrap "$target" || return 0
380
+ compat_escalate "$target"
381
+ return 0
382
+ }
383
+
384
+ # Probes a wrapped binary and climbs to the cheapest tier that starts it.
385
+ # Tier 2 is the default because it is free and needs nothing installed; tier 3
386
+ # only adds environment, tier 4 needs proot. Falls back to the best tier that
387
+ # was reachable so the caller always ends with the least capable wrapper that
388
+ # still has a chance. Never returns non-zero: adaptation must not fail an
389
+ # install.
390
+ compat_escalate() {
391
+ local target="$1" real tier="$COMPAT_TIER_LOADER"
392
+ real="$(readlink -f -- "$target" 2>/dev/null || printf '%s' "$target")"
393
+ _compat_is_wrapper "$real" || return 0
394
+
395
+ compat_probe "$real" && return 0
396
+
397
+ if compat_wrap "$real" "$COMPAT_TIER_USERLAND" && compat_probe "$real"; then
398
+ log_info "compat: $real needs the glibc userland"
399
+ return 0
400
+ fi
401
+
402
+ if compat_proot_available && compat_wrap "$real" "$COMPAT_TIER_PROOT" && compat_probe "$real"; then
403
+ log_info "compat: $real needs an FHS root, running it under proot"
404
+ return 0
405
+ fi
406
+
407
+ # Nothing higher worked; drop back to the plain loader so the tool is at
408
+ # least in its original, reversible state.
409
+ compat_wrap "$real" "$COMPAT_TIER_LOADER" || true
410
+ log_warn "compat: $real does not start at any tier"
411
+ return 0
412
+ }
413
+
414
+ # Rewrites an unrunnable shebang onto a Termux interpreter. Android has no
415
+ # /usr/bin/env, so `#!/usr/bin/env node` dies with "bad interpreter" even
416
+ # though node itself is installed.
417
+ # Returns 0 when the file was changed, 2 when it already ran, 1 when no
418
+ # interpreter could be resolved.
419
+ compat_fix_shebang() {
420
+ local file="$1" link="$1" first line interp prog args resolved new real
421
+ [[ -f "$file" && -r "$file" ]] || return 2
422
+ # Edit the target of a symlink, never the link itself: `sed -i` unlinks and
423
+ # re-creates its argument, so running it on $PREFIX/bin/npm would replace
424
+ # that symlink with a detached copy of npm-cli.js whose relative
425
+ # require('../lib/cli.js') then resolves from $PREFIX/bin instead of
426
+ # node_modules/npm. The target is what carries the shebang anyway.
427
+ if real="$(readlink -f -- "$file" 2>/dev/null)" && [[ -n "$real" && -f "$real" ]]; then
428
+ file="$real"
429
+ fi
430
+ [[ -f "$file" && -r "$file" ]] || return 2
431
+ IFS= read -r first <"$file" || true
432
+ [[ "$first" == '#!'* ]] || return 2
433
+ line="${first#\#!}"
434
+ line="${line# }"
435
+ interp="${line%% *}"
436
+ args="${line#"$interp"}"
437
+ args="${args# }"
438
+ if [[ "$interp" == */env ]]; then
439
+ prog="${args%% *}"
440
+ args="${args#"$prog"}"
441
+ args="${args# }"
442
+ else
443
+ prog="$interp"
444
+ fi
445
+ [[ -n "$prog" ]] || return 0
446
+ if [[ "$interp" == /* && -x "$interp" ]]; then
447
+ return 2
448
+ fi
449
+ resolved="$(command -v -- "$prog" 2>/dev/null || true)"
450
+ if [[ -z "$resolved" && "$prog" == */* ]]; then
451
+ resolved="$(command -v -- "${prog##*/}" 2>/dev/null || true)"
452
+ fi
453
+ [[ -n "$resolved" && -x "$resolved" ]] || return 1
454
+ [[ "$resolved" == "$interp" ]] && return 2
455
+ new="#!$resolved"
456
+ [[ -n "$args" ]] && new="$new $args"
457
+ new="${new//\\/\\\\}"
458
+ new="${new//&/\\&}"
459
+ new="${new//|/\\|}"
460
+ sed -i "1s|.*|$new|" -- "$file" || return 1
461
+ log_info "compat: fixed shebang of $link -> $new"
462
+ return 0
463
+ }
464
+
465
+ # Counts entries directly inside a directory whose interpreter path does not
466
+ # exist, which is the condition the kernel rejects with "bad interpreter" and
467
+ # exit 126. Read-only: nothing is opened for writing.
468
+ compat_count_broken_shebangs() {
469
+ local dir="${1:-$PREFIX/bin}" file first interp total=0
470
+ [[ -d "$dir" ]] || { printf '0\n'; return 0; }
471
+ for file in "$dir"/*; do
472
+ [[ -f "$file" && -r "$file" ]] || continue
473
+ IFS= read -r first <"$file" 2>/dev/null || continue
474
+ [[ "$first" == '#!'* ]] || continue
475
+ interp="${first#\#!}"
476
+ interp="${interp# }"
477
+ interp="${interp%% *}"
478
+ [[ "$interp" == /* ]] || continue
479
+ [[ -x "$interp" ]] || total=$((total + 1))
480
+ done
481
+ printf '%s\n' "$total"
482
+ }
483
+
484
+ # Repairs every shebang below a directory that cannot run on Android.
485
+ # Prints the number of files actually changed.
486
+ compat_fix_shebangs() {
487
+ local root="$1" file rc fixed=0
488
+ [[ -d "$root" ]] || { printf '0\n'; return 0; }
489
+ while IFS= read -r -d '' file; do
490
+ rc=0
491
+ compat_fix_shebang "$file" || rc=$?
492
+ if ((rc == 0)); then
493
+ fixed=$((fixed + 1))
494
+ fi
495
+ done < <(find "$root" -type f -size -2M -exec grep -IlZ '^#!' {} + 2>/dev/null)
496
+ printf '%s\n' "$fixed"
497
+ }
498
+
499
+ # Adapts whatever a tool has just put on PATH. Wraps a glibc ELF so the
500
+ # kernel can start it; leaves native binaries, scripts and text files alone.
501
+ # Always returns 0: adaptation is an optimisation, never a reason to fail an
502
+ # install or an update.
503
+ compat_adapt_installed() {
504
+ local tool="$1" path real
505
+ path="$(command -v -- "$tool" 2>/dev/null)" || return 0
506
+ [[ -n "$path" ]] || return 0
507
+ real="$(readlink -f -- "$path" 2>/dev/null || printf '%s' "$path")"
508
+ [[ -f "$real" && -x "$real" ]] || return 0
509
+ if _compat_is_wrapper "$real"; then
510
+ return 0
511
+ fi
512
+ if compat_is_glibc_elf "$real"; then
513
+ compat_wrap "$real" || true
514
+ fi
515
+ return 0
516
+ }
517
+
518
+ # Human-readable one-liner for doctor output.
519
+ compat_describe() {
520
+ local file="$1" kind interp real
521
+ real="$(readlink -f -- "$file" 2>/dev/null || printf '%s' "$file")"
522
+ if _compat_is_wrapper "$real"; then
523
+ case "$(compat_wrapper_tier "$real")" in
524
+ "$COMPAT_TIER_USERLAND") printf 'glibc (wrapped, loader + glibc userland)\n' ;;
525
+ "$COMPAT_TIER_PROOT") printf 'glibc (wrapped, proot FHS root)\n' ;;
526
+ *) printf 'glibc (wrapped, runs through the glibc loader)\n' ;;
527
+ esac
528
+ return 0
529
+ fi
530
+ kind="$(compat_classify "$file")"
531
+ case "$kind" in
532
+ glibc)
533
+ if interp="$(compat_elf_interp "$file")" && [[ -n "$interp" && ! -e "$interp" ]]; then
534
+ printf 'glibc (cannot run: missing interpreter %s)\n' "$interp"
535
+ else
536
+ printf 'glibc\n'
537
+ fi
538
+ ;;
539
+ native | script | missing | unknown)
540
+ printf '%s\n' "$kind"
541
+ ;;
542
+ esac
543
+ }
@@ -148,6 +148,9 @@ _run_tool_lifecycle_action() {
148
148
  if (( rc == 0 && protected )); then
149
149
  _mark_tool_owned "$module" "$tool" || return 1
150
150
  fi
151
+ if (( rc == 0 )); then
152
+ _tool_compat_adapt "$tool"
153
+ fi
151
154
  return "$rc"
152
155
  fi
153
156
 
@@ -164,9 +167,21 @@ _run_tool_lifecycle_action() {
164
167
  rm -f "$marker" || return 1
165
168
  fi
166
169
  fi
170
+ if (( rc == 0 )) && [[ "$action" == "install" || "$action" == "update" ]]; then
171
+ _tool_compat_adapt "$tool"
172
+ fi
167
173
  return "$rc"
168
174
  }
169
175
 
176
+ # Runs the Android compatibility layer over a tool that was just installed or
177
+ # updated, when the layer has been imported. glibc-only binaries cannot be
178
+ # exec'd on Android (their PT_INTERP does not exist) until they are wrapped.
179
+ _tool_compat_adapt() {
180
+ declare -f compat_adapt_installed >/dev/null 2>&1 || return 0
181
+ compat_adapt_installed "$1" || true
182
+ return 0
183
+ }
184
+
170
185
  _register_safe_reinstall_handlers() {
171
186
  local module="$1"
172
187
  shift
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karnel-termux",
3
- "version": "4.17.45",
3
+ "version": "4.18.0",
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"