Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 new should 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 through rustferry::storage::Store.
  • src/services/network.rs: OS path status and explicit endpoint probe kept separate.
  • src/lib.rs: shared entry point and guarded Android android_main symbol.
  • src/main.rs: short host/Apple executable entry point.
  • src/capabilities/mod.rs: generated capability module index; cargo ferry add adds 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 check validates source constraints before platform generation. The canonical editable vector source is docs/assets/rustferry-icon.svg.
  • tests/basic.rs: TestRuntime example 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 explicit network::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 for ide operations, devices, and live logs; conflicts with human verbosity flags and --json. Other commands reject it and use --json instead.
  • --dry-run: validate and show intended mutations where the command supports planning.

Commands

CommandCurrent 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
checkValidate config, then run ordinary cargo check
doctor [--all]Read-only host/toolchain inventory
doctor --fix --dry-runPrint fixes; automatic mutation is not implemented
build androidBuild-only Android request; platform readiness and validation level are in STATUS
build ios --simulatorBuild-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|iosBuild, independently validate, select an exact compatible device, then install
run android|iosBuild → validate → install → launch; --logs adds one bounded filtered snapshot where standalone logging is supported
logs android|iosFinite application-filtered history by default; --json-stream runs the live protocol stream until cancellation or platform-tool exit
signing teamsRead-only Apple Development identity/Team inventory
assets check|generateValidate release sources; generate fingerprinted Android densities and an iOS asset catalog
clean [android|ios|generated]Remove only selected generated output below target/ferry/
clean --allRemove cargo-ferry output below target/ferry/, not application source/signing inputs
config validateStrict parse and semantic validation
config show --resolvedPrint resolved defaults
config schemaPrint JSON Schema
config migrateAtomically upgrade a supported older schema; use global --dry-run to inspect first
capabilitiesList known runtime/platform state and current enablement when inside a project
examplesList 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, and TestRuntime.
  • 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

LevelRequired evidence
ImplementedConcrete code path exists; unsupported defaults do not count
Compile validatedRelevant Rust/generated host compiled for that target
Artifact validatedFinal APK, .app, or .appex passed independent structural/tool inspection
Simulator/emulator validatedBehavior was observed in a simulator/emulator
Device validatedBehavior was observed on physical hardware

Platform build paths

PathImplementedCompileArtifactSimulator/emulatorDevice
Host config/runtime/testsYesHost workspace checks; see STATUSN/AN/AN/A
Android direct APKBuild plus typed devices/install/run/logs implementedarm64 generated Rust app plus Java/DEX bridgePublic-CLI starter and Kitchen Sink APKs independently inspectedDeployment runtime not validatedDeployment runtime not validated
iOS Simulator .appBuild plus typed devices/install/run/logs implementedarm64 Slint executablesPublic-CLI starter and Kitchen Sink .app/.appex bundles independently inspectedDeployment runtime not validatedN/A
iOS physical-device appOfficial development-signing build/install/run implemented; explicit Team and provisioning controlsDeterministic arm64/Xcode plan tested; no signing identity availableNot validatedN/ANot validated

Capability evidence

CapabilityHost model/testsAndroid backend/artifactiOS backend/artifactRuntime observation
Lifecycle/event busImplementedBackend and DEX/native callback bridge compiled into inspected APKBackend and dynamic framework artifact-inspectedNone
Network status/probeImplemented model and mockConnectivity/HTTP backend enabled and bridge artifact-inspectedNWPath/URLSession backend and framework artifact-inspectedNone
JSON storageIn-memory/file backend testsApplication-private file backend installed by Android host; target-compiledApplication Support file backend and framework artifact-inspectedNone
HapticsAPI and mockBackend enabled and bridge artifact-inspectedBackend and framework artifact-inspectedNone
Clipboard/share/systemAPI and mockBackends compiled; enabled share provider artifact-inspectedBackends and framework artifact-inspectedNone
Deep linksParser/policy/event testsIntent filter/allowlist backend and bridge artifact-inspectedURL scheme/delegate allowlist and framework artifact-inspectedNone
Local notificationsAPI/model/mockBackend/receiver enabled and artifact-inspectedUserNotifications backend and framework artifact-inspectedNone
PermissionsAPI/model/mockEnabled purpose strings, exact permissions, and bridge artifact-inspectedSupported permission backends and framework artifact-inspectedNone
WidgetSnapshot model/tests + standalone exampleProvider/backend enabled and artifact-inspectedPublisher, timeline renderer, and WidgetKit .appex artifact-inspectedNone
Live ActivityState model/tests + standalone exampleOngoing-notification fallback enabled in an inspected Kitchen Sink APKActivityKit lifecycle bridge and .appex artifact-inspectedNone

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; currently 1;
  • 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 uses registry or an explicit development path;
  • 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:

