Skip to content

Commit 6ece4b7

Browse files
committed
fix(mobile): extended env actually installs — proot URL, DNS seed, wrapper refresh
The "install advanced tools on first launch" feature was silently broken end-to-end on existing installs: 1. **proot URL was dead**: proot.gitlab.io/proot/bin/proot-aarch64-static returns an HTML 404 page. The 3.7 KB HTML blob was written as runtime/bin/proot, which sh then exec'd → syntax error on line 2 ("<!DOCTYPE html>"), and every subsequent proot invocation (apk update, apk add, per-tool wrappers) failed silently. Switch to the maintained proot-me/proot v5.3.0 release on GitHub which ships a genuine static aarch64 ELF (~1.4 MB). 2. **extended_env check was too weak**: returned true as soon as the rootfs directory existed, regardless of whether `apk add` had actually succeeded. So after a failed install attempt users were left with a base Alpine minirootfs (apk-tools, busybox, libc-utils — no git, no nano, no tmux), and the frontend's "already installed, skip" path locked them out of a retry. Tighten the check to require rootfs/usr/bin/git as a sentinel — if the git binary isn't there, the install didn't complete and we must re-run it. 3. **rootfs had no DNS**: Alpine minirootfs ships without /etc/resolv.conf, and proot hides the host /etc from the guest. apk fetch, git-http, wget, ssh all errored with "bad address". Seed a resolv.conf with public resolvers at rootfs extraction time AND on every server start (idempotent refresh). 4. **proot-run wrapper baked in the rotating APK path**: the wrapper contains the full nativeLibraryDir path, which Android rotates on every APK upgrade. After an upgrade the wrapper pointed at a path that no longer existed → proot process lookup silently failed. The wrapper was only written inside install_extended_env, which only runs once per device. Regenerate the wrapper on every server start when the rootfs is present so it always matches the current install. 5. **PROOT_TMP_DIR missing**: proot needs a writable tmp for its shadow filesystem. Default lookups land in paths the app sandbox can't write, causing "can't create temporary file" errors on every invocation. Export PROOT_TMP_DIR pointing to runtime/tmp and mkdir -p it from inside the wrapper so it's always present. Net effect: first-launch users now actually get git/nano/tmux/python/ node/vim/tree/htop/fzf/rg/bat once the ExtractionProgress screen finishes, and APK upgrades keep them working without a manual re-run. Validated on Mi 10 Pro (b7163823) after manual patching with the matching shell commands.
1 parent 24fea69 commit 6ece4b7

1 file changed

Lines changed: 87 additions & 4 deletions

File tree

packages/mobile/src-tauri/src/runtime.rs

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,16 @@ pub async fn check_runtime(app: AppHandle) -> RuntimeInfo {
4141
} else {
4242
false
4343
};
44-
let extended_env = dir.join("rootfs").exists();
44+
// extended_env is only considered ready if the rootfs exists AND at least
45+
// one of the apk-installed tools is present. Checking only rootfs existence
46+
// is not enough: `apk add` may fail silently (network issue, stale proot
47+
// binary, permission errors) leaving a base-only Alpine with apk-tools but
48+
// none of the user-facing tools (git, nano, tmux, python, node). We pick
49+
// `git` as the sentinel because it's the most frequently requested tool
50+
// and its absence is the clearest signal that install_extended_env didn't
51+
// finish. If this check fails, the frontend re-runs installExtendedEnv.
52+
let rootfs_dir = dir.join("rootfs");
53+
let extended_env = rootfs_dir.exists() && rootfs_dir.join("usr/bin/git").exists();
4554

