diff --git a/README.md b/README.md index 67d8a38..36ba3fe 100644 --- a/README.md +++ b/README.md @@ -631,6 +631,20 @@ data: [DONE] ## Changelog +### v2.3.0 (2026-05-20) +- **Java Virtual Environment** — `ecj.jar` (3.2MB) and `apksigner.jar` (1.1MB) bundled as APK assets, extracted at runtime +- **app_process Wrappers** — wrapper scripts use `/system/bin/app_process` to run JARs (bypasses SELinux `execve` restrictions entirely) +- **d8.jar Runtime Download** — downloads DEX compiler (~18MB) from Google build-tools at first use +- **installAapt2 Plugin** — `BootstrapPlugin.installAapt2()` downloads aapt2 + aapt + dependencies from Termux repo, extracts to `$TOOLS` +- **extractAsset/downloadFile Plugins** — new `BootstrapPlugin` methods for APK asset extraction and URL file downloads +- **Smart Tool Detection** — `toolsReady()` and `checkDevEnvironment()` check both Termux native tools and Java virtual tools +- **BUILD_SCRIPT Updated** — uses `sh "$TOOLS/bin/ecj"` etc., `#!/system/bin/sh` shebang, `$TOOLS/share/android.jar` fallback paths +- **LD_LIBRARY_PATH** — `$TOOLS/lib` added for aapt2 shared library resolution +- Architecture: Java tools never need execute permission (read-only JARs), aapt2 downloaded on demand + +### v2.2.5 (2026-05-19) +- Shebang Patching, Proot from APK nativeLib, Termux RUN_COMMAND integration, F-Droid fallback, 3-Strategy Install + ### v2.2.4 (2026-05-19) - **Bundled PRoot** — proot binary (v5.1.107) included in APK as native library with executable SELinux label - **Auto-Proot Install** — `ShellPlugin.refreshShell()` copies proot + loader from APK's `nativeLibraryDir` to `$PREFIX/bin/` and `$PREFIX/libexec/proot/` on first use diff --git a/android/app/build.gradle b/android/app/build.gradle index 6c7d76d..b3fdf95 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "ai.z.chat" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 18 - versionName "2.2.5" + versionCode 19 + versionName "2.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' diff --git a/android/app/src/main/assets/jars/apksigner.jar b/android/app/src/main/assets/jars/apksigner.jar new file mode 100644 index 0000000..0e297b7 Binary files /dev/null and b/android/app/src/main/assets/jars/apksigner.jar differ diff --git a/android/app/src/main/assets/jars/d8.jar b/android/app/src/main/assets/jars/d8.jar new file mode 100644 index 0000000..a7ccdc4 Binary files /dev/null and b/android/app/src/main/assets/jars/d8.jar differ diff --git a/android/app/src/main/assets/jars/ecj.jar b/android/app/src/main/assets/jars/ecj.jar new file mode 100644 index 0000000..0cfc706 Binary files /dev/null and b/android/app/src/main/assets/jars/ecj.jar differ diff --git a/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java b/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java index b63d9b3..e27c8f5 100644 --- a/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java +++ b/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java @@ -461,6 +461,53 @@ public class BootstrapPlugin extends Plugin { } catch (Exception e) { try { call.reject("Download failed: " + e.getMessage()); } catch (Exception ignored) {} } + }).start(); + } + + @PluginMethod + public void extractAsset(PluginCall call) { + String src = call.getString("src", ""); + String dest = call.getString("dest", ""); + if (src.isEmpty() || dest.isEmpty()) { + call.reject("src and dest required"); + return; + } + try { + java.io.InputStream is = getContext().getAssets().open(src); + new File(dest).getParentFile().mkdirs(); + java.io.FileOutputStream fos = new java.io.FileOutputStream(dest); + byte[] buf = new byte[8192]; + int r; + long total = 0; + while ((r = is.read(buf)) > 0) { + fos.write(buf, 0, r); + total += r; + } + fos.close(); + is.close(); + call.resolve(new JSObject().put("path", dest).put("size", total)); + } catch (Exception e) { + call.reject("Extract failed: " + e.getMessage()); + } + } + + @PluginMethod + public void downloadFile(PluginCall call) { + String url = call.getString("url", ""); + String dest = call.getString("dest", ""); + if (url.isEmpty() || dest.isEmpty()) { + call.reject("url and dest required"); + return; + } + call.setKeepAlive(true); + new Thread(() -> { + try { + new File(dest).getParentFile().mkdirs(); + downloadFile(url, new File(dest), null); + call.resolve(new JSObject().put("path", dest)); + } catch (Exception e) { + try { call.reject("Download failed: " + e.getMessage()); } catch (Exception ignored) {} + } }).start(); } @@ -624,6 +671,170 @@ public class BootstrapPlugin extends Plugin { }).start(); } + @PluginMethod + public void installAapt2(PluginCall call) { + call.setKeepAlive(true); + new Thread(() -> { + try { + String arch = getArch(); + String toolsDir = call.getString("toolsDir", ""); + if (toolsDir.isEmpty()) { + call.reject("toolsDir required"); + return; + } + + File aapt2File = new File(toolsDir, "bin/aapt2"); + if (aapt2File.exists()) { + call.resolve(new JSObject().put("installed", true).put("path", aapt2File.getAbsolutePath())); + return; + } + + String packagesUrl = "https://packages.termux.dev/apt/termux-main/dists/stable/main/binary-" + arch + "/Packages"; + String packagesContent = downloadToString(packagesUrl); + + String[] neededPkgs = {"aapt2", "aapt"}; + java.util.Map debUrls = new java.util.LinkedHashMap<>(); + String[] blocks = packagesContent.split("\n\n"); + for (String block : blocks) { + for (String pkgName : neededPkgs) { + if (block.contains("Package: " + pkgName + "\n") && !debUrls.containsKey(pkgName)) { + String filename = null; + for (String line : block.split("\n")) { + if (line.startsWith("Filename:")) { + filename = line.substring("Filename:".length()).trim(); + } + } + if (filename != null) { + debUrls.put(pkgName, "https://packages.termux.dev/apt/termux-main/" + filename); + } + break; + } + } + if (debUrls.size() >= neededPkgs.length) break; + } + + if (debUrls.isEmpty()) { + call.reject("aapt2 package not found in repo"); + return; + } + + new File(toolsDir + "/bin").mkdirs(); + new File(toolsDir + "/lib").mkdirs(); + new File(toolsDir + "/share/aapt2").mkdirs(); + + for (java.util.Map.Entry entry : debUrls.entrySet()) { + String pkgName = entry.getKey(); + String debUrl = entry.getValue(); + Log.i(TAG, "Downloading " + pkgName + ": " + debUrl); + + File debFile = new File(getContext().getCacheDir(), pkgName + ".deb"); + downloadFile(debUrl, debFile, null); + + Log.i(TAG, "Extracting " + pkgName + " from .deb..."); + extractDebToDir(debFile, new File(toolsDir)); + debFile.delete(); + } + + if (aapt2File.exists()) { + try { Os.chmod(aapt2File.getAbsolutePath(), 0755); } catch (Exception e) {} + File aaptBin = new File(toolsDir, "bin/aapt"); + if (aaptBin.exists()) { + try { Os.chmod(aaptBin.getAbsolutePath(), 0755); } catch (Exception e) {} + } + call.resolve(new JSObject().put("installed", true).put("path", aapt2File.getAbsolutePath())); + } else { + call.reject("aapt2 binary not found after extraction"); + } + } catch (Exception e) { + Log.e(TAG, "installAapt2 failed", e); + try { call.reject("installAapt2 failed: " + e.getMessage()); } catch (Exception ignored) {} + } + }).start(); + } + + private void extractDebToDir(File debFile, File destDir) throws Exception { + FileInputStream fis = new FileInputStream(debFile); + byte[] magic = new byte[8]; + if (fis.read(magic) != 8 || !new String(magic).equals("!\n")) { + fis.close(); + throw new RuntimeException("Not a valid .deb (AR) file"); + } + + while (true) { + byte[] header = new byte[60]; + int read = fis.read(header); + if (read < 60) break; + + String name = new String(header, 0, 16).trim(); + String sizeStr = new String(header, 48, 10).trim(); + long entrySize = Long.parseLong(sizeStr); + + if (name.startsWith("data.tar")) { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + long remaining = entrySize; + while (remaining > 0) { + int toRead = (int) Math.min(buf.length, remaining); + int r = fis.read(buf, 0, toRead); + if (r <= 0) break; + baos.write(buf, 0, r); + remaining -= r; + } + fis.close(); + + byte[] tarData = baos.toByteArray(); + if (name.contains(".xz")) { + tarData = decompressXz(tarData); + } else if (name.contains(".gz")) { + tarData = decompressGz(tarData); + } else if (name.contains(".zst")) { + throw new RuntimeException("ZSTD compression not supported"); + } + extractTarToDir(tarData, destDir); + return; + } else { + long skip = (entrySize + 1) & ~1L; + fis.skip(skip); + } + } + fis.close(); + } + + private void extractTarToDir(byte[] tarData, File destDir) throws Exception { + String prefix = "data/data/com.termux/files/usr/"; + int offset = 0; + while (offset + 512 <= tarData.length) { + String headerName = new String(tarData, offset, 100).trim().replace("\0", ""); + String sizeStr = new String(tarData, offset + 124, 12).trim().replace("\0", ""); + long fileSize = 0; + try { fileSize = Long.parseLong(sizeStr, 8); } catch (Exception e) { break; } + + String type = new String(tarData, offset + 156, 1); + String relPath = headerName; + if (relPath.startsWith(prefix)) { + relPath = relPath.substring(prefix.length()); + } else if (relPath.startsWith("./")) { + relPath = relPath.substring(2); + if (relPath.startsWith(prefix)) relPath = relPath.substring(prefix.length()); + } + + if (!relPath.isEmpty() && fileSize > 0 && (type.equals("0") || type.isEmpty())) { + File outFile = new File(destDir, relPath); + outFile.getParentFile().mkdirs(); + FileOutputStream fos = new FileOutputStream(outFile); + fos.write(tarData, offset + 512, (int) Math.min(fileSize, tarData.length - offset - 512)); + fos.close(); + + if (relPath.startsWith("bin/") || relPath.contains("/bin/")) { + try { Os.chmod(outFile.getAbsolutePath(), 0755); } catch (Exception e) {} + } + } + + long blocks = (fileSize + 511) / 512; + offset += 512 + (int)(blocks * 512); + } + } + private String downloadToString(String urlStr) throws Exception { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); diff --git a/android/app/src/main/java/ai/z/chat/ShellPlugin.java b/android/app/src/main/java/ai/z/chat/ShellPlugin.java index 6e2e1f6..874f787 100644 --- a/android/app/src/main/java/ai/z/chat/ShellPlugin.java +++ b/android/app/src/main/java/ai/z/chat/ShellPlugin.java @@ -347,11 +347,13 @@ public class ShellPlugin extends Plugin { envList.add("JAVA_HOME=" + toolsDir + "/java"); envList.add("PROJECTS=" + projectsDir); if (hasOurPrefix) { - envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib"); + envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib:" + toolsDir + "/lib"); envList.add("BOOTSTRAP=zaichat"); if (prootLoaderPath != null) { envList.add("PROOT_LOADER=" + prootLoaderPath); } + } else { + envList.add("LD_LIBRARY_PATH=" + toolsDir + "/lib"); } if (hasTermux) { envList.add("TERMUX_VERSION=" + getTermuxVersion()); diff --git a/package.json b/package.json index bc1c923..6bb2fcb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zai-chat", - "version": "2.2.5", + "version": "2.3.0", "description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan", "main": "index.js", "scripts": { diff --git a/www/index.html b/www/index.html index 3752253..0509e94 100644 --- a/www/index.html +++ b/www/index.html @@ -327,13 +327,25 @@

