diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..f4be86e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a2ac98d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 09985c3..fbe5730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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. diff --git a/README.md b/README.md index 0d5281c..8946451 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..1104e4b --- /dev/null +++ b/RELEASING.md @@ -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 \ + --key --key-id --issuer +``` + +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 --yes +git push --delete origin v +``` + +**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. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..79df955 --- /dev/null +++ b/ROADMAP.md @@ -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. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..72744a2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,63 @@ +# Security + +## Reporting something + +Open a +[private advisory](https://github.com/Mastersam07/OpenDeviceHub/security/advisories/new). Please do +not open a public issue for a vulnerability. + +Expect an acknowledgement within a week. There is no bounty; there is credit in the advisory and in +the release notes, unless you would rather not be named. + +## What this app does to your machine + +Worth knowing, because it is unusual for an app to touch these things at all. + +**It loads private Apple frameworks from your Xcode.** `CoreSimulator`, `CoreSimDeviceIO` and +`SimulatorKit` are opened with `dlopen` at runtime from the Xcode already installed on your Mac. +Nothing is linked at build time and no Apple framework, simulator runtime or device bezel asset is +bundled or redistributed. If a class or selector is missing on your Xcode, the app reports a missing +capability rather than crashing. + +**Every private call is checked first.** Classes are looked up by name and selectors through +`respondsToSelector:` before use, and everything crossing that boundary is treated as possibly nil. +Private API access is confined to two directories, `engine/Sources/OpenDeviceHubPrivate` and +`engine/Sources/OpenDeviceHubEngine/Adapter`; no other file may touch a private symbol. + +**It can drive your simulators.** Taps, keystrokes and hardware buttons go to simulators on your +machine, from the app and from the `odhub` command. It does not touch physical devices. + +**It can write outside its own bundle**, in exactly two places, both of which you ask for: +screenshots and recordings go to your Desktop, and **Install Command Line Tool** creates one symlink +in a directory on your `PATH`. That one asks for an administrator password only when every such +directory belongs to the system, and it explains why before macOS asks. **Remove Command Line Tool** +deletes it. + +**It talks to the network for one thing only**: checking for updates. There is no analytics, no +crash reporting and no telemetry of any kind. A build from source has no update channel at all, so +it never makes a network request. + +## How releases are protected + +- **Signed and notarized.** Release builds are signed with a Developer ID Application certificate, + use the hardened runtime, and are notarized and stapled by Apple. The staple means the ticket + travels with the download, so it verifies with no network. +- **No entitlements.** The hardened runtime is enabled with none of the exceptions to it, library + validation included. Loading Xcode's frameworks was tested and needs none. +- **Checksums.** Every release lists `SHA256SUMS` generated from the final signed, notarized and + stapled artifacts, verifiable with `shasum -c`. +- **Signed updates.** The update feed is signed with an EdDSA key whose public half is compiled into + the app. An installed copy only accepts an update that verifies against the key it already has, so + a compromised feed cannot push a build to anyone. +- **Reproducible from source.** The release workflow is in this repository and the scripts it runs + are the same ones used by hand. + +## Credentials + +No credential, key or certificate is in this repository. The signing certificate, the App Store +Connect key and the update signing key live outside it and reach the release workflow as repository +secrets. + +If the update signing key were ever lost or exposed, it cannot be fixed by a later release: every +installed copy only trusts the key baked into it. That is why it is backed up outside this machine +and why its handling is treated as the most sensitive part of the release.