FieldTypeMeaning
protocol_versionintegerAlways 1 for this protocol
eventstringEvent discriminator
operation_idstringSame value for the full operation
parent_operation_idstring, optionalEnclosing operation
timestamp_msintegerUTC 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:

EventPurpose
operation_startedOpens an operation and names the command/workspace
phase_startedOpens a stable phase
progressBounded or indeterminate progress
command_startedSanitized executable plus argument array, never a shell command
diagnosticFile-bound structured diagnostic
deviceAdded or changed typed device
device_removedDevice removed from a watched snapshot
artifactValidated artifact path and metadata
application_startedConfirmed package/bundle launch
logOne application-specific log record
warningNon-fatal actionable warning
fixStandalone Rust-supplied safe action
phase_finishedCloses a phase with status and duration
operation_finishedCloses success or a typed failure
operation_cancelledCloses 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.toml diagnostics 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:

  1. absolute rustferry.cliPath;
  2. cargo-ferry on PATH;
  3. cargo ferry through cargo on PATH;
  4. the standard Cargo bin directory;
  5. an ancestor checkout’s target/debug/cargo-ferry, only with rustferry.developmentMode enabled.

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:

  1. a local parent directory;
  2. project/crate name, display name, and reverse-DNS application identifier;
  3. a template reported by the selected cargo-ferry CLI;
  4. Android, iOS, or both platforms;
  5. zero or more CLI-reported capabilities;
  6. whether to initialize Git;
  7. 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.

WorkflowCommands
ProjectCreate New Project, Refresh, Select Project, Open ferry.toml, Open src/app.rs
ValidationCheck, Doctor
TargetSelect Target, Build Android, Build iOS Simulator, Build for Physical iPhone, Build Selected Target
CapabilitiesAdd Capability, Remove Capability
Devices and signingRefresh Devices, Select Device, Select Development Team, Run iOS Doctor, Open iOS Signing Guide
DeploymentInstall, Run, Open Logs, Stop Logs
ArtifactsReveal Artifact, Copy Artifact Path, Inspect Artifact Metadata, Delete Generated Artifact…
MaintenanceClean 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.

SettingDefaultMeaning
rustferry.cliPathemptyAbsolute path to cargo-ferry or cargo; empty enables safe discovery.
rustferry.developmentModefalsePermit an ancestor RustFerry checkout’s target/debug/cargo-ferry; never permits arbitrary workspace executables.
rustferry.defaultPlatformandroidInitial target: android, ios-simulator, or ios-device.
rustferry.defaultProfiledebugInitial build profile: debug or release.
rustferry.ios.developmentTeamemptyMachine-scoped, non-secret Team ID selected from installed Apple Development identities.
rustferry.validation.debounceMs350Manifest validation delay, constrained to 100–5000 ms.
rustferry.maxProtocolLineBytes1048576Maximum 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.

OrderPackageRoleInternal prerequisites
1rustferry-coreConfiguration, validation, assets, process controlNone
1rustferryApplication runtime APINone
2rustferry-codegenProject, capability, and asset generationrustferry-core
3rustferry-appleApple generation and artifact backendrustferry-core, rustferry-codegen
3rustferry-androidDirect Android packaging backendrustferry-core, rustferry-codegen
4cargo-ferryPublic CLIAll 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:

  1. rustferry-core and rustferry;
  2. rustferry-codegen;
  3. rustferry-apple and rustferry-android;
  4. 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.toml must 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:

  1. run all checks in VSIX packaging;
  2. inspect the final VSIX allowlist and secret/path scan;
  3. record SHA-256 and size from the exact final bytes;
  4. confirm extension and workspace versions match the intended release;
  5. 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 .crate archives;
  • the versioned VSIX;
  • IDE protocol JSON Schema;
  • canonical and third-party license bundle;
  • changelog-derived release notes;
  • SHA256SUMS covering 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.py with 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.py reports 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 .crate archives and their normalized manifests; confirm both canonical license files are regular root members.

