Install on a connected physical Android device with --device - #141
Merged
Conversation
`stim android --device [serial]` builds as usual, then installs and launches on a physical device connected to this machine. With no serial it uses the one connected device and refuses with the candidate list when adb reports several. It cannot be combined with --remote. The physical path skips everything that manages an owned device: the device-capacity check, AVD creation, the boot wait, and the device record. Stim never creates, boots, shuts down, or deletes hardware, and writes no serial to the project registry, so `stop`, `gc`, and `teardown.ts` cannot reach it. The build, the fingerprint, the build cache, and the Metro port gate are unchanged. `debug_http_host` and the dev-client deep link now resolve to localhost on a physical device, which the adb reverse serves. The emulator keeps 10.0.2.2.
janicduplessis
commented
Aug 31, 2026
| if (capacity) return fail(capacity.code, capacity.message, capacity.remedy); | ||
|
|
||
| const emuLog = emulatorLogFile(root); | ||
| const limits = getLimits(); |
Collaborator
Author
There was a problem hiding this comment.
getLimits() is hoisted out of the emulator branch on purpose. The device-capacity check below it is emulator-only -- maxDevices counts devices Stim owns, and a physical device is not one -- but limits.maxBuilds gates the build slot further down, and the build runs locally whether the target is a phone or an emulator. Leaving the call inside the else branch left limits out of scope at the build-slot site.
- Read device properties through `runFile` with an argv, not a shell string built from a serial, per the single-exec-wrapper rule. Adds a timeout so a wedged USB link cannot hang the command. - Refuse a physical device that reports itself as an emulator (`ro.kernel.qemu`, `ro.hardware`). Genymotion and `adb connect` emulators appear as `host:port`, so serial shape alone let `--device` install on a user-created emulator. - Refuse an empty `--device` value. `--device=` yielded `''`, which is falsy, so the run silently used the owned emulator instead. - Let an explicit `--device` win over the `android.remote` setting, the way `--variant` wins over `android.variant`. Only an explicit `--remote` now conflicts, and both argument refusals report STIM_BAD_ARG rather than a device-missing code. - Pass `physical` to the unverified-launch recovery URL, which printed the emulator loopback as the way to reconnect a phone. - Keep the whole adb status in a refusal. `no permissions; see [url]` was truncated to `no`. - Name a signer or versionCode conflict in the install remedy instead of suggesting the cable, which a phone carrying a store build hits first. - Update the website docs and the legacy-record note, which still said Stim does not operate on physical devices.
janicduplessis
marked this pull request as ready for review
August 31, 2026 20:39
This was referenced Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #140
Description
stim androidcould only install on the owned emulator, so anything needing real hardware meant leaving Stim and driving Gradle and adb by hand.Physical-device support was removed deliberately, in 32b96b2, which deleted the old
--serialflag and wrote the ban into invariants 2 and 3 ofAGENTS.md. That was right about ownership -- Stim must not create, boot, shut down, or delete somebody's phone -- but it conflated owning a device with installing on one. This PR keeps the ownership half and drops the rest, so those two invariants are rewritten rather than ignored.Solution
stim android --device [serial]builds as usual, then installs and launches on a connected physical device. With no serial it uses the one connected device, and refuses with the candidate list when adb reports several. It cannot be combined with--remote.debug_http_hostand the dev-client deep link now resolve tolocalhoston a physical device instead of10.0.2.2. This is a fix, not a preference: the emulator host loopback is not routable from a phone, so without it the app finds no bundler and loads no JS at all. Theadb reversethat already ran serveslocalhost. Emulators keep10.0.2.2.Stim never records the phone as a device, so
stop,gc, andteardown.ts-- all of which act on the project's device record -- have nothing to act on. The physical branch also skips the device-capacity check, AVD creation, and the boot wait, but keeps the build-slot cap, since the build runs locally either way.A device that reports itself as an emulator is refused. Genymotion and
adb connectemulators appear to adb ashost:portrather thanemulator-N, soserial shape alone would have let
--deviceinstall on a user-created emulator.ro.kernel.qemuandro.hardwaresettle it.--variantis unchanged; a flavored project still needs it, physical or not.One device, many workspaces
Every workspace gets its own emulator, but they all share the one phone, so it is
worth being precise about what does and does not collide.
Two
adb reversepairs are registered:<port> -> <port>, which is what an appwith
debug_http_hostwritten actually uses, and8081 -> <port>, the fallbackfor an app whose prefs write failed. Only the first is per-workspace. Two
workspaces on 8082 and 8083 therefore do not interfere: each app follows its own
debug_http_hostto its own Metro. Registering a device port replaces anyprevious mapping for it, so the shared
8081entry does belong to whicheverworkspace ran last -- but that only decides where an app lands when its prefs
write failed, which is already a warned, degraded state.
Known limitation: two workspaces building the same
applicationIdoverwriteeach other's install, because that is one package name and one device. The last
--devicerun wins. Emulator isolation does not extend to hardware, and this PRdoes not pretend otherwise.
Risk
Contained. Nothing changes for an emulator run: the physical work is behind
--device, and the host selection defaults to the existing10.0.2.2. The one shared edit is the newphysicalparameter threaded throughwriteDebugHttpHost,androidDevClientUrl, andlaunchAndroidApp, all defaulting tofalse.The exposure worth naming is that Stim now issues
adb install,am start, andrun-asagainst hardware it does not own. Each is scoped to a serial adb already reports asdevice, and the destructive-uninstall retry on a signature conflict is still gated on release variants only, as before. No new adb verb reaches a physical device that did not already reach an emulator.Test plan
Verified end to end against a physical Samsung SM-G996W (
RFCR7081Q9L), building tlon-appsdevelop:verify ready: bundle loadedis the load-bearing line -- it is Metro actually serving the phone over the reverse, which is what the10.0.2.2tolocalhostchange buys.After the run, the project's entry in
~/.stim/config.jsonhas noplatformsrecord at all andstim statusprints no android line, sostopandgchave nothing to act on.Also exercised the real
adb -s <serial> shell getprop ro.product.modelcall behindphysicalDeviceModel, per invariant 9, and confirmed realadb devicesoutput parses to the fixtures the unit tests use.