Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Bug report
description: Something does not work the way it should.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
`odhub doctor` prints almost everything asked for below. If the command is not on your
PATH yet, use **OpenDeviceHub → Install Command Line Tool**, or run it straight out of the
bundle:

```sh
/Applications/OpenDeviceHub.app/Contents/MacOS/odhub doctor
```

- type: textarea
id: what-happened
attributes:
label: What happened
description: What you did, what you expected, and what you got instead.
placeholder: |
1. Booted an iPhone 17 on iOS 27.0
2. Clicked in the middle of the screen
3. Expected the tap to land. Nothing happened, and no error appeared.
validations:
required: true

- type: input
id: app-version
attributes:
label: OpenDeviceHub version
description: From the app menu, or `odhub --version`. Say "built from source" if it is.
placeholder: "0.1.0"
validations:
required: true

- type: input
id: macos
attributes:
label: macOS version
placeholder: "26.1"
validations:
required: true

- type: input
id: xcode
attributes:
label: Xcode version and build
description: The first lines of `odhub doctor`, or Xcode → About Xcode.
placeholder: "27.0 (27A266a)"
validations:
required: true

- type: input
id: device
attributes:
label: Simulator device
placeholder: "iPhone 17 Pro"
validations:
required: true

- type: input
id: runtime
attributes:
label: Simulator runtime
placeholder: "iOS 27.0"
validations:
required: true

- type: dropdown
id: mac
attributes:
label: Mac
options:
- Apple silicon
- Intel
validations:
required: true

- type: textarea
id: doctor
attributes:
label: odhub doctor output
description: Optional, and the single most useful thing you can attach.
render: text

- type: textarea
id: anything-else
attributes:
label: Anything else
description: Screenshots, a recording, or what you had already been doing when it started.
30 changes: 30 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Feature request
description: Something that should exist and does not.
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
The README's **Current limitations** lists what is known to be missing. If yours is there,
an issue is still welcome: it says someone wants it.

- type: textarea
id: problem
attributes:
label: What are you trying to do
description: The task, rather than the feature. It often has more than one answer.
placeholder: I record a lot of short clips for design review, and renaming each one afterwards is the slow part.
validations:
required: true

- type: textarea
id: idea
attributes:
label: What you have in mind
description: Optional. How you imagine it working, if you have a picture of it.

- type: input
id: prior-art
attributes:
label: Where you have seen it work
description: Optional. Simulator.app, Device Hub, another tool, or a link.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ under it until the next heading becomes that version's release notes in the upda

First release.

- Click the icon and a simulator opens: whatever is already running, or the one shown last. Pick any
other from the Dock icon or File, Open Simulator, and a device booted anywhere else gets a window
too.
- One window per simulator, with the device body drawn around the screen and its buttons clickable.
- Point Accurate, Pixel Accurate, Physical Size and Fit, plus full screen and windows that remember
where they were.
Expand All @@ -17,5 +20,8 @@ First release.
- A window follows its device: it shows when the device has shut down, offers a reboot, and
reattaches on its own when the device comes back.
- The `odhub` command line tool beside the app, for listing devices and driving input from a script.
One menu item puts it on your PATH, without a password on most machines.
- If Xcode is missing or its simulator frameworks will not load, the app says what is missing and
what to do about it instead of failing silently.

Known gaps are listed in the README.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ options.
- **No Flutter tooling yet** — it is the reason this project exists, and none of it is built.
- **Other Apple platforms** — tvOS, watchOS and visionOS simulators are not supported.

Want to help close one of these? Pull requests are welcome.
Want to help close one of these? Pull requests are welcome. [ROADMAP.md](ROADMAP.md) is where that
list is going: location, push, biometrics, proxy certificates, device management, and the project
aware tooling this exists for.

## Requirements

Expand Down Expand Up @@ -143,6 +145,10 @@ configured and a source build can never replace itself with a release.
Bug reports, feature ideas and pull requests are welcome. A bug report is far more useful with the
output of `odhub doctor`, which names your macOS version, Xcode version, device and runtime.

For a vulnerability, please read [SECURITY.md](SECURITY.md) instead of opening an issue. It also
describes what the app does to your machine, which is worth knowing given it loads private
frameworks out of your Xcode. Maintainers cutting a release want [RELEASING.md](RELEASING.md).

## License

[MIT](LICENSE). The Indigo touch message layout and hardware button constants are adapted from
Expand Down
148 changes: 148 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Releasing

For maintainers. Nothing here contains a credential; the values live outside this repository and
reach the workflow as repository secrets.

## Two values that must never change