VS Code extension

  • npm ci from editors/vscode.
  • npm run check passes, including VSIX structural smoke.
  • npm run test:host passes with the intended real cargo-ferry binary.
  • Ordinary Rust stays inactive; a ferry.toml workspace auto-activates, registers commands, discovers the project, and opens its manifest.
  • npm run perf results 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

  1. Run the Draft release workflow with the exact workspace version and create_draft_release disabled.
  2. Download the release assembly. Verify package count, schema, versioned VSIX, license bundle, notes, and every entry in SHA256SUMS.
  3. Configure required reviewers on the GitHub release environment.
  4. Rerun the same revision with create_draft_release enabled.
  5. Confirm the workflow accepted successful exact-SHA push jobs for CI, VS Code, both platform artifacts, and mdBook/Pages.
  6. 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-ferry from 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 Unreleased changelog 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

SurfaceThird-party materialRelease treatment
Rust source cratesDependencies resolved by the root Cargo.lockExact names, versions, sources, and SPDX expressions are in LICENSES/cargo-dependencies.json. Root source remains MIT OR Apache-2.0.
cargo-ferry binaryLinked Rust dependencies from the same lockShip the root licenses and the Cargo inventory with binary releases. Preserve any upstream notices required by the selected license branch.
VS Code extensionVS Code API plus Node built-ins; no third-party runtime import in the current production bundleVS Code is external. Packaging excludes node_modules and source maps; the VSIX smoke test enforces that boundary.
VS Code build toolchainPackages in editors/vscode/package-lock.jsonDevelopment-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 glueRustFerry-authored templatesCovered by RustFerry’s MIT OR Apache-2.0 license; no copied platform SDK source is stored in the repository.
Icons and splash assetsOriginal RustFerry development artworkNo external image or font input. Generated derivatives retain the project license. No font files are bundled.
Generated application UISlint 1.17.1 and its resolved application dependency graphThe 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:

  1. 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 AboutSlint widget or the Slint attribution badge on a readily discoverable public application page. It excludes embedded systems, standalone Slint distribution, and applications exposing Slint APIs.
  2. GPL-3.0-only covers open-source applications under GPL-compatible terms and carries GPL source and redistribution obligations for the distributed work.
  3. 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:

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-android for arm64-v8a.
  • Android SDK platform matching android.target_sdk, or any installed platform when it is installed.
  • A complete SDK Build Tools revision containing aapt2, d8, zipalign, and apksigner.
  • Android NDK with an LLVM prebuilt for the host.
  • A JDK containing java, javac, and keytool.

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

  1. Validate ferry.toml and discover a compatible SDK platform, complete Build Tools, NDK LLVM prebuilt, and JDK.
  2. Generate a deterministic AndroidManifest.xml, res/ tree, and private Java runtime bridge under target/ferry/.
  3. Run Cargo once per configured ABI with a target-specific NDK Clang linker and --message-format=json-render-diagnostics.
  4. Parse Cargo compiler-artifact messages for the cdylib. Parse build-script-executed messages and recursively collect dependency .dex files from those OUT_DIRs.
  5. Compile the generated FerryActivity, capability bridge, notification receiver, widget provider, and read-only share provider directly with javac against the selected android.jar.
  6. Compile and link resources with aapt2.
  7. Merge compiled bridge classes and dependency bytecode with d8.
  8. Add stored lib/<abi>/lib<name>.so and classes*.dex entries to the resource APK.
  9. Run zipalign -P 16 -f 4 before signing.
  10. Sign with apksigner, then verify the signature and run zipalign -c -P 16 4.
  11. 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 badging must report the configured package and org.rustferry.bridge.FerryActivity launcher.

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 ABIRust targetAPK directory
arm64-v8aaarch64-linux-androidlib/arm64-v8a/
x86_64x86_64-linux-androidlib/x86_64/
armeabi-v7aarmv7-linux-androideabilib/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/path password sources; or
  • env:VARIABLE_NAME references 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.

