v2.3.0: Java virtual environment via app_process, SELinux bypass for build tools

This commit is contained in:
admin
2026-05-20 12:28:24 +04:00
Unverified
parent 71a26e259d
commit f86a5added
10 changed files with 417 additions and 87 deletions

View File

@@ -631,6 +631,20 @@ data: [DONE]
## Changelog ## 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) ### v2.2.4 (2026-05-19)
- **Bundled PRoot** — proot binary (v5.1.107) included in APK as native library with executable SELinux label - **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 - **Auto-Proot Install** — `ShellPlugin.refreshShell()` copies proot + loader from APK's `nativeLibraryDir` to `$PREFIX/bin/` and `$PREFIX/libexec/proot/` on first use

View File

@@ -7,8 +7,8 @@ android {
applicationId "ai.z.chat" applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 18 versionCode 19
versionName "2.2.5" versionName "2.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -461,6 +461,53 @@ public class BootstrapPlugin extends Plugin {
} catch (Exception e) { } catch (Exception e) {
try { call.reject("Download failed: " + e.getMessage()); } catch (Exception ignored) {} 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(); }).start();
} }
@@ -624,6 +671,170 @@ public class BootstrapPlugin extends Plugin {
}).start(); }).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<String, String> 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<String, String> 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("!<arch>\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 { private String downloadToString(String urlStr) throws Exception {
URL url = new URL(urlStr); URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();

View File

@@ -347,11 +347,13 @@ public class ShellPlugin extends Plugin {
envList.add("JAVA_HOME=" + toolsDir + "/java"); envList.add("JAVA_HOME=" + toolsDir + "/java");
envList.add("PROJECTS=" + projectsDir); envList.add("PROJECTS=" + projectsDir);
if (hasOurPrefix) { if (hasOurPrefix) {
envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib"); envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib:" + toolsDir + "/lib");
envList.add("BOOTSTRAP=zaichat"); envList.add("BOOTSTRAP=zaichat");
if (prootLoaderPath != null) { if (prootLoaderPath != null) {
envList.add("PROOT_LOADER=" + prootLoaderPath); envList.add("PROOT_LOADER=" + prootLoaderPath);
} }
} else {
envList.add("LD_LIBRARY_PATH=" + toolsDir + "/lib");
} }
if (hasTermux) { if (hasTermux) {
envList.add("TERMUX_VERSION=" + getTermuxVersion()); envList.add("TERMUX_VERSION=" + getTermuxVersion());

View File

@@ -1,6 +1,6 @@
{ {
"name": "zai-chat", "name": "zai-chat",
"version": "2.2.5", "version": "2.3.0",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan", "description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -327,13 +327,25 @@
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h3>About</h3> <h3>About</h3>
<p class="about-text">Z.AI Chat v2.2.5</p> <p class="about-text">Z.AI Chat v2.3.0</p>
<p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p> <p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p>
<p class="about-text">Compatible with Android 15/16</p> <p class="about-text">Compatible with Android 15/16</p>
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h3>Changelog</h3> <h3>Changelog</h3>
<ul class="changelog-list"> <ul class="changelog-list">
<li>
<span class="changelog-version">v2.3.0</span>
<span class="changelog-date">2026-05-20</span>
<ul>
<li><strong>Java Virtual Environment</strong> — ecj.jar + apksigner.jar bundled in APK, d8.jar downloaded at runtime</li>
<li><strong>app_process Wrappers</strong> — Java tools run via /system/bin/app_process (bypasses SELinux execve restrictions)</li>
<li><strong>Runtime aapt2 Install</strong> — downloads aapt2 + dependencies from Termux repo on demand</li>
<li><strong>extractAsset/downloadFile</strong> — new BootstrapPlugin methods for APK asset extraction and URL downloads</li>
<li><strong>Smart Tool Detection</strong> — checks both Termux native tools and Java virtual tools</li>
<li><strong>LD_LIBRARY_PATH</strong> — tools/lib added for aapt2 shared library resolution</li>
</ul>
</li>
<li> <li>
<span class="changelog-version">v2.2.5</span> <span class="changelog-version">v2.2.5</span>
<span class="changelog-date">2026-05-19</span> <span class="changelog-date">2026-05-19</span>

View File

@@ -12,23 +12,24 @@
}; };
var BUILD_SCRIPT = [ var BUILD_SCRIPT = [
'#!/bin/sh', '#!/system/bin/sh',
'set -e', 'set -e',
'PROJECT_DIR=$(pwd)', 'PROJECT_DIR=$(pwd)',
'BUILD_DIR="$PROJECT_DIR/build"', 'BUILD_DIR="$PROJECT_DIR/build"',
'', '',
'AAPT2=$(command -v aapt2 2>/dev/null)', 'AAPT2=$(command -v aapt2 2>/dev/null || echo "$TOOLS/bin/aapt2")',
'ECJ=$(command -v ecj 2>/dev/null)', 'ECJ=$(command -v ecj 2>/dev/null || echo "$TOOLS/bin/ecj")',
'D8=$(command -v d8 2>/dev/null)', 'D8=$(command -v d8 2>/dev/null || echo "$TOOLS/bin/d8")',
'DX=$(command -v dx 2>/dev/null)', 'APKSIGNER=$(command -v apksigner 2>/dev/null || echo "$TOOLS/bin/apksigner")',
'APKSIGNER=$(command -v apksigner 2>/dev/null)',
'', '',
'if [ -z "$AAPT2" ]; then echo "[BUILD FAILED] aapt2 not found. Install: pkg install aapt2"; exit 1; fi', 'if [ ! -x "$AAPT2" ] && [ ! -f "$AAPT2" ]; then echo "[BUILD FAILED] aapt2 not found"; exit 1; fi',
'if [ -z "$ECJ" ]; then echo "[BUILD FAILED] ecj not found. Install: pkg install ecj"; exit 1; fi', 'if [ ! -f "$ECJ" ]; then echo "[BUILD FAILED] ecj not found"; exit 1; fi',
'if [ -z "$D8" ] && [ -z "$DX" ]; then echo "[BUILD FAILED] d8/dx not found. Install: pkg install d8"; exit 1; fi', 'if [ ! -f "$D8" ]; then echo "[BUILD FAILED] d8 not found"; exit 1; fi',
'', '',
'ANDROID_JAR=$(dirname "$AAPT2")/../share/aapt2/android.jar', '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"', 'rm -rf "$BUILD_DIR"',
'mkdir -p "$BUILD_DIR/gen" "$BUILD_DIR/classes" "$BUILD_DIR/apk"', '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 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', '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', '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..."', 'echo "[*] Converting to DEX..."',
'find "$BUILD_DIR/classes" -name "*.class" > "$BUILD_DIR/classfiles.txt"', '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 [ ! -s "$BUILD_DIR/classfiles.txt" ]; then echo "[BUILD FAILED] No class files compiled"; exit 1; fi',
'if [ -n "$D8" ]; then', 'sh "$D8" --output "$BUILD_DIR" @"$BUILD_DIR/classfiles.txt" 2>&1 || { echo "[BUILD FAILED] D8 failed"; exit 1; }',
' "$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',
'if [ ! -f "$BUILD_DIR/classes.dex" ]; then echo "[BUILD FAILED] classes.dex not found"; exit 1; fi', 'if [ ! -f "$BUILD_DIR/classes.dex" ]; then echo "[BUILD FAILED] classes.dex not found"; exit 1; fi',
'', '',
'echo "[*] Packaging..."', 'echo "[*] Packaging..."',
@@ -93,20 +90,12 @@
' fi', ' fi',
'fi', 'fi',
'', '',
'if [ -f "$KEYSTORE" ] && [ -n "$APKSIGNER" ]; then', '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',
' "$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"',
' 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',
'', '',
'APK_PATH="$BUILD_DIR/app-signed.apk"', 'APK_PATH="$BUILD_DIR/app-signed.apk"',
'APK_SIZE=$(du -h "$APK_PATH" 2>/dev/null | cut -f1)', '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'); ].join('\n');
var state = { var state = {
@@ -1182,8 +1171,10 @@
activePid: null, activePid: null,
activeStreamId: null, activeStreamId: null,
devToolsInstalled: false, devToolsInstalled: false,
javaToolsInstalled: false,
hasProot: false, hasProot: false,
prootPath: '', prootPath: '',
nativeLibDir: '',
commandQueue: [] commandQueue: []
}; };
@@ -1913,7 +1904,15 @@
return true; 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) { if (Shell) {
var env = await Shell.getEnv(); var env = await Shell.getEnv();
@@ -1923,79 +1922,162 @@
termState.cwd = env.CWD || env.HOME; termState.cwd = env.CWD || env.HOME;
termState.hasProot = env.hasProot === true; termState.hasProot = env.hasProot === true;
termState.prootPath = env.prootPath || ''; termState.prootPath = env.prootPath || '';
termState.nativeLibDir = env.nativeLibDir || '';
} }
var prefix = termState.homeDir ? termState.homeDir.replace('/home', '') : termState.homeDir || ''; termPrint('[*] Setting up Java build tools (app_process virtual JVM)...', 'info');
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');
showStatusToast('Installing build tools...', 'info'); showStatusToast('Installing build tools...', 'info');
var installCmd = 'export PREFIX="' + prefixUsr + '" LD_LIBRARY_PATH="' + prefixUsr + '/lib" && export PATH="' + prefixUsr + '/bin:/system/bin:$PATH" && '; var javaOk = await setupJavaTools();
if (pkgTest.exitCode === 0) { if (javaOk) return true;
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';
}
termPrint('[*] Strategy 1: Direct execution (patched shebangs)...', 'info'); var prefix = termState.homeDir ? termState.homeDir.replace('/home', '') : '';
var result = await shellExec(installCmd, termState.homeDir, false); var prefixUsr = prefix + '/usr';
if (result.output) { try { await Bootstrap.fixPermissions(); } catch(e) {}
var out = result.output;
if (out.length > 2000) out = out.substring(0, 1000) + '\n... truncated ...\n' + out.substring(out.length - 800); termPrint('[*] Trying Termux pkg install...', 'info');
termPrint(out.replace(/\n$/, ''), ''); var pkgOk = await tryPkgInstall(prefixUsr);
} if (pkgOk) return true;
if (await toolsReady()) { termPrint('[OK] Build tools installed!', 'success'); return true; }
termPrint('[*] Strategy 2: Bundled PRoot...', 'info');
if (termState.prootPath) { if (termState.prootPath) {
termPrint('[OK] PRoot from APK: ' + termState.prootPath, 'success'); termPrint('[*] Trying PRoot...', 'info');
var prootOk = await tryProotExec(termState.prootPath, prefixUsr, pkgBin, aptBin); var prootOk = await tryProotExec(termState.prootPath, prefixUsr, prefixUsr + '/bin/pkg', prefixUsr + '/bin/apt');
if (prootOk) return true; 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(); var termuxOk = await tryTermuxInstall();
if (termuxOk) return true; if (termuxOk) return true;
termPrint('', ''); termPrint('[!] All strategies failed. Install Termux from F-Droid:', 'err');
termPrint('[!] All auto-install strategies failed.', 'err'); termPrint(' https://f-droid.org/en/packages/com.termux/', 'warning');
termPrint('[*] Manual fix: Install Termux from F-Droid:', 'warning'); termPrint(' Then: pkg update && pkg install aapt2 ecj dx apksigner', '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');
termState.devToolsInstalled = false; termState.devToolsInstalled = false;
return 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() { async function toolsReady() {
var recheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); var termuxCheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false);
if (recheck.exitCode === 0) { if (termuxCheck.exitCode === 0) {
showStatusToast('Build tools installed!', 'success'); showStatusToast('Build tools installed!', 'success');
termState.devToolsInstalled = true; termState.devToolsInstalled = true;
return 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; return false;
} }
@@ -2061,13 +2143,22 @@
if (termState.devToolsInstalled) return; 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); var termuxCheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false);
if (check.exitCode === 0) { if (termuxCheck.exitCode === 0) {
termState.devToolsInstalled = true; termState.devToolsInstalled = true;
return; 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) { function showDevToolsBanner(msg) {