karnel-termux 4.9.0 → 4.9.2
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/karnel/tools/ai/cline/bin/cline +28 -0
- package/karnel/tools/ai/cline/helper/cline_helper.c +54 -0
- package/karnel/tools/ai/gga/termux.patch +70 -0
- package/karnel/tools/lang/bun/bin/bunx +27 -0
- package/karnel/tools/lang/bun/src/bun-android-shim.c +481 -0
- package/karnel/tools/lang/bun/src/bun-bundle.c +96 -0
- package/karnel/utils/env.sh +1 -1
- package/karnel/utils/log.sh +16 -8
- package/karnel/utils/version.sh +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/data/data/com.termux/files/usr/bin/bash
|
|
2
|
+
|
|
3
|
+
UBUNTU_ROOTFS="__UBUNTU_ROOTFS__"
|
|
4
|
+
|
|
5
|
+
EXCLUDE_REGEX="^(PATH|LD_PRELOAD|LD_LIBRARY_PATH|PREFIX|HOME|PWD|OLDPWD|SHELL|IFS|_|SHLVL|PROMPT_COMMAND|TERMCAP|LS_COLORS|TERM)="
|
|
6
|
+
|
|
7
|
+
ENV_ARGS=()
|
|
8
|
+
while IFS= read -r line; do
|
|
9
|
+
if [[ -n "$line" && ! "$line" =~ $EXCLUDE_REGEX ]]; then
|
|
10
|
+
ENV_ARGS+=("--env" "$line")
|
|
11
|
+
fi
|
|
12
|
+
done < <(env)
|
|
13
|
+
|
|
14
|
+
ENV_ARGS+=(
|
|
15
|
+
"--env" "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"
|
|
16
|
+
"--env" "TERM=$TERM"
|
|
17
|
+
"--env" "HOME=/root"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
unset LD_PRELOAD
|
|
21
|
+
proot-distro login \
|
|
22
|
+
"${ENV_ARGS[@]}" \
|
|
23
|
+
--termux-home \
|
|
24
|
+
--shared-tmp \
|
|
25
|
+
--bind "$UBUNTU_ROOTFS/root/.cline:/root/.cline" \
|
|
26
|
+
--work-dir $PWD \
|
|
27
|
+
ubuntu \
|
|
28
|
+
-- /root/.cline/bin/cline "$@"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#include <stdlib.h>
|
|
2
|
+
#include <unistd.h>
|
|
3
|
+
#include <string.h>
|
|
4
|
+
#include <libgen.h>
|
|
5
|
+
#include <limits.h>
|
|
6
|
+
#include <stdio.h>
|
|
7
|
+
|
|
8
|
+
int main(int argc, char** argv) {
|
|
9
|
+
// 1. Clear conflicting Android Bionic preloads and search paths
|
|
10
|
+
unsetenv("LD_PRELOAD");
|
|
11
|
+
unsetenv("LD_LIBRARY_PATH");
|
|
12
|
+
|
|
13
|
+
// 2. Set dynamic resolver and SSL configurations for Termux environment
|
|
14
|
+
setenv("SSL_CERT_FILE", "/data/data/com.termux/files/usr/etc/tls/cert.pem", 1);
|
|
15
|
+
|
|
16
|
+
// 3. Resolve current executable directory
|
|
17
|
+
char exec_path[PATH_MAX];
|
|
18
|
+
ssize_t len = readlink("/proc/self/exe", exec_path, sizeof(exec_path) - 1);
|
|
19
|
+
if (len == -1) {
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
exec_path[len] = '\0';
|
|
23
|
+
char* dir = dirname(exec_path);
|
|
24
|
+
|
|
25
|
+
// 4. Construct paths for the glibc loader and the real binary
|
|
26
|
+
char* loader = "/data/data/com.termux/files/usr/glibc/lib/ld-linux-aarch64.so.1";
|
|
27
|
+
char real_bin[] = "/data/data/com.termux/files/home/.local/share/karnel-data/cline/cline";
|
|
28
|
+
char lib_path[] = "/data/data/com.termux/files/usr/glibc/lib";
|
|
29
|
+
|
|
30
|
+
// 5. Construct argument array for execv
|
|
31
|
+
// Format: [loader, --library-path, lib_path, real_bin, ...original_args]
|
|
32
|
+
char** new_argv = malloc((argc + 4) * sizeof(char*));
|
|
33
|
+
if (!new_argv) {
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
new_argv[0] = loader;
|
|
38
|
+
new_argv[1] = "--library-path";
|
|
39
|
+
new_argv[2] = lib_path;
|
|
40
|
+
new_argv[3] = real_bin;
|
|
41
|
+
|
|
42
|
+
for (int i = 1; i < argc; i++) {
|
|
43
|
+
new_argv[i + 3] = argv[i];
|
|
44
|
+
}
|
|
45
|
+
new_argv[argc + 3] = NULL;
|
|
46
|
+
|
|
47
|
+
// 6. Execute the glibc loader to run the real binary
|
|
48
|
+
execv(loader, new_argv);
|
|
49
|
+
|
|
50
|
+
// If execv returns, an error occurred
|
|
51
|
+
perror("execv");
|
|
52
|
+
free(new_argv);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Parches Termux para gentleman-guardian-angel
|
|
3
|
+
# Se aplica al repo upstream después del clone, antes del install.sh
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
REPO_DIR="$1"
|
|
7
|
+
[ -d "$REPO_DIR" ] || { echo "Usage: $0 <repo-dir>" >&2; exit 1; }
|
|
8
|
+
|
|
9
|
+
# --- install.sh ---
|
|
10
|
+
# 1) Fix shebang for Termux
|
|
11
|
+
sed -i '1s|#!/usr/bin/env bash|#!/data/data/com.termux/files/usr/bin/bash|' "$REPO_DIR/install.sh"
|
|
12
|
+
|
|
13
|
+
# 2) Agregar detección de Termux en detect_os()
|
|
14
|
+
sed -i '/^ if \[\[ -n "\${GGA_TEST_OS:-}" \]\]; then$/,/^$/{
|
|
15
|
+
/^$/a\
|
|
16
|
+
\
|
|
17
|
+
if [[ -n "${TERMUX_VERSION:-}" ]]; then\
|
|
18
|
+
echo "termux"\
|
|
19
|
+
return 0\
|
|
20
|
+
fi
|
|
21
|
+
}' "$REPO_DIR/install.sh"
|
|
22
|
+
|
|
23
|
+
# 3) Agregar TERMUX para INSTALL_DIR (después del bloque windows)
|
|
24
|
+
sed -i '/INSTALL_DIR="\$HOME\/bin"/,/elif \[\[ -w "\/usr\/local\/bin" \]\]/{
|
|
25
|
+
/elif \[\[ -w "\/usr\/local\/bin" \]\]/i\
|
|
26
|
+
elif [[ "$GGA_OS" == "termux" ]]; then\
|
|
27
|
+
INSTALL_DIR="${PREFIX:-/data/data/com.termux/files/usr}/bin"\
|
|
28
|
+
mkdir -p "$INSTALL_DIR"
|
|
29
|
+
}' "$REPO_DIR/install.sh"
|
|
30
|
+
|
|
31
|
+
# 4) Agregar TERMUX para LIB_INSTALL_DIR (después del bloque windows)
|
|
32
|
+
sed -i '/LIB_INSTALL_DIR="\$HOME\/bin\/lib\/gga"/,/else$/{
|
|
33
|
+
/else$/i\
|
|
34
|
+
elif [[ "$GGA_OS" == "termux" ]]; then\
|
|
35
|
+
LIB_INSTALL_DIR="${PREFIX:-/data/data/com.termux/files/usr}/share/gga/lib"
|
|
36
|
+
}' "$REPO_DIR/install.sh"
|
|
37
|
+
|
|
38
|
+
# --- bin/gga ---
|
|
39
|
+
# 5) Fix shebang for Termux (/usr/bin/env doesn't exist)
|
|
40
|
+
sed -i '1s|#!/usr/bin/env bash|#!/data/data/com.termux/files/usr/bin/bash|' "$REPO_DIR/bin/gga"
|
|
41
|
+
|
|
42
|
+
# 6) Agregar detección de Termux en detect_os()
|
|
43
|
+
sed -i '/^detect_os() {$/,/^}$/{
|
|
44
|
+
/^ case "\$(uname -s)" in$/i\
|
|
45
|
+
if [[ -n "${TERMUX_VERSION:-}" ]]; then\
|
|
46
|
+
echo "termux"\
|
|
47
|
+
return 0\
|
|
48
|
+
fi
|
|
49
|
+
}' "$REPO_DIR/bin/gga"
|
|
50
|
+
|
|
51
|
+
# --- uninstall.sh ---
|
|
52
|
+
# 7) Fix shebang for Termux
|
|
53
|
+
sed -i '1s|#!/usr/bin/env bash|#!/data/data/com.termux/files/usr/bin/bash|' "$REPO_DIR/uninstall.sh"
|
|
54
|
+
|
|
55
|
+
# 8) Agregar detección de Termux en detect_os()
|
|
56
|
+
sed -i '/^detect_os() {$/,/^}$/{
|
|
57
|
+
/^ case "\$(uname -s)" in$/i\
|
|
58
|
+
if [[ -n "${TERMUX_VERSION:-}" ]]; then\
|
|
59
|
+
echo "termux"\
|
|
60
|
+
return 0\
|
|
61
|
+
fi
|
|
62
|
+
}' "$REPO_DIR/uninstall.sh"
|
|
63
|
+
|
|
64
|
+
# 9) Agregar Termux path al array LOCATIONS
|
|
65
|
+
sed -i '/ "\$HOME\/bin\/gga"/a\ "${PREFIX:-/data/data/com.termux/files/usr}/bin/gga"' "$REPO_DIR/uninstall.sh"
|
|
66
|
+
|
|
67
|
+
# 10) Agregar Termux lib path al loop de limpieza
|
|
68
|
+
sed -i 's|for lib_dir in "\$HOME/.local/share/gga" "\$HOME/bin/lib/gga";|for lib_dir in "$HOME/.local/share/gga" "$HOME/bin/lib/gga" "${PREFIX:-/data/data/com.termux/files/usr}/share/gga";|' "$REPO_DIR/uninstall.sh"
|
|
69
|
+
|
|
70
|
+
echo "Termux patches applied successfully"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/data/data/com.termux/files/usr/bin/bash
|
|
2
|
+
|
|
3
|
+
UBUNTU_ROOTFS="__UBUNTU_ROOTFS__"
|
|
4
|
+
|
|
5
|
+
EXCLUDE_REGEX="^(PATH|LD_PRELOAD|LD_LIBRARY_PATH|PREFIX|HOME|PWD|OLDPWD|SHELL|IFS|_|SHLVL|PROMPT_COMMAND|TERMCAP|LS_COLORS|TERM)="
|
|
6
|
+
|
|
7
|
+
ENV_ARGS=()
|
|
8
|
+
while IFS= read -r line; do
|
|
9
|
+
if [[ -n "$line" && ! "$line" =~ $EXCLUDE_REGEX ]]; then
|
|
10
|
+
ENV_ARGS+=("--env" "$line")
|
|
11
|
+
fi
|
|
12
|
+
done < <(env)
|
|
13
|
+
|
|
14
|
+
ENV_ARGS+=(
|
|
15
|
+
"--env" "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"
|
|
16
|
+
"--env" "TERM=$TERM"
|
|
17
|
+
"--env" "HOME=/root"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
unset LD_PRELOAD
|
|
21
|
+
proot-distro login \
|
|
22
|
+
"${ENV_ARGS[@]}" \
|
|
23
|
+
--termux-home \
|
|
24
|
+
--shared-tmp \
|
|
25
|
+
--work-dir $PWD \
|
|
26
|
+
ubuntu \
|
|
27
|
+
-- /usr/local/bin/bun "$@"
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bun-android-shim.c — LD_PRELOAD shim for Bun on Android/Termux
|
|
3
|
+
*
|
|
4
|
+
* Dual purpose:
|
|
5
|
+
*
|
|
6
|
+
* 1. opendir/openat64 interception for bionic:
|
|
7
|
+
* Bun's project-root discovery traverses up the directory tree.
|
|
8
|
+
* On Termux, Android's sandbox makes /data/ and /data/data/
|
|
9
|
+
* readable at the syscall level but returns "Permission denied"
|
|
10
|
+
* or causes Bun to abort with "Cannot read directory /data/".
|
|
11
|
+
* This shim intercepts opendir/openat64 and returns ENOENT
|
|
12
|
+
* for these paths, so Bun's traversal stops at the sandbox
|
|
13
|
+
* boundary instead of crashing.
|
|
14
|
+
*
|
|
15
|
+
* 2. mmap fix for compiled standalone binaries:
|
|
16
|
+
* `bun build --compile` produces ELF binaries where the embedded
|
|
17
|
+
* Bun runtime contains hardcoded (non-relocated) absolute pointers
|
|
18
|
+
* to the `.bun` section and other data sections (e.g., 0x5730000).
|
|
19
|
+
* On PIE loading, these addresses are never mapped, causing SEGFAULT.
|
|
20
|
+
* The constructor maps critical sections at their ELF virtual addresses
|
|
21
|
+
* via MAP_FIXED so the pointers resolve correctly.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
#define _GNU_SOURCE
|
|
25
|
+
#include <stdio.h>
|
|
26
|
+
#include <stdlib.h>
|
|
27
|
+
#include <string.h>
|
|
28
|
+
#include <unistd.h>
|
|
29
|
+
#include <fcntl.h>
|
|
30
|
+
#include <stdint.h>
|
|
31
|
+
#include <stdarg.h>
|
|
32
|
+
#include <sys/mman.h>
|
|
33
|
+
#include <sys/stat.h>
|
|
34
|
+
#include <errno.h>
|
|
35
|
+
#include <dlfcn.h>
|
|
36
|
+
#include <dirent.h>
|
|
37
|
+
|
|
38
|
+
/* ================================================================
|
|
39
|
+
* PART 1: opendir/openat64 interception
|
|
40
|
+
* ================================================================ */
|
|
41
|
+
|
|
42
|
+
/* CWD captured at shim load time — used as redirect for inaccessible ancestors */
|
|
43
|
+
static char shim_cwd[4096] = {0};
|
|
44
|
+
static int shim_cwd_len = 0;
|
|
45
|
+
|
|
46
|
+
/* Original functions (resolved via dlsym) */
|
|
47
|
+
typedef DIR *(*opendir_fn_t)(const char *);
|
|
48
|
+
typedef int (*open_fn_t)(const char *, int, ...);
|
|
49
|
+
typedef int (*openat64_fn_t)(int, const char *, int, ...);
|
|
50
|
+
static opendir_fn_t real_opendir = NULL;
|
|
51
|
+
static openat64_fn_t real_openat64 = NULL;
|
|
52
|
+
|
|
53
|
+
/* Check if a path is an inaccessible ancestor that needs redirect */
|
|
54
|
+
static int needs_redirect(const char *path) {
|
|
55
|
+
if (!path || path[0] == '\0') return 0;
|
|
56
|
+
/* Redirect "/", "/data/", and "/data/data/" to CWD */
|
|
57
|
+
if (path[0] == '/' && path[1] == '\0') return 1; /* exactly "/" */
|
|
58
|
+
if (strncmp(path, "/data/", 6) == 0) {
|
|
59
|
+
/* Don't redirect if it's the Termux sandbox itself */
|
|
60
|
+
if (strncmp(path, "/data/data/com.termux", 21) == 0)
|
|
61
|
+
return 0;
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Redirect open to CWD — uses a fresh dlsym so it works from any interceptor */
|
|
68
|
+
static int open_cwd_redirect_impl(int flags, mode_t mode) {
|
|
69
|
+
if (shim_cwd_len == 0) {
|
|
70
|
+
errno = ENOENT;
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
open_fn_t real = (open_fn_t)dlsym(RTLD_NEXT, "open");
|
|
74
|
+
if (!real) {
|
|
75
|
+
errno = ENOENT;
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
return real(shim_cwd, flags, mode);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static int should_block(const char *path) {
|
|
82
|
+
/* Block exact "/data/" paths that are NOT under /data/data/com.termux */
|
|
83
|
+
if (path && strncmp(path, "/data/", 6) == 0) {
|
|
84
|
+
if (strncmp(path, "/data/data/com.termux", 21) != 0)
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
DIR *opendir(const char *path) {
|
|
91
|
+
if (!real_opendir) {
|
|
92
|
+
real_opendir = (opendir_fn_t)dlsym(RTLD_NEXT, "opendir");
|
|
93
|
+
}
|
|
94
|
+
/* Redirect blocked ancestors to CWD so bun's directory walk succeeds */
|
|
95
|
+
if (needs_redirect(path) && shim_cwd_len > 0) {
|
|
96
|
+
return real_opendir(shim_cwd);
|
|
97
|
+
}
|
|
98
|
+
if (should_block(path)) {
|
|
99
|
+
errno = ENOENT;
|
|
100
|
+
return NULL;
|
|
101
|
+
}
|
|
102
|
+
return real_opendir(path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
int openat64(int fd, const char *path, int flags, ...) {
|
|
106
|
+
if (!real_openat64) {
|
|
107
|
+
real_openat64 = (openat64_fn_t)dlsym(RTLD_NEXT, "openat64");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Pass mode if O_CREAT is set */
|
|
111
|
+
mode_t mode = 0;
|
|
112
|
+
if (flags & O_CREAT) {
|
|
113
|
+
va_list ap;
|
|
114
|
+
va_start(ap, flags);
|
|
115
|
+
mode = (mode_t)va_arg(ap, int);
|
|
116
|
+
va_end(ap);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Redirect blocked ancestors to CWD so bun's directory walk succeeds */
|
|
120
|
+
if (needs_redirect(path) && shim_cwd_len > 0) {
|
|
121
|
+
return real_openat64(fd, shim_cwd, flags, mode);
|
|
122
|
+
}
|
|
123
|
+
if (should_block(path)) {
|
|
124
|
+
errno = ENOENT;
|
|
125
|
+
return -1;
|
|
126
|
+
}
|
|
127
|
+
return real_openat64(fd, path, flags, mode);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
/* ================================================================
|
|
132
|
+
* open/open64 interception — Android sandbox blocks open("/")
|
|
133
|
+
*
|
|
134
|
+
* Bun v1.3.14's resolver calls openat64(AT_FDCWD, "/", O_DIRECTORY)
|
|
135
|
+
* as part of its directory info walk. The kernel returns EACCES
|
|
136
|
+
* because Android's sandbox restricts root access.
|
|
137
|
+
*
|
|
138
|
+
* Returning EACCES or ENOENT both cause bun's resolver to abort
|
|
139
|
+
* with "CouldntReadCurrentDirectory" (bun treats ANY ancestor-open
|
|
140
|
+
* failure as fatal in v1.3.14 — this is fixed in a later commit,
|
|
141
|
+
* PR #28782, which skips permission-denied ancestors).
|
|
142
|
+
*
|
|
143
|
+
* Our fix: redirect "/" to the current working directory instead of
|
|
144
|
+
* failing. Bun gets a valid directory fd, completes its walk, and
|
|
145
|
+
* doesn't crash. The entries read will be the CWD entries (not root
|
|
146
|
+
* entries), but that is harmless since root never has bun config files.
|
|
147
|
+
* ================================================================ */
|
|
148
|
+
|
|
149
|
+
static open_fn_t real_open = NULL;
|
|
150
|
+
|
|
151
|
+
int open(const char *path, int flags, ...) {
|
|
152
|
+
if (!real_open)
|
|
153
|
+
real_open = (open_fn_t)dlsym(RTLD_NEXT, "open");
|
|
154
|
+
|
|
155
|
+
/* Redirect blocked ancestors to CWD so bun's directory walk succeeds */
|
|
156
|
+
if (needs_redirect(path)) {
|
|
157
|
+
mode_t m = 0;
|
|
158
|
+
if (flags & O_CREAT) {
|
|
159
|
+
va_list ap;
|
|
160
|
+
va_start(ap, flags);
|
|
161
|
+
m = (mode_t)va_arg(ap, int);
|
|
162
|
+
va_end(ap);
|
|
163
|
+
}
|
|
164
|
+
return open_cwd_redirect_impl(flags, m);
|
|
165
|
+
}
|
|
166
|
+
if (should_block(path)) {
|
|
167
|
+
errno = ENOENT;
|
|
168
|
+
return -1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
mode_t mode = 0;
|
|
172
|
+
if (flags & O_CREAT) {
|
|
173
|
+
va_list ap;
|
|
174
|
+
va_start(ap, flags);
|
|
175
|
+
mode = (mode_t)va_arg(ap, int);
|
|
176
|
+
va_end(ap);
|
|
177
|
+
}
|
|
178
|
+
return real_open(path, flags, mode);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
int open64(const char *path, int flags, ...) {
|
|
182
|
+
if (!real_open)
|
|
183
|
+
real_open = (open_fn_t)dlsym(RTLD_NEXT, "open");
|
|
184
|
+
|
|
185
|
+
/* Redirect blocked ancestors to CWD so bun's directory walk succeeds */
|
|
186
|
+
if (needs_redirect(path)) {
|
|
187
|
+
mode_t m = 0;
|
|
188
|
+
if (flags & O_CREAT) {
|
|
189
|
+
va_list ap;
|
|
190
|
+
va_start(ap, flags);
|
|
191
|
+
m = (mode_t)va_arg(ap, int);
|
|
192
|
+
va_end(ap);
|
|
193
|
+
}
|
|
194
|
+
return open_cwd_redirect_impl(flags, m);
|
|
195
|
+
}
|
|
196
|
+
if (should_block(path)) {
|
|
197
|
+
errno = ENOENT;
|
|
198
|
+
return -1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
mode_t mode = 0;
|
|
202
|
+
if (flags & O_CREAT) {
|
|
203
|
+
va_list ap;
|
|
204
|
+
va_start(ap, flags);
|
|
205
|
+
mode = (mode_t)va_arg(ap, int);
|
|
206
|
+
va_end(ap);
|
|
207
|
+
}
|
|
208
|
+
return real_open(path, flags, mode);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
/* ================================================================
|
|
213
|
+
* getcwd fallback — Android's getcwd() fails with
|
|
214
|
+
* "CouldntReadCurrentDirectory" when the process was started via
|
|
215
|
+
* execv() with a relative cwd. Read /proc/self/cwd as fallback.
|
|
216
|
+
* ================================================================ */
|
|
217
|
+
|
|
218
|
+
typedef char *(*getcwd_fn_t)(char *, size_t);
|
|
219
|
+
static getcwd_fn_t real_getcwd = NULL;
|
|
220
|
+
|
|
221
|
+
char *getcwd(char *buf, size_t size) {
|
|
222
|
+
if (!real_getcwd)
|
|
223
|
+
real_getcwd = (getcwd_fn_t)dlsym(RTLD_NEXT, "getcwd");
|
|
224
|
+
|
|
225
|
+
/* Try real getcwd first */
|
|
226
|
+
char *result = real_getcwd(buf, size);
|
|
227
|
+
if (result) return result;
|
|
228
|
+
|
|
229
|
+
/* Fallback: read /proc/self/cwd symlink */
|
|
230
|
+
ssize_t len = readlink("/proc/self/cwd", buf, size - 1);
|
|
231
|
+
if (len < 0) return NULL;
|
|
232
|
+
buf[len] = '\0';
|
|
233
|
+
return buf;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
/* ================================================================
|
|
238
|
+
* linkat/link → copy fallback
|
|
239
|
+
*
|
|
240
|
+
* Android restricts hardlink creation across mount points and app
|
|
241
|
+
* sandboxes. Bun's installer tries linkat() first; when the kernel
|
|
242
|
+
* returns EACCES/EPERM, Bun aborts instead of falling back to copy.
|
|
243
|
+
* This interception converts hardlink requests into copy_file_range
|
|
244
|
+
* (or read+write fallback), so Bun's install flow succeeds.
|
|
245
|
+
* ================================================================ */
|
|
246
|
+
|
|
247
|
+
#include <sys/sendfile.h>
|
|
248
|
+
|
|
249
|
+
static int copy_file(const char *src, const char *dst) {
|
|
250
|
+
int fd_in = open(src, O_RDONLY);
|
|
251
|
+
if (fd_in < 0) return -1;
|
|
252
|
+
|
|
253
|
+
struct stat st;
|
|
254
|
+
if (fstat(fd_in, &st) < 0) { close(fd_in); return -1; }
|
|
255
|
+
|
|
256
|
+
int fd_out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, st.st_mode);
|
|
257
|
+
if (fd_out < 0) { close(fd_in); return -1; }
|
|
258
|
+
|
|
259
|
+
ssize_t sent = 0;
|
|
260
|
+
off_t offset = 0;
|
|
261
|
+
while (offset < st.st_size) {
|
|
262
|
+
sent = sendfile(fd_out, fd_in, &offset, st.st_size - offset);
|
|
263
|
+
if (sent <= 0) break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
close(fd_in);
|
|
267
|
+
close(fd_out);
|
|
268
|
+
return (offset == st.st_size) ? 0 : -1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
int linkat(int olddirfd, const char *oldpath,
|
|
272
|
+
int newdirfd, const char *newpath, int flags) {
|
|
273
|
+
/* Resolve absolute paths for copy fallback */
|
|
274
|
+
char src[PATH_MAX], dst[PATH_MAX];
|
|
275
|
+
|
|
276
|
+
if (oldpath && oldpath[0] != '/') {
|
|
277
|
+
char dir[PATH_MAX];
|
|
278
|
+
if (olddirfd == AT_FDCWD) {
|
|
279
|
+
if (getcwd(dir, sizeof(dir)) == NULL) return -1;
|
|
280
|
+
} else {
|
|
281
|
+
char fdpath[32];
|
|
282
|
+
snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", olddirfd);
|
|
283
|
+
ssize_t n = readlink(fdpath, dir, sizeof(dir) - 1);
|
|
284
|
+
if (n < 0) return -1;
|
|
285
|
+
dir[n] = '\0';
|
|
286
|
+
}
|
|
287
|
+
snprintf(src, sizeof(src), "%s/%s", dir, oldpath);
|
|
288
|
+
} else {
|
|
289
|
+
snprintf(src, sizeof(src), "%s", oldpath ? oldpath : "");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (newpath && newpath[0] != '/') {
|
|
293
|
+
char dir[PATH_MAX];
|
|
294
|
+
if (newdirfd == AT_FDCWD) {
|
|
295
|
+
if (getcwd(dir, sizeof(dir)) == NULL) return -1;
|
|
296
|
+
} else {
|
|
297
|
+
char fdpath[32];
|
|
298
|
+
snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", newdirfd);
|
|
299
|
+
ssize_t n = readlink(fdpath, dir, sizeof(dir) - 1);
|
|
300
|
+
if (n < 0) return -1;
|
|
301
|
+
dir[n] = '\0';
|
|
302
|
+
}
|
|
303
|
+
snprintf(dst, sizeof(dst), "%s/%s", dir, newpath);
|
|
304
|
+
} else {
|
|
305
|
+
snprintf(dst, sizeof(dst), "%s", newpath ? newpath : "");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return copy_file(src, dst);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
int link(const char *oldpath, const char *newpath) {
|
|
312
|
+
return copy_file(oldpath, newpath);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* ================================================================
|
|
316
|
+
* PART 3: mmap fix for compiled standalone binaries
|
|
317
|
+
* ================================================================ */
|
|
318
|
+
|
|
319
|
+
/* ELF64 structures */
|
|
320
|
+
typedef struct {
|
|
321
|
+
unsigned char e_ident[16];
|
|
322
|
+
uint16_t e_type;
|
|
323
|
+
uint16_t e_machine;
|
|
324
|
+
uint32_t e_version;
|
|
325
|
+
uint64_t e_entry;
|
|
326
|
+
uint64_t e_phoff;
|
|
327
|
+
uint64_t e_shoff;
|
|
328
|
+
uint32_t e_flags;
|
|
329
|
+
uint16_t e_ehsize;
|
|
330
|
+
uint16_t e_phentsize;
|
|
331
|
+
uint16_t e_phnum;
|
|
332
|
+
uint16_t e_shentsize;
|
|
333
|
+
uint16_t e_shnum;
|
|
334
|
+
uint16_t e_shstrndx;
|
|
335
|
+
} Elf64_Ehdr;
|
|
336
|
+
|
|
337
|
+
typedef struct {
|
|
338
|
+
uint32_t sh_name;
|
|
339
|
+
uint32_t sh_type;
|
|
340
|
+
uint64_t sh_flags;
|
|
341
|
+
uint64_t sh_addr;
|
|
342
|
+
uint64_t sh_offset;
|
|
343
|
+
uint64_t sh_size;
|
|
344
|
+
uint32_t sh_link;
|
|
345
|
+
uint32_t sh_info;
|
|
346
|
+
uint64_t sh_addralign;
|
|
347
|
+
uint64_t sh_entsize;
|
|
348
|
+
} Elf64_Shdr;
|
|
349
|
+
|
|
350
|
+
/* Known hardcoded addresses in Bun standalone binaries
|
|
351
|
+
* These are the ELF virtual addresses that Bun's embedded runtime
|
|
352
|
+
* references with non-relocated absolute pointers.
|
|
353
|
+
* Add more as discovered from crash reports. */
|
|
354
|
+
static const uint64_t known_addresses[] = {
|
|
355
|
+
0x5730000, /* .bun section — embedded source code metadata */
|
|
356
|
+
};
|
|
357
|
+
static const int num_known_addresses =
|
|
358
|
+
sizeof(known_addresses) / sizeof(known_addresses[0]);
|
|
359
|
+
|
|
360
|
+
static inline uint64_t page_align_down(uint64_t addr) {
|
|
361
|
+
return addr & ~(uint64_t)(0xFFF);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
static inline uint64_t page_align_up(uint64_t addr) {
|
|
365
|
+
return (addr + 0xFFF) & ~(uint64_t)(0xFFF);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Find a section by name in the section header string table */
|
|
369
|
+
static int find_section_by_name(int fd, const Elf64_Ehdr *ehdr,
|
|
370
|
+
const char *name, Elf64_Shdr *out) {
|
|
371
|
+
if (ehdr->e_shstrndx >= ehdr->e_shnum) return -1;
|
|
372
|
+
|
|
373
|
+
/* Read section header string table */
|
|
374
|
+
Elf64_Shdr shstrtab;
|
|
375
|
+
if (lseek(fd, ehdr->e_shoff + ehdr->e_shstrndx * ehdr->e_shentsize,
|
|
376
|
+
SEEK_SET) < 0) return -1;
|
|
377
|
+
if (read(fd, &shstrtab, sizeof(shstrtab)) != sizeof(shstrtab)) return -1;
|
|
378
|
+
|
|
379
|
+
char *strtab = malloc(shstrtab.sh_size);
|
|
380
|
+
if (!strtab) return -1;
|
|
381
|
+
if (lseek(fd, shstrtab.sh_offset, SEEK_SET) < 0) { free(strtab); return -1; }
|
|
382
|
+
if (read(fd, strtab, shstrtab.sh_size) != (ssize_t)shstrtab.sh_size) {
|
|
383
|
+
free(strtab); return -1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
Elf64_Shdr shdr;
|
|
387
|
+
int found = 0;
|
|
388
|
+
for (int i = 0; i < ehdr->e_shnum; i++) {
|
|
389
|
+
if (lseek(fd, ehdr->e_shoff + i * ehdr->e_shentsize, SEEK_SET) < 0) break;
|
|
390
|
+
if (read(fd, &shdr, sizeof(shdr)) != sizeof(shdr)) break;
|
|
391
|
+
|
|
392
|
+
const char *sname = (shdr.sh_name < shstrtab.sh_size)
|
|
393
|
+
? strtab + shdr.sh_name : "";
|
|
394
|
+
if (strcmp(sname, name) == 0) {
|
|
395
|
+
memcpy(out, &shdr, sizeof(shdr));
|
|
396
|
+
found = 1;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
free(strtab);
|
|
401
|
+
return found ? 0 : -1;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Map a section at its virtual address with MAP_FIXED */
|
|
405
|
+
static int map_section_at_vaddr(int fd, const Elf64_Shdr *shdr) {
|
|
406
|
+
uint64_t map_addr = page_align_down(shdr->sh_addr);
|
|
407
|
+
uint64_t map_end = page_align_up(shdr->sh_addr + shdr->sh_size);
|
|
408
|
+
uint64_t map_size = map_end - map_addr;
|
|
409
|
+
|
|
410
|
+
if (shdr->sh_type == 8) { /* SHT_NOBITS */
|
|
411
|
+
void *addr = mmap((void *)(uintptr_t)map_addr, map_size,
|
|
412
|
+
PROT_READ | PROT_WRITE,
|
|
413
|
+
MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0);
|
|
414
|
+
return (addr == MAP_FAILED) ? -1 : 0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* PROGBITS: map from file */
|
|
418
|
+
uint64_t file_off = shdr->sh_offset - (shdr->sh_addr - map_addr);
|
|
419
|
+
void *addr = mmap((void *)(uintptr_t)map_addr, map_size, PROT_READ,
|
|
420
|
+
MAP_PRIVATE | MAP_FIXED, fd, file_off);
|
|
421
|
+
return (addr == MAP_FAILED) ? -1 : 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* Constructor: fix compiled standalone binaries */
|
|
425
|
+
static void fix_bun_standalone(void) {
|
|
426
|
+
const char *exe_path = "/proc/self/exe";
|
|
427
|
+
int fd = open(exe_path, O_RDONLY);
|
|
428
|
+
if (fd < 0) {
|
|
429
|
+
char cmdline[256];
|
|
430
|
+
int cfd = open("/proc/self/cmdline", O_RDONLY);
|
|
431
|
+
if (cfd >= 0) {
|
|
432
|
+
int n = read(cfd, cmdline, sizeof(cmdline) - 1);
|
|
433
|
+
close(cfd);
|
|
434
|
+
if (n > 0) {
|
|
435
|
+
cmdline[n] = '\0';
|
|
436
|
+
fd = open(cmdline, O_RDONLY);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (fd < 0) return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
Elf64_Ehdr ehdr;
|
|
443
|
+
if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) { close(fd); return; }
|
|
444
|
+
|
|
445
|
+
/* Verify ELF64 */
|
|
446
|
+
if (ehdr.e_ident[0] != 0x7f || ehdr.e_ident[1] != 'E' ||
|
|
447
|
+
ehdr.e_ident[2] != 'L' || ehdr.e_ident[3] != 'F') { close(fd); return; }
|
|
448
|
+
if (ehdr.e_ident[4] != 2) { close(fd); return; }
|
|
449
|
+
if (ehdr.e_shentsize != sizeof(Elf64_Shdr)) { close(fd); return; }
|
|
450
|
+
|
|
451
|
+
/* Try to find and map the .bun section */
|
|
452
|
+
Elf64_Shdr bun;
|
|
453
|
+
if (find_section_by_name(fd, &ehdr, ".bun", &bun) == 0) {
|
|
454
|
+
map_section_at_vaddr(fd, &bun);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Try known absolute addresses (fallback) */
|
|
458
|
+
for (int i = 0; i < num_known_addresses; i++) {
|
|
459
|
+
uint64_t vaddr = known_addresses[i];
|
|
460
|
+
/* Check if already mapped by section name above */
|
|
461
|
+
if (bun.sh_addr == vaddr && bun.sh_size > 0) continue;
|
|
462
|
+
|
|
463
|
+
/* Try direct mmap from file (vaddr == file offset in bun binaries) */
|
|
464
|
+
uint64_t pg = page_align_down(vaddr);
|
|
465
|
+
mmap((void *)(uintptr_t)pg, 0x1000, PROT_READ,
|
|
466
|
+
MAP_PRIVATE | MAP_FIXED, fd, pg);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
close(fd);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* Constructor runs at process start — captures CWD and fixes standalone bins */
|
|
473
|
+
__attribute__((constructor(1)))
|
|
474
|
+
static void init(void) {
|
|
475
|
+
/* Capture CWD for "/" redirect before any sandbox restriction matters */
|
|
476
|
+
if (getcwd(shim_cwd, sizeof(shim_cwd)) != NULL) {
|
|
477
|
+
shim_cwd_len = strlen(shim_cwd);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
fix_bun_standalone();
|
|
481
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bun-bundle — Post-process a Bun compiled standalone binary for Android Termux.
|
|
3
|
+
*
|
|
4
|
+
* Bun's `bun build --compile` produces standalone ELF binaries that crash on
|
|
5
|
+
* Android because the embedded Bun runtime contains hardcoded (non-relocated)
|
|
6
|
+
* absolute pointers. This tool wraps the compiled binary with a shell script
|
|
7
|
+
* that sets LD_PRELOAD to load the bun-android-shim.so fix library.
|
|
8
|
+
*
|
|
9
|
+
* Usage after bun build --compile:
|
|
10
|
+
* bun build --compile hello.ts
|
|
11
|
+
* bun-bundle hello
|
|
12
|
+
* ./hello # Now works!
|
|
13
|
+
*
|
|
14
|
+
* The tool renames hello -> hello.bun and creates a shell wrapper hello.
|
|
15
|
+
*/
|
|
16
|
+
#define _GNU_SOURCE
|
|
17
|
+
#include <stdio.h>
|
|
18
|
+
#include <stdlib.h>
|
|
19
|
+
#include <string.h>
|
|
20
|
+
#include <unistd.h>
|
|
21
|
+
#include <libgen.h>
|
|
22
|
+
#include <errno.h>
|
|
23
|
+
#include <sys/stat.h>
|
|
24
|
+
|
|
25
|
+
/* Default shim path on Termux */
|
|
26
|
+
#define DEFAULT_SHIM "/data/data/com.termux/files/usr/lib/bun-android-shim.so"
|
|
27
|
+
|
|
28
|
+
int main(int argc, char *argv[]) {
|
|
29
|
+
if (argc < 2) {
|
|
30
|
+
fprintf(stderr, "Usage: bun-bundle <compiled-binary> [shim-path]\n");
|
|
31
|
+
fprintf(stderr, "\nPost-processes a Bun compiled binary to work on Android Termux.\n");
|
|
32
|
+
fprintf(stderr, "Renames <binary> -> <binary>.bun and creates a shell wrapper.\n");
|
|
33
|
+
fprintf(stderr, "\nExample:\n");
|
|
34
|
+
fprintf(stderr, " bun build --compile hello.ts\n");
|
|
35
|
+
fprintf(stderr, " bun-bundle hello\n");
|
|
36
|
+
fprintf(stderr, " ./hello # Works!\n");
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const char *binary = argv[1];
|
|
41
|
+
const char *shim_path = (argc >= 3) ? argv[2] : DEFAULT_SHIM;
|
|
42
|
+
|
|
43
|
+
/* Build the .bun filename: binary + ".bun" */
|
|
44
|
+
size_t binlen = strlen(binary);
|
|
45
|
+
char *bun_binary = malloc(binlen + 5);
|
|
46
|
+
if (!bun_binary) { perror("malloc"); return 1; }
|
|
47
|
+
memcpy(bun_binary, binary, binlen);
|
|
48
|
+
memcpy(bun_binary + binlen, ".bun", 5);
|
|
49
|
+
|
|
50
|
+
/* Check the original binary exists */
|
|
51
|
+
if (access(binary, F_OK) < 0) {
|
|
52
|
+
fprintf(stderr, "Error: %s not found\n", binary);
|
|
53
|
+
free(bun_binary);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Rename binary -> binary.bun */
|
|
58
|
+
if (rename(binary, bun_binary) < 0) {
|
|
59
|
+
fprintf(stderr, "Error renaming %s -> %s: %s\n",
|
|
60
|
+
binary, bun_binary, strerror(errno));
|
|
61
|
+
free(bun_binary);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Get basename for the wrapper */
|
|
66
|
+
char *bin_copy = strdup(binary);
|
|
67
|
+
const char *base = basename(bin_copy);
|
|
68
|
+
|
|
69
|
+
/* Create the shell wrapper */
|
|
70
|
+
FILE *f = fopen(binary, "w");
|
|
71
|
+
if (!f) {
|
|
72
|
+
fprintf(stderr, "Error creating wrapper %s: %s\n",
|
|
73
|
+
binary, strerror(errno));
|
|
74
|
+
rename(bun_binary, binary);
|
|
75
|
+
free(bun_binary);
|
|
76
|
+
free(bin_copy);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fprintf(f, "#!/data/data/com.termux/files/usr/bin/bash\n");
|
|
81
|
+
fprintf(f, "# Auto-generated wrapper for Bun compiled binary\n");
|
|
82
|
+
fprintf(f, "# bun-bundle post-processor\n");
|
|
83
|
+
fprintf(f, "export LD_PRELOAD=%s\n", shim_path);
|
|
84
|
+
fprintf(f, "exec \"$(dirname \"$0\")/%s.bun\" \"$@\"\n", base);
|
|
85
|
+
|
|
86
|
+
fclose(f);
|
|
87
|
+
chmod(binary, 0755);
|
|
88
|
+
|
|
89
|
+
printf("Bundled: %s -> %s (wrapper) + %s.bun (binary)\n", binary, binary, binary);
|
|
90
|
+
printf("Shim: %s\n", shim_path);
|
|
91
|
+
printf("\nRun it: ./%s\n", base);
|
|
92
|
+
|
|
93
|
+
free(bun_binary);
|
|
94
|
+
free(bin_copy);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
package/karnel/utils/env.sh
CHANGED
package/karnel/utils/log.sh
CHANGED
|
@@ -556,6 +556,7 @@ progress_start() {
|
|
|
556
556
|
local total=$1
|
|
557
557
|
local message="${2:-Progress}"
|
|
558
558
|
_progress_current=0
|
|
559
|
+
_progress_filled=0
|
|
559
560
|
_progress_total=$total
|
|
560
561
|
_progress_width=50
|
|
561
562
|
printf " ${D_CYAN}%s${D_NC}" "$message"
|
|
@@ -572,15 +573,22 @@ progress_update() {
|
|
|
572
573
|
local current=$1
|
|
573
574
|
local total=$2
|
|
574
575
|
local width="${_progress_width:-50}"
|
|
575
|
-
local
|
|
576
|
-
|
|
577
|
-
local
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
for ((i =
|
|
576
|
+
local target_filled=$((current * width / total))
|
|
577
|
+
|
|
578
|
+
local prev="${_progress_filled:-0}"
|
|
579
|
+
[[ "$target_filled" -lt "$prev" ]] && prev=0
|
|
580
|
+
|
|
581
|
+
local anim i j bar pct
|
|
582
|
+
for ((i = prev + 1; i <= target_filled; i++)); do
|
|
583
|
+
bar=""
|
|
584
|
+
for ((j = 0; j < i; j++)); do bar="${bar}█"; done
|
|
585
|
+
for ((j = i; j < width; j++)); do bar="${bar}░"; done
|
|
586
|
+
pct=$((i * 100 / width))
|
|
587
|
+
printf "\r ${D_CYAN}[${D_NC}${D_GREEN}%s${D_NC}${D_CYAN}]${D_NC} %3d%%" "$bar" "$pct"
|
|
588
|
+
sleep 0.003 2>/dev/null || true
|
|
589
|
+
done
|
|
582
590
|
|
|
583
|
-
|
|
591
|
+
_progress_filled="$target_filled"
|
|
584
592
|
}
|
|
585
593
|
|
|
586
594
|
progress_done() {
|
package/karnel/utils/version.sh
CHANGED
|
@@ -98,8 +98,8 @@ _get_remote_github_version() {
|
|
|
98
98
|
tag=$(echo "$raw" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
|
99
99
|
|
|
100
100
|
if [ -z "$tag" ]; then
|
|
101
|
-
raw=$(_spin_capture "Checking GitHub tags" curl -fsSL "https://api.github.com/repos/$repo/tags?per_page=
|
|
102
|
-
tag=$(echo "$raw" | grep '"name":' |
|
|
101
|
+
raw=$(_spin_capture "Checking GitHub tags" curl -fsSL "https://api.github.com/repos/$repo/tags?per_page=100" 2>/dev/null)
|
|
102
|
+
tag=$(echo "$raw" | grep '"name":' | sed -E 's/.*"([^"]+)".*/\1/' | sort -V | tail -1)
|
|
103
103
|
fi
|
|
104
104
|
|
|
105
105
|
_parse_version "$tag"
|
package/package.json
CHANGED