ConfigurationAndroid manifest output
network noneno network permission
network statusACCESS_NETWORK_STATE
network optional or requiredACCESS_NETWORK_STATE, INTERNET
configured network probeINTERNET
local notificationsPOST_NOTIFICATIONS
Live Activity ongoing-notification fallbackPOST_NOTIFICATIONS
hapticsVIBRATE
cameraCAMERA
microphoneRECORD_AUDIO
location when in useACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION
photos, target API 33+READ_MEDIA_IMAGES
photos, minimum API below 33READ_EXTERNAL_STORAGE capped with android:maxSdkVersion="32"
local networkno Android declaration; Apple-only privacy permission
widget extensionwidget receiver and provider metadata
sharingnon-exported, read-only application file provider
custom deep-link schemesVIEW/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, and plutil;
  • 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

  1. Validate ferry.toml, Cargo selectors, versions, identifiers, paths, permissions, and extension settings.
  2. Discover full Xcode and the iphonesimulator SDK through xcrun.
  3. Confirm aarch64-apple-ios-sim is installed.
  4. Select CompiledCatalog when discovery finds an available iOS runtime; otherwise select the explicit SdkOnlyResources fallback. Render the corresponding deterministic FerryHost.xcodeproj, scheme, Info.plist, resources, entitlements, and enabled extension sources below target/ferry/ios/generated/.
  5. Run Cargo for aarch64-apple-ios-sim with an isolated target/ferry/ios/cargo/ target directory.
  6. Stage Cargo’s executable under its validated binary name for Xcode’s argument-free Copy Files phase.
  7. Run xcodebuild -target FerryApp -sdk iphonesimulator with 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.
  8. 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.
  9. When WidgetKit is enabled, ad-hoc re-sign the widget with Widget.entitlements, then the application with App.entitlements. Non-widget builds skip this step; signing never uses --deep.
  10. 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:

  • CompiledCatalog generates Assets.xcassets, configures AppIcon and FerryLaunch, and requires Assets.car plus the matching processed plist values. This mode is selected only when an available iOS Simulator runtime is reported.
  • SdkOnlyResources preserves SDK-only builds on hosts with no runtime. It copies FerryIcon.png and FerrySplash.png, records their plist references, byte-compares them with the validated project inputs, and rejects a stray Assets.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:

  • .app is a real directory, not a symlink;
  • Info.plist passes plutil -lint;
  • exact CFBundleIdentifier, CFBundleExecutable, and CFBundlePackageType=APPL;
  • executable exists, is non-empty, and has executable permission bits;
  • xcrun lipo -archs returns exactly arm64;
  • the pre-sign embedded executable matches Cargo’s output byte-for-byte and the signed executable retains its Mach-O UUID;
  • FerryResources.json exists;
  • asset evidence matches the selected mode: either Assets.car with AppIcon/FerryLaunch, or exact FerryIcon.png/FerrySplash.png bytes with the SDK-only plist keys;
  • each top-level framework/library is inspected;
  • expected .appex count, identifiers, executable names, extension point, and architectures match.
  • app, runtime framework, and each .appex have 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:

  • FerryWidgetExtension Xcode target;
  • FerryWidgetExtension.appex product;
  • 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 Extensions phase.

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:

  • FerryLiveActivityExtension Xcode target;
  • FerryLiveActivityExtension.appex product;
  • ActivityAttributes content state;
  • Lock Screen and expanded/compact/minimal Dynamic Island presentations;
  • main-app NSSupportsLiveActivities metadata;
  • extension identifier <app identifier>.liveactivity;
  • embed-target dependency and Embed App Extensions phase.

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 .appex beneath PlugIns/;
  • valid plist and exact bundle identifier;
  • CFBundleExecutable matching 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 .appex bundles.

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.

