23 Commits

Author SHA1 Message Date
Tony 3ac748ff28 feat: add device ID override functionality to ShorebirdUpdater
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
ci / 🦀 Build ${{ matrix.crate }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / shorebird_code_push (push) Has been cancelled
Shorebird CI / shorebird_code_push_example (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
- Introduced `setDeviceIdOverride` method in `ShorebirdUpdater` to allow clients to set a custom device ID for patch checks.
- Implemented the method in `ShorebirdUpdaterImpl` for both IO and web platforms.
- Updated the `Updater` class to handle the device ID override in native bindings.
- Added tests for the new functionality in both IO and web test suites, ensuring proper behavior when the updater is available and unavailable.
2026-06-24 03:02:58 +08:00
Eric Seidel 96fd32796e feat: stage 2 integration tests — fake patch server + golden path + rollback regression (#355)
Lands the fake HTTP server, the patch fixture pipeline, and the first
two scenarios that exercise the real FFI path end-to-end. Plus a test
that would have caught the patch-to-release rollback bug
(shorebirdtech/shorebird#3728).

Architecture follows the principles surfaced in review:

- Dart tests call the public `ShorebirdUpdater` API only — never the
  raw `Updater` FFI wrapper, never the engine API.
- Engine API stays inside the `library_test_hooks` Rust crate.
  `shorebird_test_init` constructs `AppParameters` + stub
  `FileCallbacks` internally so Dart never sees those types.
  `shorebird_test_simulate_successful_launch` wraps the
  start/success protocol so the Dart layer never knows there's a
  protocol — it just knows "the engine reported a successful boot."
- ffigen scans only the test_hooks header. The engine header
  (updater_engine.h) does not appear in the Dart bindings.
- A `TestEngine` Dart helper concentrates engine-side simulation in
  one place; the test bodies stay focused on `ShorebirdUpdater`.

Implementation choices worth flagging:

- FFI calls run via `Isolate.run`. Synchronous run blocks the main
  isolate, deadlocking against the in-isolate shelf server. The
  test's IsolateRun callback re-opens the cdylib (cheap: dlopen is
  ref-counted) and resets `Updater.bindings` in the sub-isolate
  because Dart isolates do not share static fields.
- `libapp_path` must be a real file on the desktop integration build:
  the non-Android non-iOS non-test `patch_base` reads it directly
  from disk. Tests that install a patch write the fixture's `base`
  bytes to `libapp.so` before init.
- Fake server kept minimal: shelf, no Range support, no auth, no
  concurrency knobs. Stage 3+ scenarios (download cutoff, hash
  mismatch loop, etc.) extend it as needed.

Three scenarios cover three reasons we wanted this suite:

1. `checkForUpdate returns upToDate when server has no patch` —
   baseline: confirms the harness boots cleanly and returns the
   expected enum.
2. `install a patch and boot from it` — golden path: check →
   update → simulateSuccessfulLaunch, then assert
   `readCurrentPatch` / `readNextPatch` / `checkForUpdate`
   transitions match the public API contract.
3. `checkForUpdate returns restartRequired after patch-to-release
   rollback` — regression for shorebirdtech/shorebird#3728. Pre-fix
   this returned `upToDate` and left no signal to prompt a restart.

Verified locally: 232 Rust unit tests + 44 Dart tests (41 existing
unit + 3 new integration) green; clippy/fmt/cspell clean.
2026-05-06 08:11:47 -07:00
Eric Seidel 8072ed9ad7 feat: stage 1 integration test harness (#353)
* docs: integration tests design proposal

Adds docs/integration_tests.md proposing a desktop-only integration suite
that drives the Dart `ShorebirdUpdater` API against the real Rust core via
FFI, with a fake HTTP server and per-test tempdir state. Describes a new
`library_test_hooks` cdylib that reaches into `updater` via a `test-hooks`
Cargo feature, so production C API stays clean. Status: design exploration,
not a committed plan.

* feat: stage 1 integration test harness for shorebird_code_push

Lands the test_hooks crate, the Dart-side loader, and one trivial
end-to-end test, per docs/integration_tests.md stage 1.

Pieces:

- New `library_test_hooks` workspace crate (cdylib). Depends on `updater`
  with a new `test-hooks` Cargo feature that widens the visibility of
  internal items (currently `testing_reset_config`) for sibling-crate
  access only — production builds do not enable it. The crate also
  re-exports `updater::c_api::dart::*` and `updater::c_api::engine::*`,
  which keeps the rlib's `#[no_mangle]` symbols out of DCE so the
  resulting cdylib carries both the production C API and the new
  `shorebird_test_*` hooks.
- Workspace `default-members` excludes `library_test_hooks` so plain
  `cargo build` / `cargo test` invocations don't unify the `test-hooks`
  feature into production builds.
- `shorebird_code_push/test/integration/all_test.dart` (single file,
  with a header comment explaining why) loads the cdylib via
  `DynamicLibrary.open`, reassigns the existing `@visibleForTesting`
  `Updater.bindings` setter, and exercises both surfaces. The build
  helper shells out to `cargo build -p library_test_hooks`; if it
  fails, `markTestSkipped` keeps the suite green on machines without
  a working Rust toolchain.
- ffigen config (`ffigen_test_hooks.yaml`) generates the test-only
  Dart bindings under `test/integration/generated/`, not `lib/`.
- CI: `library_test_hooks` is added to the rust_crate matrix, and the
  shorebird_code_push job triggers on `library/**` and
  `library_test_hooks/**` so cdylib changes can't break the Dart-side
  integration suite without CI noticing.

Verified locally: 232 Rust unit tests + 42 Dart tests (41 existing + 1
integration) green; clippy/fmt/cspell clean; production cdylib does
not contain `shorebird_test_reset` (`nm` confirms feature isolation).

Stages 2 (FakePatchServer + golden path) and 3 (adversarial scenarios)
land separately.

* fix(integration test): early-return on skip and bump per-test timeout

Two issues caught by Shorebird CI on PR #353:

1. `markTestSkipped` does not abort test execution — it only flags the
   test as skipped on its way out. The body kept running and crashed on
   the `late testHooks` field when the cdylib build had failed in
   setUpAll. Move the skip check into the test body itself with an
   early return; drop the (no-op) skip in `setUp`.

2. Default per-test timeout (30s) also covers `setUpAll`. A cold
   `cargo build -p library_test_hooks` compiles `updater` and ~100
   transitive deps, which can run minutes on CI. Bump to 10 minutes
   via `@Timeout` on the library.

Verified locally: passes when cargo is on PATH (1 passed), reports a
clean skip and exit 0 when cargo is removed from PATH (1 skipped).

* refactor(integration test): drop ! by promoting testHooks to late final

`testHooks` was nullable so accessing it after the skipReason check
required `!`, and `markTestSkipped(skipReason!)` had the same smell.
Make `testHooks` `late final` (non-nullable, throws if read before
setUpAll assigns) and pull `skipReason` into a non-null local before
use. Same control flow, no bang operators.
2026-05-05 16:32:57 -07:00
Eric Seidel 34509fca3c refactor: split C API into Dart and engine surfaces (#350)
The C surface in `library/src/c_api` was a single bucket of `pub extern "C"`
functions covering both consumers — `package:shorebird_code_push` (via
ffigen) and Shorebird's Flutter engine fork (via direct C++ link). That
made it hard to reason about which symbols are stable ABI versus
internal, and ffigen was generating bindings for engine-only symbols
that no Dart code calls.

Split into two self-contained submodules and two cbindgen-generated
headers:

- `c_api::dart` → `include/updater_dart.h` (stable ABI; ffigen entry
  point). Defines `UpdateResult`, the `SHOREBIRD_*` status constants, and
  the five Dart-stable functions: `shorebird_current_boot_patch_number`,
  `shorebird_next_boot_patch_number`,
  `shorebird_check_for_downloadable_update`,
  `shorebird_update_with_result`, `shorebird_free_update_result`.
- `c_api::engine` → `include/updater_engine.h` (no stability guarantee).
  Defines `AppParameters`, `FileCallbacks`, and the engine-only functions:
  `shorebird_init`, `shorebird_should_auto_update`,
  `shorebird_validate_next_boot_patch`, `shorebird_next_boot_patch_path`,
  `shorebird_free_string`, `shorebird_start_update_thread`, and the
  `shorebird_report_launch_*` trio.

Each bucket file is self-contained: cbindgen scans only the file
(`with_src` in build.rs) and emits the items it defines plus the C
types they reference. There are no exclude/include lists in the
cbindgen configs — adding a function to one bucket automatically lands
it in the right header, and items in the other bucket cannot leak.

`mod.rs` shrinks to a thin layer of private helpers shared by both
buckets (`to_rust`, `allocate_c_string`, `free_c_string`, `log_on_error`)
plus the test module.

`include/updater.h` is removed; consumers include the specific header
for their use case. The Flutter engine's
`shell/common/shorebird/updater.cc` will be updated in a follow-up
engine-repo PR to include `updater_engine.h` directly.

Also drops two retired Dart-side symbols:

- `shorebird_update` (replaced by `shorebird_update_with_result` in the
  Dart 2.0 rewrite, Nov 2024).
- `shorebird_check_for_update` (replaced by
  `shorebird_check_for_downloadable_update` in the same rewrite).

The shorebird_code_push package's `_legacyFallback` was the only path
that still called `shorebird_update`. The package's `flutter: >=3.24.5`
constraint guarantees the engine has `shorebird_update_with_result`, so
the fallback was unreachable in practice. Removing it lets us drop the
ABI symbol.

Bumps shorebird_code_push to 2.0.7. Bindings regenerated via ffigen now
contain only the five Dart-stable symbols.

Follow-up engine PR will: include `updater_engine.h` instead of the
removed `updater.h`; clean up `android_exports.lst` (drop the ghost
`shorebird_active_path` and `shorebird_active_patch_number` exports,
drop `shorebird_check_for_update`).
2026-05-04 15:56:09 -07:00
Eric Seidel 563f1b773a fix: return UpdateInProgress status instead of erroring when another update is running (#335)
When `update()` is called while another update (typically the automatic
updater thread) is already running, the Rust updater previously bailed
with `UpdateError::UpdateAlreadyInProgress`, which surfaced in Dart as
`UpdateException: Update already in progress (unknown)`. This is the
single highest-volume `UpdateException` in customer telemetry, yet the
underlying situation is benign — the in-flight update continues on its
own, the caller simply did not start a new one.

Add a new `UpdateStatus::UpdateInProgress` variant and matching C
status code `SHOREBIRD_UPDATE_IN_PROGRESS = 4`. `updater::update()`
catches the `UpdateAlreadyInProgress` error from the lock helper and
maps it to `Ok(UpdateStatus::UpdateInProgress)`. The Dart wrapper
treats the new status as a successful return alongside
`SHOREBIRD_UPDATE_INSTALLED`, so `update()` no longer throws for this
case.

Update the existing `usage_during_hung_update` c_api test to assert the
new contract, and add a Dart test covering the in-progress return path.

Version skew: new Dart on an old engine still sees the legacy
`SHOREBIRD_UPDATE_ERROR` + "Update already in progress" message and
will still throw. The fix lands once both sides ship.

Partially addresses shorebirdtech/shorebird#3682 — does not resolve the
broader asymmetry of `update()` semantics (it still does not wait for
someone else's in-flight update to finish), which remains as v2 design
work in shorebirdtech/shorebird#3684.
2026-04-08 01:22:04 +00:00
Eric Seidel b8987364e3 fix: do not throw UpdateException on SHOREBIRD_NO_UPDATE (#334)
`ShorebirdUpdater.update()` previously threw `UpdateException: No update
(noUpdate)` whenever the patch check returned no available update, even
though that is a successful outcome of calling `update()` — the app is
already running the latest patch.

The wrapper only returned normally on `SHOREBIRD_UPDATE_INSTALLED`;
every other status (including `SHOREBIRD_NO_UPDATE`, value 0) fell
through to the generic `throw UpdateException(...)` path, which is
why callers saw `UpdateException: No update (noUpdate)` in their
exception telemetry.

Treat `SHOREBIRD_NO_UPDATE` as a successful return alongside
`SHOREBIRD_UPDATE_INSTALLED`. No FFI change, safe against any engine
version (status code 0 is stable).

Fixes shorebirdtech/shorebird#3681
2026-04-07 18:05:53 -07:00
Eric Seidel 6fe57f4ec8 fix: checkForUpdate reports restartRequired when current patch is rolled back (#312)
Previously, checkForUpdate returned upToDate after a rollback because
the condition `next != null && current?.number != next.number` treated
a null next patch as "up to date". After a rollback, the Rust updater
correctly uninstalls the patch (next becomes null), but the app is still
running the rolled-back patch (current is non-null). The simplified
condition `current?.number != next?.number` correctly detects this
mismatch and returns restartRequired.

Fixes https://github.com/shorebirdtech/shorebird/issues/3206
2026-03-30 14:45:00 -07:00
0xcf2f 38efba0e6a feat(shorebird_code_push): override toString in exceptions (#266)
Co-authored-by: Felix Angelov <felix@shorebird.dev>
2025-02-07 15:47:56 -06:00
Eric Seidel 2ae6afbb65 fix: unbreak web build for 2.x (#241)
* fix: unbreak web build

* chore: add cspell config
2024-11-20 11:35:49 -06:00
Felix Angelov ee3f5ec669 feat(shorebird_code_push): track support (#232)
* feat(shorebird_code_push): track support

* cleanup and add todos

* docs

* run ffigen

* use c_char instead of char

* Add channel support

* tests

* tests

* update podfile.lock

* Update example to include tracks selector

---------

Co-authored-by: Bryan Oltman <bryan@shorebird.dev>
Co-authored-by: Bryan Oltman <bryanoltman@gmail.com>
2024-11-12 14:58:50 -05:00
Felix Angelov 6f1be35bd3 feat(shorebird_code_push): rewrite Dart API (#225) 2024-11-04 12:22:37 -06:00
Eric Seidel 3ad4166f23 feat: add a longer no-op message. (#218)
This is a common source of confusion, hopefully this will help.
2024-10-04 14:08:41 -05:00
Bryan Oltman ef20c33725 chore(shorebird_code_push): remove error log when shorebird is unavailable (#101)
* chore(shorebird_code_push): remove error log when shorebird is unavailable

* fix tests
2023-10-20 12:36:31 -04:00
Eric Seidel e14f630f91 refactor: move shorebird_code_push_io and _web into src/ (#75)
* break: move shorebird_code_push_io and _web into src/

Users were sometimes importing the _web version thinking it
was the "web safe" import, rather than shorebird_code_push.dart
and then being surprised when ShorebirdCodePush did nothing.

Fixes https://github.com/shorebirdtech/shorebird/issues/1209

* Update formating for Dart 3.1
2023-09-07 07:38:15 -07:00
Felix Angelov 4ceb37dc1e refactor(shorebird_code_push): ffi + noop implementation and web compat (#55) 2023-07-14 15:30:35 -05:00
Bryan Oltman 0be41d9d85 chore(shorebird_code_push): Release 1.0.0, change downloadUpdate to downloadUpdateIfAvailable (#46)
* Release 1.0.0, change downloadUpdate to downloadUpdateIfAvailable

* update readme and contributing

* add dart doc

* add discord badge

* remove doc checkin

* formatting
2023-06-29 16:42:56 -04:00
Eric Seidel 3af4081f55 fix: Only allow one update attempt at a time (#44)
This is all for shorebirdtech/shorebird#695

I ended up fixing a lot of things in our Rust code while I was in there, including:

* Using PathBuf instead of Sting wherever we are holding onto Path objects (this removed a bunch of code which was converting in and out of these).
* Made a variety of functions private (the default) that were previously mistakenly pub.
* Moved the android hacks around libapp_paths closer to the C++ code. Previously we were storing all of the libapp_paths on ResolvedConfig and handling the android hacks during patch install. Now we're doing the android path hacks on init, eventually we'll remove them entirely by making the C++ code pass us in the android app_dir or even the resolved .apk we need instead of the libapp_paths.
* Fixed several tests to use /dir/lib/arch/libapp.so paths instead of 'libapp.so' now that the init code applies the android hacks an expects the deeper paths. Again, this will go away when we move the android hacks into c++.
* Added a test which confirms that if we init twice we log instead of trying to init again (this was one behavior which FCM was triggering as part of fix: ANR when using Firebase Cloud Messaging with Shorebird shorebird#695
* Add a test which confirms that further calls to update while an update is ongoing just fail out quickly instead of hanging.
* Moved to serial_test instead of a separate per-thread config system for testing. This makes our tests operate more like the code does in the wild, at the cost of needing to annotate a test as #[serial] any time it needs to use shorebird_init.
* Removed ResolvedConfig and now call it UpdateConfig. ResolvedConfig previously had a bool on it to tell if it was initialized, that's now represented as Option instead which is more rusty.
* Changed how we handle NetworkHooks from being compile-time test switched to using function pointers held off of UpdateConfig.network_hooks. I think this makes more sense? I did this to remove the special ThreadConfig object which was used during unit tests, but didn't exist in production. I think the way we mock networking could still be improved.
2023-06-22 12:45:55 -07:00
Bryan Oltman 7fc761d71f feat: add restart button to example (#43) 2023-06-22 12:11:57 -04:00
Bryan Oltman 62ed676140 feat(shorebird_code_push): add download update functionality (#37)
* feat: add shorebird_current_boot_patch_number function

* Continue support for next_patch_version

* rename, return null in case of 0

* feat(shorebird_code_push): add download update functionality

* rename defaultValue to fallbackValue

* add forTest constructor to ShorebirdCodePush
2023-06-20 17:11:30 -04:00
Bryan Oltman 318d86e13f feat: add shorebird_current_boot_patch_number function (#36)
* feat: add shorebird_current_boot_patch_number function

* Continue support for next_patch_version

* rename, return null in case of 0
2023-06-20 16:45:43 -04:00
Bryan Oltman f85eda8173 feat(shorebird_code_push): update example to use code push bindings (#33)
* feat(shorebird_code_push): add updater bindings and dart support

* newlines

* docs

* feat(shorebird_code_push): update example to use code push bindings

* Move checkForUpdate into setState

* Update to reflect patch number being sent as int

* polish

* updater test coverage

* Update shorebird_code_push/example/lib/main.dart

Co-authored-by: Felix Angelov <felix@shorebird.dev>

* Analysis

---------

Co-authored-by: Felix Angelov <felix@shorebird.dev>
2023-06-20 10:15:56 -04:00
Bryan Oltman ea48f6cb4f feat(shorebird_code_push): add updater bindings and dart support (#32)
* feat(shorebird_code_push): add updater bindings and dart support

* newlines

* docs

* Remove unused support for dart cli

* clarify Android-specific setup in readme
2023-06-19 20:53:51 -04:00
Bryan Oltman 27e7984179 feat: add shorebird_code_push package (#30)
* feat: add shorebird_code_push package

* remove example and extra code

* update github workflow

* fix gha
2023-06-17 14:17:03 -04:00