| Value | Why |
|---|---|
| The bundle identifier | A new one makes macOS treat the app as a different one, orphaning every installed copy. |
| The update feed URL | The installed app keeps asking the old URL. Changing it silently ends updates for everyone, with no way to push a fix. |

Both are based on a GitHub handle rather than the product name or a domain, so the app can be
renamed and the repository moved without touching either. The feed itself lives in a separate
repository served by GitHub Pages; the enclosure URLs inside it point at this repository's release
downloads and are rewritten on every release.

## Before the first release on a new machine

- A **Developer ID Application** certificate in the login keychain. Not Apple Development, which
notarization rejects.
- An App Store Connect **Team** API key with the Developer role. An **Individual** key does not work
with `notarytool`, and the failure only shows up at submission.
- The Sparkle update key pair. The public half is compiled into the app; the private half signs the
feed.

**If the update private key is lost, no installed copy can ever be updated again.** A later release
cannot fix it, because the installed app only trusts the public key baked into the copy someone
already has. Keep it in a password manager as well as in the repository secret.

## Repository secrets

| Secret | Contents |
|---|---|
| `DEVELOPER_ID_IDENTITY` | the full identity string, `Developer ID Application: Name (TEAMID)` |
| `DEVELOPER_ID_P12` | that identity exported as a `.p12`, base64 encoded |
| `DEVELOPER_ID_P12_PASSWORD` | the password used for that export |
| `ASC_KEY_ID` | App Store Connect key ID |
| `ASC_ISSUER_ID` | App Store Connect issuer ID |
| `ASC_KEY_P8` | the contents of the `.p8` private key |
| `SPARKLE_PRIVATE_KEY` | the update signing private key |

A missing secret fails the run loudly on its first step. Signing is never skipped quietly, because an
unsigned build that reaches a release page is worse than a run that stops.

## Making a release

1. Add the version's section to `CHANGELOG.md`. The release notes on GitHub and the notes inside the
update feed both come from it, so they cannot drift apart.
2. Update `VERSION`.
3. Run the **Release** workflow from the Actions tab with `publish` left **false**. That is a dry
run: it builds, signs, notarizes, staples, packages, checksums and generates the feed, and
creates no tag, no release and no commit anywhere. The artifacts are attached to the run under a
short retention name so you can download and check them.
4. Install the dry run's DMG and use it. `stapler validate` on the app and on the DMG, and
`shasum -c SHA256SUMS` from a different directory.
5. Run the workflow again with `publish` **true**. It creates the tag and a pre-release with the DMG
and `SHA256SUMS`.
6. **Only once that release page exists**, commit the generated `appcast.xml` to the feed
repository. A feed pointing at a download that is not there yet breaks updates for everyone who
reads it in the meantime.
7. Confirm an already installed older copy finds the update, verifies it and installs it, ending on
the new version. Do this before announcing anything: a broken updater cannot be fixed by a later
release.

Everything the workflow runs is a script in `scripts/`, and each one works on its own from a
terminal. The workflow wraps them; it does not reimplement them.

### The order that matters

Two steps fail silently if they are done in the wrong order, and both are wired that way on purpose:

- **Sign inside out.** Sparkle's nested XPC services, then `Autoupdate`, then `Updater.app`, then the
framework, then the bundled command, then the app. `--deep` is for verification only, never for
signing.
- **Sign the update last.** The feed signature covers the exact bytes of the file people download, so
it is taken after notarizing, stapling and packaging. Anything that rewrites the artifact
afterwards leaves a signature that no longer matches, and Sparkle then refuses the update without
telling anyone. `scripts/appcast.sh` refuses to sign an artifact that is not stapled, and checks
the byte length it writes against the file on disk.

## Reading a notarization log

The log is fetched and saved on every submission, including successful ones, under
`build/notarization/`. `notarytool submit` reports `Accepted`, `Invalid` or `Rejected`; for anything
but `Accepted`, the log is the only thing that says why.

```sh
xcrun notarytool log <submission-id> \
--key <p8> --key-id <key-id> --issuer <issuer-id>
```

It is JSON. `issues` is the part that matters, and each entry names a `path` inside the bundle, a
`severity` and a `message`. The usual ones:

| Message | What it means |
|---|---|
| `The signature of the binary is invalid` | Something was modified after signing, or signed in the wrong order. |
| `The executable does not have the hardened runtime enabled` | A nested binary was missed. Check each one on its own, not with `--deep`. |
| `The signature does not include a secure timestamp` | Signed without `--timestamp`, usually offline. |
| `The binary is not signed with a valid Developer ID certificate` | An Apple Development identity was used. |

`status: Accepted` with a non-empty `issues` list is possible and is worth reading: those are
warnings that become failures in a later macOS.

## When a release fails midway