AreaStatusEvidence
Xcode/xcrun/SDK discoveryImplemented and host-testedXcode 26.6, iPhoneSimulator 26.5
Rust target discoveryImplemented and host-testedaarch64-apple-ios-sim
DoctorImplementedBuild prerequisites separate from optional runtime availability
Deterministic Xcode/plist/assetsImplemented; both asset modes host-testedCompiled-catalog project tests plus a real SDK-only Xcode build
Starter Simulator .appCurrent artifact validationPublic-CLI arm64 .app in Platform run 30719811812, plus the Developer Experience 0.2 SDK-only smoke app
Kitchen Sink Simulator .appCurrent artifact validationPublic-CLI arm64 .app with two embedded extensions in the same run
Runtime bridgeImplemented; current target compile/artifact inspectionarm64 FerryRuntimeBridge.framework; required exports/application hook and strict ad-hoc signature validated
WidgetKitPublisher and renderer implemented; current compile/embed/artifact inspectionRuntime app-group writer plus signed PlugIns/FerryWidgetExtension.appex
ActivityKitStart/update/end/list and presentation implemented; current compile/embed/artifact inspectionRuntime framework plus signed PlugIns/FerryLiveActivityExtension.appex
Simulator install/launch/UICLI implemented; runtime unvalidatedTyped simctl install/launch services exist; no CoreSimulator runtime/device is installed
Physical-device signing/installImplemented; signing and device unvalidatedSide-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 runtimeAndroidiOS
State/event model implemented and testedFile host/event bridge implemented and compiled; runtime unobservedFile 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 Subscription was 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 modelAndroid deliveryiOS delivery
Implemented/testedLifecycle callbacks compiled into inspected bridge artifact; runtime unobservedDelegate 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

HostAndroidiOS
Worker helper testedRust behavior available when host is linkedRust 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: use slint::spawn_local or send a result back with slint::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/mockAndroid path backendiOS path backend
Implemented/tested, including debouncingEnabled Connectivity backend and bridge artifact-inspected; runtime unobservedNWPath 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 Online as 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 mockAndroid probeiOS probe
Gate/probe semantics testedEnabled HTTP probe backend and bridge artifact-inspected; runtime unobservedURLSession 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/mockAndroid bridge/artifactiOS bridge/artifact
Full request lifecycle testedEnabled backend/receiver artifact-inspected; runtime unobservedUserNotifications 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/modelAndroid action bridgeiOS action bridge
Implemented/testedEnabled action/open receiver artifact-inspected; runtime unobservedAction/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

BoundaryStatus
Application payload model and routingAvailable as ordinary Rust code
Local notification scheduling and displayImplemented; see Local notifications
Apple Push Notification service (APNs) registration and receiptNot implemented
Firebase Cloud Messaging (FCM) registration and receiptNot implemented
Provider/server deliveryNot implemented; no built-in sender or server component
Live Activity remote updatesNot 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_now or notifications::schedule as 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 backendAndroid host installiOS host install
Atomic/corruption/migration testsApplication-private backend installed by Android host; target-compiled; runtime unobservedApplication 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 mockAndroid bridgeiOS bridge
Calls recorded/testedEnabled backend and bridge artifact-inspected; runtime unobservedBackend 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): check haptics::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 mockAndroid bridgeiOS bridge
Implemented/testedBackend implemented; bridge compiled; runtime unobservedBackend 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_text and can_write_text separately.
  • 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 mockAndroid bridgeiOS bridge
Requests recorded/testedEnabled file provider and bridge artifact-inspected; runtime UI unobservedBackend 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 mockAndroid intents/artifactiOS schemes/artifact
Implemented/testedIntent filter/allowlist bridge artifact-inspected; runtime unobservedURL 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 mockAndroid permission bridgeiOS permission bridge
Status/request/rationale testedExact enabled permissions/purpose strings and bridge artifact-inspected; runtime prompts unobservedSupported 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 to permissions::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/modelAndroid provider APKiOS WidgetKit .appex
Implemented/testedEnabled provider/backend artifact-inspected; runtime unobservedApp-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 modelAndroid fallback artifactiOS ActivityKit .appex
Lifecycle implemented/testedFallback enabled in an inspected APK; runtime unobservedLifecycle 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 snapshotiOS extension artifactSimulatorPhysical device
Implemented/testedPresentation and lifecycle framework artifact-inspectedNot validatedNot 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

LinuxmacOSWindowsMobile SDK required
Host testsHost testsHost tests/path coverageNo

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: RuntimeGuard intentionally is not Send; use rustferry::spawn or 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 contractAndroid contextiOS applicationExternal plugin registry
Implemented/testedImplemented and target-compiledImplemented and framework artifact-inspectedNot 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 true from supports while 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

