A lightweight, fast, native desktop tool — running on Linux and macOS — that lets a single developer drive the entire iOS build → install → launch → observe loop on a remote Mac without ever opening the macOS GUI or Xcode.
ostrich treats a Mac as a headless build appliance. You stay in your own editor on Linux (or macOS), connect to the Mac over SSH, and drive Xcode from a native GUI built with Dear ImGui — no screen sharing, no Xcode window, no leaving your environment.
The tool is organized around the goals laid out in context/design.md:
- Connect to a Mac over SSH with a clearly visible, persistent session state.
- Discover the available Xcode inputs — projects, schemes, build configurations, and target devices/simulators — by querying the Mac.
- Configure a run: pick a project, scheme, device, and bundle ID, with manual fallbacks for every field.
- Play — orchestrate
xcodebuild → install → launchfrom a single action. - Observe — stream build logs and device logs live, with a non-blocking worker model that keeps the UI responsive while multiple streams run concurrently.
It is a single-user personal tool. It is not an editor, does not handle
signing/provisioning, and is not built for teams. See
context/design.md for the full statement of intent,
context/workflow.md for the UI layout and interaction
model, and context/theme.md for the visual identity.
- A C/C++ toolchain (
ccandc++, defaulting to your system compiler; C11 and C++17 are required) - GNU Make
pkg-config- OpenSSL development headers (libssh2 is built against the OpenSSL backend)
Dear ImGui, GLFW, libssh2, and jsmn are vendored as submodules — Dear ImGui,
GLFW, and libssh2 are built from source; jsmn is used header-only — so you do
not need system packages for them. On Linux, GLFW is built with
both the X11 and Wayland backends and selects one at runtime (based on
XDG_SESSION_TYPE / WAYLAND_DISPLAY / DISPLAY), so the build needs the
development headers for both display stacks:
-
Linux: OpenSSL and OpenGL (Mesa) dev packages, plus:
- X11 backend —
libX11,libXcursor,libXi,libXinerama,libXrandr,libXfixesdev packages. - Wayland backend —
wayland(provides thewayland-clientheaders and thewayland-scannercodegen tool) andlibxkbcommondev packages. The Wayland protocol XML is vendored inside GLFW, sowayland-protocolsis not required.
GLFW
dlopen()s the X11/Wayland/GL client libraries at runtime rather than linking them, so they only need to be installed on the machine that runs ostrich. If you only have one display stack on a given machine, narrow the build withmake GLFW_BACKENDS=x11ormake GLFW_BACKENDS=wayland. - X11 backend —
-
macOS: the Cocoa/IOKit/OpenGL frameworks ship with the system. Install OpenSSL and pkg-config via Homebrew (
brew install openssl pkg-config); the build falls back to the Homebrew prefix automatically if pkg-config can't find OpenSSL.
The Mac that ostrich drives over SSH needs setsid available on the
non-interactive SSH PATH — the build/launch commands wrap xcodebuild in
setsid sh -c '...' so the worker can kill -- -<pgid> the whole process
tree on abort. macOS does not ship setsid, so install it from
util-linux and put it on the non-interactive PATH:
brew install util-linux
# util-linux is keg-only on macOS — expose setsid to non-interactive SSH:
echo 'export PATH="'"$(brew --prefix util-linux)"'/bin:$PATH"' >> ~/.zshenv~/.zshenv (not ~/.zshrc) is what zsh sources for non-interactive,
non-login sessions like ssh host 'cmd'. Confirm it took with
ssh <user>@<host> 'command -v setsid'. If this isn't set up you'll see
Export failed (the EXPLOIT FAILED header) in the Build Log along with
command not found: setsid when you click Execute.
Device builds also require the remote Mac's login.keychain to be unlocked —
codesign can't access the signing identity's private key through a locked
keychain. ostrich handles this with a lazy modal: the first time you press
EXECUTE against a device target in a session where the keychain is locked, a
KEYCHAIN PASSKEY prompt appears. Enter the passkey and press ENTER to
unlock the keychain for the rest of the session. Tick REMEMBER KEYCHAIN to
save the passkey on the connection record (plaintext, 0600) so future
sessions skip the prompt automatically. Press SKIP to proceed without
unlocking — the build will fail at the codesign step, but the Build Log will
show a hint reminding you the modal is available on the next EXECUTE.
The third-party dependencies live under third_party/ as git submodules. Clone
with --recurse-submodules so they come down in one step:
git clone --recurse-submodules git@github.com:Be-Like/ostrich.git
cd ostrichIf you already cloned the repository without the submodules (or pulled changes that touch them), initialize/update them in place:
git submodule update --init --recursiveRun that same command any time git pull reports changes under third_party/
to bring the vendored dependencies to the pinned revisions.
makeProduces build/ostrich along with the static libraries for each module
(libui.a, libglfw.a, libssh.a, libsession.a, libdiscovery.a, and so
on).
./build/ostrichostrich has two distinct kinds of checks: unit tests and smoke tests. They serve different purposes and are run differently.
make testThis is the suite you run routinely. It compiles and runs every test binary
under tests/ in sequence and exits non-zero on the first failure.
These tests are fast, self-contained, and require no network and no remote
Mac — they exercise the pure, state-based cores of each module:
| Test | Covers |
|---|---|
arena_test |
Arena allocator: create, aligned alloc, reset, destroy |
spsc_ring_test |
Lock-free single-producer/single-consumer ring buffer |
lexicon_test |
UI string lookup table |
framestats_test |
Frame timing / FPS (EMA filter) |
connstate_test |
Connection state machine: phases, backoff, keepalive, liveness probes |
store_test |
Saved-connections persistence (XDG config dir) |
app_test |
Form ↔ SSH-config conversion, phase reason strings, connection list |
discovery_test |
Xcode discovery: JSON parsing, project/scheme/config/device enumeration |
ui_test |
ImGui window/layout rendering |
The smoke tests live under tools/ and are not part of make test. They are manual, developer-driven integration checks — most of them open
a real SSH connection to a live Mac and need connection arguments. Build (and,
where noted, run) each one individually:
| Target | Invocation | Needs a live Mac? |
|---|---|---|
make ssh_version_smoke |
builds and runs it; prints the linked libssh2 version | No — offline sanity check that the SSH layer links |
make ssh_smoke |
builds build/ssh_smoke; run as ./build/ssh_smoke <host> <user> [port] |
Yes — exercises the full handshake/auth/probe |
make session_smoke |
builds build/session_smoke; run as ./build/session_smoke <host> <user> [port] |
Yes — exercises the worker thread + rings against a live session |
make discovery_smoke |
builds build/discovery_smoke; run as ./build/discovery_smoke <host> <user> <scan_root> [port] [depth] [--abort] |
Yes — exercises Xcode discovery against a real Mac |
In short: make test is the automated, offline regression suite; the smoke
tests are for hand-verifying real SSH and discovery behavior against an actual
Mac.
make cleanRemoves the build/ directory.
-
setsiddependency on the remote Mac.bd_build_cmdandbd_launch_cmd(seesrc/builddeploy/builddeploy.c) wrap the remote command insetsid sh -c '...'so the worker can target the whole process group on abort. macOS doesn't shipsetsid, so the remote Mac needs it installed from Homebrew'sutil-linuxkeg and put on the non-interactive SSH PATH. The Build Log surfaces the exact remediation on the first failed EXECUTE; the same steps are reproduced here for reference.Connect to the Mac (substitute your own user and host; add
-p <port>if you SSH on a non-default port):ssh <user>@<host>
Then on the Mac, run:
brew install util-linux
Then add ONE of the following to
~/.zshenv, matching your Mac:# Apple Silicon: echo 'export PATH="/opt/homebrew/opt/util-linux/bin:$PATH"' >> ~/.zshenv # Intel: echo 'export PATH="/usr/local/opt/util-linux/bin:$PATH"' >> ~/.zshenv
(Substitute your own path if
util-linuxis installed elsewhere.)Verify the fix from your local host:
ssh <user>@<host> 'command -v setsid'
See
context/projects/setsid-install-help/for the in-app guidance design. -
Remote Mac
login.keychainmust be unlocked for device builds. Compiling and building to a physical device requires the remote Mac'slogin.keychainto be unlocked at sign time —codesignpulls the signing identity's private key fromlogin.keychain, and on a headless Mac or after an auto-lock the keychain is locked, causing acodesignfailure (errSecInternalComponent) at the very end of an otherwise-clean build.Ostrich surfaces an in-app KEYCHAIN PASSKEY modal on the first affected EXECUTE and asks for the keychain password to unlock it. The password is not written to any drive unless you explicitly tick
REMEMBER KEYCHAIN, in which case it is saved on the connection record (plaintext,0600) so future sessions skip the prompt. By default the password lives only in memory for the life of the running Ostrich process and is destroyed when the application closes. Ostrich does not modify the Mac's keychain auto-lock policy; seecontext/projects/keychain-unlock/for the in-app design.
src/ C source — app shell, plus per-module dirs (ssh/, session/,
connstate/, store/, discovery/, ui/) and shared cores
(arena.c, spsc_ring.c, lexicon.c, framestats.c, main.c)
include/ Public headers for the modules in src/
tests/ Self-contained unit tests (run by `make test`)
tools/ Developer smoke tests (built/run individually)
third_party/ Vendored git submodules (imgui [docking], glfw, libssh2, jsmn)
context/ Design docs and per-project PRD/ARD/impl plans
assets/ Bundled assets (fonts)
scripts/ Helper scripts
build/ Build artifacts (git-ignored)
Ostrich is currently in beta. It connects to a remote host Mac, finds Xcode projects and devices (both physical and simulator) and provides an interface for project compilation and build/deployment.
There are some known quirks with some of the buttons. Specifically with regard to some of their boolean state.
The logs are functional, and I identified several opportunities where I could provide more explicit messaging to the user to help them resolve common issues. There may be future opportunity to provide more clear guidance to the user, but that will require some more trial and error and research in terms of what may be common error cases.
-
Device list contains physical devices that have been used previously but may not be accessible at this moment.
- This is actually how Xcode operates as well. While I don't think Xcode is one to emulate I suspect that they are approaching the problem similarly and it is just a reality of the CLI. It would require further research to determine how I might be able to make this feel like a better user experience.
-
There is an issue with the content of the project dropdown where two options can contain the same naming. The result of this is that there are two options within the dropdown that share an ID where that ID is meant to be unique.
-
Inconsistent keyboard shortcuts.
-
When building and deploying to a physical device, a user must have a valid code signing which requires that they provide a password to unlock their keychain. This is properly handled, but if the user exits out they will see an error as produced by the deploy logs that feels somewhat vague. There is an opportunity to capture that error and produce an error that is more user friendly; it also provides an opportunity to communicate with the user the reason we ask for the password to the keychain and how we handle that securely.