Stop Gradle installing on every checkout's emulator at once

The adb wrapper can only aim a call that goes through it, and Gradle's
Android tasks do not: installDebug, uninstallDebug and connectedAndroidTest
ask the adb server for every attached device and act on all of them. One
session ran installDebug with two emulators up and replaced the app on both,
which Gradle reported as success and the other session read as its own build
never landing.

`emu check` answers the question the wrapper already answers for adb --
would a command that reaches every attached device reach a stranger's
emulator from here -- and install.sh links a Gradle init script into
~/.gradle/init.d so every build on this machine asks it, including checkouts
nobody has adopted it in.

It refuses narrowly: a lone emulator, a physical phone and a caller who
named a device are all fine. ANDROID_SERIAL naming another checkout's
emulator is not, because that is what a serial exported into a long-lived
shell decays into once an emulator restarts and another's takes the port.
This commit is contained in:
iris committed 2026-08-31 15:40:58 -04:00
1 parent 1da445ae32
commit 69a22c54e7
4 files changed
+158 -2

No files matched your search

+55 -1
View File
@@ -46,9 +46,12 @@ usage: emu <command>
list every emulator attached, with its AVD and what it is costing
up start it, refusing if the machine has no room
down stop it
check refuse if a command that reaches every attached device would
reach another checkout's emulator from here
Environment: AVD_NAME overrides the name, EMU_FORCE=1 overrides the memory
refusal, DEVICE_PROFILE and SYSTEM_IMAGE decide what `up` creates.
refusal, EMU_ANY_DEVICE=1 overrides `check`, DEVICE_PROFILE and SYSTEM_IMAGE
decide what `up` creates.
USAGE
exit 2
}
@@ -266,11 +269,62 @@ cmd_down() {
echo "emu: stopped '$avd' ($serial)" >&2
}
# Whether a command that talks to "every attached device" can safely run in
# this directory.
#
# Gradle's Android install, uninstall and connected-test tasks do exactly
# that: they ask the adb server for every device and act on all of them. With
# two sessions' emulators up, `./gradlew installDebug` replaces the app on
# both, reports success, and says nothing -- from the other session it reads
# as its own build never landing. That happened on 2026-08-31.
#
# The rule is the adb wrapper's, so that "which device does a command here
# mean" has one answer: what must not be touched is *another checkout's
# emulator*. A physical phone is nobody's checkout, and this checkout's own
# emulator is this checkout's business.
cmd_check() {
[ -n "${EMU_ANY_DEVICE:-}" ] && return 0
others=$(running_avds "$adb" | awk -F'\t' -v mine="$avd" 'NF && $2 != mine')
[ -n "$others" ] || return 0
# A caller that named a device has already narrowed the fan-out to one,
# and choosing a phone or their own emulator is a decision, not a
# mistake. The single bad case is naming somebody else's -- which is
# what a serial exported into a long-lived shell decays into, once that
# emulator restarts and another checkout's takes the port.
if [ -n "${ANDROID_SERIAL:-}" ]; then
clash=$(printf '%s\n' "$others" |
awk -F'\t' -v s="$ANDROID_SERIAL" '$1 == s {print $2; exit}')
[ -n "$clash" ] || return 0
{
echo "emu: ANDROID_SERIAL=$ANDROID_SERIAL is the '$clash' checkout's emulator."
echo " This directory means '$avd'."
echo
echo " ANDROID_SERIAL=\$(emu serial) <command> aim at '$avd' instead"
} >&2
exit 1
fi
{
echo "emu: this reaches every attached device, and these belong to other checkouts:"
printf '%s\n' "$others" | while IFS=$'\t' read -r s n; do
printf ' %-16s %s\n' "$s" "$n"
done
echo
echo " ANDROID_SERIAL=\$(emu serial) <command> aim at this checkout's '$avd'"
echo " emu up start '$avd', if it is not running"
echo " EMU_ANY_DEVICE=1 <command> really mean every device"
} >&2
exit 1
}
case "${1:-}" in
name) echo "$avd" ;;
serial) avd_serial "$adb" "$avd" || { echo "emu: '$avd' is not running" >&2; exit 1; } ;;
list) cmd_list ;;
up) cmd_up ;;
down) cmd_down ;;
check) cmd_check ;;
*) usage ;;
esac