ExtensionStatus
Construct a Runtime with a custom backendImplemented and tested
Override an existing Operation methodImplemented and tested
Add a new CLI capability through an adapterNot supported
Register an adapter dynamically in generated hostsNo generic registry
Add platform declarations or generated native codeRequires 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 true from supports without overriding the corresponding trait method.
  • Replacing a full platform backend and accidentally dropping operations the application still needs.
  • Assuming a PlatformBackend implementation is automatically discovered by generated Android or Apple hosts.
  • Adding a new Operation locally 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

  1. Load Cargo metadata and strict ferry.toml.
  2. Discover one coherent SDK platform, Build Tools revision, NDK LLVM toolchain, Java toolchain, and configured Rust targets.
  3. Cross-compile the Rust cdylib for each ABI. Cargo JSON identifies the exact native artifact and constrained build-script output directories.
  4. Generate a deterministic manifest, resources, and NativeActivity metadata below target/ferry/android/.
  5. Compile and link resources with aapt2 against the selected android.jar.
  6. Collect required dependency DEX, and compile/merge generated JVM bridge code with javac and d8 only when needed.
  7. Insert uncompressed native libraries under lib/<abi>/ and sequential classes*.dex entries.
  8. Run zipalign before signing, then sign with apksigner.
  9. 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

  1. Require macOS and discover full Xcode, the selected developer directory, iPhone Simulator SDK, xcodebuild, xcrun, plutil, Cargo, and the Rust simulator target.
  2. Load Cargo metadata and strict ferry.toml.
  3. Generate deterministic host metadata below target/ferry/ios/.
  4. Cross-compile Rust for aarch64-apple-ios-sim on Apple Silicon.
  5. Stage the Rust executable into the generated host and let Xcode assemble metadata, resources, and enabled extension targets into an .app.
  6. Inspect bundle identifier, executable, architecture, metadata, resources, linked content, and every required .appex before 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 Send or Sync without 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:

MeasurementObservation
Extension Host startup to auto-activation6,191 ms
Extension activation1,327.817 ms
Project discovery44.227 ms
Initial discovery and tree refresh1,323.933 ms
Repeated tree refresh, median of 539.047 ms
Open discovered manifest75.862 ms
CLI handshake, median / p95 of 79.644 / 10.973 ms
Configuration validation, median / p95 of 711.605 / 14.342 ms
Parse 10,000 protocol events, median / p95 of 79.877 / 11.797 ms
Parse 100,000-event long stream135.939 ms
Long-stream peak / retained heap delta2,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

CandidateAndroidiOSMain issue
Slint 1.17.1First-party android-activity backend and mobile lifecycle integrationFirst-party Winit/Skia instructionsLicense choice and attribution must be explicit; mobile Skia clean builds are heavy
Dioxus 0.7.9First-party mobile workflowFirst-party mobile workflowStable Android workflow generates Gradle and Kotlin, conflicting with the default no-Gradle requirement
egui/eframe 0.35Official Android exampleeframe does not list iOS as supportediOS path and mobile IME/safe-area UX are weaker
raw Winit/android-activityPlatform event loopPlatform event loopNot a UI toolkit; choosing it would require the prohibited custom renderer/widgets/text stack

Primary references:

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.toml model and JSON Schema.
  • Typed CLI output in human and stable JSON forms.
  • Atomic cargo ferry new with shared templates and capability fragments.
  • Starter with real UI, state, lifecycle, async, network, storage, error, permission, and notification examples.
  • Host-only cargo ferry check and rustferry::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 cdylib cross-compilation for arm64-v8a.
  • Deterministic manifest, resources, native host, and optional JVM bridge generation.
  • aapt2 compile/link, optional javac/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.
  • .app build 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-android and aarch64-apple-ios-sim are 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 .appex passed structural/toolchain inspection.
  • Runtime-validated: behavior was observed on a simulator, emulator, or device.
  • Blocked: a named environmental prerequisite is absent.
