diff --git a/README.md b/README.md index 6d603c4..34a5fa9 100644 --- a/README.md +++ b/README.md @@ -631,6 +631,14 @@ data: [DONE] ## Changelog +### v2.2.2 (2026-05-19) +- **Permission Fix** — `chmod -R 755` on all bootstrap binaries after extraction (fixes "Permission denied" on bash) +- **Shell Auto-Fallback** — if bash fails with permission error, auto-falls back to `/system/bin/sh` +- **fixPermissions Plugin** — new `BootstrapPlugin.fixPermissions()` for JS to call after install +- `ensureBuildTools()` uses full paths to `pkg`/`apt` with `chmod 755` before execution +- `setPermissionsRecursive()` ensures all nested binaries get execute permission +- Shell auto-refreshes path on each execute call, re-chmods if needed + ### v2.2.1 (2026-05-19) - **Auto-Install Build Tools** — `aapt2`, `ecj`, `d8`, `apksigner` auto-installed via `pkg` with full paths and retry logic - **Dev Tools Banner** — yellow warning banner on Coding/Agentic mode if tools missing, one-tap Install button diff --git a/android/app/build.gradle b/android/app/build.gradle index da94c29..69977ea 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 14 - versionName "2.2.1" + versionCode 15 + versionName "2.2.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' 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 165358a..e513c05 100644 --- a/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java +++ b/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java @@ -131,6 +131,8 @@ public class BootstrapPlugin extends Plugin { sendProgress(call, "Setting permissions...", 85); setPermissions(new File(stagingDir, "bin")); setPermissions(new File(stagingDir, "libexec")); + setPermissionsRecursive(new File(stagingDir, "bin")); + setPermissionsRecursive(new File(stagingDir, "libexec")); new File(homeDir).mkdirs(); new File(prefixDir + "/tmp").mkdirs(); @@ -147,6 +149,15 @@ public class BootstrapPlugin extends Plugin { throw new RuntimeException("Failed to rename staging to prefix"); } + try { + Runtime rt = Runtime.getRuntime(); + rt.exec(new String[]{"chmod", "-R", "755", prefixDir + "/bin"}).waitFor(); + rt.exec(new String[]{"chmod", "-R", "755", prefixDir + "/libexec"}).waitFor(); + rt.exec(new String[]{"chmod", "755", prefixDir + "/lib"}).waitFor(); + } catch (Exception ce) { + Log.w(TAG, "chmod after rename failed: " + ce.getMessage()); + } + writeEnvFile(); writeProfileFile(); @@ -316,6 +327,37 @@ public class BootstrapPlugin extends Plugin { } } + private void setPermissionsRecursive(File dir) { + if (!dir.exists() || !dir.isDirectory()) return; + File[] children = dir.listFiles(); + if (children == null) return; + for (File f : children) { + if (f.isDirectory()) { + f.setExecutable(true, false); + setPermissionsRecursive(f); + } else if (f.isFile()) { + f.setExecutable(true, false); + f.setReadable(true, false); + } + } + } + + @PluginMethod + public void fixPermissions(PluginCall call) { + try { + File binDir = new File(prefixDir + "/bin"); + if (binDir.exists()) { + Runtime rt = Runtime.getRuntime(); + rt.exec(new String[]{"chmod", "-R", "755", prefixDir + "/bin"}).waitFor(); + rt.exec(new String[]{"chmod", "-R", "755", prefixDir + "/libexec"}).waitFor(); + setPermissionsRecursive(binDir); + } + call.resolve(new JSObject().put("fixed", true)); + } catch (Exception e) { + call.reject("Fix permissions failed: " + e.getMessage()); + } + } + private void writeEnvFile() { try { File envFile = new File(prefixDir + "/etc/termux.env"); 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 c9bfab5..050ce58 100644 --- a/android/app/src/main/java/ai/z/chat/ShellPlugin.java +++ b/android/app/src/main/java/ai/z/chat/ShellPlugin.java @@ -44,11 +44,27 @@ public class ShellPlugin extends Plugin { new File(homeDir + "/bin").mkdirs(); new File(homeDir + "/tmp").mkdirs(); + refreshShell(); + } + + private void refreshShell() { File bash = new File(prefixDir + "/bin/bash"); File sh = new File(prefixDir + "/bin/sh"); if (bash.exists()) { + if (!bash.canExecute()) { + bash.setExecutable(true, false); + try { + Runtime.getRuntime().exec(new String[]{"chmod", "755", bash.getAbsolutePath()}).waitFor(); + } catch (Exception e) {} + } shellPath = bash.getAbsolutePath(); } else if (sh.exists()) { + if (!sh.canExecute()) { + sh.setExecutable(true, false); + try { + Runtime.getRuntime().exec(new String[]{"chmod", "755", sh.getAbsolutePath()}).waitFor(); + } catch (Exception e) {} + } shellPath = sh.getAbsolutePath(); } else { shellPath = "/system/bin/sh"; @@ -70,6 +86,7 @@ public class ShellPlugin extends Plugin { if (cwd == null || cwd.isEmpty()) cwd = homeDir; try { + refreshShell(); String[] env = buildEnv(); String shell = shellPath != null ? shellPath : "sh"; ProcessBuilder pb = new ProcessBuilder(shell, "-c", command); @@ -77,7 +94,21 @@ public class ShellPlugin extends Plugin { pb.environment().putAll(toEnvMap(env)); pb.redirectErrorStream(true); - Process process = pb.start(); + Process process; + try { + process = pb.start(); + } catch (java.io.IOException ioe) { + if (shell != null && !shell.equals("/system/bin/sh")) { + Log.w(TAG, "Shell " + shell + " failed, falling back to /system/bin/sh: " + ioe.getMessage()); + pb = new ProcessBuilder("/system/bin/sh", "-c", command); + pb.directory(new File(cwd)); + pb.environment().putAll(toEnvMap(env)); + pb.redirectErrorStream(true); + process = pb.start(); + } else { + throw ioe; + } + } String processId = String.valueOf(System.currentTimeMillis()); activeProcesses.put(processId, process); diff --git a/package.json b/package.json index e10f0eb..d6b5188 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zai-chat", - "version": "2.2.1", + "version": "2.2.2", "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 4a3f8c9..f56f329 100644 --- a/www/index.html +++ b/www/index.html @@ -327,13 +327,23 @@
Z.AI Chat v2.2.1
+Z.AI Chat v2.2.2
Built with Z.AI SDK & GLM-5.1
Compatible with Android 15/16
✔ Termux environment installed!
' + - 'Full Linux shell with bash, coreutils, and package manager ready.
' + - 'Installing build tools (aapt2, ecj, d8, apksigner)...
'; - btn.querySelector('.btn-text').textContent = 'Installed'; - btn.querySelector('.btn-loader').style.display = 'none'; - termState.homeDir = result.prefixDir ? result.prefixDir.replace('/usr', '') : termState.homeDir; - termState.cwd = termState.homeDir + '/home'; + progressText.textContent = 'Fixing file permissions...'; + try { await Bootstrap.fixPermissions(); } catch(e) {} + if (Shell) { var env = await Shell.getEnv(); termState.homeDir = env.HOME; @@ -2372,13 +2369,29 @@ } updateCwdDisplay(); + var shellTest = await shellExec('echo OK', termState.homeDir, false); + if (shellTest.exitCode !== 0) { + statusEl.innerHTML = 'Shell test failed: ' + (shellTest.output || 'unknown error') + '
' + + 'Try restarting the app.
'; + btn.disabled = false; + btn.querySelector('.btn-text').textContent = 'Retry Install'; + btn.querySelector('.btn-loader').style.display = 'none'; + return; + } + + statusEl.innerHTML = '✔ Termux environment installed!
' + + 'Installing build tools (aapt2, ecj, d8, apksigner)...
'; + var toolsOk = await ensureBuildTools(); var toolsStatus = $('#devsetup-tools-status'); if (toolsStatus) { toolsStatus.innerHTML = toolsOk - ? '✔ Build tools installed (aapt2, ecj, d8, apksigner)' - : 'Build tools not installed. Run: pkg install aapt2 ecj dx apksigner'; + ? '✔ All tools installed — ready to build!' + : 'Build tools not installed. Open Terminal and run: pkg install aapt2 ecj dx apksigner'; } + + btn.querySelector('.btn-text').textContent = 'Installed'; + btn.querySelector('.btn-loader').style.display = 'none'; } catch(e) { statusEl.innerHTML = 'Install failed: ' + e.message + '
' + 'Check your internet connection and try again.
';