Skip to content
Open
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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,40 @@ jobs:
- name: Rust unit tests
run: cargo test --manifest-path packages/server/Cargo.toml

rust-non-macos:
name: Rust unit tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- name: Cache Rust build outputs
uses: actions/cache@v4
with:
path: |
~/.cargo/git
~/.cargo/registry
packages/server/target
key: rust-${{ runner.os }}-${{ hashFiles('packages/server/Cargo.lock', 'packages/server/Cargo.toml', 'packages/server/build.rs', 'packages/server/src/**/*.rs', 'packages/server/native_stubs.c') }}
restore-keys: |
rust-${{ runner.os }}-

# The macOS job cannot compile the OpenH264 software encoder or the
# native stubs, so lint and test them where they are actually built.
- name: Clippy
run: cargo clippy --manifest-path packages/server/Cargo.toml --all-targets -- -D warnings

- name: Rust unit tests
run: cargo test --manifest-path packages/server/Cargo.toml

client:
name: Client lint, build, and tests
runs-on: ubuntu-latest
Expand Down
131 changes: 122 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,34 +77,92 @@ permissions:
id-token: write # OIDC trusted publishing + npm provenance

jobs:
# The release version is resolved once, up front, so the native CLI binaries
# are compiled from the same version that gets committed and published. (Doing
# the bump only in the release job meant the binaries were built from the
# previous Cargo.toml and reported a stale `--version`.)
resolve-version:
name: Resolve ${{ inputs.package }} version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Resolve release version
id: version
shell: bash
env:
INPUT_PACKAGE: ${{ inputs.package }}
BUMP: ${{ inputs.bump }}
PREID: ${{ inputs.preid }}
run: |
set -euo pipefail
case "$INPUT_PACKAGE" in
simdeck) dir="." ;;
react-native-simdeck) dir="packages/react-native-inspector" ;;
"@nativescript/simdeck-inspector") dir="packages/nativescript-inspector" ;;
simdeck-vscode) dir="packages/vscode-extension" ;;
*)
echo "Unknown package input: $INPUT_PACKAGE" >&2
exit 1
;;
esac
cd "$dir"

if [[ "$BUMP" == "current" ]]; then
version="$(node -p "require('./package.json').version")"
elif [[ "$BUMP" == "prerelease" ]]; then
new="$(npm version prerelease --preid "$PREID" --no-git-tag-version)"
version="${new#v}"
else
new="$(npm version "$BUMP" --no-git-tag-version)"
version="${new#v}"
fi

echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "Resolved ${INPUT_PACKAGE} release version: ${version}"

build-native-artifacts:
if: ${{ inputs.package == 'simdeck' }}
name: Build ${{ matrix.platform }} native artifact
needs: resolve-version
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: macos-arm64
runner: macos-latest
binary: build/simdeck-bin-darwin-arm64
build_cmd: |
rustup target add aarch64-apple-darwin
SIMDECK_BUILD_TARGET=aarch64-apple-darwin npm run build:cli
cp build/simdeck-bin build/simdeck-bin-darwin-arm64
- platform: macos-x64
runner: macos-latest
binary: build/simdeck-bin-darwin-x64
build_cmd: |
rustup target add x86_64-apple-darwin
SIMDECK_DISABLE_X264=1 SIMDECK_BUILD_TARGET=x86_64-apple-darwin npm run build:cli
cp build/simdeck-bin build/simdeck-bin-darwin-x64
- platform: linux-x64
runner: ubuntu-latest
binary: build/simdeck-bin-linux-x64
build_cmd: |
rustup target add x86_64-unknown-linux-gnu
SIMDECK_DISABLE_X264=1 SIMDECK_BUILD_TARGET=x86_64-unknown-linux-gnu npm run build:cli
cp build/simdeck-bin build/simdeck-bin-linux-x64
- platform: windows-x64
runner: windows-latest
binary: build/simdeck-bin-win32-x64.exe
build_cmd: |
rustup target add x86_64-pc-windows-gnu
export SIMDECK_DISABLE_X264=1
Expand Down Expand Up @@ -150,12 +208,43 @@ jobs:
- name: Install root dependencies
run: npm ci

- name: Sync Rust CLI version
shell: bash
env:
NEW_VERSION: ${{ needs.resolve-version.outputs.version }}
run: |
set -euo pipefail
perl -0pi -e 's/(\[package\]\s+name = "simdeck-server"\s+version = ")[^"]+(")/$1$ENV{NEW_VERSION}$2/s' packages/server/Cargo.toml
cargo update --manifest-path packages/server/Cargo.toml -p simdeck-server --precise "$NEW_VERSION"
grep -q "^version = \"${NEW_VERSION}\"$" packages/server/Cargo.toml
echo "Building simdeck-server ${NEW_VERSION}"

- name: Build native artifact
shell: bash
run: |
set -euo pipefail
${{ matrix.build_cmd }}