4655
// Log debug info
4756
if let Some(nlib) = native_lib_dir(&dir) {
@@ -465,6 +474,49 @@ alias cls='clear'\n\
465474
let resolv_path = dir.join("resolv.conf");
466475
let _ = fs::write(&resolv_path, "nameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 1.1.1.1\n");
467476

477+
// Regenerate proot-run every launch when the Alpine rootfs is present.
478+
// The wrapper bakes in the APK install path (nativeLibraryDir) which
479+
// rotates on every upgrade — a stale wrapper points at a path that no
480+
// longer exists and causes every proot invocation (git, nano, tmux…)
481+
// to fail silently. install_extended_env writes this same file but is
482+
// only called once per device; we need it to stay fresh after upgrades.
483+
let rootfs_dir = dir.join("rootfs");
484+
if rootfs_dir.exists() {
485+
let proot_path = bin_link_dir.join("proot");
486+
let proot_run_path = bin_link_dir.join("proot-run");
487+
let proot_tmp_dir = dir.join("tmp");
488+
let _ = fs::create_dir_all(&proot_tmp_dir);
489+
// Also refresh the rootfs DNS config — if the user cleared /tmp or a
490+
// prior install shipped with no /etc/resolv.conf, apk/git/etc will
491+
// fail with "bad address" errors. Idempotent.
492+
let rootfs_etc = rootfs_dir.join("etc");
493+
let _ = fs::create_dir_all(&rootfs_etc);
494+
let _ = fs::write(
495+
rootfs_etc.join("resolv.conf"),
496+
"nameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 1.1.1.1\n",
497+
);
498+
let proot_run_content = format!(
499+
"#!/bin/sh\n\
500+
export LD_LIBRARY_PATH=\"{lib_links}:{nlib}\"\n\
501+
export PROOT_TMP_DIR=\"{tmp}\"\n\
502+
mkdir -p \"$PROOT_TMP_DIR\"\n\
503+
exec {proot} \\\n --rootfs={rootfs} \\\n --bind={bin}:/usr/local/bin \\\n --bind={nlib}:/usr/local/lib \\\n --bind=/dev:/dev \\\n --bind=/proc:/proc \\\n --bind=/sys:/sys \\\n -w /root \\\n \"$@\"\n",
504+
proot = proot_path.display(),
505+
rootfs = rootfs_dir.display(),
506+
bin = bin_link_dir.display(),
507+
nlib = nlib_dir.display(),
508+
lib_links = lib_link_dir.display(),
509+
tmp = proot_tmp_dir.display(),
510+
);
511+
if fs::write(&proot_run_path, proot_run_content).is_ok() {
512+
if let Ok(meta) = fs::metadata(&proot_run_path) {
513+
let mut p = meta.permissions();
514+
p.set_mode(0o755);
515+
let _ = fs::set_permissions(&proot_run_path, p);
516+
}
517+
}
518+
}
519+
468520
// Concatenate Android CA certificates into a single PEM file for TLS.
469521
// musl-linked Bun/BoringSSL can't find Android's certs at /system/etc/security/cacerts/.
470522
let ca_bundle_path = dir.join("ca-certificates.crt");
@@ -746,8 +798,15 @@ pub async fn install_extended_env(app: AppHandle) -> Result<(), String> {
746798
},
747799
);
748800

749-
// Download proot static binary
750-
let proot_url = "https://proot.gitlab.io/proot/bin/proot-aarch64-static";
801+
// Download proot static binary. The original gitlab pages URL
802+
// (proot.gitlab.io/proot/bin/proot-aarch64-static) is dead and returns
803+
// HTML — the resulting "proot" binary is an HTML blob that syntax-errors
804+
// when sh tries to exec it, causing every subsequent apk invocation to
805+
// fail silently (proot error leaks as sh error, apk add reports "partial
806+
// failure", rootfs stays base-only). Use the proot-me/proot GitHub
807+
// releases which host real statically-linked binaries.
808+
let proot_url =
809+
"https://github.com/proot-me/proot/releases/download/v5.3.0/proot-v5.3.0-aarch64-static";
751810
let proot_path = bin_link_dir.join("proot");
752811

753812
let client = reqwest::Client::builder()
@@ -827,10 +886,33 @@ pub async fn install_extended_env(app: AppHandle) -> Result<(), String> {
827886

828887
fs::remove_file(&alpine_path).ok();
829888

830-
// Create proot-run wrapper script
889+
// Seed the rootfs with a working /etc/resolv.conf so apk, git-http, wget,
890+
// ssh etc. can resolve DNS inside proot. Alpine minirootfs ships without
891+
// one, and proot's sandbox hides the Android /etc tree — resulting in
892+
// "bad address" errors during apk update / package fetch. We reuse the
893+
// same public resolvers we wrote to the parent runtime's resolv.conf
894+
// (see extract_runtime, line ~466) so both the host bun sidecar and
895+
// the chrooted Alpine agree on name resolution.
896+
let rootfs_etc = rootfs_dir.join("etc");
897+
let _ = fs::create_dir_all(&rootfs_etc);
898+
let _ = fs::write(
899+
rootfs_etc.join("resolv.conf"),
900+
"nameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 1.1.1.1\n",
901+
);
902+
903+
// Create proot-run wrapper script. Notes:
904+
// - PROOT_TMP_DIR must be exported: proot needs a writable tmp dir for
905+
// its shadow filesystem; without it every invocation errors with
906+
// "can't create temporary file: No such file or directory".
907+
// - mkdir -p is idempotent and cheap; keeps the wrapper self-healing
908+
// when /data/.../runtime/tmp gets cleaned.
909+
// - Copy host resolv.conf into the rootfs so apk/git/etc. can resolve
910+
// DNS — Alpine minirootfs ships without /etc/resolv.conf.
831911
let wrapper = format!(
832912
r#"#!/bin/sh
833913
export LD_LIBRARY_PATH="{lib_links}:{nlib}"
914+
export PROOT_TMP_DIR="{tmp}"
915+
mkdir -p "$PROOT_TMP_DIR"
834916
exec {proot} \
835917
--rootfs={rootfs} \
836918
--bind={bin}:/usr/local/bin \
@@ -846,6 +928,7 @@ exec {proot} \
846928
bin = bin_link_dir.display(),
847929
nlib = nlib_dir.display(),
848930
lib_links = lib_link_dir.display(),
931+
tmp = dir.join("tmp").display(),
849932
);
850933

851934
let wrapper_path = bin_link_dir.join("proot-run");

0 commit comments

Comments
 (0)