karnel-termux 4.17.45 → 4.18.1
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 +2 -2
- package/docs/CHANGELOG.md +14 -0
- package/karnel/RELEASE_COMMIT +1 -1
- package/karnel/cli/commands/doctor/fixes.sh +8 -13
- package/karnel/cli/commands/doctor.sh +70 -10
- package/karnel/cli/commands/install.sh +5 -0
- package/karnel/cli/commands/reinstall.sh +1 -0
- package/karnel/cli/commands/update.sh +1 -0
- package/karnel/tools/ai/all.sh +31 -4
- package/karnel/tools/ai/freebuff/install.sh +224 -4
- package/karnel/utils/compat.sh +550 -0
- package/karnel/utils/tools.sh +15 -0
- package/package.json +1 -1
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.
|
|
11
|
+
<img src="https://img.shields.io/badge/version-4.18.1-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.
|
|
81
|
+
version=4.18.1
|
|
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,20 @@ layout: base
|
|
|
6
6
|
|
|
7
7
|
# Documentation Changelog
|
|
8
8
|
|
|
9
|
+
## 4.18.1
|
|
10
|
+
|
|
11
|
+
- **The ladder is reachable from every installer, not just Freebuff:** `compat_adapt_installed()` is the single entry point behind `_tool_compat_adapt` (utils/tools.sh), `_ai_tool_compat_adapt` (tools/ai/all.sh) and the post-install loop in `cli/commands/install.sh`, so it already covered all 16 categories — but it only ever called `compat_wrap()`, which stops at the loader tier. Tiers 3 (glibc userland) and 4 (proot FHS root) were reachable solely from Freebuff's `compat_adapt()` call, which is why no other tool could ever climb above tier 2. It now escalates after wrapping, and re-probes wrappers already on disk so a tool that stops starting at its current tier climbs on the next install or update instead of staying broken behind a launcher nobody re-examines.
|
|
12
|
+
- **No cost for what already runs:** native binaries, scripts and plain files still return before any probe, so the added `--version` probe applies only to tools that actually needed a wrapper — the hundreds of native entries in a Termux prefix are untouched.
|
|
13
|
+
- **Tests:** three new cases pin the behaviour (ladder reachable from the installer path, existing wrappers re-evaluated, natives never probed — the last with a positive control so its zero cannot pass vacuously). Each was proven to fail first: dropping the escalation fails 3 cases, probing natives fails the efficiency case alone, removing tier 3's `PATH` export fails the 3 ladder cases, and letting tier 4 write a wrapper without a root fails the refusal case alone. The suite runs 20 cases in 7 seconds.
|
|
14
|
+
|
|
15
|
+
## 4.18.0
|
|
16
|
+
|
|
17
|
+
- **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`.
|
|
18
|
+
- **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`.
|
|
19
|
+
- **`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.
|
|
20
|
+
- **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.
|
|
21
|
+
- **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).
|
|
22
|
+
|
|
9
23
|
## 4.17.45
|
|
10
24
|
|
|
11
25
|
- **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.
|
package/karnel/RELEASE_COMMIT
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
a046270feccbad261fd440a040fb960250c8e699
|
|
@@ -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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
if [[
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
|
784
|
-
fix_descriptions+=("
|
|
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"
|
package/karnel/tools/ai/all.sh
CHANGED
|
@@ -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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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,550 @@
|
|
|
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
|
+
# Already wrapped: re-probe it, so a binary that later stops starting at
|
|
511
|
+
# this tier climbs instead of being handed to the user as-is. Escalation
|
|
512
|
+
# is what every installer reaches through this function; only freebuff
|
|
513
|
+
# calls compat_adapt() directly, so without this the glibc userland and
|
|
514
|
+
# proot tiers would stay unreachable for the rest of the catalog.
|
|
515
|
+
compat_escalate "$real" || true
|
|
516
|
+
return 0
|
|
517
|
+
fi
|
|
518
|
+
if compat_is_glibc_elf "$real"; then
|
|
519
|
+
compat_wrap "$real" || true
|
|
520
|
+
compat_escalate "$real" || true
|
|
521
|
+
fi
|
|
522
|
+
return 0
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# Human-readable one-liner for doctor output.
|
|
526
|
+
compat_describe() {
|
|
527
|
+
local file="$1" kind interp real
|
|
528
|
+
real="$(readlink -f -- "$file" 2>/dev/null || printf '%s' "$file")"
|
|
529
|
+
if _compat_is_wrapper "$real"; then
|
|
530
|
+
case "$(compat_wrapper_tier "$real")" in
|
|
531
|
+
"$COMPAT_TIER_USERLAND") printf 'glibc (wrapped, loader + glibc userland)\n' ;;
|
|
532
|
+
"$COMPAT_TIER_PROOT") printf 'glibc (wrapped, proot FHS root)\n' ;;
|
|
533
|
+
*) printf 'glibc (wrapped, runs through the glibc loader)\n' ;;
|
|
534
|
+
esac
|
|
535
|
+
return 0
|
|
536
|
+
fi
|
|
537
|
+
kind="$(compat_classify "$file")"
|
|
538
|
+
case "$kind" in
|
|
539
|
+
glibc)
|
|
540
|
+
if interp="$(compat_elf_interp "$file")" && [[ -n "$interp" && ! -e "$interp" ]]; then
|
|
541
|
+
printf 'glibc (cannot run: missing interpreter %s)\n' "$interp"
|
|
542
|
+
else
|
|
543
|
+
printf 'glibc\n'
|
|
544
|
+
fi
|
|
545
|
+
;;
|
|
546
|
+
native | script | missing | unknown)
|
|
547
|
+
printf '%s\n' "$kind"
|
|
548
|
+
;;
|
|
549
|
+
esac
|
|
550
|
+
}
|
package/karnel/utils/tools.sh
CHANGED
|
@@ -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