- name: Verify native artifact reports the release version
shell: bash
env:
EXPECTED_VERSION: ${{ needs.resolve-version.outputs.version }}
BINARY: ${{ matrix.binary }}
run: |
set -euo pipefail
if [[ "$RUNNER_OS" == "macOS" && "$BINARY" == *darwin-x64 && "$(uname -m)" == "arm64" ]] \
&& ! arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
echo "::warning::Rosetta is unavailable on this runner; skipping the runtime version check for ${BINARY}."
exit 0
fi

actual="$("$BINARY" --version)"
echo "${BINARY} --version -> ${actual}"
if [[ "${actual##* }" != "$EXPECTED_VERSION" ]]; then
echo "::error::${BINARY} reports '${actual}' but this release is ${EXPECTED_VERSION}. The binary was built from a stale crate version." >&2
exit 1
fi

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
Expand All @@ -169,8 +258,8 @@ jobs:

release:
name: Release ${{ inputs.package }} (${{ inputs.bump }})
needs: build-native-artifacts
if: ${{ always() && (needs.build-native-artifacts.result == 'success' || inputs.package != 'simdeck') }}
needs: [resolve-version, build-native-artifacts]
if: ${{ always() && needs.resolve-version.result == 'success' && (needs.build-native-artifacts.result == 'success' || inputs.package != 'simdeck') }}
runs-on: macos-latest
environment:
name: ${{ inputs.dry-run && 'npm-publish-dry-run' || 'npm-publish' }}
Expand Down Expand Up @@ -269,21 +358,29 @@ jobs:
shell: bash
env:
BUMP: ${{ inputs.bump }}
PREID: ${{ inputs.preid }}
RESOLVED_VERSION: ${{ needs.resolve-version.outputs.version }}
PKG_DIR: ${{ steps.meta.outputs.dir }}
SLUG: ${{ steps.meta.outputs.slug }}
run: |
set -euo pipefail
cd "$PKG_DIR"

# The version was resolved by the resolve-version job (and already
# compiled into the native binaries); apply the same value here.
version="$RESOLVED_VERSION"
if [[ -z "$version" ]]; then
echo "resolve-version did not produce a version" >&2
exit 1
fi

if [[ "$BUMP" == "current" ]]; then
version="$(node -p "require('./package.json').version")"
elif [[ "$BUMP" == "prerelease" ]]; then
new="$(npm version prerelease --preid "$PREID" --no-git-tag-version)"
version="${new#v}"
current="$(node -p "require('./package.json').version")"
if [[ "$current" != "$version" ]]; then
echo "Resolved version ${version} does not match package.json (${current})" >&2
exit 1
fi
else
new="$(npm version "$BUMP" --no-git-tag-version)"
version="${new#v}"
npm version "$version" --no-git-tag-version --allow-same-version >/dev/null
fi

tag="${SLUG}-v${version}"
Expand Down Expand Up @@ -339,7 +436,11 @@ jobs:

- name: Verify CLI artifacts are published for all supported hosts
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
shell: bash
env:
NEW_VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
for artifact in \
build/simdeck-bin \
build/simdeck-bin-darwin-arm64 \
Expand All @@ -348,6 +449,7 @@ jobs:
build/simdeck-bin-win32-x64.exe
do
test -f "$artifact"
chmod +x "$artifact"
done

file build/simdeck-bin
Expand All @@ -358,6 +460,17 @@ jobs:
file build/simdeck-bin-linux-x64 | grep -q 'ELF'
file build/simdeck-bin-win32-x64.exe | grep -Eq 'PE32|PE32\+'

# The universal binary is what macOS users run; make sure it was
# compiled from this release's crate version, not the previous one.
for artifact in build/simdeck-bin build/simdeck-bin-darwin-arm64; do
actual="$("$artifact" --version)"
echo "${artifact} --version -> ${actual}"
if [[ "${actual##* }" != "$NEW_VERSION" ]]; then
echo "::error::${artifact} reports '${actual}' but this release is ${NEW_VERSION}." >&2
exit 1
fi
done

# ---------- Apple codesign + notarize (root simdeck only) ----------

- name: Detect Apple signing capability
Expand Down
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
Defines REST routes for simulator control, health, metrics, and chrome assets.
- `packages/server/src/transport/webrtc.rs`
Exposes the H.264 WebRTC offer/answer endpoint for browser live video.
- `packages/server/src/platform.rs`
Single source of truth for host platform capabilities. iOS simulators exist
only in the macOS build; the Windows and Linux builds compile
`packages/server/native_stubs.c` in place of the native bridge. Health,
stream-quality, the WebRTC offer error, the CLI banner, and the browser
client all read this module's `liveVideo` capability instead of hard-coding
platform checks.
- `packages/server/src/transport/software_h264.rs`
OpenH264 software encoder (the `openh264` crate, compiled from source) used
for Android emulator WebRTC streams on non-macOS builds. It emits Annex B
baseline H.264 so `transport/webrtc.rs` packetizes it exactly like the macOS
native encoder output. macOS keeps encoding Android frames through
`XCWH264Encoder` behind the C ABI.
- `packages/server/src/webkit.rs`
Discovers simulator WebKit Remote Inspector targets and bridges WebInspectorUI
WebSocket traffic to the simulator `webinspectord` binary-plist socket.
Expand Down
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,14 @@ manually from the Actions tab and pick the package, bump type, and dist-tag.