AreaAndroidiOS SimulatorHost testsEvidence
Workspace/configShared model implementedShared model implementedTargeted tests passStrict schema, semantic validation, CLI JSON/human tests
Starter generationImplemented; all eight templates host-checkImplemented; all eight templates host-checkActual CLI generation/check passedAtomic generator plus six standalone projects
UI backendSlint 1.17.1 target-compiled into a public-CLI APKSlint 1.17.1 public-CLI .app artifact-validatedSix Slint examples compileADR-001
Build pipelineDirect arm64 public-CLI APKs artifact-validatedPublic-CLI starter, widget, and Live Activity artifacts validatedBuild-plan/golden testsDirect packager and generated Xcode host
Lifecycle/network/storageBackends implemented; bridge compiled into inspected APK; runtime unobservedBackends implemented; framework artifact-inspected; runtime unobservedModels, mocks, and examples passTestRuntime plus platform bridge tests
Local notificationsBackend/receiver implemented and artifact-inspected; runtime unobservedBackend implemented and framework artifact-inspected; runtime unobservedModel and complete mock flow passNotifications example plus generated-bridge tests
WidgetProvider/backend implemented and artifact-inspected; runtime unobservedState publisher, WidgetKit .appex, and framework artifact-inspectedSnapshot model/example passAndroid probe plus combined iOS extension app
Live ActivityOngoing-notification fallback enabled in an inspected APK; runtime unobservedActivityKit lifecycle bridge and .appex artifact-inspectedState model/example passPublic-CLI Kitchen Sink plus Live Score
Devices/install/run/logsTyped ADB services and IDE protocol implemented; runtime unobservedTyped simctl services and IDE protocol implemented; no Simulator runtime/deviceService, parser, protocol, schema, fixture, and CLI tests passOfficial-tool argument arrays, explicit device IDs, validated artifacts, bounded application logs; no runtime claim
Physical iOS developmentN/AOfficial arm64/Xcode build plan, explicit Team/provisioning policy, recursive signature/profile/entitlement validation, devicectl install/launch implementedSigning/deployment unit tests and public CLI dry-run passNo identity, Team, profile, artifact, or physical device available; not artifact/device validated
IDE and VS CodeSame CLI build/deploy serviceSame CLI build/deploy serviceProtocol 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 CLIInstallable VSIX and real Extension Host smoke-tested; no mobile runtime claim
AssetsFive-density launcher icons and splash integrated into an inspected signed/aligned arm64 APKCompiledCatalog implemented/tested; runtime-free SdkOnlyResources integrated into an inspected signed arm64 .appSource validation, SHA-256 cache integrity, concurrent publication, tamper rejection, packaging, and artifact tests passFull 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.toml validation uses bounded UTF-8 stdin and never writes the editor buffer to disk.
  • Human CLI exposes devices, install, run, logs, signing teams, and assets check/generate. cargo ferry logs --json-stream shares 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-run produces 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-name are 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 audit with 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-256 ba8cac7e8d5ec10d3c7a96082f405c3d4d5cdd64afef82bc1f50a5a3d183ce6d.
  • 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 .app bundles passed plist, resource, architecture, and deep/strict ad-hoc signature checks. Inspection covered FerryRuntimeBridge.framework, its required exports and application hook, embedded WidgetKit and ActivityKit .appex products, 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 rustferry and its doctests passed.
  • Every Rust fence across the 20 cookbook pages compiled and ran through rustdoc --test.
  • cargo check --all-targets and the focused TestRuntime integration test passed for Counter, Network Guard, Notifications, Widget Counter, Live Score, and Kitchen Sink.
  • Actual CLI generation followed by cargo ferry check passed for starter, minimal, counter, network, notifications, widget, live-activity, and kitchen-sink with the source runtime override.
  • All six example ferry.toml files passed cargo 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-msvc all-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.acceptance and org.cargopocket.kitchensink, API 26 minimum, API 35 target, generated icon/resources, and org.cargopocket.bridge.PocketActivity as a singleTop launcher;
  • the compiled icon and splash resources byte-match both then-current project inputs (SHA-256 751ec3d49aff1e091c1fe0037060cd71701e3b03fd55031b078201afd10b7464);
  • classes.dex with the generated activity, file provider, notification receiver, and widget provider classes, each matching an exact manifest component;
  • the configured acceptance and kitchensink deep-link schemes;
  • the exact configured permission/component sets, including the enabled notification receiver, widget provider, and private file provider in Kitchen Sink;
  • one arm64-v8a ELF64 AArch64 Rust library with android_main and 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.appex and PocketLiveActivityExtension.appex, each with an arm64 executable, exact plist metadata, and com.apple.widgetkit-extension extension point; the Activity extension links the runtime framework by its exact @rpath install 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-darwin installed.
  • 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, and keytool available.
  • Rust targets aarch64-linux-android and aarch64-apple-ios-sim are 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-ferry communicate 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/ferry root.
  • 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, and sms, 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 .apk or .app suffixes 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_modules from 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

Apple

Security