Quickstart
This walkthrough validates ordinary Rust development first. It does not require Android Studio, Xcode, an emulator, a simulator, or a phone.
1. Install the current source
From the cargo-ferry checkout:
cargo install --path crates/cargo-ferry
The package requires Rust 1.92 or newer. Because rustferry 0.1.0 is not published yet, the contributor command below selects the checkout explicitly. A published release defaults to the registry and needs no path override. See Installation for PowerShell syntax and mobile toolchains.
2. Generate the starter
cargo ferry new weather \
--id com.example.weather \
--runtime-source path \
--runtime-path "$PWD/crates/rustferry"
cd weather
Generation is staged in a temporary sibling directory and refuses to overwrite an existing destination. By default it initializes Git and runs cargo check; use --no-git or --no-check to opt out.
Open src/app.rs first. The starter keeps its Slint UI, click handlers, lifecycle binding, async notification request, and error presentation together. State and the network service are in small neighboring modules.
3. Check normal Rust code
cargo ferry check
cargo test
cargo ferry check validates strict ferry.toml configuration before invoking cargo check.
4. Inspect prerequisites
cargo ferry doctor --all
Doctor is read-only. cargo ferry doctor --fix --dry-run prints suggestions; automatic mutation is not currently enabled.
5. Build an artifact
Android build is build-only: it must not require or contact a device.
cargo ferry build android
On macOS with full Xcode:
cargo ferry build ios --simulator
A zero exit from a finished platform build means the produced artifact passed the pipeline’s inspections. Dry-run output, generated manifests, or skipped CI jobs are not artifact evidence. Check Support matrix and Implementation status for the exact validated level in this revision.
Deployment is a separate, explicit level:
cargo ferry devices
cargo ferry install android --device SERIAL
cargo ferry run ios --simulator SIMULATOR_UDID
The current evidence does not include a running emulator, Simulator, or physical device.
6. Add capabilities deliberately
cargo ferry add notifications
cargo ferry add widget
cargo ferry capabilities
Permission prompts are never automatic. Application code chooses the user-initiated moment.
Slint license choice
Starter UI uses Slint and retains an accessible AboutSlint component. Before distributing binaries, choose and satisfy GPL-3.0, Slint’s royalty-free application license and attribution condition, or a commercial license. See ADR-001. This is not legal advice.
Next: Project structure, Configuration, and State and events.
Installation
Host-only development
Required:
- Rust and Cargo 1.92 or newer;
- Git when
cargo ferry newshould initialize a repository.
Published-release flow (available after the coordinated crates are released):
cargo install cargo-ferry
cargo ferry new weather
Install from this checkout for contributor development:
cargo install --locked --path crates/cargo-ferry
cargo ferry --version
cargo ferry new weather --runtime-source path --runtime-path "$PWD/crates/rustferry"
Normal generation writes an exact registry dependency and does not contain a developer checkout path. Contributors can select --runtime-source workspace or --runtime-source path --runtime-path ABSOLUTE_PATH. CARGO_FERRY_RUNTIME_PATH remains an optional development override; it must be UTF-8, absolute, canonicalizable, and contain Cargo.toml. Invalid inputs fail before files are written. The current repository is pre-release: neither cargo-ferry nor rustferry 0.1.0 is claimed available from crates.io.
Checking a generated Slint/Skia application on Linux also requires pkg-config and the system Fontconfig development package (libfontconfig1-dev on Debian and Ubuntu). RustFerry reports the underlying Cargo diagnostic when either prerequisite is missing.
PowerShell equivalent:
cargo install --path crates/cargo-ferry
cargo ferry --version
cargo ferry new weather --runtime-source path --runtime-path (Resolve-Path crates/rustferry).Path
Android artifacts
Android builds require an Android SDK platform, Build Tools (aapt2, d8, zipalign, apksigner), an NDK with LLVM, Java/Javac, and each configured Rust Android target. A connected device and ADB are not build prerequisites.
Use:
cargo ferry doctor --all
Then follow Android setup. cargo-ferry does not invoke sudo, accept licenses, or silently download executables.
iOS Simulator artifacts
iOS builds require macOS, full Xcode with an iPhone Simulator SDK, and aarch64-apple-ios-sim for Apple Silicon builds. A booted Simulator, Apple account, physical iPhone, and development signing are not build-only prerequisites.
Follow iOS setup. Physical-device builds have separate signing/provisioning requirements and a separate validation level.
Shell completions
Generate definitions to a file appropriate for your shell:
cargo ferry completions zsh > cargo-ferry.zsh
Supported shell names come from Clap; run cargo ferry completions --help for the active list.
Project structure
The default starter is a normal Rust package. Platform scaffolding is generated later and never becomes application source.
weather/
├── .gitignore
├── Cargo.toml
├── Cargo.lock
├── ferry.toml
├── README.md
├── assets/
│ ├── icon.png
│ └── splash.png
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── app.rs
│ ├── state.rs
│ ├── capabilities/
│ │ └── mod.rs
│ └── services/
│ ├── mod.rs
│ └── network.rs
└── tests/
└── basic.rs
src/app.rs: Slint component, window setup, click handlers, lifecycle/network subscriptions, async notification flow, and error display.src/state.rs: serializable application state throughrustferry::storage::Store.src/services/network.rs: OS path status and explicit endpoint probe kept separate.src/lib.rs: shared entry point and guarded Androidandroid_mainsymbol.src/main.rs: short host/Apple executable entry point.src/capabilities/mod.rs: generated capability module index;cargo ferry addadds focused Rust files beside it.ferry.toml: versioned identity, target, capability, permission-derived, and extension configuration..gitignore: excludes Rust and generated RustFerry build output.assets/: validated 1024×1024 opaque RustFerry-branded default PNGs. Replace them for product identity;cargo ferry assets checkvalidates source constraints before platform generation. The canonical editable vector source isdocs/assets/rustferry-icon.svg.tests/basic.rs:TestRuntimeexample with no mobile SDK.
Capability scaffolds are placed below src/capabilities/ only when missing; existing application files are not force-rewritten. Widget and Live Activity templates add restricted Rust snapshot modules rather than user-authored Kotlin or Swift.
Build output belongs below:
target/ferry/
├── assets/
│ └── <source-fingerprint>/
├── android/
└── ios/
The fingerprint cache contains a SHA-256 manifest, Android density resources, and the iOS catalog inputs consumed by runtime-present builds. Treat generated hosts and caches as disposable. Never put signing files there as their only copy. See Generated files.
Configuration
ferry.toml uses schema version 1, rejects unknown fields, and is semantically validated before expensive work. A representative configuration is:
schema_version = 1
platforms = ["android", "ios"]
[app]
name = "Weather"
identifier = "com.example.weather"
version = "0.1.0"
display_version = "0.1.0"
[app.window]
orientation = "automatic"
theme = "system"
[android]
min_sdk = 26
target_sdk = "installed"
abis = ["arm64-v8a"]
[ios]
min_version = "16.0"
[capabilities.network]
mode = "status"
probe_timeout_ms = 3000
[capabilities.notifications]
local = true
push = false
[capabilities.storage]
enabled = true
[capabilities.haptics]
enabled = true
[capabilities.clipboard]
enabled = false
[capabilities.share]
enabled = false
[capabilities.deep_links]
schemes = ["weather"]
allowed_hosts = []
allowed_actions = []
[extensions.widget]
enabled = false
[extensions.live_activity]
enabled = false
android_fallback = "ongoing-notification"
Inspect and validate
cargo ferry config validate
cargo ferry config show --resolved
cargo ferry config schema > ferry.schema.json
cargo ferry --dry-run config migrate
The active parser validates application identifiers, nonempty platforms and ABIs, Android SDK bounds, iOS major/minor syntax, network mode/probe conflicts, remote-push rejection, deep-link schemes, widget app groups, and the iOS 16.1 Live Activity floor.
Schema mismatches are rejected. cargo ferry config migrate upgrades a supported older schema atomically after parsing and validating the result; dry-run reports the proposed version change without writing. A newer, unknown schema is never downgraded—use a cargo-ferry version that understands it.
Network modes
none: no status/probe configuration; omit related platform permissions where possible.status: observe the OS path only.optional: application may operate offline and may probe explicitly.required: application code uses an explicitnetwork::require_online()gate; build never tests the developer machine’s internet connection.
An OS path does not prove a backend is reachable. probe_url, when used, belongs to the application and must be paired with a nonzero timeout.
Extensions
A widget needs an app_group, normally group.<app.identifier>. Enabling Live Activity raises the configured iOS minimum to at least 16.1 and keeps Android’s honest ongoing-notification fallback.
Secrets
Never store keystore passwords, private keys, tokens, certificates, or provisioning profiles in ferry.toml. Machine-specific and signing data belong in platform credential stores, environment input, protected files, or cargo-ferry’s system configuration directory. See Security policy.
CLI reference
Commands work as a Cargo subcommand (cargo ferry ...) or direct binary (cargo-ferry ...). Run --help on the installed revision for exact parsing.
Global flags
--verbose: external-command/discovery detail in human mode; conflicts with--quiet,--json, and--json-stream; secrets must be redacted.--quiet: suppress successful human output; conflicts with--verbose,--json, and--json-stream.--json: schema-versioned JSON without terminal styling; conflicts with human verbosity flags and--json-stream. Current output schema is version 1.--json-stream: protocol-v1 NDJSON forideoperations,devices, and livelogs; conflicts with human verbosity flags and--json. Other commands reject it and use--jsoninstead.--dry-run: validate and show intended mutations where the command supports planning.
Commands
| Command | Current contract |
|---|---|
new <name> | Atomic generation; --display-name, --id, --template, --platform, runtime source controls, --no-git, --no-check, --parent |
add <capability> | Preserve TOML formatting, enable config/Cargo feature, create a missing example module, support dry-run |
remove <capability> | Disable config/Cargo feature; preserve example source |
check | Validate config, then run ordinary cargo check |
doctor [--all] | Read-only host/toolchain inventory |
doctor --fix --dry-run | Print fixes; automatic mutation is not implemented |
build android | Build-only Android request; platform readiness and validation level are in STATUS |
build ios --simulator | Build-only Simulator request; no automatic boot/install/launch |
build ios --device --team <id> | Implemented official arm64/Xcode development-signing build; provisioning updates remain explicit; no identity, Team, profile, or signed artifact was available for artifact validation, and no device was available for device validation |
devices [--platform all|android|ios] | Typed ADB/simctl/devicectl inventory; --watch --json-stream emits the initial snapshot followed by polling deltas until cancelled |
install android|ios | Build, independently validate, select an exact compatible device, then install |
run android|ios | Build → validate → install → launch; --logs adds one bounded filtered snapshot where standalone logging is supported |
logs android|ios | Finite application-filtered history by default; --json-stream runs the live protocol stream until cancellation or platform-tool exit |
signing teams | Read-only Apple Development identity/Team inventory |
assets check|generate | Validate release sources; generate fingerprinted Android densities and an iOS asset catalog |
clean [android|ios|generated] | Remove only selected generated output below target/ferry/ |
clean --all | Remove cargo-ferry output below target/ferry/, not application source/signing inputs |
config validate | Strict parse and semantic validation |
config show --resolved | Print resolved defaults |
config schema | Print JSON Schema |
config migrate | Atomically upgrade a supported older schema; use global --dry-run to inspect first |
capabilities | List known runtime/platform state and current enablement when inside a project |
examples | List bundled template choices and generation commands |
docs [topic] | Show the source-tree page when available; otherwise print packaged embedded content |
completions <shell> | Generate shell completion definitions |
Capabilities accepted by add/remove: network, notifications, storage, haptics, clipboard, deep-links, share, widget, and live-activity.
Templates accepted by new: starter, minimal, counter, network, notifications, widget, live-activity, and kitchen-sink. They share a template engine and feature fragments rather than copied project trees.
Runtime controls are source-specific. --runtime-source registry accepts an optional semantic --runtime-version; --runtime-source workspace accepts neither version nor path; --runtime-source path requires --runtime-path naming an absolute, existing directory containing Cargo.toml. Version/path flags are rejected without an explicit source. With no runtime flag, generation uses the CLI’s registry version unless the contributor-only CARGO_FERRY_RUNTIME_PATH override is set.
Device watch mode requires --json-stream. It emits the current devices and warnings first, then polls at --interval-ms (2,000 ms by default, clamped to 500–60,000 ms) and emits only added, changed, removed, or warning changes. Ctrl+C ends watch mode with cancellation status.
logs without --json-stream collects a finite snapshot using --since-seconds, --max-entries, --max-bytes, and --level. --json-stream selects continuous, application-filtered Android or iOS Simulator logging and emits protocol events incrementally. Standalone physical-iOS logs is currently unsupported; CoreDevice console attachment is not exposed as this command.
build never discovers, boots, installs on, or launches a device. Those side effects exist only behind the explicit deployment commands. Android reinstall/downgrade/permission grant/data clear, Simulator boot, process termination, and Xcode provisioning updates are opt-in.
JSON failures
Failures include schema_version, status, and a stable error object with code, message, optional help, and safe details. Nonzero exit classes distinguish usage/configuration, missing/unsupported prerequisites, external command failure, and filesystem/safety failure.
Architecture
RustFerry separates application intent from disposable platform packaging.
Rust application + ferry.toml
|
cargo-ferry CLI
/ | \
config codegen tool discovery
| |
target/ferry/ generated hosts
/ \
direct Android SDK/NDK Apple Xcode toolchain
| |
inspected APK inspected .app/.appex
Workspace responsibilities
cargo-ferry: command parsing, project discovery, human/JSON reporting, orchestration.rustferry-core: strict configuration, naming, validation, schema, shared platform types.rustferry-codegen: atomic user-project generation and deterministic template fragments.rustferry(crates/rustferry): public capability API, event bus, storage, backend contract, andTestRuntime.rustferry-android: SDK/NDK discovery, direct packaging plans/builds, signing, and independent APK checks.rustferry-apple: Xcode/SDK discovery, generated-host plans/builds, and Apple bundle checks.
Rust-only application boundary
Application authors edit Rust and assets. Android Java/DEX or Apple Swift/Objective-C/Xcode metadata may exist when a system API requires them, but cargo-ferry generates it below target/ferry/. Generated glue contains adaptation, not application business logic.
Runtime boundary
Public capability functions look up an installed Runtime. Each backend advertises granular Operation support; absent operations return typed Unsupported errors instead of fake success. TestRuntime installs a thread-scoped deterministic backend for host tests.
Event subscriptions are owned values. Dropping a subscription prevents later callback starts; one source preserves its serial order, while concurrent sources may interleave. Mobile operating systems may omit termination/background events.
Platform packaging
Android’s default design invokes Cargo, NDK LLVM, aapt2, optional javac/d8, zipalign, and apksigner directly. It does not create a Gradle project in user source. See Android without Gradle.
Apple builds generate a hidden host project and metadata, then invoke official Xcode tooling. This is not a claim of pure Rust internals; it is a Rust-only application-source contract. See Apple generated host.
Trust and evidence
Paths, config, assets, metadata, external tools, callbacks, and archives cross trust boundaries. The threat model defines controls. The support matrix and status log separate code existence from compile, artifact, simulator/emulator, and device evidence.
Support matrix
This matrix deliberately separates validation levels. “Model tested” means host-side API/model behavior passed deterministic tests; it is not mobile runtime evidence. STATUS is the dated evidence log and overrides stale prose.
Validation vocabulary
| Level | Required evidence |
|---|---|
| Implemented | Concrete code path exists; unsupported defaults do not count |
| Compile validated | Relevant Rust/generated host compiled for that target |
| Artifact validated | Final APK, .app, or .appex passed independent structural/tool inspection |
| Simulator/emulator validated | Behavior was observed in a simulator/emulator |
| Device validated | Behavior was observed on physical hardware |
Platform build paths
| Path | Implemented | Compile | Artifact | Simulator/emulator | Device |
|---|---|---|---|---|---|
| Host config/runtime/tests | Yes | Host workspace checks; see STATUS | N/A | N/A | N/A |
| Android direct APK | Build plus typed devices/install/run/logs implemented | arm64 generated Rust app plus Java/DEX bridge | Public-CLI starter and Kitchen Sink APKs independently inspected | Deployment runtime not validated | Deployment runtime not validated |
iOS Simulator .app | Build plus typed devices/install/run/logs implemented | arm64 Slint executables | Public-CLI starter and Kitchen Sink .app/.appex bundles independently inspected | Deployment runtime not validated | N/A |
| iOS physical-device app | Official development-signing build/install/run implemented; explicit Team and provisioning controls | Deterministic arm64/Xcode plan tested; no signing identity available | Not validated | N/A | Not validated |
Capability evidence
| Capability | Host model/tests | Android backend/artifact | iOS backend/artifact | Runtime observation |
|---|---|---|---|---|
| Lifecycle/event bus | Implemented | Backend and DEX/native callback bridge compiled into inspected APK | Backend and dynamic framework artifact-inspected | None |
| Network status/probe | Implemented model and mock | Connectivity/HTTP backend enabled and bridge artifact-inspected | NWPath/URLSession backend and framework artifact-inspected | None |
| JSON storage | In-memory/file backend tests | Application-private file backend installed by Android host; target-compiled | Application Support file backend and framework artifact-inspected | None |
| Haptics | API and mock | Backend enabled and bridge artifact-inspected | Backend and framework artifact-inspected | None |
| Clipboard/share/system | API and mock | Backends compiled; enabled share provider artifact-inspected | Backends and framework artifact-inspected | None |
| Deep links | Parser/policy/event tests | Intent filter/allowlist backend and bridge artifact-inspected | URL scheme/delegate allowlist and framework artifact-inspected | None |
| Local notifications | API/model/mock | Backend/receiver enabled and artifact-inspected | UserNotifications backend and framework artifact-inspected | None |
| Permissions | API/model/mock | Enabled purpose strings, exact permissions, and bridge artifact-inspected | Supported permission backends and framework artifact-inspected | None |
| Widget | Snapshot model/tests + standalone example | Provider/backend enabled and artifact-inspected | Publisher, timeline renderer, and WidgetKit .appex artifact-inspected | None |
| Live Activity | State model/tests + standalone example | Ongoing-notification fallback enabled in an inspected Kitchen Sink APK | ActivityKit lifecycle bridge and .appex artifact-inspected | None |
Remote push is unavailable; schema version 1 rejects push = true. Device discovery and deployment use exact stable IDs, validated artifacts, bounded application-filtered logs, and official ADB/simctl/devicectl commands. No device behavior is inferred from those implemented paths.
CI interpretation
Linux, macOS, and Windows host jobs run independently. Android and Apple artifact jobs are not repository-variable gates: they install/select their build prerequisites, invoke real pipelines, repeat independent checks, and upload only non-empty expected artifacts. A cancelled or skipped job means “no evidence,” never “passed”; missing prerequisites, a failed build, or a missing/invalid artifact fails the job.
RustFerry IDE protocol
The RustFerry IDE protocol is the stable machine boundary between cargo-ferry and editor clients. Rust owns project parsing, validation, builds, deployment, signing, artifact metadata, and diagnostics. Clients must not parse human CLI output as a fallback.
Protocol version 1 uses UTF-8 JSON. Unary commands write one JSON object. Long-running commands write newline-delimited JSON (NDJSON): one compact, complete object followed by \n per event. Protocol stdout never contains ANSI styling, progress bars, raw child-process output, or binary data.
The canonical generated schema is ../schemas/ide-protocol-v1.schema.json. Rust structs under crates/cargo-ferry/src/ide/ are the source of truth. cargo ferry ide schema --json prints the schema represented by the running executable.
Negotiation
Run a handshake before any other operation:
cargo ferry ide handshake --json
The direct response contains:
protocol_version: selected version; currently1;tool: executable name and package version;host: extension-host operating system and architecture;supported_protocol_versions;supported_platforms;supported_commands;supported_event_types;features: explicit booleans for build, deployment, logs, physical iOS, and cancellation;build: profile, host target, development-build state, and optional injected Git commit;runtime_dependency: whether project-generation runtime resolution is usable and whether it usesregistryor an explicit developmentpath;templates: generator-owned IDs and descriptions.
Clients must reject a protocol_version outside their supported range. Feature availability comes from the response, not OS guessing.
Unary commands
cargo ferry ide project --workspace /absolute/project --json
cargo ferry ide validate --workspace /absolute/project --json
cargo ferry ide doctor --workspace /absolute/project --all --json
cargo ferry ide devices --platform all --json
cargo ferry ide signing-teams --workspace /absolute/project --json
cargo ferry ide schema --json
project returns canonical absolute paths, application identity, crate name, versions, platforms, enabled capabilities, resolved Android/iOS configuration, the generated-output boundary, and template metadata.
validate returns every available diagnostic. A configuration problem is a successful protocol exchange with valid: false; it is not a bootstrap failure. Diagnostics include an absolute file path and a zero-based, half-open range. character counts UTF-16 code units so it maps directly to VS Code positions. A safe fix is present only when Rust supplies an exact edit or registered command.
Editor clients validate unsaved text without writing it to disk or placing it in arguments:
cargo ferry ide validate --workspace /absolute/project --manifest-stdin --json
With --manifest-stdin, the command reads at most 1 MiB of UTF-8 ferry.toml source from standard input. Rust still resolves the real project and reports the canonical manifest path, while ranges refer to the supplied source. Omitting the flag validates the saved file. Clients must discard results when the editor URI, document version, content digest, or dirty state changes during the request. Disk-backed quick fixes must not be offered for dirty-source results.
devices invokes installed ADB/CoreSimulator/CoreDevice tools through argument arrays. Platform failures are independent warnings, so one missing tool does not hide other device families. Each record reports build, install, launch, and application-log capabilities independently. devices --watch --json-stream polls every 2,000 ms by default; --interval-ms is clamped to 500–60,000 ms. Watch mode emits one initial snapshot, suppresses identical later snapshots, emits added/changed device records and device_removed, and stops through the normal cancellation path.
signing-teams returns installed usable Apple Development identities as non-secret team_id, identity-label, and public certificate-fingerprint fields. It never returns a private key, credential, or provisioning profile.
Unary bootstrap failures have this shape:
{
"protocol_version": 1,
"error": {
"code": "project_not_found",
"message": "No RustFerry application was found",
"help": "Open a directory containing ferry.toml and Cargo.toml"
}
}
Unknown optional fields may be ignored. Missing required fields are incompatible input.
Streaming commands
cargo ferry ide check \
--workspace /absolute/project \
--operation-id vscode:check-8 \
--json-stream
cargo ferry ide build \
--workspace /absolute/project \
--platform android \
--profile debug \
--operation-id vscode:build-42 \
--json-stream
cargo ferry ide install \
--workspace /absolute/project \
--platform android \
--device emulator-5554 \
--operation-id vscode:install-7 \
--json-stream
cargo ferry ide run \
--workspace /absolute/project \
--platform ios-simulator \
--device 00000000-0000-0000-0000-000000000000 \
--json-stream
cargo ferry ide build \
--workspace /absolute/project \
--platform ios-device \
--team ABCDE12345 \
--json-stream
cargo ferry ide install \
--workspace /absolute/project \
--platform ios-device \
--device 00008110-000000000000001E \
--team ABCDE12345 \
--json-stream
cargo ferry ide logs \
--workspace /absolute/project \
--platform android \
--device emulator-5554 \
--json-stream
--operation-id is optional. When omitted, the CLI creates an opaque UUID. A caller may supply 1–128 ASCII letters, digits, ., _, :, or -. --parent-operation-id links nested operations.
check runs Cargo’s JSON message stream through Rust-owned decoding. Compiler diagnostics use canonical absolute paths, zero-based half-open UTF-16 ranges, severity, code, help, and documentation URLs. Diagnostics are emitted before the terminal event even when compilation fails, so editor Problems remain useful. The ordinary human cargo ferry check command renders the same diagnostics into a readable bounded log under target/ferry/logs/.
Every event has these fields:
| Field | Type | Meaning |
|---|---|---|
protocol_version | integer | Always 1 for this protocol |
event | string | Event discriminator |
operation_id | string | Same value for the full operation |
parent_operation_id | string, optional | Enclosing operation |
timestamp_ms | integer | UTC Unix epoch milliseconds |
Event order is deterministic for a given operation path. Object field order is stable but clients must not depend on JSON key order.
install and run require one exact stable device ID. They first produce a fresh debug artifact through the ordinary builder, require completed independent validation, structurally recheck that output, then call only official platform tools with argument arrays. Conservative defaults do not clear Android application data, request a downgrade, boot a shutdown Simulator, or terminate an existing process. run installs before launch and emits application_started only after the platform confirms startup.
ios-device build, install, and run require --team. --provisioning-profile NAME_OR_UUID selects manual signing and --allow-provisioning-updates explicitly permits Xcode provisioning mutation; both require a Team and neither is enabled implicitly. The physical builder cross-compiles aarch64-apple-ios, stages it in the generated Xcode host, signs through Xcode, then independently checks the expected executable and Mach-O identity, architectures, recursive signatures, leaf certificate/profile authorization, Team, entitlements, and exact profile expiration before emitting an artifact. Install and Run then use the exact CoreDevice identifier.
An explicit --artifact is rejected until RustFerry can load persisted validator-owned metadata for it; a path or extension alone is never treated as deployment proof. Physical iOS deployment uses the implemented development-signed builder and rejects missing or inconsistent Team, executable, signature, certificate, profile, entitlement, and artifact evidence as typed protocol errors.
logs remains active until cancellation or platform-tool exit and emits each application record as a complete NDJSON event as soon as it is decoded. Android uses adb logcat --pid for the exact running package process; Simulator unified logging uses log stream --style ndjson with the exact project process/bundle predicate. It never clears or substitutes the global system log. One source line is capped at 256 KiB, the reader-to-emitter queue is capped at 1,024 records, and stderr retention remains bounded. Backpressure reaches the official platform tool instead of accumulating unbounded memory. A platform-tool exit ends the operation; version 1 does not reconnect automatically. Standalone physical-iOS log streaming is reported as unsupported when CoreDevice cannot provide the same application boundary.
The human command cargo ferry logs remains a finite snapshot. Adding the global --json-stream flag routes it through the same live, bounded stream and terminal-event lifecycle as cargo ferry ide logs.
Version 1 defines:
| Event | Purpose |
|---|---|
operation_started | Opens an operation and names the command/workspace |
phase_started | Opens a stable phase |
progress | Bounded or indeterminate progress |
command_started | Sanitized executable plus argument array, never a shell command |
diagnostic | File-bound structured diagnostic |
device | Added or changed typed device |
device_removed | Device removed from a watched snapshot |
artifact | Validated artifact path and metadata |
application_started | Confirmed package/bundle launch |
log | One application-specific log record |
warning | Non-fatal actionable warning |
fix | Standalone Rust-supplied safe action |
phase_finished | Closes a phase with status and duration |
operation_finished | Closes success or a typed failure |
operation_cancelled | Closes cancellation |
Clients must ignore an unknown event after validating the common fields and protocol version. A partial final line is not an event and must be discarded as a truncated stream.
Cancellation and process failures
The client cancels by terminating the cargo-ferry process (SIGINT is preferred where available). RustFerry’s process-control layer terminates the tracked child process tree. A cooperative streaming command emits exactly one operation_cancelled event and exits with status 130. Editor shutdown must still terminate the process tree even if stdout is no longer readable.
A normal tool or build failure emits structured diagnostics, closes any open phase, emits operation_finished with success: false and a typed error, then returns a non-zero status. Raw child stdout/stderr never becomes protocol framing.
Paths, text, and secrets
- Project, diagnostic, and artifact paths are canonical absolute UTF-8 paths. A field that is not a file path is explicitly named otherwise.
- Spaces, Windows separators, and Unicode are ordinary JSON string content.
- Invalid child-process UTF-8 is converted safely before it reaches a text field.
- Argument arrays preserve process boundaries; shell command concatenation is forbidden.
- Passwords, tokens, signing secrets, private keys, provisioning contents, and broad environment dumps are forbidden. Known credential values are redacted as
<redacted>. - Logs must be bounded by the producer/consumer and must not contain binary data.
Compatibility policy
Protocol v1 may add optional object fields and new event types. Existing required fields do not change meaning. Removing a required field, changing its type/meaning, or changing framing requires a new protocol version. Clients reject incompatible versions with an actionable update message and never fall back to human-output parsing.
Compatibility tests cover Rust serialization, external fixtures, optional fields, unknown events, missing fields, incompatible versions, Unicode, Windows paths, spaces, cancellation, invalid UTF-8, and truncated streams.
Protocol and editor-host tests do not establish mobile runtime evidence. The current environment had no Android emulator/device, iOS Simulator runtime/device, Apple Development identity, Team, provisioning profile, or attached iPhone; physical signing and device operations therefore remain unvalidated outside deterministic host tests.
Visual Studio Code
Build the extension package from a source checkout, then install the resulting VSIX:
cd editors/vscode
npm ci
npm run package
code --install-extension dist/rustferry-vscode.vsix
The final command can instead be completed from the VS Code Extensions view with Install from VSIX… and editors/vscode/dist/rustferry-vscode.vsix.
The extension activates for a trusted workspace containing ferry.toml. It discovers cargo-ferry, negotiates IDE protocol v1, and provides:
- Project, Devices, and Artifacts trees;
- status and build progress;
- saved and unsaved
ferry.tomldiagnostics in Problems through the bounded IDE protocol; - Create Project, Check, Doctor, Build, Install, Run, Logs, capability, documentation, reveal, and copy-path commands;
- native Quick Pick/Input Box project creation;
- generated cancellable Terminal tasks that run the documented human CLI commands.
Multi-root workspaces retain independent project/device/artifact state. Virtual and untrusted workspaces cannot execute or mutate projects. Trusted file-backed Remote SSH, WSL, Dev Container, and Codespaces workspaces execute the extension and CLI in the remote extension host; platform operations are available only when its SDK tools and device connections are available. Editor commands consume the structured IDE protocol, while generated Terminal tasks intentionally display the human CLI output. The extension does not implement platform builds, store Apple credentials, or edit generated native projects.
Open Logs starts the application-filtered IDE stream and keeps it active. Stop Logs sends cancellation to the CLI process tree; the CLI closes the protocol with operation_cancelled, and no global Android or Apple log stream is used.
For troubleshooting and exact settings, see the extension’s README and support guide. Protocol details are in IDE protocol.
Install the VS Code extension
RustFerry requires Visual Studio Code 1.100 or newer and a compatible cargo-ferry executable. The extension has not been published to the Visual Studio Marketplace; install the inspected VSIX from a source checkout or release candidate.
cargo build --locked -p cargo-ferry
cd editors/vscode
npm ci
npm run package
code --install-extension dist/rustferry-vscode.vsix
The final command may be replaced with Extensions: Install from VSIX…. Reinstall with --force when testing a rebuilt package.
The extension resolves the CLI in this order:
- absolute
rustferry.cliPath; cargo-ferryonPATH;cargo ferrythroughcargoonPATH;- the standard Cargo bin directory;
- an ancestor checkout’s
target/debug/cargo-ferry, only withrustferry.developmentModeenabled.
Open a trusted, file-backed folder containing ferry.toml. Virtual workspaces remain non-executable. In Remote SSH, WSL, Dev Containers, and Codespaces, the extension and CLI run in the trusted remote extension host; commands work only when that host can see the required SDK tools and device connections.
See VSIX packaging for package verification and settings for explicit CLI selection.
VS Code project wizard
Run RustFerry: Create New Project from a trusted workspace. The wizard collects, in order:
- a local parent directory;
- project/crate name, display name, and reverse-DNS application identifier;
- a template reported by the selected
cargo-ferryCLI; - Android, iOS, or both platforms;
- zero or more CLI-reported capabilities;
- whether to initialize Git;
- whether to open in the current window, a new window, or the current multi-root workspace.
RustFerry validates names and identifiers before generation and refuses an existing destination. A final modal shows the exact destination and choices. No build starts automatically.
The extension creates the base project first, then adds selected capabilities one at a time. If capability setup fails, the created project is preserved, the remaining capability names are reported, and the project can be opened to finish setup. Cancellation stops the active CLI process tree.
Opening or adding the project starts discovery and the Getting Started walkthrough. Generated user code stays Rust-only; native glue is generated later below target/ferry/.
The equivalent CLI surface is documented under cargo ferry new.
VS Code commands
Commands are available from the Command Palette under RustFerry and context menus where applicable.
| Workflow | Commands |
|---|---|
| Project | Create New Project, Refresh, Select Project, Open ferry.toml, Open src/app.rs |
| Validation | Check, Doctor |
| Target | Select Target, Build Android, Build iOS Simulator, Build for Physical iPhone, Build Selected Target |
| Capabilities | Add Capability, Remove Capability |
| Devices and signing | Refresh Devices, Select Device, Select Development Team, Run iOS Doctor, Open iOS Signing Guide |
| Deployment | Install, Run, Open Logs, Stop Logs |
| Artifacts | Reveal Artifact, Copy Artifact Path, Inspect Artifact Metadata, Delete Generated Artifact… |
| Maintenance | Clean Generated Files, Manage Workspace Trust, Select cargo-ferry Executable, Open Documentation |
Check and Build publish structured diagnostics to Problems. Build records only artifacts reported as validated by the CLI. Install and Run require a compatible selected device; Open Logs starts an application-filtered stream, while Stop Logs cancels its CLI process tree. There is no automatic log reconnection.
Delete and Clean require confirmation and are limited to generated RustFerry output. Untrusted or virtual workspaces cannot execute or mutate projects.
Physical-iPhone Build, Install, and Run use an exact CoreDevice ID and a Team selected from installed Apple Development identities. The extension stores only the non-secret Team ID; explicit provisioning-profile selection and permission for Xcode to update provisioning assets remain CLI controls. Standalone physical-iPhone logs are unavailable because the current CoreDevice path cannot guarantee an application-only stream. See the physical iPhone workflow. RustFerry also intentionally exposes no fake VS Code debugger; see ADR-004.
VS Code settings
Project settings have resource scope, so multi-root folders may use independent values. The selected Apple Development Team is machine-scoped because it describes identities installed on that extension host.
| Setting | Default | Meaning |
|---|---|---|
rustferry.cliPath | empty | Absolute path to cargo-ferry or cargo; empty enables safe discovery. |
rustferry.developmentMode | false | Permit an ancestor RustFerry checkout’s target/debug/cargo-ferry; never permits arbitrary workspace executables. |
rustferry.defaultPlatform | android | Initial target: android, ios-simulator, or ios-device. |
rustferry.defaultProfile | debug | Initial build profile: debug or release. |
rustferry.ios.developmentTeam | empty | Machine-scoped, non-secret Team ID selected from installed Apple Development identities. |
rustferry.validation.debounceMs | 350 | Manifest validation delay, constrained to 100–5000 ms. |
rustferry.maxProtocolLineBytes | 1048576 | Maximum UTF-8 bytes in one protocol event, constrained to 65536–4194304. |
Selected project, platform, profile, device ID, and recent artifact metadata live in VS Code workspace state rather than ferry.toml. Changing target clears an incompatible selected device.
Prefer RustFerry: Select cargo-ferry Executable over editing JSON; it writes the path at workspace or workspace-folder scope. The path must be absolute and executable.
Remote settings resolve in the remote extension host. A local macOS SDK or USB connection does not become visible to an extension running remotely.
VS Code tasks
RustFerry contributes a rustferry task provider. In a trusted, executable project it generates Check, Doctor, Android Build, iOS Simulator Build, selected-target Build, and Clean tasks. Install, Run, and Logs appear only when the negotiated CLI protocol advertises those features.
Generated tasks use the human CLI, not the editor JSON protocol, and run in a dedicated terminal with the project directory as cwd. Build Selected Target is assigned to VS Code’s standard Build task group; it is not marked as the default task.
A checked-in task may use the same definition:
{
"version": "2.0.0",
"tasks": [
{
"type": "rustferry",
"action": "build",
"platform": "android",
"profile": "release",
"project": "${workspaceFolder}"
}
]
}
Supported actions are check, doctor, build, install, run, logs, and clean. Platforms are android, ios-simulator, and ios-device; profiles are debug and release. The physical-iPhone build task appears only when the protocol advertises physical iOS support and passes the configured non-secret Team ID when one is selected. Deployment tasks may include an exact stable device ID. Physical-iPhone install and run require both that ID and a Team; standalone physical-iPhone logs remain unavailable.
Task argument construction uses process executable/argument arrays. It does not build a shell command string. Tasks are absent in untrusted workspaces and unresolved when the project or CLI cannot execute.
VS Code diagnostics
RustFerry publishes ferry.toml and Rust compiler diagnostics to VS Code Problems with protocol-provided severity, code, help, documentation target, and source range.
Manifest validation is debounced on open, edit, save, and relevant workspace changes. An unsaved buffer is sent to cargo ferry ide validate --manifest-stdin; the bounded UTF-8 request is validated without writing it to disk. A newer document version cancels or invalidates an older result, and results are discarded if the manifest path or content changes during validation.
Check and Build use Cargo’s structured compiler messages. Rust source diagnostics retain real file paths and ranges, including UTF-16 column conversion for VS Code. A failed build still publishes diagnostics gathered before the failure.
Protocol text edits become Quick Fixes only for a clean, file-backed manifest whose version, digest, canonical path, and real file contents still match validation. Fixes are rejected across symbolic-link boundaries, outside the project root, or after any intervening edit. Dirty-buffer diagnostics remain visible but receive no disk-backed mutation.
Use Run RustFerry Doctor for environment failures and Open RustFerry documentation when a diagnostic provides a documentation URL. Human terminal output is never scraped into Problems; see IDE protocol.
VS Code devices
The Devices view normalizes Android physical devices/emulators, iOS Simulators, and paired physical Apple devices returned by cargo-ferry. Refresh is explicit; state such as offline, unauthorized, shutdown, unavailable, unpaired, or disconnected remains visible.
Select a target first, then choose a compatible device by its stable ADB serial, Simulator UDID, or CoreDevice identifier. The selection is stored per project. Switching platform clears an incompatible selection, and refreshed inventory restores a selection only when the exact ID remains compatible.
Install, Run, and Logs prompt for a device if none is selected. Each device record carries separate build, install, launch, and application-log capabilities; unsupported or stale device state fails closed in the CLI even when the extension-level feature exists. Multi-root workspaces retain separate project, target, device, and artifact state.
For a physical iPhone, select iOS Device, choose the paired device by its exact CoreDevice identifier, then run RustFerry: Select Development Team. The extension queries cargo ferry ide signing-teams, shows the Team ID, identity label, and public certificate fingerprint, and stores only the selected Team ID in the global rustferry.ios.developmentTeam setting. It does not collect credentials, private keys, or profile contents.
Build for Physical iPhone, Install, and Run pass that Team to the same official Xcode/devicectl pipeline used by the CLI. Install and Run create and independently validate a fresh development-signed artifact before using the selected device. The editor uses automatic signing without provisioning mutation; manual profile selection and opt-in -allowProvisioningUpdates remain explicit CLI/protocol controls. Standalone physical-iPhone Logs is unavailable because the current CoreDevice path does not provide the same application-only boundary as Android and Simulator logging.
In a trusted file-backed remote workspace, discovery runs remotely. Only SDK tools and connections visible to that remote extension host can appear. The current validation environment had no Android emulator/device, iOS Simulator runtime/device, Apple Development identity, Team, provisioning profile, or attached iPhone. The editor flow is implemented and host-tested, but physical signing and all mobile runtime operations remain unobserved. See the general device discovery contract.
VS Code troubleshooting
No project appears
Open a file-backed workspace containing ferry.toml, then run RustFerry: Refresh. Discovery excludes target, node_modules, and .git and is bounded to the workspace. Virtual workspaces cannot execute RustFerry.
CLI not found or incompatible
Run RustFerry: Select cargo-ferry Executable and choose an absolute executable, or install cargo-ferry on the extension host’s PATH. Update the CLI when protocol v1 negotiation fails. In a remote window, installing only on the local machine is insufficient.
Commands are disabled
Trust the workspace. Remote execution also requires a file-backed folder and the needed Rust/platform tools in the remote extension host.
No device or operation
Run Doctor and Refresh Devices. Use exact stable IDs and resolve offline, unauthorized, shutdown, unpaired, or disabled Developer Mode state with platform tooling. For a physical iPhone, select the iOS Device target, choose the exact CoreDevice ID, then run RustFerry: Select Development Team. No team is shown until Keychain contains a usable Apple Development identity. Install and Run are available in the editor; manual profile selection and provisioning updates remain explicit CLI controls.
Diagnostics or Quick Fixes disappear
RustFerry discards stale validation results. Save or stop editing, wait for validation, and retry. Quick Fixes intentionally require a clean unchanged regular file.
Logs stop
Open Logs attaches to the currently running selected application. Stop Logs cancels it. A platform-tool exit ends the stream; automatic reconnect is not implemented, so relaunch the application if needed and run Open Logs again.
Standalone physical-iPhone logs are intentionally unavailable: the current CoreDevice path cannot enforce the same application-only log boundary. Build, Install, and Run support does not imply that Logs is supported for the same device.
For a reproducible report, include extension and CLI versions, extension-host environment, target, operation ID, and sanitized RustFerry output. Never attach keys, profiles, tokens, passwords, or complete environment dumps. See the extension support guide.
VS Code extension development
The extension source is editors/vscode/. It targets Node 20 and VS Code 1.100, bundles production code with esbuild, and keeps mobile build logic in cargo-ferry.
cargo build --locked -p cargo-ferry
cd editors/vscode
npm ci
npm run typecheck
npm run lint
npm test
npm run test:host
npm run perf
npm run package
npm run vsix:smoke
Set RUSTFERRY_TEST_CLI to an alternate real CLI; otherwise host and performance tests expect ../../target/debug/cargo-ferry. Extension Host smoke uses isolated user/extension directories and checks both an ordinary Rust workspace that must remain inactive and a Ferry workspace that must activate, discover, validate, refresh views, and open its manifest.
Unit tests cover protocol framing, process bounds/cancellation, discovery, tasks, validation freshness, fix safety, and project input validation. The host smoke does not build, install, launch, or observe a mobile application.
For contributor use, rustferry.developmentMode may resolve the checkout’s debug CLI. Keep it disabled for normal projects. See VSIX packaging for the exact bundle boundary.
VS Code extension release
An extension candidate is the exact VSIX produced by npm run package, followed by npm run vsix:smoke. Before approval, run the complete npm run check, real-CLI Extension Host smoke, performance measurement, npm audit, and repository license policy.
The package allowlist contains user-facing docs, license, production bundle, icon, walkthrough media, and snippets. Source maps, TypeScript sources, tests, package locks, node_modules, workflows, and nested VSIX files must not ship. Record size and SHA-256 only from the final bytes; do not copy a stale value into documentation.
The repository’s extension workflow checks Linux, macOS, and Windows. Linux additionally runs the real CLI integration, Extension Host smoke, measurements, license policy, and uploads the verified VSIX as a workflow artifact.
No automatic Marketplace publication exists, and the extension has not been published there. A release requires separate protected manual approval and verification described in VS Code Marketplace. The draft GitHub Release assembly includes a versioned copy of the same inspected VSIX without changing its bytes.
Device discovery
cargo ferry devices normalizes Android physical devices/emulators, iOS Simulators, and paired CoreDevice hardware without making one missing tool hide the others.
cargo ferry devices
cargo ferry devices --platform android --json
cargo ferry devices --platform ios
cargo ferry devices --watch --json-stream
Use the exact ADB serial, Simulator UDID, or CoreDevice identifier shown by the command. Display names are not stable selectors. Automatic selection succeeds only when exactly one compatible device exists; ambiguity fails with the candidate IDs.
States such as offline, unauthorized, shutdown, unavailable, unpaired, and disconnected remain visible and never count as install/launch success. Watch mode emits protocol-v1 NDJSON until Ctrl+C; cancellation stops the complete child process tree.
No emulator, Simulator runtime, or physical device was available for the current validation pass. Parser, selection, capability, watch, cancellation, and real empty-inventory discovery paths were tested.
Install, run, and logs
Deployment consumes only freshly built, independently validated artifacts. A file suffix alone never makes an APK or app deployable.
Android:
cargo ferry install android --device SERIAL
cargo ferry run android --device SERIAL
cargo ferry run android --device SERIAL --logs
cargo ferry logs android --device SERIAL --since-seconds 300
iOS Simulator:
cargo ferry install ios --simulator
cargo ferry run ios --simulator SIMULATOR_UDID
cargo ferry logs ios --simulator SIMULATOR_UDID
install composes build → validation → install. run adds launch. Logs are never attached implicitly unless run --logs is present, and that option collects one finite snapshot rather than leaving a process running.
Safe defaults do not reinstall/downgrade, grant permissions, clear Android data, boot a Simulator, terminate an existing process, or mutate provisioning. Each behavior has a named opt-in flag. RustFerry never uninstalls an application or clears the global log buffer.
Android logs are PID-filtered; Simulator logs use an application predicate. Entry count, bytes, history, command runtime, and retained output are bounded. Standalone physical-iOS historical logs remain unsupported when CoreDevice does not expose an application-filtered operation; RustFerry will not substitute the full system log.
Install on Android
Connect or start an Android target, confirm it with cargo ferry devices --platform android, then install using its exact ADB serial:
cargo ferry install android --device SERIAL
Install composes a fresh Android build, independent APK validation, a final integrity recheck, then adb -s SERIAL install. Automatic selection is allowed only when exactly one compatible target exists.
Conservative defaults do not replace an installed package, permit a downgrade, grant runtime permissions, or clear application data. Each behavior is explicit:
cargo ferry install android --device SERIAL --reinstall
cargo ferry install android --device SERIAL --reinstall --allow-downgrade
cargo ferry install android --device SERIAL --grant-permissions
cargo ferry install android --device SERIAL --clear-data
--clear-data affects only this application and runs after a successful install. RustFerry never uninstalls an app or clears global device logs. --release builds the release profile before installation.
No Android device or emulator was available in the current validation environment, so the command path and tool parsing are tested but live installation was not observed. See Android build and the shared deployment contract.
Run on Android
Run builds, validates, installs, and launches the configured application on an Android physical device or emulator:
cargo ferry run android --device SERIAL
Use cargo ferry devices --platform android for the exact serial. Omitting it is safe only when discovery returns exactly one compatible target. Install flags remain opt-in:
cargo ferry run android --device SERIAL --reinstall --grant-permissions
RustFerry does not terminate an existing process unless --terminate-existing is present. --logs collects one finite, application-filtered snapshot after launch; it does not leave a background stream running:
cargo ferry run android --device SERIAL --terminate-existing --logs
Use the separate Android logs command for history or a continuous JSON stream. A successful Run report requires the official platform tools to confirm install and launch; a built APK alone is not launch evidence.
No Android device or emulator was available in the current validation environment, so launch behavior was not observed on hardware or an emulator.
Android logs
The human command returns a finite snapshot for the running application selected by package PID:
cargo ferry logs android --device SERIAL
cargo ferry logs android --device SERIAL --since-seconds 60 --level warning
Bounds default to a five-minute window, 2,000 retained entries, and 2 MiB. Override them with --since-seconds, --max-entries, and --max-bytes. RustFerry uses adb logcat --pid and never clears the global log buffer. The application must already be running.
For continuous protocol output, add --json-stream:
cargo ferry logs android --device SERIAL --json-stream
Each application-filtered entry is emitted as a bounded protocol event until Ctrl+C or platform-tool exit. Cancellation terminates the process tree. There is no automatic reconnect after process restart, disconnect, or adb logcat exit.
The VS Code Open Logs command consumes this structured live stream; Stop Logs cancels it. No Android target was available in the current validation environment, so live device output was not observed.
Install on iOS Simulator
List Simulators and use the exact UDID:
cargo ferry devices --platform ios
cargo ferry install ios --simulator SIMULATOR_UDID
--simulator without a value means automatic selection and succeeds only when exactly one compatible Simulator exists. Install composes a fresh Simulator build, independent .app validation, a final integrity recheck, then xcrun simctl install.
A shutdown Simulator is rejected by default. Boot only the selected Simulator explicitly:
cargo ferry install ios --simulator SIMULATOR_UDID --boot-on-demand
RustFerry waits for boot completion before the final integrity check and install. It does not select, boot, or mutate another Simulator. Simulator applications use the validated ad-hoc signing path and require no Apple Team.
The current host had no installed Simulator runtime or device. SDK-only .app build validation was completed, but Simulator installation was not observed. See Simulator build.
Run on iOS Simulator
Run builds, validates, installs, and launches the configured bundle on one exact Simulator:
cargo ferry run ios --simulator SIMULATOR_UDID
Use --boot-on-demand to boot a selected shutdown Simulator. Existing application processes are left alone unless termination is explicit:
cargo ferry run ios --simulator SIMULATOR_UDID \
--boot-on-demand --terminate-existing
--logs collects one finite application-filtered snapshot after launch. It does not open a persistent stream; use iOS logs for that.
A successful Run report requires simctl to confirm both installation and launch. A validated .app alone is not runtime evidence. Automatic selection is permitted only with exactly one compatible Simulator.
No Simulator runtime or device was present in the current validation environment, so installation, launch, UI, and runtime callbacks were not observed.
iOS logs
For an iOS Simulator, the human command returns a finite unified-log snapshot filtered to the configured application process/bundle:
cargo ferry logs ios --simulator SIMULATOR_UDID
cargo ferry logs ios --simulator SIMULATOR_UDID --since-seconds 60 --level debug
The command uses simctl spawn … log show --style ndjson with an application predicate. Entry count, UTF-8 bytes, history, and command runtime are bounded. It never returns the entire Simulator system log.
Continuous protocol output uses log stream:
cargo ferry logs ios --simulator SIMULATOR_UDID --json-stream
The stream ends on Ctrl+C or platform-tool exit and has no automatic reconnect. VS Code Open Logs consumes this stream; Stop Logs cancels the process tree.
Standalone physical-iPhone historical or live logs are unsupported when the installed CoreDevice API lacks a safe application-filtered operation. RustFerry will not substitute a global device log. No Simulator runtime or physical device was available for current live-log validation.
Physical iPhone deployment
RustFerry uses official Xcode development signing and CoreDevice operations. It has no signing bypass and never stores Apple credentials, private keys, profiles, or passwords in ferry.toml.
Inspect available development identities and connected devices:
cargo ferry signing teams
cargo ferry devices --platform ios
Build, install, and run with an explicit Team and exact CoreDevice identifier:
cargo ferry build ios --device --team ABCDE12345
cargo ferry install ios --device DEVICE_ID --team ABCDE12345
cargo ferry run ios --device DEVICE_ID --team ABCDE12345
Provisioning mutation is disabled by default. Enable Xcode account/profile updates only deliberately with --allow-provisioning-updates, or choose an explicit manual profile with --provisioning-profile NAME_OR_UUID. RustFerry independently checks arm64 architecture, bundle structure, nested extensions, signatures, profiles, entitlements, Team ID, and bundle IDs before deployment.
The VS Code extension can select an installed Apple Development Team and an exact paired device, then build, install, and run through the same pipeline. Manual profile selection and opt-in provisioning updates remain explicit CLI controls. Standalone physical-device logs are unsupported unless CoreDevice exposes a safe application-filtered operation; RustFerry does not stream the full device log.
No Apple Development identity, Team, profile, or attached device was available in the current environment. The pipeline and deterministic tests exist, but a physical artifact was not built, installed, launched, or observed here. See physical iOS development for implementation details.
Rust package readiness
RustFerry has six publishable crates. All versions come from
workspace.package; a release must keep them identical.
| Order | Package | Role | Internal prerequisites |
|---|---|---|---|
| 1 | rustferry-core | Configuration, validation, assets, process control | None |
| 1 | rustferry | Application runtime API | None |
| 2 | rustferry-codegen | Project, capability, and asset generation | rustferry-core |
| 3 | rustferry-apple | Apple generation and artifact backend | rustferry-core, rustferry-codegen |
| 3 | rustferry-android | Direct Android packaging backend | rustferry-core, rustferry-codegen |
| 4 | cargo-ferry | Public CLI | All backend/core/codegen crates |
Wait for each prerequisite version to appear in the registry index before publishing the next group. The automated release workflow never publishes to crates.io.
Manifest contract
Every crate must declare its name, version, Rust version, description, repository, homepage, documentation URL, dual-license expression, README, keywords, categories, and an explicit include set. Internal path dependencies must also carry the exact release version so Cargo removes the path when it normalizes the package.
Each crate root links LICENSE-MIT and LICENSE-APACHE to the canonical
workspace files and includes both names explicitly. Cargo dereferences those
links into regular files in the portable archive. The archive scanner requires
both members at the package root and verifies their exact SHA-256 digests.
The committed workspace Cargo.lock is the release lock. Use --locked for
every gate. Generated package archives contain only their declared source,
templates, tests, and embedded docs; they must not contain target/, signing
material, absolute developer paths, or missing include_bytes!/include_str!
inputs.
Package gates
Run from the repository root on a clean release revision:
cargo metadata --locked --no-deps --format-version 1
python3 scripts/check-release-contract.py
cargo package --workspace --locked --list
cargo package --workspace --locked
cargo publish --workspace --dry-run --locked --no-verify
python3 scripts/check-release-archives.py \
--check-sources \
--target-dir target/package-source-check \
target/package/*.crate
cargo package --workspace verifies the normalized archives together, so
unpublished internal dependencies resolve from the package set. The following
publish --dry-run --no-verify repeats registry upload checks without compiling
the same archives a second time. It performs no upload.
Inspect the produced archives before approving a draft:
find target/package -maxdepth 1 -name '*.crate' -print | sort
for archive in target/package/*.crate; do tar -tzf "$archive"; done
The manual draft-release workflow copies all six .crate files into one
release assembly, adds the schema, VSIX, license bundle, release notes, and
SHA-256 checksums, then uploads that assembly as a workflow artifact.
For the 2026-08-01 local candidate, archive/source/handshake validation passed for 6/6 crates, the package Python suite passed 19/19, and the release contract confirmed 17/17 exact internal dependency edges. The largest archive was 152.9 KiB, below the scanner’s 10 MiB compressed-size limit. These are package-gate results, not a crates.io publication. The full local workspace suite separately passed 258 Rust tests with 0 failures and 7 deliberate platform/hardware ignores.
Publish procedure
Publishing is a separate, protected manual operation. Run a complete dry-run,
then publish one group at a time in the order above. Confirm each version with
cargo search or the crates.io API before continuing. Do not use --no-verify
for the real publish. Do not bump versions or publish from a dirty checkout.
After publication, install the CLI from the registry into an isolated Cargo root and generate/check a new project without a runtime-path environment override. This is the acceptance test for registry-based template resolution; workspace-path tests alone are insufficient.
Publish Rust crates
RustFerry contains six publishable crates with one workspace version. They must be published in dependency order:
rustferry-coreandrustferry;rustferry-codegen;rustferry-appleandrustferry-android;cargo-ferry.
Run package and upload dry-runs from a clean release revision:
cargo package --workspace --locked --list
cargo package --workspace --locked
cargo publish --workspace --dry-run --locked --no-verify
python3 scripts/check-release-contract.py
python3 scripts/check-release-archives.py \
--check-sources \
--target-dir target/package-source-check \
target/package/*.crate
Inspect every normalized archive, its manifest, canonical root license files, embedded templates/docs, and absence of generated output, signing material, or developer paths. The draft-release workflow assembles .crate files but never publishes them.
Real publication is manual. Wait for each prerequisite group to become visible in the crates.io index before continuing, and do not use --no-verify for the actual upload. Afterward, install cargo-ferry into an isolated Cargo root and generate/check a project using registry dependencies.
No RustFerry crate has been published as part of the current work. See package readiness for the complete manifest and archive contract.
VSIX packaging
The extension source is under editors/vscode. It uses a pinned local
@vscode/vsce dependency and never requires a globally installed packager.
Build and inspect
cargo build -p cargo-ferry
cd editors/vscode
npm ci
npm run typecheck
npm run lint
npm test
npm run perf
npm run test:host
npm run package
npm run vsix:smoke
test:host requires the real debug CLI at ../../target/debug/cargo-ferry.
Set RUSTFERRY_TEST_CLI to use another executable. perf has the same rule.
npm run package produces dist/rustferry-vscode.vsix. Release assembly
renames it to rustferry-vscode-<version>.vsix without changing its bytes.
The 2026-08-01 acceptance candidate contains 18 entries, is 44,435 bytes, and
has SHA-256 ba8cac7e8d5ec10d3c7a96082f405c3d4d5cdd64afef82bc1f50a5a3d183ce6d.
These values identify the locally inspected candidate; publication has not
occurred.
The base extension run passes 42 tests and skips 4 live-CLI tests when no CLI is supplied. With the final CLI supplied, all 46 tests pass across 12 files. The real Extension Host smoke also passed.
The production bundle targets Node 20. Source maps are generated locally for
development with sourcesContent disabled, then excluded from the VSIX.
node_modules, TypeScript sources, tests, package locks, repository workflows,
and nested VSIX files are also excluded. The smoke script verifies required
entries, rejects development files, scans text entries for common secrets and
absolute developer paths, and prints size plus SHA-256.
Extension Host smoke
npm run test:host uses pinned @vscode/test-electron and VS Code 1.100.0.
It launches two isolated Extension Hosts with disposable user and extension
directories:
- an ordinary Rust workspace must leave RustFerry inactive;
- a workspace containing
ferry.tomlmust auto-activate, register the core commands, discover exactly one project, refresh its trees, open the discovered manifest, and validate a dirty manifest buffer without changing the file on disk.
The ferry fixture uses the real cargo-ferry protocol for discovery and
validation. The smoke does not build, install, or run a mobile application and
does not require an Android SDK, emulator, simulator, or device. Host profiles
are removed after every run. The downloaded VS Code runtime is cached outside
the repository; set RUSTFERRY_VSCODE_TEST_CACHE to choose that cache.
Linux CI runs the command through xvfb-run --auto-servernum. Local Linux
reproduction uses the same wrapper:
RUSTFERRY_TEST_CLI="$PWD/../../target/debug/cargo-ferry" \
xvfb-run --auto-servernum npm run test:host
The test prints a RUSTFERRY_HOST_PERF JSON line with host activation,
discovery, tree refresh, and manifest-open observations. These are diagnostic
measurements, not performance promises.
CI and publication
The VS Code workflow runs npm ci and npm run check on Linux, macOS, and
Windows. Linux additionally builds the real cargo-ferry binary, runs the
headless Extension Host smoke and performance measurements, and enables the
protocol integration test. Only the Linux VSIX is uploaded, avoiding three
identical artifacts.
Marketplace publication is deliberately absent from push and pull-request workflows. If enabled later, it must remain a protected manual job using a Marketplace token secret after the VSIX, license, and draft-release checks are complete.
Publish to the VS Code Marketplace
Marketplace publication is a separate protected operation. Push and pull-request workflows only verify the extension and upload a VSIX workflow artifact; they do not hold a Marketplace token or publish.
Before publication:
- run all checks in VSIX packaging;
- inspect the final VSIX allowlist and secret/path scan;
- record SHA-256 and size from the exact final bytes;
- confirm extension and workspace versions match the intended release;
- obtain protected-environment approval for the Marketplace token.
Publish the already-inspected VSIX, not a rebuilt package. Then verify the public publisher, version, listing, and install the Marketplace version into an isolated VS Code profile. Compare the installed package with the approved candidate where the Marketplace tooling permits it.
Do not publish automatically from ordinary CI, expose the token to pull requests, or claim publication from a successful VSIX build. RustFerry has not been published to the Marketplace in the current work.
Create a GitHub Release
The manual Draft release workflow verifies one explicit version on the default branch, runs Rust and extension gates, packages all six crates, and assembles:
- six
.cratearchives; - the versioned VSIX;
- IDE protocol JSON Schema;
- canonical and third-party license bundle;
- changelog-derived release notes;
SHA256SUMScovering the assembly.
First run the workflow with create_draft_release disabled. Download the workflow artifact, verify every checksum and expected member, and inspect release notes. A second run at the same intended revision may create a draft only after approval through the protected release environment. Before creating it, the workflow also requires successful exact-SHA push jobs from CI, the VS Code workflow, both platform-artifact jobs, and the mdBook/Pages workflow; pull-request, skipped, stale, duplicate, or incomplete results do not satisfy that gate.
The workflow rejects a mismatched version, missing release notes, missing archive, failed gate, or existing release tag. A draft is not a publication. Publish it only after crates.io and Marketplace verification, then verify the tag, target revision, title, notes, attachments, and checksums from GitHub.
No GitHub Release was created or published in the current work. Version bumps and release publication require an explicit release decision. See the release checklist.
Release checklist
No step below implies that a release or registry publication has occurred.
Source and version
- Clean checkout on the intended protected revision; CI green.
- Git author and repository destination verified.
- One explicit version across all workspace crates and the extension.
- Changelog contains complete notes for that version.
- Documentation no longer describes the selected version as unpublished.
- No signing files, environment files, generated platform artifacts, or developer-specific paths tracked.
Licensing
- Run
python3 scripts/check-licenses.py --generate; inspect the inventory diff instead of accepting it mechanically. - Run
python3 scripts/check-licenses.pywith no stale inventory. - Confirm RustFerry root licenses and the release license bundle are present.
- Record the Slint license path for every generated mobile binary considered for attachment. Do not attach such binaries when the choice is unresolved.
- Recheck third-party notices if the VSIX gains any production npm import.
Rust and packages
-
cargo fmt --all -- --check -
cargo clippy --locked --workspace --all-targets --all-features -- -D warnings -
cargo test --locked --workspace --all-features -
RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace --all-features --no-deps -
python3 scripts/check-release-contract.pyreports every internal edge exact. -
cargo package --workspace --locked --list -
cargo package --workspace --locked -
cargo publish --workspace --dry-run --locked --no-verify -
python3 scripts/check-release-archives.py --check-sources --target-dir target/package-source-check target/package/*.crate - Inspect all six
.cratearchives and their normalized manifests; confirm both canonical license files are regular root members.
VS Code extension
-
npm cifromeditors/vscode. -
npm run checkpasses, including VSIX structural smoke. -
npm run test:hostpasses with the intended realcargo-ferrybinary. - Ordinary Rust stays inactive; a
ferry.tomlworkspace auto-activates, registers commands, discovers the project, and opens its manifest. -
npm run perfresults reviewed and recorded with host and revision context. - Views and trust gating checked manually when their behavior changed.
- VSIX size and SHA-256 recorded from the final bytes.
Platform evidence
- Android and iOS Simulator artifact workflows green for the release commit.
- Exact artifact validation recorded; no simulator/device/signing claim beyond observed evidence.
- Physical-device binaries excluded unless signing, installation, launch, and required license notices were actually validated.
Draft release
- Run the
Draft releaseworkflow with the exact workspace version andcreate_draft_releasedisabled. - Download the release assembly. Verify package count, schema, versioned VSIX,
license bundle, notes, and every entry in
SHA256SUMS. - Configure required reviewers on the GitHub
releaseenvironment. - Rerun the same revision with
create_draft_releaseenabled. - Confirm the workflow accepted successful exact-SHA
pushjobs for CI, VS Code, both platform artifacts, and mdBook/Pages. - Inspect the draft body and attachments before any publish operation.
Registry and Marketplace publication
- Publish crates manually in the order documented in Rust package readiness, waiting for registry visibility between dependency groups.
- Verify each crate version, tarball, checksum, dependency metadata, docs.rs build, and registry timestamp.
- Install
cargo-ferryfrom crates.io into an isolated Cargo root; generate and check a project without a runtime-path override. - Publish the already-inspected VSIX to Marketplace only through a protected manual environment with the required secret.
- Verify Marketplace version and install the public extension into an isolated VS Code profile.
- Publish the GitHub draft only after registry and Marketplace verification.
- Create the next patch
Unreleasedchangelog section and commit release closeout.
Third-party licenses
This inventory separates what RustFerry distributes from what its development tooling or generated applications resolve. It is release-engineering context, not legal advice.
Distribution surfaces
| Surface | Third-party material | Release treatment |
|---|---|---|
| Rust source crates | Dependencies resolved by the root Cargo.lock | Exact names, versions, sources, and SPDX expressions are in LICENSES/cargo-dependencies.json. Root source remains MIT OR Apache-2.0. |
cargo-ferry binary | Linked Rust dependencies from the same lock | Ship the root licenses and the Cargo inventory with binary releases. Preserve any upstream notices required by the selected license branch. |
| VS Code extension | VS Code API plus Node built-ins; no third-party runtime import in the current production bundle | VS Code is external. Packaging excludes node_modules and source maps; the VSIX smoke test enforces that boundary. |
| VS Code build toolchain | Packages in editors/vscode/package-lock.json | Development-only inventory in LICENSES/vscode-development-dependencies.json. It includes @vscode/vsce-sign, whose Microsoft terms restrict it to use with Visual Studio products and services. It is not bundled in the VSIX. |
| Generated Java and Swift glue | RustFerry-authored templates | Covered by RustFerry’s MIT OR Apache-2.0 license; no copied platform SDK source is stored in the repository. |
| Icons and splash assets | Original RustFerry development artwork | No external image or font input. Generated derivatives retain the project license. No font files are bundled. |
| Generated application UI | Slint 1.17.1 and its resolved application dependency graph | The application distributor must choose and satisfy a Slint license and audit the generated application’s own lockfile. RustFerry release packages do not redistribute Slint itself. |
The current root lock includes option-ext under MPL-2.0 and ICU4X-related
packages under Unicode-3.0. r-efi declares an OR expression that offers MIT
or Apache-2.0 in addition to LGPL-2.1-or-later; accepting the expression in CI
does not itself select a branch. Any future linked CLI binary distribution must
record its selected branches and carry the corresponding notices. The current
draft workflow ships source .crate packages, not a linked CLI binary.
Slint 1.17.1
Slint 1.17.1 offers three framework license paths:
- The Slint Royalty-free License 2.0 covers proprietary or open desktop,
mobile, and web applications at no cost when its attribution condition is
met. The license permits either an accessible
AboutSlintwidget or the Slint attribution badge on a readily discoverable public application page. It excludes embedded systems, standalone Slint distribution, and applications exposing Slint APIs. - GPL-3.0-only covers open-source applications under GPL-compatible terms and carries GPL source and redistribution obligations for the distributed work.
- A commercial Slint license covers use cases outside the two paths above, including proprietary embedded applications or applications without the royalty-free attribution.
Generated RustFerry starters keep AboutSlint visible. That is useful for the
royalty-free path, but it does not make a license selection for the application
author. The pinned royalty-free text is copied in
LICENSES/SLINT-ROYALTY-FREE-2.0.md; the canonical Slint 1.17.1 overview and
license texts remain at:
- https://github.com/slint-ui/slint/blob/v1.17.1/LICENSE.md
- https://github.com/slint-ui/slint/tree/v1.17.1/LICENSES
Do not attach a generated APK, .app, or .appex containing Slint to a public
release until that artifact’s license path and notices are recorded.
Dependency policy
scripts/check-licenses.py enforces reviewed Cargo and npm license-expression
sets and deterministic machine inventories. The npm lock currently describes
build/test/package tools only. If extension production code gains a third-party
runtime import, add that package’s notice to the VSIX and update this document
before release.
After any lockfile change:
python3 scripts/check-licenses.py --generate
git diff -- LICENSES/
python3 scripts/check-licenses.py
Review changed licenses and notices; never accept a newly observed expression only to make CI green.
Android setup
RustFerry’s Android backend calls the SDK, NDK, Rust, and JDK tools directly. It does not require Android Studio, Gradle, an emulator, adb, or a connected device for build.
Required tools
- Rust, Cargo, and rustup.
- One Rust Android target for every configured ABI. The starter uses
aarch64-linux-androidforarm64-v8a. - Android SDK platform matching
android.target_sdk, or any installed platform when it isinstalled. - A complete SDK Build Tools revision containing
aapt2,d8,zipalign, andapksigner. - Android NDK with an LLVM prebuilt for the host.
- A JDK containing
java,javac, andkeytool.
Typical setup for the starter:
rustup target add aarch64-linux-android
sdkmanager "platforms;android-35" "build-tools;35.0.0" "ndk;29.0.14206865"
cargo ferry doctor
Accept SDK licenses deliberately with the Android SDK tooling. cargo ferry doctor is read-only and never accepts licenses or installs components.
Discovery order
The SDK root is resolved from an explicit CLI/build request, then ANDROID_SDK_ROOT or ANDROID_HOME, then the conventional host location:
- macOS:
~/Library/Android/sdk - Linux:
~/Android/Sdk - Windows:
%USERPROFILE%\AppData\Local\Android\Sdk
The NDK is resolved from an explicit path, ANDROID_NDK_HOME or ANDROID_NDK_ROOT, versioned directories below <sdk>/ndk/, then <sdk>/ndk-bundle. Installed platforms, Build Tools, and NDKs are sorted numerically; the newest complete compatible installation is selected. JAVA_HOME/bin takes precedence over PATH for JDK tools.
Doctor scope
The Android report separates build requirements from optional deployment tools. Missing adb or emulator tooling is a warning because APK creation does not use either. Missing SDK platform, Build Tools, NDK/linker, Cargo, Rust target, Java runtime, or keytool blocks a build and includes the searched paths and a concrete install command.
References: Android command-line build tools, Rust platform support, and Slint Android setup.
Direct Android build
cargo ferry build android
The command builds only. It does not discover devices, start an emulator, install the APK, launch the application, or stream logs.
Pipeline
- Validate
ferry.tomland discover a compatible SDK platform, complete Build Tools, NDK LLVM prebuilt, and JDK. - Generate a deterministic
AndroidManifest.xml,res/tree, and private Java runtime bridge undertarget/ferry/. - Run Cargo once per configured ABI with a target-specific NDK Clang linker and
--message-format=json-render-diagnostics. - Parse Cargo
compiler-artifactmessages for thecdylib. Parsebuild-script-executedmessages and recursively collect dependency.dexfiles from thoseOUT_DIRs. - Compile the generated
FerryActivity, capability bridge, notification receiver, widget provider, and read-only share provider directly withjavacagainst the selectedandroid.jar. - Compile and link resources with
aapt2. - Merge compiled bridge classes and dependency bytecode with
d8. - Add stored
lib/<abi>/lib<name>.soandclasses*.dexentries to the resource APK. - Run
zipalign -P 16 -f 4before signing. - Sign with
apksigner, then verify the signature and runzipalign -c -P 16 4. - Independently inspect the ZIP: reject unsafe/duplicate names; require manifest, resources, icon, sequential DEX files, exact ABI libraries, and matching ELF class/machine headers. Enabled manifest components must have class definitions in merged DEX.
aapt2 dump badgingmust report the configured package andorg.rustferry.bridge.FerryActivitylauncher.
All external processes receive an executable plus an argument array. Paths with spaces or Unicode are not joined into a shell command. Each stage has a bounded runtime and a log below target/ferry/android/<profile>/logs/.
Output
The debug artifact is:
target/ferry/android/debug/<native-library-name>.apk
Intermediates and generated platform glue remain under target/ferry/; no Gradle project, Java/Kotlin source, or Android Studio project is written into user source.
Runtime bridge
FerryActivity is a generated subclass of Android’s NativeActivity, so the Rust entry point remains android_main(AndroidApp). The generated starter installs rustferry::android before Slint. Typed RustFerry calls cross one private JSON/JNI method; Java performs Android framework and main-thread work, while Rust owns typed results and application events.
Capability flags are baked into the bridge. rustferry::supports is true only when the corresponding configuration enables a concrete Android implementation. Persistent ordinary storage uses FileStorage below the application’s internal data directory. rustferry::android::with_context is an advanced synchronous escape hatch; raw JNI references must not outlive its callback.
Android emits foreground, background, resume, pause, low-memory, theme, window-size, network, deep-link, and notification-open events when the platform provides them. It deliberately does not translate Activity.onDestroy into Terminating: configuration changes also destroy activities, while process termination may deliver no callback. Persist important state eagerly.
Current Android widget rendering supports title/value/caption text plus one text, link, or button content node. Other widget content, image, or progress shapes return a backend error instead of reporting false success. File sharing accepts files inside application-owned files/cache directories through the generated read-only content provider. Android Live Activities use the configured ongoing-notification fallback.
ABIs
ferry.toml ABI | Rust target | APK directory |
|---|---|---|
arm64-v8a | aarch64-linux-android | lib/arm64-v8a/ |
x86_64 | x86_64-linux-android | lib/x86_64/ |
armeabi-v7a | armv7-linux-androideabi | lib/armeabi-v7a/ |
Each configured Rust target must already be installed. Cargo’s own target directory provides incremental Rust compilation. Generated resource and D8 intermediates use content fingerprints plus output-digest completion markers. Interrupted or modified intermediates are rebuilt instead of counted as cache hits, and builds sharing one profile output are serialized with a lock.
Dry run
cargo ferry build android --dry-run performs discovery and request validation, then returns an ordered plan without writing generated files, creating a keystore, or launching commands. Commands are represented as redacted argument arrays. The D8 step explicitly records that its inputs are deferred until Cargo JSON is parsed.
The custom-tool order follows Android’s documentation for AAPT2, zipalign, and apksigner.
Recorded artifact evidence
Current RustFerry evidence comes from Platform artifacts run 30719811812 at commit 8ed0192. The public CLI generated and built default Starter and Kitchen Sink projects for arm64. Both APKs passed ZIP integrity, v2/v3 signature, 16 KiB-aware alignment, package/launcher/API, classes.dex, compiled resources and icon, AArch64 ELF, android_main, and JNI callback checks. The Kitchen Sink APK additionally passed exact permission, deep-link, notification receiver, file provider, widget provider, and Live Activity fallback inspection.
Historical pre-rename evidence remains reproducible by its exact path: on 2026-08-01, generated_minimal_project_produces_verified_apk built target/android-e2e/pocket/android/debug/android_probe.apk with the installed SDK 35, Build Tools, NDK 29, Java 21, and aarch64-linux-android target. APK signature verification, 16 KiB-aware alignment, package com.example.androidprobe, singleTop launcher, ZIP integrity, DEX classes, the arm64-v8a ELF header, android_main, and the JNI callback passed inspection. Signed-binary XML inspection also proved the exact configured permission set, Activity/FileProvider/NotificationReceiver/WidgetProvider components, and scheme=probe;host=open.example;pathPrefix=/details deep-link filter. A repeated build reported cache hits for AAPT2 compile/link and D8. The pre-rename public CLI also completed new then build for fresh Starter and Kitchen Sink projects; exact legacy paths and scope are recorded in the historical status record.
No emulator or device behavior was observed in either the current or historical validation.
Android signing
Every successful APK is signed and then verified. Alignment happens before apksigner; no archive mutation occurs after signing.
Persistent debug identity
The first debug build creates a machine-local PKCS#12 key and a random password file under cargo-ferry’s operating-system configuration directory:
- macOS:
~/Library/Application Support/cargo-ferry/android/ - Linux:
~/.config/cargo-ferry/android/ - Windows: the user’s roaming application-data config directory under
cargo-ferry\android\
The keystore is reused for later builds, allowing debug upgrades instead of producing a new signing identity every time. Creation is protected by a file lock for concurrent first builds. On Unix, the directory is mode 0700 and the keystore/password files are mode 0600.
RustFerry validates an existing key with keytool -list, checks the certificate expiry date, and refuses to overwrite it. An expired certificate fails before APK signing. Move the keystore and password file aside together to let the next build create a fresh identity. If the keystore exists but its password file is missing, restore the password file or move the old pair aside deliberately.
Password handling
Passwords never belong in ferry.toml, the project repository, normal output, a rendered dry-run plan, or an inline pass:<value> process argument. Signing accepts only:
file:/absolute/pathpassword sources; orenv:VARIABLE_NAMEreferences for an already exported exact variable.
Debug key creation passes the password file through keytool’s -storepass:file form. apksigner receives a password-file or environment reference, not the value. Full tool output is saved to the generated build log directory; command lines there are redacted.
Release keys
Release signing uses an explicit keystore, key alias, store-password source, and optional distinct key-password source. RustFerry checks that the keystore/password files exist before executing the build. It never copies release keys into target/ferry/ or user source.
Back up a release key and its credentials separately. Losing the signing identity prevents upgrades to an application distributed under that identity.
Android manifest permissions
Manifest declarations come only from enabled capabilities and permission settings. Purpose text is validated in ferry.toml for cross-platform UX but is not embedded in Android’s manifest; Android rationale UI remains application code.
| Configuration | Android manifest output |
|---|---|
network none | no network permission |
network status | ACCESS_NETWORK_STATE |
network optional or required | ACCESS_NETWORK_STATE, INTERNET |
| configured network probe | INTERNET |
| local notifications | POST_NOTIFICATIONS |
| Live Activity ongoing-notification fallback | POST_NOTIFICATIONS |
| haptics | VIBRATE |
| camera | CAMERA |
| microphone | RECORD_AUDIO |
| location when in use | ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION |
| photos, target API 33+ | READ_MEDIA_IMAGES |
| photos, minimum API below 33 | READ_EXTERNAL_STORAGE capped with android:maxSdkVersion="32" |
| local network | no Android declaration; Apple-only privacy permission |
| widget extension | widget receiver and provider metadata |
| sharing | non-exported, read-only application file provider |
| custom deep-link schemes | VIEW/BROWSABLE intent filter on the launcher activity |
Deep-link filters are the Cartesian product of configured schemes, allowed_hosts, and
allowed_actions; actions become path prefixes. The generated bridge repeats the same checks for
cold-start and running-app intents, and rejects undeclared scheme/host/action values before Rust
receives an event. An empty host or action list leaves that dimension unrestricted.
The notification permission is declared only for local notifications or the enabled ongoing-notification fallback. The runtime requests it only in response to an explicit application call on Android versions where a runtime request exists. On older versions it also checks whether the user disabled this application’s notifications in system settings.
Notification, widget, and share components are omitted when their capabilities are disabled. Every generated activity, receiver, or provider is checked against the merged DEX before packaging completes.
Review generated output at:
target/ferry/android/<profile>/generated/<fingerprint>/AndroidManifest.xml
The final binary manifest and package metadata are checked again from the signed APK.
Android troubleshooting
Android SDK or platform not found
Run cargo ferry doctor and inspect every searched SDK root. Set ANDROID_SDK_ROOT to the SDK directory or install the requested platform with sdkmanager "platforms;android-<api>".
Complete Build Tools not found
One revision must contain all four tools: aapt2, d8, zipalign, and apksigner. An incomplete newer directory is skipped in favor of the newest complete revision. Reinstall with sdkmanager "build-tools;<version>".
NDK linker not found
The error names the Rust target and exact expected Clang driver below toolchains/llvm/prebuilt/<host>/bin/. Install a complete NDK, check ANDROID_NDK_HOME, and rerun cargo ferry doctor.
Rust target missing
Install the target corresponding to each configured ABI, for example:
rustup target add aarch64-linux-android
Generated bridge does not compile
The runtime bridge is mandatory and DexPolicy::None is rejected. A javac failure points to the full generated source and log below target/ferry/android/<profile>/. Confirm that JAVA_HOME selects a complete JDK and that the selected SDK platform contains android.jar.
APK validation reports a missing component class
RustFerry checks FerryActivity and every enabled receiver/provider in the merged DEX. Do not disable hasCode or remove the component. Inspect javac.log and d8.log; stale or incomplete content-addressed outputs are rebuilt automatically.
D8 reports duplicate classes
Identical DEX files emitted once per ABI are content-deduplicated automatically. A remaining duplicate usually means two different bridge dependencies define the same class. Remove one bridge input or align dependency versions; do not delete arbitrary classes from a DEX archive.
Debug keystore validation failed
The persistent key or password file may be damaged or mismatched. The error includes the machine-local path and keytool log. Restore the pair from backup. RustFerry will not overwrite an existing signing identity automatically.
Signature succeeds but alignment fails
The final verification is zipalign -c -P 16 4. Native libraries are injected uncompressed before alignment and signing. Inspect the logged zipalign and apksigner versions; no tool may modify the APK after signing.
APK validation rejects an ABI
RustFerry checks both the lib/<abi>/ path and ELF header. This catches a library copied into the wrong ABI directory. Verify android.abis, installed Rust targets, and the target-specific NDK linker environment in the Cargo log.
Device or adb missing
This does not affect cargo ferry build android, which stops after artifact validation and never discovers or mutates a device. Device inventory, install, and launch are separate explicit commands:
cargo ferry devices --platform android
cargo ferry install android --device SERIAL
cargo ferry run android --device SERIAL
The device selector may be omitted only when exactly one compatible target exists. These ADB deployment paths are implemented and covered by host-side tests, but this repository has no emulator or physical-device runtime validation; successful APK inspection alone does not prove install or launch behavior.
iOS setup
iOS builds require macOS, full Xcode, and the Apple Silicon Simulator Rust target. An Apple Account, provisioning profile, connected iPhone, booted Simulator, and installed Simulator runtime are not required for build-only use.
Install Xcode, open it once so its components finish installing, then select it if necessary:
xcode-select -p
xcodebuild -version
xcrun --sdk iphonesimulator --show-sdk-path
rustup target add aarch64-apple-ios-sim
cargo ferry doctor
cargo ferry doctor is read-only. It reports:
- selected
DEVELOPER_DIR/Xcode; xcodebuild,xcrun, andplutil;- iPhone Simulator SDK path and version;
- Cargo, rustup, and
aarch64-apple-ios-sim; - installed CoreSimulator runtimes as an optional run-time check.
A missing runtime is a warning, not a build failure. Install a runtime from Xcode Settings > Platforms only when install or run support needs one.
Xcode selection
Discovery honors an explicit DEVELOPER_DIR, then xcode-select -p. It rejects Command Line Tools-only developer directories because they do not contain iPhoneSimulator.platform.
The tool never invokes sudo, changes the selected Xcode, accepts licenses, installs SDKs, or downloads executables. Apply any system-level fix yourself, then rerun cargo ferry doctor.
Security boundary
All generated Apple files remain under the application’s target/ferry/ios/ directory. Generated writes reject absolute paths, parent traversal, and symlinked output components. External programs receive argument arrays; project/config values are not interpolated into a shell command.
Build an iOS Simulator application
From a generated Rust project on macOS:
cargo ferry build ios --simulator
The build produces:
target/ferry/ios/debug/<binary>.app
Use --release for target/ferry/ios/release/<binary>.app. Building neither boots nor installs a Simulator.
Pipeline
- Validate
ferry.toml, Cargo selectors, versions, identifiers, paths, permissions, and extension settings. - Discover full Xcode and the
iphonesimulatorSDK throughxcrun. - Confirm
aarch64-apple-ios-simis installed. - Select
CompiledCatalogwhen discovery finds an available iOS runtime; otherwise select the explicitSdkOnlyResourcesfallback. Render the corresponding deterministicFerryHost.xcodeproj, scheme,Info.plist, resources, entitlements, and enabled extension sources belowtarget/ferry/ios/generated/. - Run Cargo for
aarch64-apple-ios-simwith an isolatedtarget/ferry/ios/cargo/target directory. - Stage Cargo’s executable under its validated binary name for Xcode’s argument-free Copy Files phase.
- Run
xcodebuild -target FerryApp -sdk iphonesimulatorwith deterministic ad-hoc signing. Target-based SDK selection avoids booting or selecting a Simulator device. RustFerry requires an available runtime before selecting catalog compilation; the SDK-only fallback avoids that tool step. - Xcode creates the bundle, processes
Info.plist, compiles or copies the selected asset representation, builds and signs nested dependencies, installs the prebuilt Rust executable at$TARGET_BUILD_DIR/$EXECUTABLE_PATH, and signs the application last. - When WidgetKit is enabled, ad-hoc re-sign the widget with
Widget.entitlements, then the application withApp.entitlements. Non-widget builds skip this step; signing never uses--deep. - Independently inspect the app and fail unless every invariant matches.
This follows Slint’s iOS architecture: the application is a Rust executable built for the simulator, using Slint’s Winit backend and Skia renderer, placed at Xcode’s expected executable path. The generated Xcode project contains packaging metadata and minimal platform extension code, not user business logic.
Asset packaging is explicit in the build plan and validation report:
CompiledCataloggeneratesAssets.xcassets, configuresAppIconandFerryLaunch, and requiresAssets.carplus the matching processed plist values. This mode is selected only when an available iOS Simulator runtime is reported.SdkOnlyResourcespreserves SDK-only builds on hosts with no runtime. It copiesFerryIcon.pngandFerrySplash.png, records their plist references, byte-compares them with the validated project inputs, and rejects a strayAssets.car.
The two modes use separate Xcode intermediate directories, so switching runtime availability cannot reuse stale catalog output.
Validation evidence
A successful result records and checks:
.appis a real directory, not a symlink;Info.plistpassesplutil -lint;- exact
CFBundleIdentifier,CFBundleExecutable, andCFBundlePackageType=APPL; - executable exists, is non-empty, and has executable permission bits;
xcrun lipo -archsreturns exactlyarm64;- the pre-sign embedded executable matches Cargo’s output byte-for-byte and the signed executable retains its Mach-O UUID;
FerryResources.jsonexists;- asset evidence matches the selected mode: either
Assets.carwithAppIcon/FerryLaunch, or exactFerryIcon.png/FerrySplash.pngbytes with the SDK-only plist keys; - each top-level framework/library is inspected;
- expected
.appexcount, identifiers, executable names, extension point, and architectures match. - app, runtime framework, and each
.appexhave sealed plists/resources, exact signature identifiers, and strict-valid ad-hoc signatures; - the application passes
codesign --verify --deep --strict; - application and widget signatures contain exactly the configured application group when enabled.
Command logs live below target/ferry/ios/logs/<profile>/. Logged argv/environment values use the plan’s redaction metadata.
Dry run
The Apple build API exposes a stable schema-versioned plan containing generated paths, Cargo/Xcode argument arrays, environment overrides, the selected asset packaging mode, the executable staging copy, and expected artifact path. A dry run returns this plan with no artifact or validation claim and performs no generation/build after read-only discovery.
Developer Experience 0.2 asset evidence
On the current Xcode 26.6 host, simctl reports no installed Simulator runtimes. The real smoke build therefore selected SdkOnlyResources and produced an arm64 .app. Independent inspection confirmed exact icon/splash bytes, bundle metadata, the Cargo Mach-O UUID, the generated runtime framework, and strict/deep ad-hoc signatures. The CompiledCatalog project/catalog/cache path is implemented and tested, but a complete Assets.car application artifact was not produced in this environment.
Verified environment
At commit 8ed0192, Platform artifacts run 30719811812 built and validated RustFerry-named arm64 Starter and Kitchen Sink .app bundles. FerryRuntimeBridge.framework, its required exports and application hook, WidgetKit and ActivityKit .appex products, Activity framework linkage, signatures, sealed resources, and application-group entitlements passed inspection. Before the rename, the equivalent pipeline also built and validated arm64 base, Slint 1.17.1, and extension-bearing .app artifacts with Xcode 26.6 and the iPhoneSimulator 26.5 SDK on 2026-08-01. Neither validation used a Simulator runtime or device, so install, launch, callbacks, UI, and other runtime interaction remain unvalidated.
iOS signing
iOS Simulator builds use Xcode’s local ad-hoc identity (-). Xcode signs the framework and extensions before sealing the application. When WidgetKit is enabled, the pipeline then re-signs only the widget and containing application, inside-out and without --deep, so their generated application-group entitlements are embedded in the effective signatures. Artifact validation requires exact signature identifiers, sealed plists/resources, strict verification for every bundle, and recursive strict verification for the application. No Apple Account, team, certificate, or provisioning profile is needed.
Physical-device signing is an implemented official Xcode development pipeline with an explicit user-selected Team. Provisioning updates are disabled unless requested, and manual signing can name a profile. After Xcode builds, RustFerry checks the expected executable and Cargo provenance, arm64 architecture, signatures, signing certificate, embedded profiles, expiration, Team and bundle identifiers, entitlement authorization, and embedded extensions before returning a validated artifact.
The implementation and deterministic signing tests do not establish a real signing or device result. This repository had no Apple Development identity, Team, provisioning profile, signed physical artifact, or attached device available, so the path is neither artifact-validated nor device-validated. Widget application groups and other entitlements can require additional profile capabilities. No signing identity, private key, password, profile contents, or account token is stored in ferry.toml or generated logs. See Physical iPhone development and STATUS for the current evidence level.
Physical iPhone development
RustFerry implements an official development pipeline for physical iPhone and iPad targets. It cross-compiles the Rust executable for aarch64-apple-ios, generates the hidden Xcode host below target/ferry/ios-device/, asks Xcode to development-sign it for an explicit Team, then independently checks the app, embedded extensions, signatures, profiles, entitlements, Team ID, bundle IDs, and arm64 architecture.
List usable identities:
cargo ferry signing teams
Build without changing provisioning assets:
cargo ferry build ios --device --team ABCDE12345
Permit Xcode account/profile updates only when intended:
cargo ferry build ios --device --team ABCDE12345 --allow-provisioning-updates
Manual signing accepts --provisioning-profile NAME_OR_UUID. No password, private key, profile contents, or account token belongs in ferry.toml or CLI output.
Install and run use an exact CoreDevice identifier from cargo ferry devices --platform ios:
cargo ferry install ios --device DEVICE_ID --team ABCDE12345
cargo ferry run ios --device DEVICE_ID --team ABCDE12345
An unsigned or ad-hoc Simulator bundle is never accepted for a physical device. Provisioning mutation is off by default, no device is needed for build, and no signing bypass exists.
Implementation and deterministic signing-plan tests are complete, but this repository has not produced, installed, or launched a physical-device artifact in the current environment: no Apple Development identity, Team, profile, or attached device was available. See STATUS for the evidence level.
WidgetKit and ActivityKit extensions
Enabled Apple extensions are generated as separate Swift/Xcode application-extension targets, built as target dependencies, embedded under <app>.app/PlugIns/, and independently validated. Users do not create or edit Swift or Xcode files.
WidgetKit
Configuration requires an application group:
[extensions.widget]
enabled = true
app_group = "group.com.example.weather"
Generation adds:
FerryWidgetExtensionXcode target;FerryWidgetExtension.appexproduct;- WidgetKit/SwiftUI timeline provider and a small-system-family view;
- main-app and extension app-group entitlements;
- extension identifier
<app identifier>.widget; - embed-target dependency and
Embed App Extensionsphase.
The Rust widgets::update path validates and writes the serialized snapshot plus title, value, caption, progress, deep link, and constrained action data to the configured app-group UserDefaults suite, then requests a WidgetKit timeline reload. The generated provider reads that snapshot and renders the supported fields. The current Ferry* publisher, framework, provider, and embedded extension compiled and passed artifact inspection in Platform artifacts run 30719811812, including exact application-group entitlements. Their behavior has not been observed in a running Simulator.
ActivityKit and Dynamic Island
[ios]
min_version = "16.1"
[extensions.live_activity]
enabled = true
android_fallback = "ongoing-notification"
Generation adds:
FerryLiveActivityExtensionXcode target;FerryLiveActivityExtension.appexproduct;ActivityAttributescontent state;- Lock Screen and expanded/compact/minimal Dynamic Island presentations;
- main-app
NSSupportsLiveActivitiesmetadata; - extension identifier
<app identifier>.liveactivity; - embed-target dependency and
Embed App Extensionsphase.
The Rust start, update, end, and list_active paths call the generated ActivityKit application bridge. The current Ferry* main-app framework and presentation extension compiled, linked, embedded, and passed artifact inspection in Platform artifacts run 30719811812. No ActivityKit session has been started in a running Simulator or device, so this is not runtime validation. Push-based updates remain unavailable.
Artifact validation
For each enabled extension, the build requires:
- expected
.appexbeneathPlugIns/; - valid plist and exact bundle identifier;
CFBundleExecutablematching a non-empty executable;NSExtensionPointIdentifier=com.apple.widgetkit-extension;- exact arm64 Simulator Mach-O architecture;
- sealed plist/resources, exact signature identifier, and strict-valid ad-hoc signature;
- exact configured application-group entitlement on the widget signature;
- no unexpected extra
.appexbundles.
At commit 8ed0192, Platform artifacts run 30719811812 built a RustFerry-named Kitchen Sink app embedding FerryWidgetExtension.appex and FerryLiveActivityExtension.appex. Both arm64 products passed identifier, plist, extension-point, resource-sealing, and strict ad-hoc signature checks; the ActivityKit product also passed exact runtime-framework linkage inspection, and the widget and containing app carried the exact configured application-group entitlement. Before the rename, the equivalent legacy-named targets, standalone .appex products, and combined app were also built and validated with Xcode 26.6/iPhoneSimulator 26.5 without a Simulator runtime.
Physical-device signing status
Simulator builds use local ad-hoc signing and require no team. Widget builds re-sign the widget and then the containing app with their generated application-group entitlements; non-widget builds retain Xcode’s signatures unchanged.
The physical-development flow is implemented with explicit Team selection, Apple Development identity/profile resolution, generated entitlements, recursive signature/profile/entitlement inspection, and devicectl install/launch services. Extension-bearing device builds must preserve the configured application-group capability in the app and widget profiles. This environment had no identity, Team, provisioning profile, signed device artifact, or attached iPhone, so physical signing, installation, launch, and extension behavior remain unvalidated.
Apple implementation status
Last validated: 2026-08-01
Current RustFerry-named Apple artifact evidence comes from Platform artifacts run 30719811812 at commit 8ed0192. Exact legacy paths, identifiers, symbols, and bundle names remain below as a separate historical record.
| Area | Status | Evidence |
|---|---|---|
| Xcode/xcrun/SDK discovery | Implemented and host-tested | Xcode 26.6, iPhoneSimulator 26.5 |
| Rust target discovery | Implemented and host-tested | aarch64-apple-ios-sim |
| Doctor | Implemented | Build prerequisites separate from optional runtime availability |
| Deterministic Xcode/plist/assets | Implemented; both asset modes host-tested | Compiled-catalog project tests plus a real SDK-only Xcode build |
Starter Simulator .app | Current artifact validation | Public-CLI arm64 .app in Platform run 30719811812, plus the Developer Experience 0.2 SDK-only smoke app |
Kitchen Sink Simulator .app | Current artifact validation | Public-CLI arm64 .app with two embedded extensions in the same run |
| Runtime bridge | Implemented; current target compile/artifact inspection | arm64 FerryRuntimeBridge.framework; required exports/application hook and strict ad-hoc signature validated |
| WidgetKit | Publisher and renderer implemented; current compile/embed/artifact inspection | Runtime app-group writer plus signed PlugIns/FerryWidgetExtension.appex |
| ActivityKit | Start/update/end/list and presentation implemented; current compile/embed/artifact inspection | Runtime framework plus signed PlugIns/FerryLiveActivityExtension.appex |
| Simulator install/launch/UI | CLI implemented; runtime unvalidated | Typed simctl install/launch services exist; no CoreSimulator runtime/device is installed |
| Physical-device signing/install | Implemented; signing and device unvalidated | Side-effect-free official-tool planning passes without Xcode; no identity, Team, profile, signed artifact, or device was available |
Platform artifacts run 30719811812 generated default Starter and Kitchen Sink projects with the public CLI. Both RustFerry-named application bundles have arm64 executables and passed plist/resource inspection plus deep/strict ad-hoc signature verification. The run also checked FerryRuntimeBridge.framework, its required exports and application hook, both embedded .appex products, exact identifiers and framework linkage, and the exact group.org.rustferry.ciextensions application-group entitlement on the Kitchen Sink app and widget.
No application was installed or launched; these checks do not establish Simulator or device runtime behavior.
Developer Experience 0.2 selects one of two explicit asset modes. With an available iOS Simulator runtime, CompiledCatalog emits Assets.xcassets, selects AppIcon and FerryLaunch, and requires a compiled Assets.car during artifact validation. That path is implemented and covered by deterministic project, catalog, cache, and plist tests, but it was not artifact-validated on this host because simctl reports no installed runtimes. Without a runtime, SdkOnlyResources emits FerryIcon.png and FerrySplash.png; a real Xcode 26.6 build produced a signed arm64 .app, and inspection verified the exact source bytes, plist references, Mach-O identity, resources, and strict/deep ad-hoc signature. The SDK-only report does not claim an Assets.car.
Historical pre-rename evidence
The historical combined extension artifact is:
target/final-acceptance-kitchen/target/pocket/ios/debug/final-acceptance-kitchen.app
Its app executable and both embedded extension executables are arm64. Validated identifiers:
org.cargopocket.kitchensink
org.cargopocket.kitchensink.widget
org.cargopocket.kitchensink.liveactivity
org.cargo-pocket.runtime-bridge
The pre-rename public CLI’s schema-5 artifact report accepted both application bundles only after exact identifier, arm64 Mach-O, sealed Info.plist and resources, ad-hoc signature, and strict/deep signature checks passed. The Kitchen Sink app and widget signatures contain the exact group.org.cargopocket.kitchensink application-group entitlement. Both extensions report com.apple.widgetkit-extension, have strict ad-hoc signatures with their exact identifiers, and are present beneath the app’s PlugIns/ directory.
The Activity extension links @rpath/PocketRuntimeBridge.framework/PocketRuntimeBridge; the strictly verified, ad-hoc-signed framework exports _pocket_bridge_call, _pocket_bridge_free, _pocket_bridge_init, _pocket_bridge_install, and _pocket_bridge_with_application. Artifact validation also required the PocketApplicationDelegate and exact application-initializer hook markers.
Runtime limitations
- Widget publication/reload and ActivityKit start/update/end/list are implemented and compiled, but none was invoked in a running Simulator application.
- Simulator install, run, and log commands are implemented, but this host has no installed runtime/device. No lifecycle callback, deep-link open, notification UI/action, permission prompt, widget timeline, or Live Activity session was observed.
- Asset-catalog compilation is selected only when discovery finds an available iOS runtime. The runtime-free mode keeps SDK-only builds working with exact, sealed PNG resources and reports that fallback explicitly.
- Physical development signing, install, and launch paths are implemented but were not exercised without an Apple Development identity, Team, provisioning profile, signed device artifact, or attached iPhone.
These limitations do not weaken the current or historical compile/link/artifact evidence, but they prevent any simulator-runtime or device-validation claim.
iOS troubleshooting
Full Xcode was not found
/Library/Developer/CommandLineTools is not enough. Install Xcode, then verify:
xcode-select -p
xcodebuild -version
xcrun --sdk iphonesimulator --show-sdk-path
If several Xcode versions are installed, set DEVELOPER_DIR for the command or select the intended version yourself. cargo-ferry never runs sudo xcode-select.
Rust target is missing
rustup target add aarch64-apple-ios-sim
cargo ferry doctor
No Simulator runtime
This does not block cargo ferry build ios --simulator. The build uses the installed SDK without a destination. A runtime/device is required only to install or launch the result.
Cargo failed before Xcode
Inspect:
target/ferry/ios/logs/debug/01-cargo-build.log
Confirm the requested binary exists in Cargo.toml, features are spelled correctly, Slint enables backend-winit and renderer-skia, and the project compiles for aarch64-apple-ios-sim.
Xcode failed
Inspect:
target/ferry/ios/logs/debug/02-xcodebuild.log
The generated project is disposable. Do not repair it manually; fix ferry.toml or the generator and rebuild. All generated paths remain below target/ferry/ios/.
Bundle validation failed
Validation errors name the exact failed invariant: plist key/value, missing executable/resource, architecture, Cargo-binary mismatch, framework entry, extension count, extension point, or bundle identifier. A non-zero result means the .app must not be distributed as a successful artifact.
Widget shows fallback values
Call widgets::update after the iOS runtime is installed, then verify the configured app-group identifier is identical for the app and extension. The bridge writes rustferry.widget.snapshot and requests a timeline reload, but WidgetKit controls refresh timing. Fallback values mean no readable snapshot reached that app-group suite; inspect the typed Rust error before treating it as a presentation bug.
Live Activity cannot start
The ActivityKit bridge is compiled and linked. Check that Live Activity is enabled, the deployment target is iOS 16.1 or newer, the runtime was installed before the call, and live_activity::is_supported() is true. Surface the typed start error: OS availability or user settings can reject a request even when the .appex is structurally valid. Runtime behavior has not yet been observed in this project’s Simulator environment.
State and events
What it does
Store<T> persists serializable state. app_events delivers typed lifecycle, deep-link, notification, network, theme, and window events until the returned subscription is dropped.
Support matrix
| Host test runtime | Android | iOS |
|---|---|---|
| State/event model implemented and tested | File host/event bridge implemented and compiled; runtime unobserved | File host/delegate bridge and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::app_events::{self, AppEvent};
use rustferry::storage::Store;
use rustferry::testing::TestRuntime;
use std::sync::{Arc, Mutex};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let count = Store::<u32>::open("count")?;
count.save(&41)?;
count.save(&(count.load()?.unwrap_or_default() + 1))?;
let latest = Arc::new(Mutex::new(None));
let observed = Arc::clone(&latest);
let _subscription = app_events::subscribe(move |event| {
*observed.lock().unwrap() = Some(event);
});
runtime.send_event(AppEvent::Foregrounded);
assert_eq!(count.load()?, Some(42));
assert_eq!(*latest.lock().unwrap(), Some(AppEvent::Foregrounded));
Ok(())
}
Keep the subscription in application state. Dropping it ends delivery.
Configuration
[capabilities.storage]
enabled = true
Events themselves need no blanket capability; payload-producing features are generated only when enabled.
Permissions and entitlements
Storage and basic lifecycle events need no runtime prompt. Specific event sources can require notification, network, deep-link, or extension configuration.
Expected result
The count reloads as 42; the injected foreground event reaches the callback exactly while its subscription is alive.
Common errors
Unsupported(Storage): no storage backend is installed or the capability is disabled.- Lost callbacks: the
Subscriptionwas assigned to_and immediately dropped. - Missing final event: mobile operating systems do not guarantee termination delivery.
Platform differences
One source preserves serial delivery; concurrent sources may interleave. Persist important state eagerly on both platforms.
Test example
Use TestRuntime::send_event, then drop the subscription and inject another event to assert no later callback starts.
Example project
See the persistent state and event subscription in the Counter example, plus Project structure.
Lifecycle
What it does
AppEvent represents startup, foreground/background, resume/pause, low-memory, optional termination, and related platform events. use_app_events is an alias suited to UI ownership.
Support matrix
| Host model | Android delivery | iOS delivery |
|---|---|---|
| Implemented/tested | Lifecycle callbacks compiled into inspected bridge artifact; runtime unobserved | Delegate callbacks and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::app_events::{self, AppEvent};
use rustferry::testing::TestRuntime;
use std::sync::{Arc, Mutex};
fn main() {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let events = Arc::new(Mutex::new(Vec::new()));
let observed = Arc::clone(&events);
let _subscription = app_events::use_app_events(move |event| {
observed.lock().unwrap().push(event);
});
runtime.send_event(AppEvent::Started);
runtime.send_event(AppEvent::Backgrounded);
assert_eq!(events.lock().unwrap().len(), 2);
}
Configuration
No ferry.toml section is required for core lifecycle events.
Permissions and entitlements
None for lifecycle delivery. Payload-specific events may need their own capability.
Expected result
The test observes Started then Backgrounded in source order.
Common errors
- Callback never runs: the subscription was dropped.
- Assuming
Terminating: the OS may kill an application without announcing it. - Updating UI directly from an arbitrary callback thread: dispatch through the UI backend; the starter uses
slint::invoke_from_event_loop.
Platform differences
Exact native callbacks mapped to foreground/resume and background/pause differ. Treat cross-platform events as semantic states, not one-to-one native callback names.
Test example
TestRuntime::send_event(AppEvent::LowMemory) exercises cleanup logic deterministically; no sleeps are needed.
Example project
The Counter example owns a lifecycle subscription until its window closes.
Async tasks
What it does
rustferry::spawn runs an independent Send + 'static future on a worker thread and propagates the active RustFerry runtime. Slint-local futures can instead use slint::spawn_local and return UI changes through its event loop.
Support matrix
| Host | Android | iOS |
|---|---|---|
| Worker helper tested | Rust behavior available when host is linked | Rust behavior available when host is linked |
Minimal complete example
use rustferry::network::{self, NetworkStatus, NetworkTransport};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_network_status(NetworkStatus::online(NetworkTransport::Wifi));
let task = rustferry::spawn(async { network::is_online() });
assert!(task.join().expect("worker did not panic")?);
Ok(())
}
Configuration
Configure only the capability used by the task; the spawn helper itself has no section.
Permissions and entitlements
Determined by the called capability. Never use a background task to bypass a platform permission or UI-thread requirement.
Expected result
The worker sees the same scoped test runtime and returns true.
Common errors
- Borrowed data does not live long enough: move owned values into the future.
- UI handle is not
Send: useslint::spawn_localor send a result back withslint::invoke_from_event_loop. - Worker panic: inspect the
JoinHandle; do not convert it to success.
Platform differences
rustferry::spawn is a small thread-based helper, not a mobile background-execution service. OS background execution limits still apply.
Test example
Inject network/permission/probe behavior into TestRuntime, spawn the async operation, join it, and inspect recorded calls.
Example project
The Notifications example uses a Slint-local future for its permission request.
Network status
What it does
network::current, subscribe, and use_network_status expose the operating system’s path state. This does not prove internet or backend reachability; use probe separately.
Support matrix
| Host model/mock | Android path backend | iOS path backend |
|---|---|---|
| Implemented/tested, including debouncing | Enabled Connectivity backend and bridge artifact-inspected; runtime unobserved | NWPath backend and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::network::{self, NetworkState, NetworkStatus, NetworkTransport};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let monitor = network::use_network_status()?;
assert_eq!(monitor.current().state, NetworkState::Offline);
runtime.set_network_status(NetworkStatus::online(NetworkTransport::Wifi));
assert_eq!(monitor.current().state, NetworkState::Online);
Ok(())
}
Configuration
[capabilities.network]
mode = "status"
probe_timeout_ms = 3000
Permissions and entitlements
Generated Android permissions depend on mode; none should omit network permissions where possible. iOS path monitoring does not authorize local-network discovery. Check platform docs before enabling LocalNetwork.
Expected result
The monitor updates from Offline to Online; duplicate equal statuses are debounced.
Common errors
- Treating
Onlineas backend health: perform an explicit probe. - Dropping a subscription: retain it or use
NetworkMonitor. Unsupported(NetworkStatus): capability/backend is absent.
Platform differences
Transport, expensive, and constrained fields can be unknown. VPN classification and path timing differ by OS.
Test example
Use set_network_status; assert a duplicate call returns false and produces no second event.
Example project
See the live path subscription in the Network Guard example.
Require internet
What it does
network::require_online gates one operation on reported path state. network::probe independently checks an application-supplied HTTP(S) endpoint with a timeout on a worker.
Support matrix
| Host mock | Android probe | iOS probe |
|---|---|---|
| Gate/probe semantics tested | Enabled HTTP probe backend and bridge artifact-inspected; runtime unobserved | URLSession probe backend and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::network::{self, NetworkStatus, NetworkTransport};
use rustferry::testing::TestRuntime;
use std::time::Duration;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_network_status(NetworkStatus::online(NetworkTransport::Wifi));
runtime.set_probe_result(false, Some(503), Duration::from_millis(12));
network::require_online()?;
let result = rustferry::spawn(async {
network::probe("https://example.test/health", Duration::from_secs(1)).await
})
.join()
.expect("probe worker did not panic")?;
assert!(!result.reachable);
assert_eq!(result.status_code, Some(503));
Ok(())
}
Configuration
[capabilities.network]
mode = "required"
probe_url = "https://api.example.com/health"
probe_timeout_ms = 3000
The build does not probe this URL. Application code decides when to probe.
Permissions and entitlements
Android needs INTERNET for HTTP(S); status inspection can also need network-state access. Apple transport-security policy applies to insecure endpoints. Prefer HTTPS.
Expected result
Path gating succeeds while the endpoint probe reports a distinct 503 failure.
Common errors
file:or another scheme: only HTTP(S) probes are accepted.- Zero timeout: rejected before backend dispatch.
- Blocking the UI thread with
wait_until_online: use it only on a worker/test thread.
Platform differences
OS reachability and HTTP behavior remain separate everywhere. Captive portals, VPNs, DNS, and proxy policy can produce different results.
Test example
Configure set_probe_result, call probe, and inspect probe_requests() to assert URL and timeout without network traffic.
Example project
See the guarded action and independent retry in the Network Guard example, plus Network status.
Local notifications
What it does
The notification API queries/requests authorization, shows immediately, schedules, cancels, and lists pending/delivered local notifications. Permission is requested only when application code calls it.
Support matrix
| Host model/mock | Android bridge/artifact | iOS bridge/artifact |
|---|---|---|
| Full request lifecycle tested | Enabled backend/receiver artifact-inspected; runtime unobserved | UserNotifications backend and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::notifications::{self, Notification, PermissionStatus, UnixTimestamp};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_time(1_000);
runtime.set_notification_permission(
PermissionStatus::NotDetermined,
PermissionStatus::Granted,
);
let status = rustferry::spawn(async { notifications::request_permission().await })
.join()
.expect("permission worker did not panic")?;
assert_eq!(status, PermissionStatus::Granted);
let request = Notification::new("tea", "Tea", "Your timer finished")
.scheduled_at(UnixTimestamp(2_000));
notifications::schedule(request)?;
assert_eq!(notifications::pending()?.len(), 1);
Ok(())
}
Configuration
[capabilities.notifications]
local = true
push = false
Or run cargo ferry add notifications.
Permissions and entitlements
Request authorization from a user-initiated UI action. Android 13+ may require POST_NOTIFICATIONS; iOS uses UserNotifications authorization. Remote push credentials/entitlements are not part of local notification support.
Expected result
The test grants authorization and records one future request. On a validated platform backend, the OS owns actual delivery timing.
Common errors
- Scheduling without
scheduled_at: rejected. - Empty ID or empty title and body: rejected before the backend.
- Assuming exact delivery time: both operating systems may defer delivery.
push = true: schema version 1 rejects remote push.
Platform differences
Android channels are explicit and newer Android versions have a runtime permission. iOS authorization states and delivered-list semantics follow UserNotifications. Repeating minimums differ.
Test example
Use set_notification_permission, set_time, scheduled_notifications, and delivered_notifications; no OS prompt is shown.
Example project
See the complete local flow in the Notifications example.
Notification actions
What it does
Actions add stable buttons to a local notification. When the user opens a notification or chooses an action, RustFerry delivers AppEvent::NotificationOpened and the filtered on_notification_opened callback.
Support matrix
| Host event/model | Android action bridge | iOS action bridge |
|---|---|---|
| Implemented/tested | Enabled action/open receiver artifact-inspected; runtime unobserved | Action/open delegate and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::app_events;
use rustferry::notifications::{Notification, NotificationAction, NotificationId};
use rustferry::testing::TestRuntime;
use std::sync::{Arc, Mutex};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let opened = Arc::new(Mutex::new(None));
let observed = Arc::clone(&opened);
let _subscription = app_events::on_notification_opened(
move |id, action, _payload, _link| {
*observed.lock().unwrap() = Some((id, action));
},
);
let _request = Notification::new("message", "Message", "Reply?").action(
NotificationAction {
id: "reply".into(),
title: "Reply".into(),
foreground: true,
authentication_required: false,
},
);
runtime.open_notification(
NotificationId::parse("message")?,
Some("reply".into()),
None,
None,
);
assert_eq!(opened.lock().unwrap().as_ref().unwrap().1.as_deref(), Some("reply"));
Ok(())
}
Configuration
[capabilities.notifications]
local = true
push = false
Permissions and entitlements
Same authorization as local notifications. An authentication-required action asks the OS to enforce device authentication; it is not application authorization.
Expected result
The filtered callback receives notification ID message and action ID reply.
Common errors
- Empty action ID/title: rejected when dispatching the notification.
- Subscription dropped before open.
- Treating payload/action input as trusted authorization: validate routes and ownership in Rust.
Platform differences
Presentation, action count, foreground behavior, and authentication UI differ. The stable action ID is the cross-platform contract.
Test example
Call TestRuntime::open_notification with each action and assert routing without showing a notification.
Example project
See the Open action and typed callback in the Notifications example, plus Local notifications.
Remote push
What it does
Remote push transport is not implemented or stable. cargo-ferry has no public API for device-token registration, APNs or FCM receipt, background delivery, or provider-side sending. The supported notification API is limited to local notifications.
Application-owned payload types can still be designed and tested in Rust. That keeps message routing separate from a future platform transport without implying that a device can receive the message today.
Support matrix
| Boundary | Status |
|---|---|
| Application payload model and routing | Available as ordinary Rust code |
| Local notification scheduling and display | Implemented; see Local notifications |
| Apple Push Notification service (APNs) registration and receipt | Not implemented |
| Firebase Cloud Messaging (FCM) registration and receipt | Not implemented |
| Provider/server delivery | Not implemented; no built-in sender or server component |
| Live Activity remote updates | Not implemented |
Minimal complete example
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct RemotePayload {
route: String,
item_id: u64,
}
fn destination(payload: &RemotePayload) -> String {
format!("{}/{}", payload.route.trim_end_matches('/'), payload.item_id)
}
fn main() {
let payload = RemotePayload {
route: "/orders".to_owned(),
item_id: 42,
};
assert_eq!(destination(&payload), "/orders/42");
}
This example handles an in-memory application value. It does not register a device, receive a remote notification, verify provider input, or contact a server.
Configuration
Schema version 1 accepts only local notification support:
[capabilities.notifications]
local = true
push = false
push = true is rejected. No current ferry.toml setting enables remote push.
Permissions and entitlements
A future Apple transport would need the appropriate aps-environment entitlement, provisioning, device-token handling, and APNs authentication on a server. A future Android transport would need FCM client configuration and generated service components; displaying notifications may also require POST_NOTIFICATIONS on applicable Android versions.
Provider credentials, APNs signing keys, and FCM server credentials belong in a protected server environment, never in ferry.toml, generated client source, or an application repository.
Expected result
The example constructs and routes one payload in memory. It produces no device token, operating-system registration, network request, notification, or background wake-up.
Common errors
- Enabling
push = true; schema version 1 rejects it. - Treating
notifications::show_nowornotifications::scheduleas remote delivery. - Shipping APNs or FCM provider credentials in the client application.
- Calling host-side payload tests proof of APNs, FCM, background, or device behavior.
- Treating simulator behavior as physical-device delivery evidence.
Future extension boundaries
A future implementation should keep these surfaces separate:
- Device registration: platform token acquisition, refresh, revocation, and typed lifecycle events.
- Event ingress: OS-delivered data converted into a validated Rust payload across cold, warm, foreground, and background states.
- Provider delivery: an application-owned server component using APNs or FCM credentials; not a mobile runtime responsibility.
- Presentation: local display policy after receipt, distinct from transport success.
- Live Activity push: ActivityKit push-token acquisition and remote update/end delivery, distinct from the existing local start/update/end API.
None of these boundaries is a stable API commitment yet. A configuration or public API should be added only with implemented platform backends, generated artifacts, lifecycle tests, and real-device evidence.
Platform differences
APNs and FCM use different token formats, credential models, payload limits, delivery semantics, and background restrictions. Apple Live Activity push tokens and update payloads form a separate ActivityKit path; the current Live Activities API performs local lifecycle operations only.
Test example
Test payload validation and routing as pure Rust functions. A future transport also needs deterministic token/event adapter tests, generated-entitlement and service-component inspection, and physical-device delivery tests for cold, warm, foreground, and background states. Record each evidence level separately.
Example project
No remote-push example exists because no transport exists. The Notifications example covers local notifications, and the Live Score example covers locally initiated Live Activity updates; neither demonstrates remote delivery.
Storage
What it does
storage::{set,get,remove,contains,clear} stores serde values. Store<T> adds a named schema version and optional migration. It is ordinary local storage, not a database or secret vault.
Support matrix
| In-memory/file backend | Android host install | iOS host install |
|---|---|---|
| Atomic/corruption/migration tests | Application-private backend installed by Android host; target-compiled; runtime unobserved | Application Support backend and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::storage::Store;
use rustferry::testing::TestRuntime;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Settings {
count: u32,
}
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let settings = Store::<Settings>::open("settings")?;
settings.save(&Settings { count: 42 })?;
assert_eq!(settings.load()?, Some(Settings { count: 42 }));
Ok(())
}
Configuration
[capabilities.storage]
enabled = true
Or run cargo ferry add storage.
Permissions and entitlements
Application-private ordinary storage normally needs no prompt. Do not store passwords, tokens, signing keys, or private keys here; a secure-storage capability is separate future work.
Expected result
The typed value round-trips. File writes use a same-directory temporary record, sync, rename, and record checksum; corruption returns a typed error.
Common errors
MigrationRequired: stored/current versions differ without a migration hook.CorruptStorage: record/checksum/serde decoding failed; do not silently replace data.- Empty or overlong key: rejected.
Platform differences
Platform hosts choose the application-private directory. Backup/eviction behavior is platform policy and is not currently promised by the cross-platform API.
Test example
TestRuntime::storage() exposes the in-memory backend. FileStorage tests cover truncated records and migration persistence.
Example project
See eager typed persistence in the Counter example.
Haptics
What it does
haptics::impact, notification, and selection request semantic feedback and return a typed error if the active backend cannot perform it.
Support matrix
| Host mock | Android bridge | iOS bridge |
|---|---|---|
| Calls recorded/tested | Enabled backend and bridge artifact-inspected; runtime unobserved | Backend implemented; framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::haptics::{self, ImpactStyle};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
haptics::impact(ImpactStyle::Light)?;
haptics::selection()?;
assert_eq!(runtime.haptic_calls().len(), 2);
Ok(())
}
Configuration
[capabilities.haptics]
enabled = true
Or run cargo ferry add haptics.
Permissions and entitlements
No runtime permission is normally required. Device settings and hardware still control whether feedback is perceptible.
Expected result
The host test records Impact(Light) then Selection.
Common errors
Unsupported(Haptics): checkhaptics::is_supported()or handle the result.- Using haptics as the only feedback channel: always keep visible/accessibility feedback.
Platform differences
Intensity and waveform are semantic approximations; simulators may not produce physical feedback. Device observation is the meaningful validation level.
Test example
Assert the ordered runtime.haptic_calls() vector after application actions.
Example project
See the explicit haptic button in the Kitchen Sink example.
Clipboard
What it does
clipboard::read_text and write_text access text only. Read and write support are queried separately.
Support matrix
| Host mock | Android bridge | iOS bridge |
|---|---|---|
| Implemented/tested | Backend implemented; bridge compiled; runtime unobserved | Backend implemented; framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::{clipboard, testing::TestRuntime};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
clipboard::write_text("copied from Rust")?;
assert_eq!(clipboard::read_text()?.as_deref(), Some("copied from Rust"));
Ok(())
}
Configuration
[capabilities.clipboard]
enabled = true
Or run cargo ferry add clipboard; cargo ferry remove clipboard reverses the generated config and feature changes.
Permissions and entitlements
No declared runtime permission is modeled, but operating systems can show privacy UI or restrict background reads. Read only after clear user intent.
Expected result
The test backend returns the text just written.
Common errors
- Read and write support differ: check
can_read_textandcan_write_textseparately. - Assuming clipboard contents remain: another app or the OS may replace/expire them.
Platform differences
Privacy notifications, focus requirements, and paste consent evolve independently. Runtime/device validation is required before UX claims.
Test example
Write text, then inspect runtime.clipboard_text() without touching the host clipboard.
Example project
See the copy action in the Kitchen Sink example.
Sharing
What it does
share::text, share::url, and share::files request the native share sheet. They report unsupported/failed operations rather than pretending content was shared.
Support matrix
| Host mock | Android bridge | iOS bridge |
|---|---|---|
| Requests recorded/tested | Enabled file provider and bridge artifact-inspected; runtime UI unobserved | Backend and framework artifact-inspected; runtime UI unobserved |
Minimal complete example
use rustferry::{share, testing::TestRuntime};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
share::text("Forecast: clear")?;
share::url("https://example.com/forecast")?;
assert_eq!(runtime.share_requests().len(), 2);
Ok(())
}
Configuration
[capabilities.share]
enabled = true
Or run cargo ferry add share.
Permissions and entitlements
Text/URL sharing normally needs no prompt. File URLs still need to be readable by the app and safely exposed through platform sharing mechanisms.
Expected result
The host test records two share requests. A platform backend should present its system chooser; cancellation is not failure unless the platform reports it as such.
Common errors
- Invalid/nonabsolute URL: rejected.
- Empty file list: rejected.
- Assuming a recipient completed the share: opening the sheet is the API boundary.
Platform differences
Available recipients, previews, file grants, and cancellation callbacks differ.
Test example
Inspect TestRuntime::share_requests() and match the ShareRequest variants.
Example project
See the user-initiated share action in the Kitchen Sink example.
Deep links
What it does
DeepLink parses absolute URLs, DeepLinkPolicy applies explicit scheme/host/action allowlists, initial reads a cold-start link, and subscribe receives links while the runtime is alive.
Support matrix
| Host parser/event mock | Android intents/artifact | iOS schemes/artifact |
|---|---|---|
| Implemented/tested | Intent filter/allowlist bridge artifact-inspected; runtime unobserved | URL scheme/delegate allowlist and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::deep_links::{self, DeepLink, DeepLinkPolicy};
use rustferry::testing::TestRuntime;
use std::sync::{Arc, Mutex};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let policy = DeepLinkPolicy::new()
.allow_scheme("weather")
.allow_host("forecast")
.allow_action("today");
let link = DeepLink::parse("weather://forecast/today")?;
policy.validate(&link)?;
let received = Arc::new(Mutex::new(None));
let observed = Arc::clone(&received);
let _subscription = deep_links::subscribe(move |link| {
*observed.lock().unwrap() = Some(link);
});
runtime.send_deep_link(link.clone());
assert_eq!(*received.lock().unwrap(), Some(link));
Ok(())
}
Configuration
[capabilities.deep_links]
schemes = ["weather"]
allowed_hosts = ["forecast"]
allowed_actions = ["today"]
Or run cargo ferry add deep-links for a generated scheme.
Permissions and entitlements
Custom schemes normally need generated manifest/plist declarations, not a runtime prompt. Universal/app links require domain association and are advanced configuration not claimed complete here.
Expected result
The allowlisted link reaches the running-app callback. set_initial_deep_link separately tests cold start.
Common errors
- Relative URL: an absolute scheme is required.
- Allowlist mismatch: reject before routing.
- Treating a deep link as authorization: re-check identity/ownership for every sensitive action.
Platform differences
Android uses intent filters; Apple uses URL types. Cold-start and already-running delivery enter through different native callbacks but converge on the typed Rust event.
Test example
Test denied hosts/actions and both set_initial_deep_link plus send_deep_link.
Example project
The Widget Counter example routes a widget action back into Rust.
Permissions
What it does
The unified API queries and explicitly requests notifications, network state, local network, photos, camera, microphone, and foreground location. Unsupported permission/platform pairs return Unsupported status or typed operation failure.
Support matrix
| Host mock | Android permission bridge | iOS permission bridge |
|---|---|---|
| Status/request/rationale tested | Exact enabled permissions/purpose strings and bridge artifact-inspected; runtime prompts unobserved | Supported permission backends and framework artifact-inspected; runtime prompts unobserved |
Minimal complete example
use rustferry::permissions::{self, Permission, PermissionStatus};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_permission(
Permission::Camera,
PermissionStatus::NotDetermined,
PermissionStatus::Granted,
);
assert_eq!(permissions::status(Permission::Camera)?, PermissionStatus::NotDetermined);
let status = rustferry::spawn(async {
permissions::request_with_rationale(
Permission::Camera,
Some("Scan a receipt after you tap Continue"),
)
.await
})
.join()
.expect("permission worker did not panic")?;
assert_eq!(status, PermissionStatus::Granted);
Ok(())
}
Configuration
Permission declarations and purpose strings are capability-specific. Do not add every permission preemptively. The current schema models implemented capability fields; camera/location feature APIs are not otherwise claimed complete.
Permissions and entitlements
The user chooses the request moment. Platform purpose strings must explain the real use before build. No bulk startup prompt, permission bypass, or synthetic granted result is acceptable.
Expected result
The test records a user-initiated request with rationale and returns the configured result.
Common errors
PermanentlyDenied: direct the user topermissions::open_settings(); do not loop prompts.Unsupported: hide/disable the feature or provide a real fallback.- Missing purpose string: platform build should fail before packaging.
Platform differences
Android/iOS distinguish denial/restriction and repeat requests differently. PermanentlyDenied is used only when meaningful; platform restrictions can remain Restricted.
Test example
Use set_permission_supported(false) for fallback UI and inspect permission_requests() for rationale and ordering.
Example project
The Notifications example requests authorization only after a button press.
Widgets
What it does
RustFerry exposes a restricted, serializable WidgetSnapshot with title, value, caption, progress, deep-link, and one optional text/link/button content node. Both generated adapters render this common model as an Android App Widget and an Apple WidgetKit extension; this is not a general layout engine.
Support matrix
| Host snapshot/model | Android provider APK | iOS WidgetKit .appex |
|---|---|---|
| Implemented/tested | Enabled provider/backend artifact-inspected; runtime unobserved | App-group publisher, framework, and .appex artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::deep_links::DeepLink;
use rustferry::testing::TestRuntime;
use rustferry::widgets::{self, WidgetId, WidgetSnapshot};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let id = WidgetId::parse("counter")?;
let snapshot = WidgetSnapshot::new()
.title("Counter")
.value("42")
.caption("Tap to open")
.progress(0.42)
.deep_link(DeepLink::parse("counter://open/current")?);
widgets::update(&id, snapshot.clone())?;
assert_eq!(runtime.widget_snapshot(&id), Some(snapshot));
Ok(())
}
Configuration
[extensions.widget]
enabled = true
app_group = "group.com.example.counter"
Run cargo ferry add widget to add the config/feature and a Rust snapshot fragment.
Permissions and entitlements
iOS shared state requires an application group and compatible signing/provisioning on devices. Android needs generated provider/receiver metadata, not an overlay permission.
Expected result
The host test records the latest snapshot. Apple publisher/extension artifacts and an Android APK with the enabled provider pass inspection. Neither platform has runtime-observation evidence.
Common errors
- Missing
app_group: strict config rejects it. - Progress outside
0.0..=1.0: rejected before backend dispatch. - Expecting arbitrary Rust UI: platform widgets use the constrained schema.
Platform differences
Android uses RemoteViews/provider scheduling; Apple uses WidgetKit timelines and families. Update timing is controlled by each OS.
Test example
Publish several snapshots and assert widget_snapshot(&id) contains the last valid one; assert invalid progress fails.
Example project
See shared state, snapshots, and an action route in the Widget Counter example.
Live Activities
What it does
The API starts, updates, lists, and ends a serializable activity with an optional constrained presentation snapshot. The iOS adapter uses ActivityKit; Android’s honest fallback is an ongoing notification.
Support matrix
| Host activity model | Android fallback artifact | iOS ActivityKit .appex |
|---|---|---|
| Lifecycle implemented/tested | Fallback enabled in an inspected APK; runtime unobserved | Lifecycle bridge, framework, and .appex artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::live_activity::{self, LiveActivitySnapshot};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let id = live_activity::start_with_snapshot(
&"final-match",
&0_u32,
LiveActivitySnapshot::new().title("Score").status("0–0").progress(0.0),
)?;
live_activity::update(&id, &1_u32)?;
assert_eq!(live_activity::list_active()?[0].state, 1);
live_activity::end(&id, &2_u32)?;
assert!(live_activity::list_active()?.is_empty());
Ok(())
}
Configuration
[ios]
min_version = "16.1"
[extensions.live_activity]
enabled = true
android_fallback = "ongoing-notification"
Or run cargo ferry add live-activity.
Permissions and entitlements
iOS availability, user settings, ActivityKit/WidgetKit extension configuration, and signing constraints apply. Remote push updates require APNs/server credentials and are not implemented. Android fallback uses normal notification requirements.
Expected result
The host test tracks one activity through start/update/end. Apple compile/link/artifact checks cover the bridge and extension; an inspected Android Kitchen Sink APK contains the enabled fallback bridge and notification prerequisites. Neither platform has runtime-observation evidence.
Common errors
- iOS minimum below 16.1: strict config rejects it.
- Invalid progress: rejected.
- Assuming
is_supported()from OS version alone: backend and user settings also matter.
Platform differences
ActivityKit is Apple-specific. The Android backend maps the operation to an ongoing notification, never an overlay or imitation Dynamic Island; runtime delivery remains unobserved.
Test example
Use runtime.active_activities() on TestRuntime or live_activity::list_active() to assert attributes/state/snapshot after every transition.
Example project
See the complete start/update/end flow in the Live Score example.
Dynamic Island
What it does
LiveActivitySnapshot supplies title/status/progress plus compact leading/trailing text for a generated ActivityKit presentation. RustFerry does not expose arbitrary SwiftUI from application source.
Support matrix
| Host snapshot | iOS extension artifact | Simulator | Physical device |
|---|---|---|---|
| Implemented/tested | Presentation and lifecycle framework artifact-inspected | Not validated | Not validated |
Minimal complete example
use rustferry::deep_links::DeepLink;
use rustferry::live_activity::LiveActivitySnapshot;
fn score_snapshot() -> rustferry::Result<LiveActivitySnapshot> {
Ok(LiveActivitySnapshot::new()
.title("Final")
.status("3–2")
.progress(0.75)
.leading_text("HOME")
.trailing_text("75′")
.deep_link(DeepLink::parse("score://match/current")?))
}
fn main() -> rustferry::Result<()> {
let snapshot = score_snapshot()?;
assert_eq!(snapshot.trailing_text.as_deref(), Some("75′"));
Ok(())
}
Configuration
Use the same iOS 16.1+ and [extensions.live_activity] configuration as Live Activities.
Permissions and entitlements
ActivityKit availability and user settings apply. Device extension signing/provisioning can impose additional constraints. No overlay permission is used on Android.
Expected result
The snapshot serializes the compact labels. Visual presentation is not validated until the generated extension runs on supporting Simulator/device hardware.
Common errors
- Expecting every iPhone/iPad to show Dynamic Island: hardware and current OS context decide presentation.
- Packing long text into compact fields: keep it glanceable.
- Claiming visual parity from a serialization test.
Platform differences
Lock Screen presentation can exist without Dynamic Island. Android uses the configured ongoing-notification fallback.
Test example
Test snapshot serialization and 0.0..=1.0 progress validation; use device/simulator screenshots only as separate runtime evidence.
Example project
See the compact leading/trailing fields in the Live Score example and Live Activities.
Testing
What it does
TestRuntime installs a deterministic thread-scoped backend with in-memory storage and inspection/injection helpers for network, lifecycle, notifications, permissions, haptics, clipboard, sharing, URLs, widgets, and Live Activities.
Support matrix
| Linux | macOS | Windows | Mobile SDK required |
|---|---|---|---|
| Host tests | Host tests | Host tests/path coverage | No |
Minimal complete example
use rustferry::haptics;
use rustferry::network::{self, NetworkStatus, NetworkTransport};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_network_status(NetworkStatus::online(NetworkTransport::Ethernet));
assert!(network::is_online()?);
haptics::selection()?;
assert_eq!(runtime.haptic_calls().len(), 1);
Ok(())
}
Configuration
No ferry.toml configuration is needed for unit tests. Generated projects still include their real config so cargo ferry check covers it.
Permissions and entitlements
None. The mock does not grant real OS access and must never be counted as device validation.
Expected result
Application logic runs with no SDK, emulator, simulator, phone, prompt, or host clipboard/network effect.
Common errors
- Guard dropped too early: convenience APIs fall back to an unsupported runtime.
- Sharing a guard across threads:
RuntimeGuardintentionally is notSend; userustferry::spawnor enter the runtime on that thread. - Treating mock success as platform success.
Platform differences
The same deterministic model runs on all hosts. OS callback order, timing, UI, permissions, and hardware need separate artifact/runtime tests.
Test example
#![allow(unused)]
fn main() {
#[test]
fn unsupported_ui_path_is_visible() {
let runtime = rustferry::testing::TestRuntime::new();
let _guard = runtime.enter();
runtime.set_supported(rustferry::Operation::Haptics, false);
assert!(rustferry::haptics::selection().is_err());
}
}
Example project
Every standalone project under examples/ includes a focused TestRuntime integration test; start with Counter.
Custom platform code
What it does
The safe PlatformBackend trait lets platform-host code implement granular operations. For an operation that cannot fit the shared API, target-gated application code can synchronously borrow the active Android context through rustferry::android::with_context or the captured iOS application through rustferry::ios::with_application. Neither API grants cross-thread ownership or permits retaining raw handles after the callback.
Support matrix
| Backend contract | Android context | iOS application | External plugin registry |
|---|---|---|---|
| Implemented/tested | Implemented and target-compiled | Implemented and framework artifact-inspected | Not implemented |
Minimal complete example
use rustferry::{PlatformBackend, Runtime};
use std::sync::Arc;
struct EmptyBackend;
impl PlatformBackend for EmptyBackend {}
fn main() {
let runtime = Runtime::new(Arc::new(EmptyBackend));
let _guard = runtime.enter();
assert!(!rustferry::haptics::is_supported());
assert!(rustferry::haptics::selection().is_err());
}
Default trait methods are intentionally unsupported. A real adapter must advertise only operations it performs and override the matching method.
The target-specific escape hatches are available only behind #[cfg(target_os = "android")] or #[cfg(target_os = "ios")]. Keep every borrow inside its callback and dispatch UI work according to the platform API’s thread rules.
Configuration
An adapter for an existing operation can use the public PlatformBackend contract without changing the CLI or schema; see Custom adapters. Add a capability to ferry.toml only after its core schema, generated components, and validation are implemented.
Permissions and entitlements
A custom adapter must derive minimal manifest/plist/entitlement changes from validated configuration. It must not request permissions automatically or bypass platform policy.
Expected result
The empty backend reports an explicit unsupported error, never false success.
Common errors
- Returning
truefromsupportswhile keeping the default unsupported method. - Passing raw handles across threads/lifetimes without a platform guarantee.
- Letting Rust panic or a native exception cross FFI.
- Editing generated files directly; regeneration replaces them.
Platform differences
Android adapters may require generated Java/JNI/manifest components. Apple adapters may require Swift/Objective-C, frameworks, plist entries, entitlements, or an extension target. Keep those beneath target/ferry/.
Test example
Start with deterministic backend tests, golden generated-source tests, then target compilation and final artifact inspection. Simulator/device observation is a separate level.
Example project
The Kitchen Sink example demonstrates the portable application boundary. Use an escape hatch only for a narrowly scoped target-specific operation; study Architecture and FFI safety before changing a platform crate.
Custom adapters
What it does
PlatformBackend lets a host or test harness implement an existing runtime operation without adding a CLI command or configuration key. The adapter advertises only the operations it implements; every other trait method keeps its explicit unsupported default.
This is an extension point for the operations already listed by Operation, not a dynamic plugin system. A new capability, operation, manifest rule, entitlement, generated component, or bridge entry point still requires a core change.
Support matrix
| Extension | Status |
|---|---|
Construct a Runtime with a custom backend | Implemented and tested |
Override an existing Operation method | Implemented and tested |
| Add a new CLI capability through an adapter | Not supported |
| Register an adapter dynamically in generated hosts | No generic registry |
| Add platform declarations or generated native code | Requires platform/code-generation changes |
Minimal complete example
This adapter implements the existing haptics operation and records calls. It changes neither the CLI nor the configuration schema.
use rustferry::haptics::{self, HapticCall, ImpactStyle};
use rustferry::{Operation, PlatformBackend, Runtime};
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct RecordingHaptics {
calls: Mutex<Vec<HapticCall>>,
}
impl PlatformBackend for RecordingHaptics {
fn supports(&self, operation: Operation) -> bool {
operation == Operation::Haptics
}
fn haptic(&self, call: HapticCall) -> rustferry::Result<()> {
self.calls.lock().expect("haptic call lock poisoned").push(call);
Ok(())
}
}
fn main() -> rustferry::Result<()> {
let backend = Arc::new(RecordingHaptics::default());
let runtime = Runtime::new(backend.clone());
let _guard = runtime.enter();
haptics::impact(ImpactStyle::Light)?;
assert_eq!(
backend.calls.lock().expect("haptic call lock poisoned").as_slice(),
&[HapticCall::Impact(ImpactStyle::Light)]
);
assert!(!rustferry::clipboard::can_write_text());
Ok(())
}
supports and the matching method must agree. Advertising an operation while leaving its default method in place produces an unsupported error.
Configuration
The recording adapter needs no ferry.toml entry. A generated mobile application must still enable the existing capability when that capability controls manifest, entitlement, framework, or native-component generation. For haptics:
[capabilities.haptics]
enabled = true
Adapters cannot add unknown configuration fields. Extend the schema and validation only when introducing a real core capability.
Permissions and entitlements
An adapter cannot bypass platform permissions or signing policy. Use the existing validated capability configuration when it already models the required declaration. New manifest entries, plist keys, entitlements, frameworks, or extension targets require corresponding platform generation and artifact checks.
Expected result
The scoped runtime reports haptics support, routes one light-impact call to RecordingHaptics, and records it. Other operations remain unsupported unless the adapter implements them.
Common errors
- Returning
truefromsupportswithout overriding the corresponding trait method. - Replacing a full platform backend and accidentally dropping operations the application still needs.
- Assuming a
PlatformBackendimplementation is automatically discovered by generated Android or Apple hosts. - Adding a new
Operationlocally without updating runtime APIs, platform bridges, configuration, code generation, and tests together. - Returning fabricated success from an adapter that performed no platform work.
Platform differences
The example is portable because it records calls in memory. A production Android adapter may need Java/JNI code, manifest components, and Android thread handling. An Apple adapter may need Swift or Objective-C code, frameworks, plist entries, entitlements, or an extension target. Generated mobile hosts install their platform backend explicitly; there is no runtime plugin loader.
Use the target-specific borrowing APIs in Custom platform code for a narrow operation that does not belong in the shared backend contract. Keep generated glue beneath target/ferry/.
Test example
Exercise an adapter through the public capability function, as the minimal example does, rather than calling its trait method directly. Also assert unsupported behavior for operations the adapter does not advertise. Platform adapters then need target compilation, generated-source checks, artifact inspection, and separate simulator or device observation.
Example project
The Kitchen Sink example exercises the built-in capability boundary. Use the recording adapter above as the starting point for host-side tests; no example currently demonstrates third-party adapter discovery in a generated mobile host because that registry does not exist.
Android without Gradle
The default Android design is a direct build-system integration. The user project contains no build.gradle, Gradle wrapper, Android Studio project, Java source, or Kotlin source.
Pipeline
- Load Cargo metadata and strict
ferry.toml. - Discover one coherent SDK platform, Build Tools revision, NDK LLVM toolchain, Java toolchain, and configured Rust targets.
- Cross-compile the Rust
cdylibfor each ABI. Cargo JSON identifies the exact native artifact and constrained build-script output directories. - Generate a deterministic manifest, resources, and NativeActivity metadata below
target/ferry/android/. - Compile and link resources with
aapt2against the selectedandroid.jar. - Collect required dependency DEX, and compile/merge generated JVM bridge code with
javacandd8only when needed. - Insert uncompressed native libraries under
lib/<abi>/and sequentialclasses*.dexentries. - Run
zipalignbefore signing, then sign withapksigner. - Verify signature, 16 KiB native-library alignment, package/launcher metadata, safe/unique ZIP entries, resources, DEX headers, and ELF ABI headers.
AAPT2’s linked APK is incomplete by itself: it lacks the Rust library, dependency DEX, and signature. Generated strings or an unsigned intermediate must never be reported as the final artifact.
Slint consequence
Slint’s Android integration may produce an internal Java helper/DEX from a Cargo build script. The packager therefore consumes Cargo JSON output and merges constrained DEX inputs. If DEX exists, the manifest must not declare android:hasCode="false".
Signing
Debug signing material belongs in cargo-ferry’s machine configuration directory, not in an application repository. Release signing is explicit. Password sources must be redacted and should avoid process arguments when the selected tool permits safer input.
Build versus deployment
build needs no ADB, emulator, USB connection, or phone and never installs or launches. The explicit install android and run android commands now compose a fresh, independently validated APK with typed ADB operations for one exact device ID, or for the sole compatible device when selection is unambiguous. logs android collects a bounded application-filtered snapshot by default; --json-stream selects the live protocol stream.
Those deployment paths are implemented and host-tested, but no emulator or physical Android device was available for runtime validation. Artifact status and device status remain independent in the Support matrix.
See ADR-002, Android build, and the official AAPT2, zipalign, apksigner, and NDK custom build-system documentation.
Apple generated host
iOS uses the official Apple build system through a generated host. The application author does not create or maintain an Xcode project, plist, Swift target, or extension target.
Simulator pipeline
- Require macOS and discover full Xcode, the selected developer directory, iPhone Simulator SDK,
xcodebuild,xcrun,plutil, Cargo, and the Rust simulator target. - Load Cargo metadata and strict
ferry.toml. - Generate deterministic host metadata below
target/ferry/ios/. - Cross-compile Rust for
aarch64-apple-ios-simon Apple Silicon. - Stage the Rust executable into the generated host and let Xcode assemble metadata, resources, and enabled extension targets into an
.app. - Inspect bundle identifier, executable, architecture, metadata, resources, linked content, and every required
.appexbefore success.
A Simulator runtime/device is optional for build-only work. It is required for install/launch observation, which has a separate status.
Extensions
Widgets and Live Activities require generated SwiftUI/WidgetKit/ActivityKit adapters because Apple owns their presentation and lifecycle. cargo-ferry exposes a restricted serializable Rust snapshot rather than attempting to reproduce SwiftUI. An extension is not complete until its .appex, executable, plist, identifier, and embedding relationship are inspected.
Physical devices
Device builds use only official Xcode signing/provisioning. Team, profile, entitlement, and application-group constraints can differ from the Simulator. No unsigned install, jailbreak, or signing bypass is supported. Device compile, artifact, install, and launch evidence are recorded separately.
See ADR-003, iOS Simulator, and Apple’s Xcode build system.
FFI safety
Platform adapters cross Rust/native/JVM/Swift boundaries. These are security and correctness boundaries, not ordinary internal calls.
Required invariants
- Catch Rust panics before every exported callback returns across FFI.
- Convert platform exceptions/errors into typed Rust errors; never report fabricated success.
- Document raw pointer ownership, nullability, lifetime, thread affinity, and release responsibility.
- Do not mark raw handles
SendorSyncwithout a platform guarantee. - Dispatch UI work on the required platform/UI thread.
- Stop new callbacks after runtime shutdown; dropping a subscription must prevent later callback starts.
- Copy or retain callback payloads according to the platform contract before their source lifetime ends.
- Keep generated bridge surface minimal and free of application business logic.
The workspace currently denies unsafe Rust in its crates. Generated platform glue still needs language-specific exception and lifetime handling; the Rust lint alone is not FFI validation.
Event ordering
Events sent serially by one source retain source order. Independent concurrent sources may interleave. Operating systems can terminate a process without a final lifecycle callback, so important state must be persisted eagerly.
Verification
Host tests cover event teardown, panic containment where expressible, typed model conversion, and mock behavior. Platform artifact tests must also prove bridge symbols/components were linked. Simulator/device observation is required before claiming callback behavior on that environment.
See the Threat model and Support matrix.
Generated files
All platform scaffolding and cargo-ferry artifacts belong below the project’s target/ferry/ directory. User source, assets, Cargo manifests, ferry.toml, and signing inputs remain outside that disposable boundary.
Properties
- Deterministic from validated config, Cargo metadata, capability set, assets, bridge version, and selected toolchain.
- Written without traversing parent directories or following output paths outside the validated root.
- Safe to regenerate after application changes.
- Never the only location of a user signing key or provisioning asset.
- Independently inspected after packaging; cached existence is not sufficient.
Cleaning
cargo ferry clean
cargo ferry clean android
cargo ferry clean ios
cargo ferry clean generated
cargo ferry clean --all
Each target is normalized and constrained below target/ferry/ before recursive removal. A missing target is a no-op. --dry-run prints the intended scope.
Do not manually edit a generated host: changes will be replaced. Extend a capability adapter in the cargo-ferry workspace instead; see Custom platform code.
Measurements
These are reproducible developer checks, not product performance promises. Wall-clock results depend on the host, filesystem cache, dependency state, and Rust/toolchain revision. The first four sections were recorded before the RustFerry rename on the Apple Silicon Mac described in STATUS on 2026-08-01. Their commands use current package names for reproduction; their timings require a fresh run before they describe the renamed revision. Later sections state their own environment.
Strict configuration parsing
cargo bench -p rustferry-core --bench config_parsing
The benchmark parses the generated starter configuration 10,000 times through FerryConfig::parse. Recorded result: 10,000 parses in 85.56775 ms.
Atomic starter generation
cargo bench -p rustferry-codegen --bench template_generation
The benchmark creates 100 complete starter projects in a temporary directory through the same atomic ProjectGenerator::generate path used by the CLI. Recorded result: 100 projects in 6.405533875 s.
Android incremental-cache assertion
cargo test -p rustferry-android --test real_android \
generated_minimal_project_produces_verified_apk -- \
--ignored --nocapture
This SDK/NDK integration test performs two real builds. The second build must report cache hits for aapt2-compile, aapt2-link, and d8; a missing hit fails the test. The check records cache correctness only—no elapsed-time or speedup guarantee is attached to it.
Android cache calculation and no-change planning
cargo test -p rustferry-android \
measures_cache_calculation_and_incremental_no_change_planning \
-- --ignored --nocapture
The ignored developer measurement hashes 16 one-KiB inputs and recreates the same side-effect-free Android plan 1,000 times. Recorded result: 1,000 cache calculations in 819.703292 ms; 1,000 incremental no-change plans in 3.112419209 s. Equality assertions verify stable cache and generated-content identities; these observations are not performance guarantees.
VS Code extension protocol and activation
cargo build -p cargo-ferry
cd editors/vscode
npm run perf
npm run test:host
Recorded on 2026-08-01 on Apple Silicon macOS with the debug cargo-ferry,
Node v22.23.1 for the isolated protocol benchmark, and pinned VS Code 1.100.0:
| Measurement | Observation |
|---|---|
| Extension Host startup to auto-activation | 6,191 ms |
| Extension activation | 1,327.817 ms |
| Project discovery | 44.227 ms |
| Initial discovery and tree refresh | 1,323.933 ms |
| Repeated tree refresh, median of 5 | 39.047 ms |
| Open discovered manifest | 75.862 ms |
| CLI handshake, median / p95 of 7 | 9.644 / 10.973 ms |
| Configuration validation, median / p95 of 7 | 11.605 / 14.342 ms |
| Parse 10,000 protocol events, median / p95 of 7 | 9.877 / 11.797 ms |
| Parse 100,000-event long stream | 135.939 ms |
| Long-stream peak / retained heap delta | 2,060,328 / 14,368 bytes |
The CLI samples use two warmups and include process startup. The host values are one smoke-run observation, except for repeated tree refresh. The memory values come from an isolated Node decoder process with exposed garbage collection; complete Extension Host peak memory is unavailable from this harness. No Android SDK, simulator, emulator, device, installation, or mobile build runs in either command.
ADR-001: Slint as the first UI backend
- Status: accepted for the initial vertical slice
- Date: 2026-07-31
- Version evaluated: Slint 1.17.1
Context
The application author must write Rust only, while generated hosts may contain minimal native glue. The first backend needs maintained Android and iOS support, touch/text/buttons/state/async behavior, and a path to a direct no-Gradle Android artifact. Building a renderer, text stack, or widget toolkit is out of scope.
Decision
Use Slint 1.17.1 with inline slint::slint! UI in src/app.rs. This keeps the starter’s interface and callbacks visible in the first file an application author opens. The runtime/service layer remains backend-neutral so another UI integration can be added later without changing platform capabilities.
The project MSRV is Rust 1.92, matching Slint 1.17.1. Android targets API 26 or later, matching Slint’s supported minimum. Platform glue remains generated and outside user source.
Evidence
| Candidate | Android | iOS | Main issue |
|---|---|---|---|
| Slint 1.17.1 | First-party android-activity backend and mobile lifecycle integration | First-party Winit/Skia instructions | License choice and attribution must be explicit; mobile Skia clean builds are heavy |
| Dioxus 0.7.9 | First-party mobile workflow | First-party mobile workflow | Stable Android workflow generates Gradle and Kotlin, conflicting with the default no-Gradle requirement |
| egui/eframe 0.35 | Official Android example | eframe does not list iOS as supported | iOS path and mobile IME/safe-area UX are weaker |
| raw Winit/android-activity | Platform event loop | Platform event loop | Not a UI toolkit; choosing it would require the prohibited custom renderer/widgets/text stack |
Primary references:
- Slint Android
- Slint iOS
- Slint mobile integration
- Slint backends and renderer tradeoffs
- Dioxus mobile guide
- egui Android example
- Winit platform support
Licensing consequence
Slint 1.17.1 is triple-licensed under GPL-3.0-only, the Slint Royalty-free License 2.0, or a commercial license; it is not an MIT/Apache runtime dependency. The royalty-free path covers desktop, mobile, and web applications when the application either exposes AboutSlint from its top-level UI or displays the Slint attribution badge on a readily discoverable public page. It excludes embedded systems, standalone Slint distribution, and applications that expose Slint APIs. The GPL path carries GPL source and redistribution obligations; the commercial path covers proprietary embedded applications and applications that cannot meet the royalty-free conditions.
Generated starters retain AboutSlint, but RustFerry does not select a license for downstream applications. Before distributing a generated binary, its author must select a Slint path and audit the application’s resolved dependency tree. The pinned royalty-free text and full dependency policy live in LICENSES/ and Third-party licenses. Canonical versioned terms remain in Slint 1.17.1 LICENSE.md and its LICENSES directory.
This ADR is technical context, not legal advice. Applications with incompatible distribution requirements can use a commercial Slint license or a future backend once implemented.
Android packaging consequence
Slint’s Android backend compiles an internal Java helper and DEX during its Cargo build script. The direct packager must consume Cargo JSON build-script output, merge dependency DEX with any enabled RustFerry bridge through d8, and package classes.dex. A NativeActivity manifest must not claim android:hasCode="false" when DEX is present.
Follow-up measurements
Record clean and incremental build duration plus stripped APK/.app sizes after the first artifacts. No unmeasured binary-size or startup promises are accepted.
ADR-002: Direct Android SDK/NDK packaging
- Status: accepted
- Date: 2026-07-31
Context
The primary user flow must produce an APK without a user-maintained Android Studio project, Gradle files, Java, or Kotlin. Build must not depend on a connected device. Slint and system capabilities can still require dependency DEX or generated JVM adapters.
Decision
Use the Android SDK/NDK tools directly: Cargo plus NDK LLVM for Rust native libraries, AAPT2 for resources/manifest, optional Javac/D8 for generated bridge and dependency bytecode, ZIP assembly, zipalign, and apksigner. Generated inputs and intermediates live below target/ferry/android/.
Gradle is not the default backend. Any future hidden-Gradle fallback must be capability-specific, explicit, generated outside user source, and explain why the direct path is insufficient.
Consequences
- cargo-ferry owns tool discovery, version coherence, argument construction, diagnostics, and packaging order.
- Cargo JSON output is the authority for exact native and build-script artifacts; recursive broad target scans are unsafe and ambiguous.
- AAPT2 output alone is not deployable. Native libraries/DEX must be inserted before alignment; alignment must precede signing.
- Success requires independent package, launcher, resource, DEX, ELF ABI, alignment, and signature checks.
- CI needs a real SDK, NDK, Rust Android target, Java, and Build Tools. A cancelled or skipped artifact job creates no platform evidence.
Security
Every external process uses an executable plus argument array, timeouts, checked exit status, constrained working directory, captured diagnostics, and redacted secrets. Archive entry names and output paths are validated before use.
See Android without Gradle and Threat model.
ADR-003: Generated minimal platform bridges
- Status: accepted
- Date: 2026-07-31
Context
Android and Apple expose several required APIs only through platform frameworks, lifecycle components, or extension targets. Requiring application authors to maintain Java, Kotlin, Swift, Objective-C, manifests, plists, or Xcode targets would break the Rust-only application-source contract.
Decision
Allow small deterministic adapters generated below target/ferry/. Public application APIs remain typed Rust and backend-neutral. Android adapters may use framework Java/JNI/DEX; Apple adapters may use Swift/Objective-C/C, Xcode targets, WidgetKit, ActivityKit, and UserNotifications.
The bridge contains conversion and lifecycle plumbing only. Application state and business rules stay in Rust. A platform backend advertises support per operation; absent adapters return Unsupported rather than no-op success.
Consequences
- The project says “application code is Rust-only,” not “the complete binary is pure Rust.”
- Generated bridge sources need golden/determinism tests; final bridge presence needs artifact inspection.
- Panics/exceptions, thread affinity, ownership, and shutdown form an explicit FFI contract.
- Restricted widget/Live Activity snapshot models are preferable to a custom renderer or arbitrary native plugin surface.
- An advanced platform escape hatch may be added only with narrow lifetimes and explicit portability warnings; it is not the default architecture.
See FFI safety, Custom platform code, and Threat model.
ADR-004: VS Code debugging
Status: accepted for Developer Experience 0.2
Decision
The accepted Developer Experience 0.2 surface includes Build, Install, Run, and application-filtered Logs. It does not contribute a RustFerry: Debug command, a debug type, or a generated launch.json.
A debugger integration will use an existing LLDB extension rather than a RustFerry Debug Adapter Protocol implementation. It becomes eligible for the stable editor surface only after an artifact produced by RustFerry passes all of these checks on the target platform:
- a source breakpoint in application Rust code is hit;
- stack frames and local variables are readable;
- generated-host frames do not break source mapping;
- cancellation and process cleanup leave no debug server or forwarded port;
- Android and Apple signing/deployment behavior remains identical to the normal Run path.
Rationale
CodeLLDB is the appropriate first integration candidate, but RustFerry still needs platform-specific launch and attach proof. Android requires an NDK-compatible lldb-server, package/process selection, symbol preservation, and path mapping. iOS Simulator requires Xcode/LLDB attach behavior and verified Rust source mapping. Physical iOS adds signing, Developer Mode, device transport, and Xcode restrictions.
This repository has artifact evidence for Android and iOS Simulator builds, but no emulator, Simulator runtime, or physical device was available for breakpoint validation. Calling Run through a debug-shaped command would therefore create a false capability.
Follow-up gate
Start with an opt-in iOS Simulator or Android prototype against the existing validated-artifact and explicit-device model. Record the exact CodeLLDB version, generated dynamic configuration, breakpoint result, stack trace, source mapping, symbol policy, and cleanup result before exposing F5 or documenting debugger support.
Execution plan
This plan is ordered by executable product risk. A milestone closes only when its stated command and artifact checks pass; generated files alone are not completion.
1. Foundation
- Strict, versioned
ferry.tomlmodel and JSON Schema. - Typed CLI output in human and stable JSON forms.
- Atomic
cargo ferry newwith shared templates and capability fragments. - Starter with real UI, state, lifecycle, async, network, storage, error, permission, and notification examples.
- Host-only
cargo ferry checkandrustferry::testing::TestRuntime.
Acceptance: a newly generated starter completes cargo check without Android SDK, Xcode, emulator, or device.
2. Direct Android artifact
- SDK/NDK discovery and actionable doctor results.
- Rust
cdylibcross-compilation forarm64-v8a. - Deterministic manifest, resources, native host, and optional JVM bridge generation.
aapt2compile/link, optionaljavac/d8, APK assembly, zip alignment, debug signing, and independent artifact verification.
Acceptance: a signed, aligned APK containing the correct application ID, launcher, resources, ABI library, and required DEX entries. No Gradle or user-authored Java/Kotlin in the generated project.
3. iOS Simulator artifact
- Xcode/SDK discovery and Rust simulator cross-compilation.
- Deterministic hidden Xcode host, plist, assets, and extension targets.
.appbuild and independent bundle/executable/architecture validation.
Acceptance: a simulator .app with the expected identifier, executable, linked Rust code, and resources. No Xcode project in the user project.
4. Runtime and bridges
- Lifecycle/events, network path versus probe, atomic storage, haptics, clipboard, share, system information, deep links, and permissions.
- Platform bridge callbacks with typed errors, main-thread delivery, teardown safety, and no panic crossing FFI.
- Deterministic mock backends for application tests.
5. Notifications and extensions
- Local-notification permission, schedule/show/cancel/query/open flows on Android and iOS.
- Restricted widget snapshot model, Android provider, WidgetKit extension, and shared state.
- Restricted Live Activity snapshot model, ActivityKit extension, and Android ongoing-notification fallback.
Acceptance requires platform components in inspected artifacts plus compiling examples and documentation. Runtime/device status remains separate.
6. Deployment and hardening
- Optional install/run/log/device commands, never coupled to
build. - Content-addressed generated/build cache and narrowly scoped clean operations.
- Linux/macOS/Windows CI, platform artifact jobs, docs, security review, and release packaging.
- Final acceptance run and factual support matrix.
Current environment constraints
- Host: Apple Silicon macOS 26.5.2, Rust 1.96.0, Xcode 26.6 with iOS 26.5 SDK.
- Android SDK platforms/build-tools and NDK 29.0.14206865 are installed.
- Rust targets
aarch64-linux-androidandaarch64-apple-ios-simare installed. - No iOS Simulator runtime/device is installed. Build-only artifact validation can proceed; install/launch validation cannot.
- Physical-device signing, provisioning, installation, and launch are not assumed.
Implementation status
Last updated: 2026-08-01
Source and documentation now use RustFerry. Platform artifacts run 30719811812 at commit 8ed0192 produced and inspected current RustFerry-named Android and iOS artifacts. Exact pre-rename cargo-pocket/Pocket* paths, identifiers, symbols, and hashes remain below as historical evidence.
Status terms:
- Implemented: backend code exists and deterministic tests pass.
- Artifact-validated: a produced APK,
.app, or.appexpassed structural/toolchain inspection. - Runtime-validated: behavior was observed on a simulator, emulator, or device.
- Blocked: a named environmental prerequisite is absent.
| Area | Android | iOS Simulator | Host tests | Evidence |
|---|---|---|---|---|
| Workspace/config | Shared model implemented | Shared model implemented | Targeted tests pass | Strict schema, semantic validation, CLI JSON/human tests |
| Starter generation | Implemented; all eight templates host-check | Implemented; all eight templates host-check | Actual CLI generation/check passed | Atomic generator plus six standalone projects |
| UI backend | Slint 1.17.1 target-compiled into a public-CLI APK | Slint 1.17.1 public-CLI .app artifact-validated | Six Slint examples compile | ADR-001 |
| Build pipeline | Direct arm64 public-CLI APKs artifact-validated | Public-CLI starter, widget, and Live Activity artifacts validated | Build-plan/golden tests | Direct packager and generated Xcode host |
| Lifecycle/network/storage | Backends implemented; bridge compiled into inspected APK; runtime unobserved | Backends implemented; framework artifact-inspected; runtime unobserved | Models, mocks, and examples pass | TestRuntime plus platform bridge tests |
| Local notifications | Backend/receiver implemented and artifact-inspected; runtime unobserved | Backend implemented and framework artifact-inspected; runtime unobserved | Model and complete mock flow pass | Notifications example plus generated-bridge tests |
| Widget | Provider/backend implemented and artifact-inspected; runtime unobserved | State publisher, WidgetKit .appex, and framework artifact-inspected | Snapshot model/example pass | Android probe plus combined iOS extension app |
| Live Activity | Ongoing-notification fallback enabled in an inspected APK; runtime unobserved | ActivityKit lifecycle bridge and .appex artifact-inspected | State model/example pass | Public-CLI Kitchen Sink plus Live Score |
| Devices/install/run/logs | Typed ADB services and IDE protocol implemented; runtime unobserved | Typed simctl services and IDE protocol implemented; no Simulator runtime/device | Service, parser, protocol, schema, fixture, and CLI tests pass | Official-tool argument arrays, explicit device IDs, validated artifacts, bounded application logs; no runtime claim |
| Physical iOS development | N/A | Official arm64/Xcode build plan, explicit Team/provisioning policy, recursive signature/profile/entitlement validation, devicectl install/launch implemented | Signing/deployment unit tests and public CLI dry-run pass | No identity, Team, profile, artifact, or physical device available; not artifact/device validated |
| IDE and VS Code | Same CLI build/deploy service | Same CLI build/deploy service | Protocol v1 tests; extension TypeScript/lint; 42 base tests pass and 4 real-CLI tests skip when no CLI is supplied; all 46 pass with the final CLI | Installable VSIX and real Extension Host smoke-tested; no mobile runtime claim |
| Assets | Five-density launcher icons and splash integrated into an inspected signed/aligned arm64 APK | CompiledCatalog implemented/tested; runtime-free SdkOnlyResources integrated into an inspected signed arm64 .app | Source validation, SHA-256 cache integrity, concurrent publication, tamper rejection, packaging, and artifact tests pass | Full Assets.car artifact validation still needs an installed iOS Simulator runtime |
Real arm64 Android APK, iOS Simulator .app, and .appex artifacts have been produced and inspected from projects generated and built through the public CLI. No simulator, emulator, or physical-device behavior has been validated.
Developer Experience 0.2 evidence
- IDE protocol v1 implements direct JSON handshake/project/validate/doctor/schema and bounded NDJSON check/devices/watch/build/install/run/logs. Targeted protocol and black-box CLI tests, checked-in schema equality, and strict cargo-ferry Clippy pass. Dirty
ferry.tomlvalidation uses bounded UTF-8 stdin and never writes the editor buffer to disk. - Human CLI exposes
devices,install,run,logs,signing teams, andassets check/generate.cargo ferry logs --json-streamshares the live application-filtered protocol implementation; the default human command remains a finite snapshot. Install/run always rebuild and independently validate before selecting a device; explicit arbitrary artifact paths remain rejected until persisted validator metadata exists. - Physical iOS uses
aarch64-apple-ios, hidden Xcode generation, Apple Development signing, explicit Team selection, opt-in provisioning updates, and post-build recursive verification.cargo ferry build ios --device --team ABCDE12345 --dry-runproduces the same side-effect-free official-tool plan without requiring Xcode, including in Ubuntu integration tests; no signed artifact was produced. - Generated projects default to the exact registry version with no checkout path. Explicit registry, workspace, and canonical local-path modes plus independent
--display-nameare covered by generator and black-box CLI tests. Publication has not occurred, so the 0.1.0 protocol handshake deliberately reports the registry runtime dependency as unusable instead of promising an unavailable crate. - The VS Code extension passed TypeScript, ESLint, all 46 tests across 12 files with the final CLI supplied,
npm auditwith zero findings, VSIX packaging/content checks, an isolated VS Code CLI install/list smoke, and a real Extension Host smoke. Without a supplied CLI, the same suite passes 42 base tests and skips 4 live-CLI tests. The host proved ordinary Rust workspaces stay inactive and Ferry workspaces auto-activate, discover, validate, and diagnose an unsaved manifest without changing the saved file. The final VSIX has 18 entries, is 44,435 bytes, and has SHA-256ba8cac7e8d5ec10d3c7a96082f405c3d4d5cdd64afef82bc1f50a5a3d183ce6d. - Completed local gates include formatting, workspace all-target/all-feature check, strict Clippy, 258 Rust tests with 0 failures and 7 deliberate platform/hardware ignores (including 24 doctests), Rustdoc with warnings denied, Rust 1.92 all-target/all-feature checks, all 20 cookbook pages, license inventories, workflow lint, and archive guards. Package archive/source/handshake validation passed for all 6 crates; the largest archive is 152.9 KiB. Package Python tests passed 19/19, and the release contract found all 17/17 internal dependency edges exact. Exact-commit GitHub CI remains the remote full-suite gate.
Asset integration has two separately reported Apple modes. An available iOS runtime selects CompiledCatalog, which emits Assets.xcassets and requires Assets.car; generation, project wiring, cache consumption, plist selection, and rejection tests pass, but this host could not produce that artifact because no runtime is installed. With zero runtimes, SdkOnlyResources produced a real Xcode-built arm64 .app; inspection verified exact source PNG bytes, plist references, Cargo Mach-O identity, resources, and strict/deep ad-hoc signing without claiming a compiled catalog. The Android integration test produced and inspected all five launcher densities plus the splash in a v2/v3-signed, 16 KiB-aligned arm64 APK.
Current RustFerry artifact evidence
At commit 8ed0192, Platform artifacts run 30719811812 generated default Starter and Kitchen Sink projects with the public CLI, then built and independently checked both platforms:
- Android: both arm64 APKs passed ZIP integrity, v2/v3 signature, 16 KiB-aware alignment, package/launcher/API, DEX, resources, and AArch64 ELF checks. The Kitchen Sink APK also passed permission, deep-link, notification, provider, widget, and Live Activity fallback inspection.
- iOS Simulator: both arm64
.appbundles passed plist, resource, architecture, and deep/strict ad-hoc signature checks. Inspection coveredFerryRuntimeBridge.framework, its required exports and application hook, embedded WidgetKit and ActivityKit.appexproducts, exact identifiers, framework linkage, and application-group entitlements.
This is artifact validation only. The workflow did not boot an emulator or Simulator, install or launch either application, or exercise behavior on a physical device.
Recorded checks
The local results in this subsection predate the rename and remain historical host/test evidence. The Platform run above supplies current rename-integration artifact evidence; commands below use RustFerry names for reproduction.
cargo test -p rustferryand its doctests passed.- Every Rust fence across the 20 cookbook pages compiled and ran through
rustdoc --test. cargo check --all-targetsand the focusedTestRuntimeintegration test passed for Counter, Network Guard, Notifications, Widget Counter, Live Score, and Kitchen Sink.- Actual CLI generation followed by
cargo ferry checkpassed forstarter,minimal,counter,network,notifications,widget,live-activity, andkitchen-sinkwith the source runtime override. - All six example
ferry.tomlfiles passedcargo ferry config validate. - The packaged CLI source list contains all 16 embedded documentation files, and an isolated checkout without the repository-level
docs/directory compiled successfully. - The three command runners and shared process-control crate pass
x86_64-pc-windows-msvcall-target checks; the Job Object runtime regression is compiled but was not executed on this macOS host. - GitHub Actions YAML parses and passes
actionlint; CI repeats workspace, template, example, rustdoc, cookbook, Markdown-link, and mdBook checks.
Template/configuration benchmarks, cache calculation and no-change planning observations, and the Android second-build cache assertion are recorded in Measurements.
Historical Android artifact evidence
Before the RustFerry rename, the public CLI produced target/final-acceptance-starter/target/pocket/android/debug/final_acceptance_starter.apk and target/final-acceptance-kitchen/target/pocket/android/debug/final_acceptance_kitchen.apk from freshly generated projects. Independent inspection verified:
- APK ZIP integrity, v2/v3 APK signatures, and 16 KiB-aware ZIP alignment;
- packages
org.cargopocket.acceptanceandorg.cargopocket.kitchensink, API 26 minimum, API 35 target, generated icon/resources, andorg.cargopocket.bridge.PocketActivityas asingleToplauncher; - the compiled icon and splash resources byte-match both then-current project inputs (SHA-256
751ec3d49aff1e091c1fe0037060cd71701e3b03fd55031b078201afd10b7464); classes.dexwith the generated activity, file provider, notification receiver, and widget provider classes, each matching an exact manifest component;- the configured
acceptanceandkitchensinkdeep-link schemes; - the exact configured permission/component sets, including the enabled notification receiver, widget provider, and private file provider in Kitchen Sink;
- one
arm64-v8aELF64 AArch64 Rust library withandroid_mainand the JNI callback.
This is artifact evidence, not emulator/device behavior. The Kitchen Sink DEX contains the enabled start/update/end/list Live Activity fallback bridge and the inspected manifest contains its notification prerequisites.
Historical Apple artifact evidence
Before the RustFerry rename, the Xcode 26.6/iPhoneSimulator 26.5 build-only pipeline produced and independently validated through the public CLI:
- starter app:
target/final-acceptance-starter/target/pocket/ios/debug/final-acceptance-starter.app; - Kitchen Sink app:
target/final-acceptance-kitchen/target/pocket/ios/debug/final-acceptance-kitchen.app; PocketRuntimeBridge.framework, with the expected install name, arm64 executable, exported call/free/init/install/application functions, and application-delegate hook markers;- embedded
PocketWidgetExtension.appexandPocketLiveActivityExtension.appex, each with an arm64 executable, exact plist metadata, andcom.apple.widgetkit-extensionextension point; the Activity extension links the runtime framework by its exact@rpathinstall name.
Both rebuilt application bundles contain PocketIcon.png and PocketSplash.png that byte-match the then-current project inputs (SHA-256 751ec3d49aff1e091c1fe0037060cd71701e3b03fd55031b078201afd10b7464) and remain valid under codesign --verify --deep --strict after archival/restoration.
See Apple implementation status for identifiers, checks, and runtime limitations.
Toolchain inventory
- Rust/Cargo 1.96.0; host target
aarch64-apple-darwininstalled. - Xcode 26.6; iPhoneOS and iPhoneSimulator 26.5 SDKs available.
- Android SDK roots resolve to
~/Library/Android/sdk; platforms 35 and 37.0, build-tools 34.0.0 and 37.0.0, and NDK 29.0.14206865 are available. aapt2,d8,zipalign,apksigner,adb, Java 21,javac, andkeytoolavailable.- Rust targets
aarch64-linux-androidandaarch64-apple-ios-simare installed. No Android emulator/device, iOS Simulator runtime/device, Apple signing identity, Team, provisioning profile, or attached iPhone was available for runtime or physical-signing validation.
Threat model
Assets
- User source, assets, configuration, and existing build output.
- Signing keys, passwords, provisioning profiles, and platform credentials.
- Integrity and provenance of generated APK and Apple bundles.
- Developer workstation SDKs and executable search paths.
- Editor workspace trust, diagnostics, quick fixes, tasks, and extension settings.
- Device identifiers, application logs, pairing state, and development-team metadata.
Trust boundaries and entry points
- CLI arguments,
ferry.toml, Cargo metadata, filenames, assets, URLs, environment overrides, and external tool output are untrusted input. - Building a project executes its Cargo build scripts and procedural macros with the developer account’s privileges; do not build an untrusted project outside an isolated environment.
- Rust/native/JVM/Swift callbacks cross memory-management, exception, panic, and thread-affinity boundaries.
- SDK, NDK, Xcode, signing tools, devices, and emulators are external processes or systems.
- Generated archives and bundles are inspected independently before success is reported.
- The VS Code extension and
cargo-ferrycommunicate across a child-process boundary. Every stdout line, protocol version, operation ID, path, diagnostic range, and external-tool field is untrusted even when the extension started the process. - A workspace can contain malicious build scripts, symlinks, configuration, tasks, and very large output. Opening a folder is not consent to execute it.
- A selected device is a separate trust domain. Its reported name is display-only; stable IDs and capabilities come from ADB, CoreSimulator, or CoreDevice and can become stale between discovery and deployment.
- Application logs may contain user data. Log collection is explicit, application-filtered, bounded, and never clears the platform log buffer.
Required controls
- Canonicalize and constrain every generated or cleaned path below the expected project
target/ferryroot. - Invoke executables directly with argument arrays; check every exit status; preserve diagnostic logs; redact signing values.
- Never place private keys or passwords in
ferry.toml, generated source, process arguments when avoidable, or normal output. - Reject path traversal, malformed identifiers, unknown configuration fields, URL schemes outside
http,https,mailto,tel, andsms, missing purpose strings, and incompatible capabilities before expensive builds. - Catch panics at FFI entry points, translate platform failures to typed errors, document pointer ownership, and stop callbacks after runtime shutdown.
- Add only permissions, manifest components, plist keys, and entitlements required by enabled capabilities.
- Treat cache entries as untrusted until their key and expected outputs validate.
- Require VS Code Workspace Trust before project mutation, build, install, launch, logs, or custom executable settings. Virtual and remote workspaces must be rejected when local platform tools cannot safely operate on them.
- Parse only the negotiated IDE protocol version. Bound line length, retained output, diagnostic counts, and log bytes; reject malformed or trailing JSON. Give finite external operations explicit deadlines, keep intentional watch streams cancellable, and terminate the complete child process tree on cancellation or timeout.
- Keep CLI discovery deterministic. An explicit executable path must be a regular executable file; never invoke a workspace-controlled shell command or concatenate arguments into a command line.
- Apply structured quick fixes only to the file and version that produced the diagnostic, after checking the edit range. Capability changes remain CLI-owned and idempotent.
- Install and launch only artifacts carrying independent build validation metadata, then recheck
their path, type, identity, executable, archive structure, and physical-device signature before
deployment. Never infer trust from
.apkor.appsuffixes alone. - Select devices by exact ADB serial, Simulator UDID, or CoreDevice identifier. Ambiguous automatic selection fails closed; offline, unauthorized, unpaired, or capability-incompatible devices are typed errors.
- Physical iOS builds use the official Cargo/Xcode/codesign/provisioning path. Require an explicit Development Team; keep provisioning updates opt-in; reject ad-hoc signatures, team/profile/ entitlement mismatches, expired profiles, missing extensions, and non-arm64 device binaries.
- Validate PNG type, dimensions, opacity, byte bounds, canonical containment, and cache manifests
before generating platform assets. Generate below
target/ferry, reject symlink boundaries, and commit a complete fingerprint directory atomically. - Package the extension from an allowlisted manifest and inspect the VSIX contents. Exclude source
maps, tests, development paths, workspace data, secrets, logs, and
node_modulesfrom release artifacts.
Residual risks
- Deployment rechecks the validator-owned artifact digest immediately before invoking the native installer. A separate process running as the same user can still replace a pathname after that check and before ADB, simctl, or devicectl opens it. Those tools do not expose one portable descriptor-based install API, so this final cross-process TOCTOU window cannot be eliminated by the current design. Do not build or deploy alongside untrusted same-user processes.
Out of scope
- Jailbreaks, unsigned iPhone installation, signing bypass, arbitrary executable downloads, store upload, remote push infrastructure, native debugging, remote device farms, and attacks against third-party systems.
Security status is evidence-based in docs/STATUS.md; this document is not a claim that unfinished code is secure.
References
Primary sources used for platform and UI decisions:
Rust and Cargo
Slint
License terms can change; review the current source before distributing an application. ADR-001 records this repository’s technical decision and attribution default.
Android
- AAPT2
- D8
- zipalign
- apksigner
- Use the NDK with other build systems
- App manifest overview
- Notification runtime permission
- App widgets
Apple
- Xcode build system
- Building and running an app
- UserNotifications
- WidgetKit
- ActivityKit
- Bundle resources