diff --git a/CHANGELOG.md b/CHANGELOG.md index 63dc9d3..3b1a2e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [Unreleased] + +### Fixed + +- **Android deploys no longer crash concurrent Gradle builds (MOB-468).** + Before every `assembleDebug`, mob_dev deleted `~/.gradle/native/**/*.lock` + as "stale locks". native-platform holds those only while it extracts (they + are never left stale), and their content marks a finished extraction: without one, the next Gradle JVM rewrites + `libnative-platform.so` in place, over the inode every running Gradle JVM + (another project's build, a `--no-daemon` wrapper and its daemon) has + mapped, and those JVMs die with SIGSEGV (`ld-linux-x86-64.so.2+0x10f2` in + the wrapper, a JNI `stat`/`getPid` in the daemon). The markers are kept now; + the wrapper, daemon-registry and cache locks are still cleared. + ## [0.7.22] - 2026-10-09 ### Fixed diff --git a/lib/mob_dev/native_build.ex b/lib/mob_dev/native_build.ex index e3528e2..c1de292 100644 --- a/lib/mob_dev/native_build.ex +++ b/lib/mob_dev/native_build.ex @@ -1574,15 +1574,22 @@ defmodule MobDev.NativeBuild do # Remove stale Gradle lock files left behind when a build is interrupted # (Ctrl+C, kill, etc.). These cause the next run to hang indefinitely while # the wrapper waits to acquire the lock. - defp clear_stale_gradle_locks do - gradle_home = - System.get_env("GRADLE_USER_HOME") || - Path.join(System.user_home!(), ".gradle") - + # + # Never `/native/**/*.lock` (MOB-468): native-platform holds + # each only while it extracts (never left stale), and its 1-byte content + # marks a finished extraction. Without one, the next Gradle JVM re-extracts + # `libnative-platform.so` (and friends) in place, over the inode every + # running Gradle JVM has mapped, and those JVMs SIGSEGV (the wrapper in + # `ld-linux` at exit, the daemon in a JNI `stat`/`getPid`). + @doc false + @spec clear_stale_gradle_locks(Path.t()) :: :ok + def clear_stale_gradle_locks( + gradle_home \\ System.get_env("GRADLE_USER_HOME") || + Path.join(System.user_home!(), ".gradle") + ) do patterns = [ "#{gradle_home}/daemon/*/registry.bin.lock", "#{gradle_home}/wrapper/dists/**/*.lck", - "#{gradle_home}/native/**/*.lock", "#{gradle_home}/caches/**/*.lock", "#{gradle_home}/caches/**/*.lck" ] diff --git a/test/mob_dev/native_build_test.exs b/test/mob_dev/native_build_test.exs index f837a4b..bae7642 100644 --- a/test/mob_dev/native_build_test.exs +++ b/test/mob_dev/native_build_test.exs @@ -2400,6 +2400,34 @@ defmodule MobDev.NativeBuildTest do end end + describe "clear_stale_gradle_locks/1" do + @describetag :tmp_dir + + # MOB-468: native-platform's `.so.lock` marks a finished extraction. + # Deleting it made the next Gradle JVM rewrite the mapped .so in place and + # crashed every concurrent Gradle JVM (SIGSEGV in ld-linux / JNI). + test "clears stale wrapper, daemon and cache locks but keeps native-platform's extraction markers", + %{tmp_dir: home} do + marker = Path.join(home, "native/68d5/linux-amd64/libnative-platform.so.lock") + lib = Path.join(home, "native/68d5/linux-amd64/libnative-platform.so") + + stale = [ + Path.join(home, "daemon/8.2.1/registry.bin.lock"), + Path.join(home, "caches/8.2.1/fileHashes/fileHashes.lock") + ] + + for f <- [marker, lib | stale] do + File.mkdir_p!(Path.dirname(f)) + File.write!(f, <<1>>) + end + + assert NativeBuild.clear_stale_gradle_locks(home) == :ok + assert File.exists?(marker) + assert File.exists?(lib) + for f <- stale, do: refute(File.exists?(f), f) + end + end + # ── ios_bundle_id/1 ─────────────────────────────────────────────────────────── # # The sim bundle, the device bundle, and code signing must all stamp the