The workflow:

- Resolves the new version up front so every job builds from the same number
- Builds the per-platform native binaries for the root `simdeck` package with
`packages/server/Cargo.toml` synced to that version, and fails if any binary's
`--version` output disagrees with it
- Bumps the chosen package's version, commits, and tags as `<slug>-v<version>`
- Builds a universal native binary for the root `simdeck` package, codesigns
with the team's Developer ID Application certificate, and notarizes with
Apple's notary service
- Merges the macOS binaries into a universal binary, codesigns it with the
team's Developer ID Application certificate, and notarizes with Apple's
notary service
- Publishes npm packages over OIDC trusted publishing (no NPM_TOKEN required)
- Publishes the VS Code extension via `vsce` using a stored PAT
- Creates a GitHub Release with the signed binary attached
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ view inside the editor.

## Features

- Supports native H.264 streaming for both iOS simulators and Android emulators
- Supports native H.264 streaming for both iOS simulators and Android emulators (iOS simulators need the macOS build; Windows and Linux stream Android emulators with a built-in OpenH264 software encoder)
- Full simulator control & inspection using private iOS accessibility APIs and Android UIAutomator - available using `simdeck` CLI
- Real-time screen `describe` command using accessibility view tree - available in token-efficient format for agents
- Profiling built-in: CPU, memory, disk writes, network throughput, hang signals, and stack sampling
Expand Down
19 changes: 19 additions & 0 deletions docs/api/health.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Example:
"serverKind": "launchAgent",
"timestamp": 1714094761.234,
"videoCodec": "auto",
"hostOs": "macos",
"liveVideo": {
"supported": true,
"encoder": "native",
"iosSimulator": true,
"androidEmulator": true
},
"androidGpu": "host",
"lowLatency": false,
"realtimeStream": true,
Expand All @@ -47,10 +54,22 @@ Important fields:
| `httpPort` | Port serving UI and API |
| `serverKind` | `launchAgent` or `standalone` |
| `videoCodec` | Requested codec mode: `auto`, `hardware`, or `software` |
| `hostOs` | Server build target: `macos`, `windows`, or `linux` |
| `liveVideo` | Live stream encoder and per-platform device support |
| `androidGpu` | Android emulator renderer mode for SimDeck-owned boots |
| `streamQuality` | Active stream profile and limits |
| `webRtc` | ICE settings the browser should use |

`liveVideo.encoder` is `native` on macOS (VideoToolbox or x264 through the
simulator bridge) and `openh264` on Windows and Linux, where Android emulator
frames are encoded in software. `androidEmulator` is `true` everywhere.
`iosSimulator` is `true` only on macOS; other builds add an
`iosSimulatorReason` string, and the WebRTC offer endpoint answers iOS
simulator offers there with `501 Not Implemented` and the same message.
`supported` is `true` for every shipped build; a client should treat
`supported: false` plus `reason` as "do not open a WebRTC offer".
`GET /api/stream-quality` includes the same `liveVideo` block.

When auth is required, the `401` JSON body still includes `serverId`, `advertiseHost`, `hostId`, `hostName`, `httpPort`, and `serverKind` so native clients can group endpoints before pairing.

## Metrics
Expand Down
6 changes: 5 additions & 1 deletion docs/api/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ curl -X POST \
| `GET` | `/api/stream-quality` | Current stream quality settings |
| `POST` | `/api/stream-quality` | Update stream quality settings |

See [Health and metrics](/api/health) for details.
See [Health and metrics](/api/health) for details. Both `/api/health` and
`/api/stream-quality` include a `liveVideo` block. Android emulators stream on
every platform (`encoder` is `native` on macOS and `openh264` elsewhere); check
`liveVideo.iosSimulator` before opening an iOS simulator WebRTC offer, because
Windows and Linux builds answer those with `501` and `iosSimulatorReason`.

## Devices

Expand Down
21 changes: 20 additions & 1 deletion docs/guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,29 @@

## Requirements

- macOS on Apple Silicon.
- macOS on Apple Silicon for the full experience.
- Xcode with the simulator runtimes you want to use.
- Node.js 18 or newer.

## Platform support

The npm package installs a native CLI for macOS, Windows, and Linux. Only the
macOS build includes the private simulator bridge, so iOS simulators need a
Mac. Android emulators work everywhere, including the live browser stream.

| Capability | macOS | Windows / Linux |
| ------------------------------------------------ | -------------------- | ---------------------- |
| iOS simulator control, inspection, and streaming | Yes | No |
| Android emulator control and inspection | Yes | Yes |
| Android emulator live H.264 browser stream | VideoToolbox or x264 | OpenH264 software |
| `--video-codec hardware` | VideoToolbox | Falls back to software |

On Windows and Linux the CLI prints a `Live video:` note after the service
URLs, and `GET /api/health` reports `liveVideo.encoder: "openh264"` with
`liveVideo.iosSimulator: false`. Selecting an iOS simulator through the API
there returns `501` with the same explanation. See
[Video and streaming](./video.md) for the encoder details.

Check Xcode selection if you have multiple installs:

```sh
Expand Down
Loading
Loading