v2.3.0: Java virtual environment via app_process, SELinux bypass for build tools
This commit is contained in:
14
README.md
14
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
|
||||
|
||||
@@ -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:!*~'
|
||||
|
||||
BIN
android/app/src/main/assets/jars/apksigner.jar
Normal file
BIN
android/app/src/main/assets/jars/apksigner.jar
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/jars/d8.jar
Normal file
BIN
android/app/src/main/assets/jars/d8.jar
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/jars/ecj.jar
Normal file
BIN
android/app/src/main/assets/jars/ecj.jar
Normal file
Binary file not shown.
@@ -464,6 +464,53 @@ public class BootstrapPlugin extends Plugin {
|
||||
}).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();
|
||||
}
|
||||
|
||||
private void writeEnvFile() {
|
||||
try {
|
||||
File envFile = new File(prefixDir + "/etc/termux.env");
|
||||
@@ -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<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 {
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -327,13 +327,25 @@
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<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 & GLM-5.1</p>
|
||||
<p class="about-text">Compatible with Android 15/16</p>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>Changelog</h3>
|
||||
<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>
|
||||
<span class="changelog-version">v2.2.5</span>
|
||||
<span class="changelog-date">2026-05-19</span>
|
||||
|
||||
255
www/js/app.js
255
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) {
|
||||
|
||||
Reference in New Issue
Block a user