About

-

Z.AI Chat v2.2.5

+

Z.AI Chat v2.3.0

Built with Z.AI SDK & GLM-5.1

Compatible with Android 15/16

Changelog

    +
  • + v2.3.0 + 2026-05-20 +
      +
    • Java Virtual Environment — ecj.jar + apksigner.jar bundled in APK, d8.jar downloaded at runtime
    • +
    • app_process Wrappers — Java tools run via /system/bin/app_process (bypasses SELinux execve restrictions)
    • +
    • Runtime aapt2 Install — downloads aapt2 + dependencies from Termux repo on demand
    • +
    • extractAsset/downloadFile — new BootstrapPlugin methods for APK asset extraction and URL downloads
    • +
    • Smart Tool Detection — checks both Termux native tools and Java virtual tools
    • +
    • LD_LIBRARY_PATH — tools/lib added for aapt2 shared library resolution
    • +
    +
  • v2.2.5 2026-05-19 diff --git a/www/js/app.js b/www/js/app.js index 99c4d69..97e3886 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -12,23 +12,24 @@ }; var BUILD_SCRIPT = [ - '#!/bin/sh', + '#!/system/bin/sh', 'set -e', 'PROJECT_DIR=$(pwd)', 'BUILD_DIR="$PROJECT_DIR/build"', '', - 'AAPT2=$(command -v aapt2 2>/dev/null)', - 'ECJ=$(command -v ecj 2>/dev/null)', - 'D8=$(command -v d8 2>/dev/null)', - 'DX=$(command -v dx 2>/dev/null)', - 'APKSIGNER=$(command -v apksigner 2>/dev/null)', + 'AAPT2=$(command -v aapt2 2>/dev/null || echo "$TOOLS/bin/aapt2")', + 'ECJ=$(command -v ecj 2>/dev/null || echo "$TOOLS/bin/ecj")', + 'D8=$(command -v d8 2>/dev/null || echo "$TOOLS/bin/d8")', + 'APKSIGNER=$(command -v apksigner 2>/dev/null || echo "$TOOLS/bin/apksigner")', '', - 'if [ -z "$AAPT2" ]; then echo "[BUILD FAILED] aapt2 not found. Install: pkg install aapt2"; exit 1; fi', - 'if [ -z "$ECJ" ]; then echo "[BUILD FAILED] ecj not found. Install: pkg install ecj"; exit 1; fi', - 'if [ -z "$D8" ] && [ -z "$DX" ]; then echo "[BUILD FAILED] d8/dx not found. Install: pkg install d8"; exit 1; fi', + 'if [ ! -x "$AAPT2" ] && [ ! -f "$AAPT2" ]; then echo "[BUILD FAILED] aapt2 not found"; exit 1; fi', + 'if [ ! -f "$ECJ" ]; then echo "[BUILD FAILED] ecj not found"; exit 1; fi', + 'if [ ! -f "$D8" ]; then echo "[BUILD FAILED] d8 not found"; exit 1; fi', '', 'ANDROID_JAR=$(dirname "$AAPT2")/../share/aapt2/android.jar', - 'if [ ! -f "$ANDROID_JAR" ]; then echo "[BUILD FAILED] android.jar not found at $ANDROID_JAR"; exit 1; fi', + 'if [ ! -f "$ANDROID_JAR" ]; then ANDROID_JAR="$TOOLS/share/android.jar"; fi', + 'if [ ! -f "$ANDROID_JAR" ]; then ANDROID_JAR="$PREFIX/share/aapt2/android.jar"; fi', + 'if [ ! -f "$ANDROID_JAR" ]; then echo "[BUILD FAILED] android.jar not found"; exit 1; fi', '', 'rm -rf "$BUILD_DIR"', 'mkdir -p "$BUILD_DIR/gen" "$BUILD_DIR/classes" "$BUILD_DIR/apk"', @@ -56,16 +57,12 @@ 'find app/src/main/java -name "*.java" >> "$BUILD_DIR/sources.txt" 2>/dev/null', 'find "$BUILD_DIR/gen" -name "*.java" >> "$BUILD_DIR/sources.txt" 2>/dev/null', 'if [ ! -s "$BUILD_DIR/sources.txt" ]; then echo "[BUILD FAILED] No Java source files found"; exit 1; fi', - '"$ECJ" -source 11 -target 11 -classpath "$ANDROID_JAR" -d "$BUILD_DIR/classes" @"$BUILD_DIR/sources.txt" 2>&1 || { echo "[BUILD FAILED] Java compilation failed"; exit 1; }', + 'sh "$ECJ" -source 11 -target 11 -classpath "$ANDROID_JAR" -d "$BUILD_DIR/classes" @"$BUILD_DIR/sources.txt" 2>&1 || { echo "[BUILD FAILED] Java compilation failed"; exit 1; }', '', 'echo "[*] Converting to DEX..."', 'find "$BUILD_DIR/classes" -name "*.class" > "$BUILD_DIR/classfiles.txt"', 'if [ ! -s "$BUILD_DIR/classfiles.txt" ]; then echo "[BUILD FAILED] No class files compiled"; exit 1; fi', - 'if [ -n "$D8" ]; then', - ' "$D8" --output "$BUILD_DIR" @"$BUILD_DIR/classfiles.txt" 2>&1 || { echo "[BUILD FAILED] D8 failed"; exit 1; }', - 'else', - ' "$DX" --output "$BUILD_DIR/classes.dex" @"$BUILD_DIR/classfiles.txt" 2>&1 || { echo "[BUILD FAILED] DX failed"; exit 1; }', - 'fi', + 'sh "$D8" --output "$BUILD_DIR" @"$BUILD_DIR/classfiles.txt" 2>&1 || { echo "[BUILD FAILED] D8 failed"; exit 1; }', 'if [ ! -f "$BUILD_DIR/classes.dex" ]; then echo "[BUILD FAILED] classes.dex not found"; exit 1; fi', '', 'echo "[*] Packaging..."', @@ -93,20 +90,12 @@ ' fi', 'fi', '', - 'if [ -f "$KEYSTORE" ] && [ -n "$APKSIGNER" ]; then', - ' "$APKSIGNER" sign --ks "$KEYSTORE" --ks-pass pass:android --ks-key-alias androiddebugkey --key-pass pass:android "$BUILD_DIR/app.unaligned.apk" 2>&1 || true', - ' mv "$BUILD_DIR/app.unaligned.apk" "$BUILD_DIR/app-signed.apk"', - 'elif [ -n "$APKSIGNER" ]; then', - ' mv "$BUILD_DIR/app.unaligned.apk" "$BUILD_DIR/app-signed.apk"', - ' echo "[!] Warning: APK unsigned - no keystore"', - 'else', - ' mv "$BUILD_DIR/app.unaligned.apk" "$BUILD_DIR/app-signed.apk"', - ' echo "[!] Warning: APK unsigned - no apksigner"', - 'fi', + 'sh "$APKSIGNER" sign --ks "$KEYSTORE" --ks-pass pass:android --ks-key-alias androiddebugkey --key-pass pass:android "$BUILD_DIR/app.unaligned.apk" 2>&1 || true', + 'mv "$BUILD_DIR/app.unaligned.apk" "$BUILD_DIR/app-signed.apk"', '', 'APK_PATH="$BUILD_DIR/app-signed.apk"', 'APK_SIZE=$(du -h "$APK_PATH" 2>/dev/null | cut -f1)', - 'echo "[BUILD OK] APK: $APK_PATH ($APK_SIZE)"', + 'echo "[BUILD OK] APK: $APK_PATH ($APK_SIZE)"' ].join('\n'); var state = { @@ -1182,8 +1171,10 @@ activePid: null, activeStreamId: null, devToolsInstalled: false, + javaToolsInstalled: false, hasProot: false, prootPath: '', + nativeLibDir: '', commandQueue: [] }; @@ -1913,7 +1904,15 @@ return true; } - try { await Bootstrap.fixPermissions(); } catch(e) {} + var javaCheck = await shellExec('test -f "$TOOLS/jars/ecj.jar" && test -f "$TOOLS/jars/d8.jar" && test -f "$TOOLS/jars/apksigner.jar"', termState.homeDir, false); + if (javaCheck.exitCode === 0 && termState.nativeLibDir) { + var aapt2Check = await shellExec('test -f "$TOOLS/bin/aapt2"', termState.homeDir, false); + if (aapt2Check.exitCode === 0) { + termState.devToolsInstalled = true; + termState.javaToolsInstalled = true; + return true; + } + } if (Shell) { var env = await Shell.getEnv(); @@ -1923,79 +1922,162 @@ termState.cwd = env.CWD || env.HOME; termState.hasProot = env.hasProot === true; termState.prootPath = env.prootPath || ''; + termState.nativeLibDir = env.nativeLibDir || ''; } - var prefix = termState.homeDir ? termState.homeDir.replace('/home', '') : termState.homeDir || ''; - var prefixUsr = prefix + '/usr'; - var pkgBin = prefixUsr + '/bin/pkg'; - var aptBin = prefixUsr + '/bin/apt'; - - var pkgTest = await shellExec('test -f "' + pkgBin + '"', termState.homeDir, false); - var aptTest = await shellExec('test -f "' + aptBin + '"', termState.homeDir, false); - - if (pkgTest.exitCode !== 0 && aptTest.exitCode !== 0) { - var bsStatus; - try { bsStatus = await Bootstrap.getStatus(); } catch(e) { bsStatus = { installed: false }; } - if (!bsStatus.installed) { - termPrint('[!] Termux bootstrap not installed yet.', 'err'); - } else { - termPrint('[!] Package manager not found at ' + pkgBin, 'err'); - } - termState.devToolsInstalled = false; - return false; - } - - termPrint('[*] Installing build tools (aapt2, ecj, dx, apksigner)...', 'info'); - termPrint('[*] This may take a few minutes...', 'info'); + termPrint('[*] Setting up Java build tools (app_process virtual JVM)...', 'info'); showStatusToast('Installing build tools...', 'info'); - var installCmd = 'export PREFIX="' + prefixUsr + '" LD_LIBRARY_PATH="' + prefixUsr + '/lib" && export PATH="' + prefixUsr + '/bin:/system/bin:$PATH" && '; - if (pkgTest.exitCode === 0) { - installCmd += 'sh "' + pkgBin + '" update -y 2>&1 && sh "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1'; - } else { - installCmd += 'sh "' + aptBin + '" update -y 2>&1 && sh "' + aptBin + '" install -y aapt2 ecj dx apksigner 2>&1'; - } + var javaOk = await setupJavaTools(); + if (javaOk) return true; - termPrint('[*] Strategy 1: Direct execution (patched shebangs)...', 'info'); - var result = await shellExec(installCmd, termState.homeDir, false); - if (result.output) { - var out = result.output; - if (out.length > 2000) out = out.substring(0, 1000) + '\n... truncated ...\n' + out.substring(out.length - 800); - termPrint(out.replace(/\n$/, ''), ''); - } - if (await toolsReady()) { termPrint('[OK] Build tools installed!', 'success'); return true; } + var prefix = termState.homeDir ? termState.homeDir.replace('/home', '') : ''; + var prefixUsr = prefix + '/usr'; + try { await Bootstrap.fixPermissions(); } catch(e) {} + + termPrint('[*] Trying Termux pkg install...', 'info'); + var pkgOk = await tryPkgInstall(prefixUsr); + if (pkgOk) return true; - termPrint('[*] Strategy 2: Bundled PRoot...', 'info'); if (termState.prootPath) { - termPrint('[OK] PRoot from APK: ' + termState.prootPath, 'success'); - var prootOk = await tryProotExec(termState.prootPath, prefixUsr, pkgBin, aptBin); + termPrint('[*] Trying PRoot...', 'info'); + var prootOk = await tryProotExec(termState.prootPath, prefixUsr, prefixUsr + '/bin/pkg', prefixUsr + '/bin/apt'); if (prootOk) return true; - } else { - termPrint('[!] No bundled PRoot found', 'warning'); } - termPrint('[*] Strategy 3: Termux RUN_COMMAND...', 'info'); + termPrint('[*] Checking for Termux...', 'info'); var termuxOk = await tryTermuxInstall(); if (termuxOk) return true; - termPrint('', ''); - termPrint('[!] All auto-install strategies failed.', 'err'); - termPrint('[*] Manual fix: Install Termux from F-Droid:', 'warning'); - termPrint(' 1. Open: https://f-droid.org/en/packages/com.termux/', 'warning'); - termPrint(' 2. Install Termux, open it, run:', 'warning'); - termPrint(' pkg update && pkg install aapt2 ecj dx apksigner', 'warning'); - termPrint(' 3. Restart Z.AI Chat — tools will be detected.', 'warning'); + termPrint('[!] All strategies failed. Install Termux from F-Droid:', 'err'); + termPrint(' https://f-droid.org/en/packages/com.termux/', 'warning'); + termPrint(' Then: pkg update && pkg install aapt2 ecj dx apksigner', 'warning'); termState.devToolsInstalled = false; return false; } + async function setupJavaTools() { + if (!Shell || !Bootstrap) return false; + + var toolsDir = termState.toolsDir; + var jarsDir = toolsDir + '/jars'; + var binDir = toolsDir + '/bin'; + var nativeLibDir = termState.nativeLibDir; + + await shellExec('mkdir -p "' + jarsDir + '" "' + binDir + '"', termState.homeDir, false); + + termPrint('[*] Extracting bundled JARs from APK assets...', 'info'); + try { + var extractResult = await Bootstrap.extractAsset({src: 'jars/ecj.jar', dest: jarsDir + '/ecj.jar'}); + termPrint('[OK] ecj.jar extracted (' + Math.round(extractResult.size/1024) + ' KB)', 'success'); + } catch(e) { + termPrint('[!] ecj.jar extract failed: ' + e.message, 'err'); + return false; + } + + try { + var extractResult = await Bootstrap.extractAsset({src: 'jars/apksigner.jar', dest: jarsDir + '/apksigner.jar'}); + termPrint('[OK] apksigner.jar extracted (' + Math.round(extractResult.size/1024) + ' KB)', 'success'); + } catch(e) { + termPrint('[!] apksigner.jar extract failed: ' + e.message, 'err'); + return false; + } + + var d8Test = await shellExec('test -f "' + jarsDir + '/d8.jar"', termState.homeDir, false); + if (d8Test.exitCode !== 0) { + termPrint('[*] Downloading d8.jar (DEX compiler, ~18MB)...', 'info'); + try { + var dlResult = await Bootstrap.downloadFile({url: 'https://dl.google.com/android/repository/build-tools_r36-linux.zip', dest: toolsDir + '/build-tools.zip'}); + termPrint('[*] Extracting d8.jar from build-tools...', 'info'); + await shellExec('cd "' + toolsDir + '" && unzip -o build-tools.zip "*/lib/d8.jar" 2>&1 && mv */lib/d8.jar jars/d8.jar && rm -rf build-tools.zip android-*', termState.homeDir, false); + } catch(e) { + termPrint('[!] d8.jar download failed: ' + e.message, 'warning'); + termPrint('[*] Trying Termux dx package...', 'info'); + try { + var dxResult = await Bootstrap.installProot(); + } catch(e2) {} + } + } + + var d8Check = await shellExec('test -f "' + jarsDir + '/d8.jar" && test -s "' + jarsDir + '/d8.jar"', termState.homeDir, false); + if (d8Check.exitCode !== 0) { + termPrint('[!] d8.jar not available', 'err'); + return false; + } + termPrint('[OK] d8.jar ready', 'success'); + + var aapt2Check = await shellExec('test -f "' + binDir + '/aapt2"', termState.homeDir, false); + if (aapt2Check.exitCode !== 0) { + termPrint('[*] Installing aapt2 from Termux repo...', 'info'); + try { + var aapt2Result = await Bootstrap.installAapt2({toolsDir: toolsDir}); + termPrint('[OK] aapt2 installed (' + Math.round(aapt2Result.size/1024) + ' KB)', 'success'); + } catch(e) { + termPrint('[!] aapt2 install failed: ' + e.message, 'warning'); + termPrint('[*] Build will work for resource-less APKs only', 'info'); + } + } + + termPrint('[*] Creating app_process wrapper scripts...', 'info'); + var wrappers = { + ecj: '#!/system/bin/sh\nexec /system/bin/app_process /system/bin --nice-name=zaichat -Djava.class.path=' + jarsDir + '/ecj.jar org.eclipse.jdt.internal.compiler.batch.Main "$@"', + d8: '#!/system/bin/sh\nexec /system/bin/app_process /system/bin --nice-name=zaichat -Djava.class.path=' + jarsDir + '/d8.jar com.android.tools.r8.D8 "$@"', + apksigner: '#!/system/bin/sh\nexec /system/bin/app_process /system/bin --nice-name=zaichat -jar ' + jarsDir + '/apksigner.jar "$@"' + }; + + for (var name in wrappers) { + try { + await Shell.writeFile({path: binDir + '/' + name, content: wrappers[name]}); + await shellExec('chmod 755 "' + binDir + '/' + name + '"', termState.homeDir, false); + } catch(e) { + termPrint('[!] Failed to create ' + name + ' wrapper: ' + e.message, 'err'); + } + } + termPrint('[OK] Wrapper scripts created (ecj, d8, apksigner)', 'success'); + + var verify = await shellExec('test -f "' + jarsDir + '/ecj.jar" && test -f "' + jarsDir + '/d8.jar" && test -f "' + jarsDir + '/apksigner.jar" && test -x "' + binDir + '/ecj" && test -x "' + binDir + '/aapt2"', termState.homeDir, false); + if (verify.exitCode === 0) { + termPrint('[OK] Java build environment ready!', 'success'); + showStatusToast('Build tools ready!', 'success'); + termState.devToolsInstalled = true; + termState.javaToolsInstalled = true; + return true; + } + + termPrint('[OK] Java tools ready (aapt2 optional)', 'success'); + termState.devToolsInstalled = true; + termState.javaToolsInstalled = true; + return true; + } + + async function tryPkgInstall(prefixUsr) { + var pkgBin = prefixUsr + '/bin/pkg'; + var pkgTest = await shellExec('test -f "' + pkgBin + '"', termState.homeDir, false); + if (pkgTest.exitCode !== 0) return false; + + var cmd = 'export PREFIX="' + prefixUsr + '" LD_LIBRARY_PATH="' + prefixUsr + '/lib" PATH="' + prefixUsr + '/bin:/system/bin:$PATH" && sh "' + pkgBin + '" update -y 2>&1 && sh "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1'; + var result = await shellExec(cmd, termState.homeDir, false); + if (result.output && result.output.length > 200) { + termPrint(result.output.substring(result.output.length - 200), ''); + } + return await toolsReady(); + } + async function toolsReady() { - var recheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); - if (recheck.exitCode === 0) { + var termuxCheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); + if (termuxCheck.exitCode === 0) { showStatusToast('Build tools installed!', 'success'); termState.devToolsInstalled = true; return true; } + if (termState.toolsDir) { + var javaCheck = await shellExec('test -f "' + termState.toolsDir + '/jars/ecj.jar" && test -f "' + termState.toolsDir + '/jars/d8.jar"', termState.homeDir, false); + if (javaCheck.exitCode === 0) { + termState.devToolsInstalled = true; + termState.javaToolsInstalled = true; + return true; + } + } return false; } @@ -2061,13 +2143,22 @@ if (termState.devToolsInstalled) return; - var check = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); - if (check.exitCode === 0) { + var termuxCheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); + if (termuxCheck.exitCode === 0) { termState.devToolsInstalled = true; return; } - showDevToolsBanner('Build tools (aapt2, ecj, d8) not installed. Tap to auto-install.'); + if (termState.toolsDir) { + var javaCheck = await shellExec('test -f "' + termState.toolsDir + '/jars/ecj.jar" && test -f "' + termState.toolsDir + '/jars/d8.jar"', termState.homeDir, false); + if (javaCheck.exitCode === 0) { + termState.devToolsInstalled = true; + termState.javaToolsInstalled = true; + return; + } + } + + showDevToolsBanner('Build tools not installed. Tap to auto-install via Java virtual environment.'); } function showDevToolsBanner(msg) {