Nothing before the publish step changes anything outside the runner, so a failure there costs
nothing: fix it and run the dry run again.

After that, work backwards from how far it got.

**The tag and release exist, but the artifacts are wrong.** Delete the release and the tag, then run
again. Do this only if nobody can have downloaded it yet, and never once the feed points at it.

```sh
gh release delete v<version> --yes
git push --delete origin v<version>
```

**The release is published and the feed is not updated yet.** Nothing is broken. Either finish by
committing the feed, or leave it: people who downloaded the DMG have a working app, and no installed
copy is looking for that version.

**The feed points at a release that is wrong or gone.** This is the one that hurts, because installed
copies are reading it. Revert the feed repository to its previous commit first, which stops the
damage, then fix the release. Do not delete the release while the feed still points at it.

**Notarization is stuck in progress.** Submissions can take minutes or hours. `notarytool history`
shows where it is. Do not resubmit: a second submission of the same bytes gets a second ticket and
tells you nothing new.

**The published version is broken for everyone.** Release a fixed version rather than trying to undo
one. Mark the bad release as a pre-release or delete it so nobody new downloads it, publish the fix,
and update the feed. Installed copies will update themselves. Never reuse a version number: Sparkle
compares build numbers, and a copy that has already seen that number will not offer it again.

## The runner

The release runs on a pinned runner image and asks for a preferred Xcode major, falling back to the
newest installed Xcode with a macOS SDK of 26.0 or newer. Both are workflow inputs, so a runner image
that changes or is withdrawn can be worked around by dispatching with a different one rather than
editing a file under time pressure.

The SDK floor is the real requirement. The linked SDK decides whether AppKit gives the app its
current appearance or its pre macOS 26 one, and an older SDK builds, signs and notarizes perfectly
while shipping a window that looks years out of date. Every run prints each Xcode on the runner with
its SDK version and records the one it chose, so what shipped is never a matter of inference.
61 changes: 61 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Roadmap

What is being worked towards, roughly in the order it is likely to happen. Nothing here is a promise
or a date, and anything can move. The README's **Current limitations** is the honest list of what is
missing today; this is where that list is going.

If you want something sooner, say so in an issue. A feature two people ask for beats one nobody has
mentioned.

## Bringing back what went missing

Things the classic Simulator had, or that a simulator should simply be able to do.

- **Location** — presets, a search, GPX routes, and per-project saved places.
- **Push notifications** — send a payload to an app, with a saved library of them and templates for
the fiddly parts.
- **Biometrics** — enrol, match and fail a match for Face ID and Touch ID.
- **Device and runtime management** — create, rename, erase and delete simulators, and install
runtimes, without going back to Xcode.
- **Proxy certificates** — drop a `.cer` onto a device, or install a debugging proxy's root
certificate on every booted device at once. Removed from Device Hub, and badly missed by anyone
who inspects traffic for a living.
- **Status bar presets** — 9:41, full battery, full signal, for clean App Store screenshots.
- **Locale, region and Dynamic Type** — switch them without digging through Settings.
- **Privacy permissions** — grant, revoke and reset per app, so the first-run flow can be tested more
than once.

## Tooling for the app you are actually building

The reason this project exists, and none of it is built yet. The theme is everything a mobile
developer scripts by hand today, one click away, remembered per project.

- **Project awareness** — point it at a project and it learns the bundle identifiers, flavours and
entry points, then remembers your deep links, payloads, proxy settings and favourite devices
alongside them.
- **Run, hot reload and hot restart** — on any device, flavour aware, from the device window.
- **DevTools** — a link on the window once the app is attached, and attaching to an app that is
already running.
- **Deep links** — saved per app, custom schemes and universal links, one click to fire.
- **Screenshot sets** — the same screen captured across several devices in one action, named and
organised for the App Store.

## Harder, or not yet certain

Listed separately because the honest answer is that these might not work, or might not be worth what
they cost.

- **iPad pointer and mouse input** — the hardest item here, and the one most likely to slip.
- **Network conditioning presets** — 3G, lossy, offline. Apple's own conditioner is system wide on
macOS rather than per simulator, so either a per-device approach turns up or this gets documented
as the compromise it is.
- **Audio routing and external displays** — wanted, not investigated.
- **Android emulators in the same window** — cross platform developers juggle both all day. Genuinely
useful, a long way out, and not started.

## Not planned

- **tvOS, watchOS and visionOS.** Not out of dislike; the device shapes, input model and chrome are
a different project's worth of work, and doing them badly would be worse than not doing them.
- **Physical devices.** This is a simulator tool. Xcode and Device Hub handle hardware.
- **Anything that phones home.** No analytics, no crash reporting, no telemetry, now or later.
Loading
Loading