{"id":50509951,"url":"https://github.com/rahuletto/droidmount","last_synced_at":"2026-06-02T19:03:59.355Z","repository":{"id":356270400,"uuid":"1229781261","full_name":"Rahuletto/droidmount","owner":"Rahuletto","description":"Connect your android device with macOS like its another external drive","archived":false,"fork":false,"pushed_at":"2026-05-07T09:32:08.000Z","size":6262,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-07T11:36:44.707Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Rahuletto.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-05T11:31:21.000Z","updated_at":"2026-05-07T09:32:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Rahuletto/droidmount","commit_stats":null,"previous_names":["rahuletto/droidmount"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/Rahuletto/droidmount","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rahuletto%2Fdroidmount","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rahuletto%2Fdroidmount/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rahuletto%2Fdroidmount/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rahuletto%2Fdroidmount/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Rahuletto","download_url":"https://codeload.github.com/Rahuletto/droidmount/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rahuletto%2Fdroidmount/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33833280,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-02T19:03:57.886Z","updated_at":"2026-06-02T19:03:59.339Z","avatar_url":"https://github.com/Rahuletto.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AndroidMount\n\nA native macOS menu-bar app that auto-mounts your Android phone as a\nreal Finder volume over MTP, using **libmtp** + **macFUSE**.\n\nNo window. No drag-and-drop app. Plug your phone in, pick \"File\nTransfer\" on the device, and use the **menu bar** item → **Open in\nFinder**. The mount lives under **`~/.AndroidMount/\u003cdevice-name\u003e/`**\n(a hidden folder in your home directory; `volname` still labels the\nvolume in Finder).\n\n```\n┌──────────────────────┐    USB    ┌─────────────┐\n│  AndroidMount.app    │◄─────────►│  Android    │\n│  (Swift, menu bar)   │   IOKit   │  phone      │\n│        │             │           │  (MTP mode) │\n│        ▼             │           └─────────────┘\n│  spawns `mtpfuse`    │\n│  (C, libmtp + FUSE)  │──► ~/.AndroidMount/... (macFUSE volume)\n└──────────────────────┘\n```\n\n## Prerequisites\n\n1. **Xcode command line tools** – `xcode-select --install`\n2. **Homebrew + libmtp** – `brew install libmtp`\n3. **macFUSE** – download from \u003chttps://osxfuse.github.io\u003e, install,\n   and **reboot** (it needs to load a kernel extension).\n4. Android device with **MTP / \"File Transfer\"** mode enabled (not PTP\n   or charge-only).\n\n## Build and run\n\nFrom the directory that contains this `Makefile` (repository root):\n\n```bash\nmake check-deps     # verifies macFUSE + libmtp are reachable\nmake                # builds build/AndroidMount.app\nmake run            # rebuild bundle, kill AndroidMount/mtpfuse/Finder, open the app\n./scripts/run.sh    # clean + full build + open\n```\n\nUse `ANDROIDMOUNT_SKIP_KILL=1 make run` to open without killing processes first.\n\n**Using it:** unlock the phone, set USB to **File Transfer / MTP**, then\nlaunch the app (or keep it running). When the menu shows **Connected**,\nchoose **Open in Finder** (⌘O). **Eject** before unplugging.\n\n**Debug `mtpfuse`:** to see `[LOAD]` / libmtp logs on stderr, run the\nhelper by hand after the phone is connected (adjust paths if needed):\n\n```bash\nmkdir -p \"$HOME/.AndroidMount/Debug\"\n./build/mtpfuse -f -o volname=Debug \"$HOME/.AndroidMount/Debug\"\n```\n\nThe Makefile compiles two binaries:\n\n| Binary    | Source        | Purpose                                 |\n|-----------|---------------|-----------------------------------------|\n| `AndroidMount` | `AndroidMount/*.swift` | menu-bar app, USB watcher |\n| `mtpfuse`      | `MTPFuse/*.c`          | FUSE daemon (libmtp bridge) |\n\nBoth are bundled into `build/AndroidMount.app`. The app finds the\nhelper via `Bundle.main.bundleURL/Contents/MacOS/mtpfuse`.\n\n**Developers \u0026 coding agents:** see **`AGENTS.md`** (repo overview) and the\nproject Cursor skill **`.cursor/skills/droidmount-mtp/SKILL.md`** (MTP/FUSE\nbehavior, env vars, partial vs full reads, mutex rules). Update those when you\nchange bridge or FUSE semantics.\n\nThe build is **ad-hoc signed** (`codesign -s -`). No Apple Developer\naccount is required, and Gatekeeper will allow the app on the machine\nthat built it. macOS may still prompt the first time you launch.\n\n## How it works\n\n* `USBWatcher.swift` – `IOServiceAddMatchingNotification` on\n  `kIOUSBDeviceClassName`, filtering by known Android vendor IDs\n  (Google 0x18D1, Samsung 0x04E8, etc.) and \"MTP\" interface strings.\n* `MountManager.swift` – on connect, spawns `mtpfuse -f -o … volname=…`\n  on `~/.AndroidMount/\u003cdevice\u003e/`. On disconnect (or \"Eject\"), runs\n  `diskutil unmount force` then `terminate()` + SIGKILL fallback.\n* `mtp_bridge.c` – wraps libmtp: `LIBMTP_Init`, `Detect_Raw_Devices` /\n  `Open_Raw_Device_Uncached`, `Get_Storage`, `Get_Files_And_Folders`\n  **one folder level at a time** (lazy tree under `path → object_id`).\n* `fs_ops.c` – FUSE 2.x `getattr / readdir / read / write / rename /\n  unlink / mkdir / rmdir / create / release / truncate`. Reads/writes are\n  staged through a per-handle temp file because MTP is request/response\n  and not seekable; on `release()` a dirty handle is streamed back to the\n  device with `LIBMTP_Send_File_From_File_Descriptor` (no full-file\n  `malloc`).\n\n## Limitations\n\n* **Several phones at once:** the menu-bar app starts one `mtpfuse` per\n  USB `locationID` under `~/.AndroidMount/\u003cname\u003e_\u003clocationHex\u003e/`. Each\n  helper sets `MTP_USB_BUS_LOCATION` so libmtp opens the matching raw\n  device (`bus_location`). If a device is not found, set `MTP_RAW_INDEX`\n  or check stderr for the listed `bus_location` values.\n* Writes happen on `release()`, so very large copies still need enough\n  free disk space for the staging file under `/tmp`.\n* **Rename / move** uses `LIBMTP_Set_Object_Filename` and\n  `LIBMTP_Move_Object`; moving a folder to another parent refreshes the\n  in-memory tree. Some vendor MTP stacks may behave oddly.\n* There is no widely used “modern” replacement for **libmtp** on macOS\n  for Android file transfer; this project stays on libmtp for device\n  compatibility.\n* macFUSE is required; if the kext is missing the app pops a one-shot\n  alert with a link to the installer instead of crashing.\n\n## Project layout\n\n```\nAndroidMount/\n├── Makefile\n├── README.md\n├── AndroidMount/         ← Swift menu-bar app\n│   ├── AppDelegate.swift\n│   ├── USBWatcher.swift\n│   ├── MountManager.swift\n│   └── Info.plist        ← LSUIElement = YES\n└── MTPFuse/              ← C FUSE daemon\n    ├── main.c\n    ├── fs_ops.c\n    ├── fs_ops.h\n    ├── mtp_bridge.c\n    └── mtp_bridge.h\n```\n\n## Troubleshooting\n\n* **Action log (FUSE + MTP):** the menu-bar app sets `MTPFUSE_LOG=1` and\n  `MTPFUSE_LOG_PATH=~/.AndroidMount/mtpfuse.log` (append, session banners per\n  process). Each FUSE op is logged with `op=…`, `path=…`, `rc=`, and\n  `dt=…ms` (and `read`/`write` include size and offset). The bridge’s existing\n  `mtp_log` lines (MTP I/O) go to the same file when enabled. Watch live:\n\n  ```bash\n  tail -F ~/.AndroidMount/mtpfuse.log\n  ```\n\n  A stable symlink is created at `/tmp/mtpfuse-debug-latest.log` when the log\n  file opens. Disable file logging: `MTPFUSE_LOG=0`. Enable from the shell\n  without the app: `MTPFUSE_LOG=1` and optionally\n  `MTPFUSE_LOG_PATH=~/.AndroidMount/mtpfuse.log` (same rules as\n  `MTPFUSE_DEBUG_LOG` for a custom path). `MTPFUSE_LOG_SYNC=1` flushes every\n  line (slower, safer if the process is killed). `MTPFUSE_DEBUG=1` also turns\n  logging on. Hot-path volume queries: if `statfs` lines are too noisy, we can\n  add a filter in a follow-up.\n\n* **Finder stuck on “Loading…”** – use the log above. Long gaps between\n  `readdir_snapshot` and `Get_Files_And_Folders` mean the phone or MTP listing\n  is slow for that folder; many `fuse op=read` / bridge `mtp_read` lines mean\n  Finder is reading data (e.g. previews).\n\n* **“Mount point is already in use”** – usually a leftover macFUSE mount after\n  Finder or the app hung. On the next connect, the app **force-unmounts** that\n  path and stops matching `mtpfuse` processes automatically; if it still fails,\n  run **Eject** in the menu or `diskutil unmount force ~/.AndroidMount/\u003cdevice\u003e`.\n\n* **\"MTPFuse helper binary not found\"** – run `make` (or `make run`);\n  the app looks for `mtpfuse` next to its own executable.\n* **`fuse: device not found, try 'modprobe fuse' first`** – macFUSE\n  kext was blocked. Open *System Settings → Privacy \u0026 Security* and\n  click *Allow* next to \"System software from developer …\", then\n  reboot.\n* **`No raw devices found`** when the daemon starts – the phone is\n  still in *Charging* mode. Pull down the notification shade on the\n  device and switch USB usage to *File Transfer*.\n* **`LIBMTP_Open_Raw_Device` fails** while the phone shows as connected –\n  another app may already hold the MTP USB session (same class of issue\n  [SwiftMTP](https://github.com/Neighbor-Z/SwiftMTP) documents for\n  `LIBUSB_ERROR_NOT_FOUND`): quit **Preview**, **Image Capture**, and\n  **Android File Transfer** (including its background agent in Activity\n  Monitor), then unplug/replug. `mtpfuse` retries detection/open a few\n  times with a short delay to ride out USB settle.\n* **Optional environment (passed to `mtpfuse`):** set `MTP_SKIP_HIDDEN=1`\n  or `MTP_HIDE_DOTFILES=1` to omit device entries whose names start with\n  `.` (Finder-style “no dotfiles” listing; off by default).\n* **Mount appears but is empty** – tap *Allow* on the phone the very\n  first time the Mac connects; MTP requires per-host authorization.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frahuletto%2Fdroidmount","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frahuletto%2Fdroidmount","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frahuletto%2Fdroidmount/lists"}