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`).
This commit is contained in:
Eric Seidel
2026-05-04 15:56:09 -07:00
committed by GitHub
parent 10aaca0f8b
commit 34509fca3c
20 changed files with 1407 additions and 1018 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ Design principle: "fail open" — always fall back to the currently installed ve
## Key Details ## Key Details
- C header (`include/updater.h`) is auto-generated by `cbindgen` via `build.rs` — don't edit manually. - C headers (`include/updater_dart.h` for the ffigen-consumed Dart-stable surface, `include/updater_engine.h` for the engine-internal surface) are auto-generated by `cbindgen` via `build.rs` — don't edit manually. The two configs live at `library/cbindgen_dart.toml` and `library/cbindgen_engine.toml`.
- Dart FFI bindings (`updater_bindings.g.dart`) are generated by `ffigen` — don't edit manually. - Dart FFI bindings (`updater_bindings.g.dart`) are generated by `ffigen` — don't edit manually.
- Library builds as three crate types: `lib` (Rust tests), `cdylib` (Dart FFI testing), `staticlib` (engine linking). - Library builds as three crate types: `lib` (Rust tests), `cdylib` (Dart FFI testing), `staticlib` (engine linking).
- Boot state machine docs: `docs/boot_state_machine.md`. - Boot state machine docs: `docs/boot_state_machine.md`.
+40 -8
View File
@@ -1,25 +1,57 @@
extern crate cbindgen; extern crate cbindgen;
use std::env; use std::env;
use std::path::{Path, PathBuf};
// See: // See:
// <https://github.com/eqrion/cbindgen/blob/master/docs.md#buildrs> // <https://github.com/eqrion/cbindgen/blob/master/docs.md#buildrs>
// <https://doc.rust-lang.org/cargo/reference/build-scripts.html> // <https://doc.rust-lang.org/cargo/reference/build-scripts.html>
// <https://doc.rust-lang.org/cargo/reference/build-script-examples.html> // <https://doc.rust-lang.org/cargo/reference/build-script-examples.html>
fn main() { fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
// Should this write to the out dir (target) instead? // Each header is generated from a single source file. cbindgen scans
let result = cbindgen::generate(crate_dir); // exactly that file and emits the `pub extern "C"` items it defines plus
// the C types they reference. Since each bucket file is self-contained
// (defines its own types), there is no cross-bucket leak and no need for
// exclusion lists in the cbindgen configs.
generate_header(
&crate_dir,
"cbindgen_dart.toml",
"src/c_api/dart.rs",
"include/updater_dart.h",
);
generate_header(
&crate_dir,
"cbindgen_engine.toml",
"src/c_api/engine.rs",
"include/updater_engine.h",
);
}
fn generate_header(crate_dir: &Path, config_name: &str, src_relative: &str, output_path: &str) {
let config_path = crate_dir.join(config_name);
let config = match cbindgen::Config::from_file(&config_path) {
Ok(config) => config,
Err(e) => {
println!("cargo:warning=Error loading {}: {e}", config_path.display());
return;
}
};
let src_path = crate_dir.join(src_relative);
let result = cbindgen::Builder::new()
.with_src(&src_path)
.with_config(config)
.generate();
match result { match result {
Ok(contents) => { Ok(contents) => {
contents.write_to_file("include/updater.h"); contents.write_to_file(output_path);
} }
Err(e) => { Err(e) => {
println!("cargo:warning=Error generating bindings: {e}"); println!("cargo:warning=Error generating {output_path}: {e}");
// If we were to exit 1 here we would stop local rust // We don't exit non-zero here so local rust-analyzer keeps
// analysis from working. So we just print the error // working when cbindgen has an issue.
// and continue.
} }
} }
} }
-20
View File
@@ -1,20 +0,0 @@
# See https://github.com/eqrion/cbindgen/blob/master/docs.md#cbindgentoml
# for detailed documentation of every option here.
language = "C"
include_guard = "updater_h"
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
cpp_compat = true
line_length = 80
# I don't know if these are required to export the shorebird_ symbols
# since I've hit multiple levels of export trouble in libflutter.so.
# But I'm leaving them here for now.
after_includes = """
#ifdef _WIN32
#define SHOREBIRD_EXPORT __declspec(dllexport)
#else
#define SHOREBIRD_EXPORT __attribute__((visibility("default")))
#endif
"""
[fn]
prefix = "SHOREBIRD_EXPORT"
+30
View File
@@ -0,0 +1,30 @@
# cbindgen configuration for the Dart-stable C surface.
#
# Output: include/updater_dart.h — consumed by ffigen in
# shorebird_code_push. Anything reachable from this header is part of the
# package's public ABI; do not break changes here without bumping the
# package version.
#
# build.rs runs cbindgen with `with_src("src/c_api/dart.rs")`, so cbindgen
# scans exactly that one file and emits the `pub extern "C"` items it
# defines plus the types they reference. Items defined in
# `src/c_api/engine.rs` cannot leak into this header. No exclude/include
# lists needed.
#
# See https://github.com/eqrion/cbindgen/blob/master/docs.md#cbindgentoml
language = "C"
include_guard = "updater_dart_h"
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
cpp_compat = true
line_length = 80
after_includes = """
#ifdef _WIN32
#define SHOREBIRD_EXPORT __declspec(dllexport)
#else
#define SHOREBIRD_EXPORT __attribute__((visibility("default")))
#endif
"""
[fn]
prefix = "SHOREBIRD_EXPORT"
+30
View File
@@ -0,0 +1,30 @@
# cbindgen configuration for the engine-internal C surface.
#
# Output: include/updater_engine.h — consumed only by Shorebird's Flutter
# engine fork. No stability guarantee; both sides ship together as part of
# the engine, so this surface changes freely as the engine integration
# evolves.
#
# build.rs runs cbindgen with `with_src("src/c_api/engine.rs")`, so cbindgen
# scans exactly that one file and emits the `pub extern "C"` items it
# defines plus the types they reference. Items defined in
# `src/c_api/dart.rs` cannot leak into this header. No exclude/include
# lists needed.
#
# See https://github.com/eqrion/cbindgen/blob/master/docs.md#cbindgentoml
language = "C"
include_guard = "updater_engine_h"
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
cpp_compat = true
line_length = 80
after_includes = """
#ifdef _WIN32
#define SHOREBIRD_EXPORT __declspec(dllexport)
#else
#define SHOREBIRD_EXPORT __attribute__((visibility("default")))
#endif
"""
[fn]
prefix = "SHOREBIRD_EXPORT"
-248
View File
@@ -1,248 +0,0 @@
#ifndef updater_h
#define updater_h
/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#ifdef _WIN32
#define SHOREBIRD_EXPORT __declspec(dllexport)
#else
#define SHOREBIRD_EXPORT __attribute__((visibility("default")))
#endif
/**
* An unknown error occurred while updating. The update was not installed.
* This is a catch-all for errors that don't fit into the other categories.
*/
#define SHOREBIRD_UPDATE_ERROR -1
/**
* No update is available (e.g. the app is already up-to-date)
*/
#define SHOREBIRD_NO_UPDATE 0
/**
* An update was installed successfully. It will boot from the update on the
* next app launch.
*/
#define SHOREBIRD_UPDATE_INSTALLED 1
/**
* An error occurred while updating. The update was not installed.
*/
#define SHOREBIRD_UPDATE_HAD_ERROR 2
/**
* The downloaded patch was not installed because it was invalid.
*/
#define SHOREBIRD_UPDATE_IS_BAD_PATCH 3
/**
* Another update was already in progress when this call was made. The
* already-running update will continue; the caller did not start a new one.
* This is a benign outcome, not an error.
*/
#define SHOREBIRD_UPDATE_IN_PROGRESS 4
/**
* Struct containing configuration parameters for the updater.
* Passed to all updater functions.
* NOTE: If this struct is changed all language bindings must be updated.
*/
typedef struct AppParameters {
/**
* release_version, required. Named version of the app, off of which
* updates are based. Can be either a version number or a hash.
*/
const char *release_version;
/**
* Array of paths to the original aot library, required. For Flutter apps
* these are the paths to the bundled libapp.so. May be used for
* compression downloaded artifacts.
*/
const char *const *original_libapp_paths;
/**
* Length of the original_libapp_paths array.
*/
int original_libapp_paths_size;
/**
* Path to app storage directory where the updater will store serialized
* state and other data that persists between releases.
*/
const char *app_storage_dir;
/**
* Path to cache directory where the updater will store downloaded
* artifacts and data that can be deleted when a new release is detected.
*/
const char *code_cache_dir;
} AppParameters;
typedef struct FileCallbacks {
/**
* Opens the "file" (actually an in-memory buffer) and returns a handle.
*/
void *(*open)(void);
/**
* Reads count bytes from the file into buffer. Returns the number of
* bytes read.
*/
uintptr_t (*read)(void *file_handle, uint8_t *buffer, uintptr_t count);
/**
* Moves the file pointer to the given offset relative from whence (one of
* libc::SEEK_SET, libc::SEEK_CUR, or libc::SEEK_END). Returns the new
* offset relative to the start of the file.
*/
int64_t (*seek)(void *file_handle, int64_t offset, int32_t whence);
/**
* Closes and frees the file handle.
*/
void (*close)(void *file_handle);
} FileCallbacks;
typedef struct UpdateResult {
int32_t status;
const char *message;
} UpdateResult;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* Configures updater. First parameter is a struct containing configuration
* from the running app. Second parameter is a YAML string containing
* configuration compiled into the app. Returns true on success and false on
* failure. If false is returned, the updater library will not be usable.
*/
SHOREBIRD_EXPORT
bool shorebird_init(const struct AppParameters *c_params,
struct FileCallbacks c_file_callbacks,
const char *c_yaml);
/**
* Returns if the app should run the updater automatically on launch.
*/
SHOREBIRD_EXPORT bool shorebird_should_auto_update(void);
/**
* The currently running patch number, or 0 if the release has not been
* patched. The internal name for this concept is `running_patch`; the
* FFI symbol keeps the historical `current_boot_patch_number` spelling
* because Flutter Engine links against it.
*/
SHOREBIRD_EXPORT uintptr_t shorebird_current_boot_patch_number(void);
/**
* The patch number that will boot on the next run of the app, or 0 if there is
* no next patch.
*/
SHOREBIRD_EXPORT uintptr_t shorebird_next_boot_patch_number(void);
/**
* Performs integrity checks on the next boot patch. If the patch fails these checks, the patch
* will be deleted and the next boot patch will be set to the last successfully booted patch or
* the base release if there is no last successfully booted patch.
*/
SHOREBIRD_EXPORT
void shorebird_validate_next_boot_patch(void);
/**
* The path to the patch that will boot on the next run of the app, or NULL if
* there is no next patch.
*/
SHOREBIRD_EXPORT char *shorebird_next_boot_patch_path(void);
/**
* Free a string returned by the updater library.
* # Safety
*
* If this function is called with a non-null pointer, it must be a pointer
* returned by the updater library.
*/
SHOREBIRD_EXPORT void shorebird_free_string(const char *c_string);
/**
* Frees an `UpdateResult` previously returned by `shorebird_check_for_update`.
*
* # Safety
*
* `result` must be a valid pointer returned by `shorebird_check_for_update`,
* or null (in which case this is a no-op).
*/
SHOREBIRD_EXPORT void shorebird_free_update_result(struct UpdateResult *result);
/**
* Check for an update. Returns true if an update is available.
*/
SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
/**
* Check for an update on the first non-null channel of:
* 1. `c_channel`
* 2. The channel specified in shorebird.yaml
* 3. The default "stable" channel
*
* Returns true if an update exists that has not yet been downloaded.
*/
SHOREBIRD_EXPORT
bool shorebird_check_for_downloadable_update(const char *c_channel);
/**
* Synchronously download an update if one is available.
*/
SHOREBIRD_EXPORT void shorebird_update(void);
/**
* Synchronously download an update on the first non-null channel of:
* 1. `c_channel`
* 2. The channel specified in shorebird.yaml
* 3. The default "stable" channel
*
* Returns an [UpdateResult] indicating whether the update was successful.
*/
SHOREBIRD_EXPORT
const struct UpdateResult *shorebird_update_with_result(const char *c_channel);
/**
* Start a thread to download an update if one is available.
*/
SHOREBIRD_EXPORT void shorebird_start_update_thread(void);
/**
* Tell the updater that we're launching from what it told us was the
* next patch to boot from. This will copy the next boot patch to be the
* `current_boot` patch.
*
* It is required to call this function before calling
* `shorebird_report_launch_success` or `shorebird_report_launch_failure`.
*/
SHOREBIRD_EXPORT void shorebird_report_launch_start(void);
/**
* Report that the app failed to launch. This will cause the updater to
* attempt to roll back to the previous version if this version has not
* been launched successfully before.
*/
SHOREBIRD_EXPORT void shorebird_report_launch_failure(void);
/**
* Report that the app launched successfully. This will mark the current
* as having been launched successfully. We don't currently do anything
* with this information, but it could be used to record a point at which
* we will not roll back from.
*
* This is not currently wired up to be called from the Engine. It's unclear
* where best to connect it. Expo waits 5 seconds after the app launches
* and then marks the launch as successful. We could do something similar.
*/
SHOREBIRD_EXPORT void shorebird_report_launch_success(void);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* updater_h */
+113
View File
@@ -0,0 +1,113 @@
#ifndef updater_dart_h
#define updater_dart_h
/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#ifdef _WIN32
#define SHOREBIRD_EXPORT __declspec(dllexport)
#else
#define SHOREBIRD_EXPORT __attribute__((visibility("default")))
#endif
/**
* An unknown error occurred while updating. The update was not installed.
* This is a catch-all for errors that don't fit into the other categories.
*/
#define SHOREBIRD_UPDATE_ERROR -1
/**
* No update is available (e.g. the app is already up-to-date)
*/
#define SHOREBIRD_NO_UPDATE 0
/**
* An update was installed successfully. It will boot from the update on the
* next app launch.
*/
#define SHOREBIRD_UPDATE_INSTALLED 1
/**
* An error occurred while updating. The update was not installed.
*/
#define SHOREBIRD_UPDATE_HAD_ERROR 2
/**
* The downloaded patch was not installed because it was invalid.
*/
#define SHOREBIRD_UPDATE_IS_BAD_PATCH 3
/**
* Another update was already in progress when this call was made. The
* already-running update will continue; the caller did not start a new one.
* This is a benign outcome, not an error.
*/
#define SHOREBIRD_UPDATE_IN_PROGRESS 4
typedef struct UpdateResult {
int32_t status;
const char *message;
} UpdateResult;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* The currently running patch number, or 0 if the release has not been
* patched. The internal name for this concept is `running_patch`; the
* FFI symbol keeps the historical `current_boot_patch_number` spelling
* because the Flutter Engine and existing pub releases of
* `shorebird_code_push` link against it.
*/
SHOREBIRD_EXPORT uintptr_t shorebird_current_boot_patch_number(void);
/**
* The patch number that will boot on the next run of the app, or 0 if there is
* no next patch.
*/
SHOREBIRD_EXPORT uintptr_t shorebird_next_boot_patch_number(void);
/**
* Check for an update on the first non-null channel of:
* 1. `c_channel`
* 2. The channel specified in shorebird.yaml
* 3. The default "stable" channel
*
* Returns true if an update exists that has not yet been downloaded.
*/
SHOREBIRD_EXPORT
bool shorebird_check_for_downloadable_update(const char *c_channel);
/**
* Synchronously download an update on the first non-null channel of:
* 1. `c_channel`
* 2. The channel specified in shorebird.yaml
* 3. The default "stable" channel
*
* Returns an [UpdateResult] indicating whether the update was successful.
*/
SHOREBIRD_EXPORT
const struct UpdateResult *shorebird_update_with_result(const char *c_channel);
/**
* Frees an `UpdateResult` previously returned by
* `shorebird_update_with_result`. Frees the embedded `message` string and
* the result allocation itself.
*
* # Safety
*
* `result` must be a valid pointer returned by `shorebird_update_with_result`,
* or null (in which case this is a no-op).
*/
SHOREBIRD_EXPORT void shorebird_free_update_result(struct UpdateResult *result);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* updater_dart_h */
+151
View File
@@ -0,0 +1,151 @@
#ifndef updater_engine_h
#define updater_engine_h
/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#ifdef _WIN32
#define SHOREBIRD_EXPORT __declspec(dllexport)
#else
#define SHOREBIRD_EXPORT __attribute__((visibility("default")))
#endif
/**
* Struct containing configuration parameters for the updater.
* Passed to `shorebird_init`.
* NOTE: If this struct is changed all language bindings must be updated.
*/
typedef struct AppParameters {
/**
* release_version, required. Named version of the app, off of which
* updates are based. Can be either a version number or a hash.
*/
const char *release_version;
/**
* Array of paths to the original aot library, required. For Flutter apps
* these are the paths to the bundled libapp.so. May be used for
* compression downloaded artifacts.
*/
const char *const *original_libapp_paths;
/**
* Length of the original_libapp_paths array.
*/
int original_libapp_paths_size;
/**
* Path to app storage directory where the updater will store serialized
* state and other data that persists between releases.
*/
const char *app_storage_dir;
/**
* Path to cache directory where the updater will store downloaded
* artifacts and data that can be deleted when a new release is detected.
*/
const char *code_cache_dir;
} AppParameters;
typedef struct FileCallbacks {
/**
* Opens the "file" (actually an in-memory buffer) and returns a handle.
*/
void *(*open)(void);
/**
* Reads count bytes from the file into buffer. Returns the number of
* bytes read.
*/
uintptr_t (*read)(void *file_handle, uint8_t *buffer, uintptr_t count);
/**
* Moves the file pointer to the given offset relative from whence (one of
* libc::SEEK_SET, libc::SEEK_CUR, or libc::SEEK_END). Returns the new
* offset relative to the start of the file.
*/
int64_t (*seek)(void *file_handle, int64_t offset, int32_t whence);
/**
* Closes and frees the file handle.
*/
void (*close)(void *file_handle);
} FileCallbacks;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* Free a string returned by the updater library.
*
* # Safety
*
* If this function is called with a non-null pointer, it must be a pointer
* returned by the updater library.
*/
SHOREBIRD_EXPORT void shorebird_free_string(const char *c_string);
/**
* Configures the updater. First parameter is a struct containing
* configuration from the running app. Second parameter is a YAML string
* containing configuration compiled into the app. Returns true on success
* and false on failure. If false is returned, the updater library will not
* be usable.
*/
SHOREBIRD_EXPORT
bool shorebird_init(const struct AppParameters *c_params,
struct FileCallbacks c_file_callbacks,
const char *c_yaml);
/**
* Returns if the app should run the updater automatically on launch.
*/
SHOREBIRD_EXPORT bool shorebird_should_auto_update(void);
/**
* Performs integrity checks on the next boot patch. If the patch fails
* these checks, the patch will be deleted and the next boot patch will be
* set to the last successfully booted patch or the base release if there is
* no last successfully booted patch.
*/
SHOREBIRD_EXPORT void shorebird_validate_next_boot_patch(void);
/**
* The path to the patch that will boot on the next run of the app, or NULL
* if there is no next patch. The caller must free the returned string with
* `shorebird_free_string`.
*/
SHOREBIRD_EXPORT char *shorebird_next_boot_patch_path(void);
/**
* Start a thread to download an update if one is available.
*/
SHOREBIRD_EXPORT void shorebird_start_update_thread(void);
/**
* Tell the updater that we're launching from what it told us was the
* next patch to boot from. This will copy the next boot patch to be the
* `current_boot` patch.
*
* It is required to call this function before calling
* `shorebird_report_launch_success` or `shorebird_report_launch_failure`.
*/
SHOREBIRD_EXPORT void shorebird_report_launch_start(void);
/**
* Report that the app failed to launch. This will cause the updater to
* attempt to roll back to the previous version if this version has not been
* launched successfully before.
*/
SHOREBIRD_EXPORT void shorebird_report_launch_failure(void);
/**
* Report that the app launched successfully. The Shell constructor calls
* this once per process when the VM has finished booting; it pairs with
* `shorebird_report_launch_start` to mark a patch as having booted cleanly.
*/
SHOREBIRD_EXPORT void shorebird_report_launch_success(void);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* updater_engine_h */
+1 -1
View File
@@ -2,7 +2,7 @@ use std::io::{Read, Seek};
use crate::{ExternalFileProvider, ReadSeek}; use crate::{ExternalFileProvider, ReadSeek};
use super::FileCallbacks; use super::engine::FileCallbacks;
struct CFile { struct CFile {
file_callbacks: FileCallbacks, file_callbacks: FileCallbacks,
+138
View File
@@ -0,0 +1,138 @@
//! Dart-stable C API surface.
//!
//! These symbols are consumed by `package:shorebird_code_push` via FFI. The
//! header generated for this module (`include/updater_dart.h`) is the input
//! to ffigen, so anything declared here is part of the package's public ABI.
//! Do not break changes here without bumping the package version.
//!
//! cbindgen reads this file directly (see `build.rs`) and emits exactly the
//! `pub extern "C"` items found here plus the types they reference. Adding
//! a function to this file automatically adds it to the Dart header — no
//! cbindgen-config update required. Conversely, anything declared in
//! `c_api::engine` cannot leak into this header.
//!
//! For symbols consumed only by Shorebird's Flutter engine, see
//! `c_api::engine` — that surface is unstable and changes freely.
use std::os::raw::c_char;
use super::{allocate_c_string, free_c_string, log_on_error, to_rust_option};
use crate::{updater, UpdateStatus};
/// An unknown error occurred while updating. The update was not installed.
/// This is a catch-all for errors that don't fit into the other categories.
pub const SHOREBIRD_UPDATE_ERROR: i32 = -1;
/// No update is available (e.g. the app is already up-to-date)
pub const SHOREBIRD_NO_UPDATE: i32 = 0;
/// An update was installed successfully. It will boot from the update on the
/// next app launch.
pub const SHOREBIRD_UPDATE_INSTALLED: i32 = 1;
/// An error occurred while updating. The update was not installed.
pub const SHOREBIRD_UPDATE_HAD_ERROR: i32 = 2;
/// The downloaded patch was not installed because it was invalid.
pub const SHOREBIRD_UPDATE_IS_BAD_PATCH: i32 = 3;
/// Another update was already in progress when this call was made. The
/// already-running update will continue; the caller did not start a new one.
/// This is a benign outcome, not an error.
pub const SHOREBIRD_UPDATE_IN_PROGRESS: i32 = 4;
#[repr(C)]
pub struct UpdateResult {
pub status: i32,
pub message: *const libc::c_char,
}
fn to_update_result(status: anyhow::Result<UpdateStatus>) -> UpdateResult {
match status {
Ok(status) => {
let message = status.to_string();
UpdateResult {
status: status as i32,
message: allocate_c_string(message.as_str()).unwrap_or(std::ptr::null_mut()),
}
}
Err(err) => UpdateResult {
status: SHOREBIRD_UPDATE_ERROR,
message: allocate_c_string(&err.to_string()).unwrap_or(std::ptr::null_mut()),
},
}
}
/// The currently running patch number, or 0 if the release has not been
/// patched. The internal name for this concept is `running_patch`; the
/// FFI symbol keeps the historical `current_boot_patch_number` spelling
/// because the Flutter Engine and existing pub releases of
/// `shorebird_code_push` link against it.
#[no_mangle]
pub extern "C" fn shorebird_current_boot_patch_number() -> usize {
log_on_error(
|| Ok(updater::running_patch()?.map_or(0, |p| p.number)),
"fetching running_patch_number",
0,
)
}
/// The patch number that will boot on the next run of the app, or 0 if there is
/// no next patch.
#[no_mangle]
pub extern "C" fn shorebird_next_boot_patch_number() -> usize {
log_on_error(
|| Ok(updater::next_boot_patch()?.map_or(0, |p| p.number)),
"fetching next_boot_patch_number",
0,
)
}
/// Check for an update on the first non-null channel of:
/// 1. `c_channel`
/// 2. The channel specified in shorebird.yaml
/// 3. The default "stable" channel
///
/// Returns true if an update exists that has not yet been downloaded.
#[no_mangle]
pub extern "C" fn shorebird_check_for_downloadable_update(c_channel: *const c_char) -> bool {
log_on_error(
|| {
let channel = to_rust_option(c_channel)?;
updater::check_for_downloadable_update(channel.as_deref())
},
"checking for update",
false,
)
}
/// Synchronously download an update on the first non-null channel of:
/// 1. `c_channel`
/// 2. The channel specified in shorebird.yaml
/// 3. The default "stable" channel
///
/// Returns an [UpdateResult] indicating whether the update was successful.
#[no_mangle]
pub extern "C" fn shorebird_update_with_result(c_channel: *const c_char) -> *const UpdateResult {
let result = match to_rust_option(c_channel) {
Ok(channel) => to_update_result(updater::update(channel.as_deref())),
Err(err) => to_update_result(Err(err)),
};
Box::into_raw(Box::new(result))
}
/// Frees an `UpdateResult` previously returned by
/// `shorebird_update_with_result`. Frees the embedded `message` string and
/// the result allocation itself.
///
/// # Safety
///
/// `result` must be a valid pointer returned by `shorebird_update_with_result`,
/// or null (in which case this is a no-op).
#[no_mangle]
pub unsafe extern "C" fn shorebird_free_update_result(result: *mut UpdateResult) {
if result.is_null() {
return;
}
let result = unsafe { Box::from_raw(result) };
unsafe { free_c_string(result.message) };
}
+220
View File
@@ -0,0 +1,220 @@
//! Engine-internal C API surface.
//!
//! These symbols are consumed by Shorebird's Flutter engine
//! (`flutter::shorebird::Updater` in `shell/common/shorebird/updater.cc`) and
//! by no other public consumer. Both sides ship together as part of the
//! engine, so this surface has no stability guarantee — change freely as the
//! engine integration evolves.
//!
//! cbindgen reads this file directly (see `build.rs`) and emits exactly the
//! `pub extern "C"` items found here plus the types they reference. Adding
//! a function to this file automatically adds it to the engine header — no
//! cbindgen-config update required. Conversely, anything declared in
//! `c_api::dart` cannot leak into this header.
//!
//! For the stable Dart surface consumed by `package:shorebird_code_push`,
//! see `c_api::dart`.
use std::os::raw::c_char;
use std::path::PathBuf;
use super::{allocate_c_string, free_c_string, log_on_error, to_rust};
use crate::c_api::c_file::CFileProvider;
use crate::updater;
/// Struct containing configuration parameters for the updater.
/// Passed to `shorebird_init`.
/// NOTE: If this struct is changed all language bindings must be updated.
#[repr(C)]
pub struct AppParameters {
/// release_version, required. Named version of the app, off of which
/// updates are based. Can be either a version number or a hash.
pub release_version: *const libc::c_char,
/// Array of paths to the original aot library, required. For Flutter apps
/// these are the paths to the bundled libapp.so. May be used for
/// compression downloaded artifacts.
pub original_libapp_paths: *const *const libc::c_char,
/// Length of the original_libapp_paths array.
pub original_libapp_paths_size: libc::c_int,
/// Path to app storage directory where the updater will store serialized
/// state and other data that persists between releases.
pub app_storage_dir: *const libc::c_char,
/// Path to cache directory where the updater will store downloaded
/// artifacts and data that can be deleted when a new release is detected.
pub code_cache_dir: *const libc::c_char,
}
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct FileCallbacks {
/// Opens the "file" (actually an in-memory buffer) and returns a handle.
pub open: extern "C" fn() -> *mut libc::c_void,
/// Reads count bytes from the file into buffer. Returns the number of
/// bytes read.
pub read: extern "C" fn(file_handle: *mut libc::c_void, buffer: *mut u8, count: usize) -> usize,
/// Moves the file pointer to the given offset relative from whence (one of
/// libc::SEEK_SET, libc::SEEK_CUR, or libc::SEEK_END). Returns the new
/// offset relative to the start of the file.
pub seek: extern "C" fn(file_handle: *mut libc::c_void, offset: i64, whence: i32) -> i64,
/// Closes and frees the file handle.
pub close: extern "C" fn(file_handle: *mut libc::c_void),
}
fn to_rust_vector(
c_array: *const *const libc::c_char,
size: libc::c_int,
) -> anyhow::Result<Vec<String>> {
let mut result = Vec::new();
for i in 0..size {
let c_string = unsafe { *c_array.offset(i as isize) };
result.push(to_rust(c_string)?);
}
Ok(result)
}
fn app_config_from_c(c_params: *const AppParameters) -> anyhow::Result<updater::AppConfig> {
anyhow::ensure!(
!c_params.is_null(),
"Null parameters passed to app_config_from_c"
);
let c_params_ref = unsafe { &*c_params };
Ok(updater::AppConfig {
app_storage_dir: to_rust(c_params_ref.app_storage_dir)?,
code_cache_dir: to_rust(c_params_ref.code_cache_dir)?,
release_version: to_rust(c_params_ref.release_version)?,
original_libapp_paths: to_rust_vector(
c_params_ref.original_libapp_paths,
c_params_ref.original_libapp_paths_size,
)?,
})
}
fn path_to_c_string(path: Option<PathBuf>) -> anyhow::Result<*mut c_char> {
Ok(match path {
Some(v) => allocate_c_string(v.to_str().unwrap())?,
None => std::ptr::null_mut(),
})
}
/// Free a string returned by the updater library.
///
/// # Safety
///
/// If this function is called with a non-null pointer, it must be a pointer
/// returned by the updater library.
#[no_mangle]
pub unsafe extern "C" fn shorebird_free_string(c_string: *const c_char) {
unsafe { free_c_string(c_string) }
}
/// Configures the updater. First parameter is a struct containing
/// configuration from the running app. Second parameter is a YAML string
/// containing configuration compiled into the app. Returns true on success
/// and false on failure. If false is returned, the updater library will not
/// be usable.
#[no_mangle]
pub extern "C" fn shorebird_init(
c_params: *const AppParameters,
c_file_callbacks: FileCallbacks,
c_yaml: *const libc::c_char,
) -> bool {
log_on_error(
|| {
let config = app_config_from_c(c_params)?;
let file_provider = Box::new(CFileProvider {
file_callbacks: c_file_callbacks,
});
let yaml_string = to_rust(c_yaml)?;
updater::init(config, file_provider, &yaml_string)?;
Ok(true)
},
"initializing updater",
false,
)
}
/// Returns if the app should run the updater automatically on launch.
#[no_mangle]
pub extern "C" fn shorebird_should_auto_update() -> bool {
log_on_error(
updater::should_auto_update,
"fetching update behavior",
true,
)
}
/// Performs integrity checks on the next boot patch. If the patch fails
/// these checks, the patch will be deleted and the next boot patch will be
/// set to the last successfully booted patch or the base release if there is
/// no last successfully booted patch.
#[no_mangle]
pub extern "C" fn shorebird_validate_next_boot_patch() {
log_on_error(
updater::validate_next_boot_patch,
"validating next_boot_patch",
(),
);
}
/// The path to the patch that will boot on the next run of the app, or NULL
/// if there is no next patch. The caller must free the returned string with
/// `shorebird_free_string`.
#[no_mangle]
pub extern "C" fn shorebird_next_boot_patch_path() -> *mut c_char {
log_on_error(
|| {
let maybe_path = updater::next_boot_patch()?.map(|p| p.path);
path_to_c_string(maybe_path)
},
"fetching next_boot_patch_path",
std::ptr::null_mut(),
)
}
/// Start a thread to download an update if one is available.
#[no_mangle]
pub extern "C" fn shorebird_start_update_thread() {
updater::start_update_thread();
}
/// Tell the updater that we're launching from what it told us was the
/// next patch to boot from. This will copy the next boot patch to be the
/// `current_boot` patch.
///
/// It is required to call this function before calling
/// `shorebird_report_launch_success` or `shorebird_report_launch_failure`.
#[no_mangle]
pub extern "C" fn shorebird_report_launch_start() {
log_on_error(updater::report_launch_start, "reporting launch start", ());
}
/// Report that the app failed to launch. This will cause the updater to
/// attempt to roll back to the previous version if this version has not been
/// launched successfully before.
#[no_mangle]
pub extern "C" fn shorebird_report_launch_failure() {
log_on_error(
updater::report_launch_failure,
"reporting launch failure",
(),
);
}
/// Report that the app launched successfully. The Shell constructor calls
/// this once per process when the VM has finished booting; it pairs with
/// `shorebird_report_launch_start` to mark a patch as having booted cleanly.
#[no_mangle]
pub extern "C" fn shorebird_report_launch_success() {
log_on_error(
updater::report_launch_success,
"reporting launch success",
(),
);
}
+131 -376
View File
@@ -1,106 +1,41 @@
// This file handles translating the updater library's types into C types. // This module translates the updater library's types into C types.
//
// Currently manually prefixing all functions with "shorebird_" to avoid // The C surface is split into two submodules, each fully self-contained:
// name collisions with other libraries. // - `dart` — stable surface consumed by `package:shorebird_code_push`
// `cbindgen:prefix-with-name` could do this for us. // (header: `include/updater_dart.h`, driven by ffigen).
// - `engine` — surface consumed only by Shorebird's Flutter engine
/// This file contains the C API for the updater library. // (header: `include/updater_engine.h`, no stability guarantee).
/// It is intended to be used by language bindings, and is not intended to be //
/// used directly by Rust code. // Each submodule defines the `pub extern "C"` items it exports plus any C
/// The C API is not stable and may change at any time. // types those items reference. cbindgen scans the submodule files directly
/// You can see usage of this API in Shorebird's Flutter engine: // (see `build.rs`) — there is no cross-bucket exclusion list, and a new
/// <https://github.com/shorebirdtech/engine/blob/shorebird/dev/shell/common/shorebird.cc> // function added to one bucket cannot leak into the other bucket's header.
//
// Items in this file are private Rust helpers shared between the two
// buckets. They are not `extern "C"`, so cbindgen never emits them.
//
// Engine-side usage lives at `engine/src/flutter/shell/common/shorebird/updater.cc`
// in the Shorebird Flutter monorepo: <https://github.com/shorebirdtech/flutter>.
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::os::raw::c_char; use std::os::raw::c_char;
use std::path::PathBuf;
use crate::{updater, UpdateStatus};
use self::c_file::CFileProvider;
mod c_file; mod c_file;
pub mod dart;
pub mod engine;
/// Struct containing configuration parameters for the updater. #[cfg(test)]
/// Passed to all updater functions. pub use self::dart::*;
/// NOTE: If this struct is changed all language bindings must be updated. #[cfg(test)]
#[repr(C)] pub use self::engine::*;
pub struct AppParameters {
/// release_version, required. Named version of the app, off of which
/// updates are based. Can be either a version number or a hash.
pub release_version: *const libc::c_char,
/// Array of paths to the original aot library, required. For Flutter apps
/// these are the paths to the bundled libapp.so. May be used for
/// compression downloaded artifacts.
pub original_libapp_paths: *const *const libc::c_char,
/// Length of the original_libapp_paths array.
pub original_libapp_paths_size: libc::c_int,
/// Path to app storage directory where the updater will store serialized
/// state and other data that persists between releases.
pub app_storage_dir: *const libc::c_char,
/// Path to cache directory where the updater will store downloaded
/// artifacts and data that can be deleted when a new release is detected.
pub code_cache_dir: *const libc::c_char,
}
/// An unknown error occurred while updating. The update was not installed.
/// This is a catch-all for errors that don't fit into the other categories.
pub const SHOREBIRD_UPDATE_ERROR: i32 = -1;
/// No update is available (e.g. the app is already up-to-date)
pub const SHOREBIRD_NO_UPDATE: i32 = 0;
/// An update was installed successfully. It will boot from the update on the
/// next app launch.
pub const SHOREBIRD_UPDATE_INSTALLED: i32 = 1;
/// An error occurred while updating. The update was not installed.
pub const SHOREBIRD_UPDATE_HAD_ERROR: i32 = 2;
/// The downloaded patch was not installed because it was invalid.
pub const SHOREBIRD_UPDATE_IS_BAD_PATCH: i32 = 3;
/// Another update was already in progress when this call was made. The
/// already-running update will continue; the caller did not start a new one.
/// This is a benign outcome, not an error.
pub const SHOREBIRD_UPDATE_IN_PROGRESS: i32 = 4;
#[repr(C)]
pub struct UpdateResult {
pub status: i32,
pub message: *const libc::c_char,
}
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct FileCallbacks {
/// Opens the "file" (actually an in-memory buffer) and returns a handle.
pub open: extern "C" fn() -> *mut libc::c_void,
/// Reads count bytes from the file into buffer. Returns the number of
/// bytes read.
pub read: extern "C" fn(file_handle: *mut libc::c_void, buffer: *mut u8, count: usize) -> usize,
/// Moves the file pointer to the given offset relative from whence (one of
/// libc::SEEK_SET, libc::SEEK_CUR, or libc::SEEK_END). Returns the new
/// offset relative to the start of the file.
pub seek: extern "C" fn(file_handle: *mut libc::c_void, offset: i64, whence: i32) -> i64,
/// Closes and frees the file handle.
pub close: extern "C" fn(file_handle: *mut libc::c_void),
}
/// Converts a C string to a Rust string, does not free the C string. /// Converts a C string to a Rust string, does not free the C string.
fn to_rust(c_string: *const libc::c_char) -> anyhow::Result<String> { pub(super) fn to_rust(c_string: *const libc::c_char) -> anyhow::Result<String> {
anyhow::ensure!(!c_string.is_null(), "Null string passed to to_rust"); anyhow::ensure!(!c_string.is_null(), "Null string passed to to_rust");
let c_str = unsafe { CStr::from_ptr(c_string) }; let c_str = unsafe { CStr::from_ptr(c_string) };
Ok(c_str.to_str()?.to_string()) Ok(c_str.to_str()?.to_string())
} }
fn to_rust_option(c_string: *const c_char) -> anyhow::Result<Option<String>> { pub(super) fn to_rust_option(c_string: *const c_char) -> anyhow::Result<Option<String>> {
if c_string.is_null() { if c_string.is_null() {
return Ok(None); return Ok(None);
} }
@@ -108,168 +43,21 @@ fn to_rust_option(c_string: *const c_char) -> anyhow::Result<Option<String>> {
} }
/// Converts a Rust string to a C string, caller must free the C string. /// Converts a Rust string to a C string, caller must free the C string.
fn allocate_c_string(rust_string: &str) -> anyhow::Result<*mut c_char> { pub(super) fn allocate_c_string(rust_string: &str) -> anyhow::Result<*mut c_char> {
let c_str = CString::new(rust_string)?; let c_str = CString::new(rust_string)?;
Ok(c_str.into_raw()) Ok(c_str.into_raw())
} }
fn to_rust_vector( /// Drops a C string previously allocated by `allocate_c_string`. No-op on
c_array: *const *const libc::c_char, /// null. Callable by both buckets — `engine::shorebird_free_string` and
size: libc::c_int, /// `dart::shorebird_free_update_result` both delegate here so the
) -> anyhow::Result<Vec<String>> { /// CString-from-raw unsafe ownership logic lives in one place.
let mut result = Vec::new(); ///
for i in 0..size {
let c_string = unsafe { *c_array.offset(i as isize) };
result.push(to_rust(c_string)?);
}
Ok(result)
}
fn app_config_from_c(c_params: *const AppParameters) -> anyhow::Result<updater::AppConfig> {
anyhow::ensure!(
!c_params.is_null(),
"Null parameters passed to app_config_from_c"
);
let c_params_ref = unsafe { &*c_params };
Ok(updater::AppConfig {
app_storage_dir: to_rust(c_params_ref.app_storage_dir)?,
code_cache_dir: to_rust(c_params_ref.code_cache_dir)?,
release_version: to_rust(c_params_ref.release_version)?,
original_libapp_paths: to_rust_vector(
c_params_ref.original_libapp_paths,
c_params_ref.original_libapp_paths_size,
)?,
})
}
/// Helper function to log errors instead of panicking or returning a result.
fn log_on_error<F, R>(f: F, context: &str, error_result: R) -> R
where
F: FnOnce() -> Result<R, anyhow::Error>,
{
f().unwrap_or_else(|e| {
shorebird_error!("Error {}: {:?}", context, e);
error_result
})
}
/// Configures updater. First parameter is a struct containing configuration
/// from the running app. Second parameter is a YAML string containing
/// configuration compiled into the app. Returns true on success and false on
/// failure. If false is returned, the updater library will not be usable.
#[no_mangle]
pub extern "C" fn shorebird_init(
c_params: *const AppParameters,
c_file_callbacks: FileCallbacks,
c_yaml: *const libc::c_char,
) -> bool {
log_on_error(
|| {
let config = app_config_from_c(c_params)?;
let file_provider = Box::new(CFileProvider {
file_callbacks: c_file_callbacks,
});
let yaml_string = to_rust(c_yaml)?;
updater::init(config, file_provider, &yaml_string)?;
Ok(true)
},
"initializing updater",
false,
)
}
/// Returns if the app should run the updater automatically on launch.
#[no_mangle]
pub extern "C" fn shorebird_should_auto_update() -> bool {
log_on_error(
updater::should_auto_update,
"fetching update behavior",
true,
)
}
/// The currently running patch number, or 0 if the release has not been
/// patched. The internal name for this concept is `running_patch`; the
/// FFI symbol keeps the historical `current_boot_patch_number` spelling
/// because Flutter Engine links against it.
#[no_mangle]
pub extern "C" fn shorebird_current_boot_patch_number() -> usize {
log_on_error(
|| Ok(updater::running_patch()?.map_or(0, |p| p.number)),
"fetching running_patch_number",
0,
)
}
/// The patch number that will boot on the next run of the app, or 0 if there is
/// no next patch.
#[no_mangle]
pub extern "C" fn shorebird_next_boot_patch_number() -> usize {
log_on_error(
|| Ok(updater::next_boot_patch()?.map_or(0, |p| p.number)),
"fetching next_boot_patch_number",
0,
)
}
fn path_to_c_string(path: Option<PathBuf>) -> anyhow::Result<*mut c_char> {
Ok(match path {
Some(v) => allocate_c_string(v.to_str().unwrap())?,
None => std::ptr::null_mut(),
})
}
fn to_update_result(status: anyhow::Result<UpdateStatus>) -> UpdateResult {
let result = match status {
Ok(status) => {
let message = status.to_string();
return UpdateResult {
status: status as i32,
message: allocate_c_string(message.as_str()).unwrap_or(std::ptr::null_mut()),
};
}
Err(err) => UpdateResult {
status: SHOREBIRD_UPDATE_ERROR,
message: allocate_c_string(&err.to_string()).unwrap_or(std::ptr::null_mut()),
},
};
result
}
/// Performs integrity checks on the next boot patch. If the patch fails these checks, the patch
/// will be deleted and the next boot patch will be set to the last successfully booted patch or
/// the base release if there is no last successfully booted patch.
#[no_mangle]
pub extern "C" fn shorebird_validate_next_boot_patch() {
log_on_error(
updater::validate_next_boot_patch,
"validating next_boot_patch",
(),
);
}
/// The path to the patch that will boot on the next run of the app, or NULL if
/// there is no next patch.
#[no_mangle]
pub extern "C" fn shorebird_next_boot_patch_path() -> *mut c_char {
log_on_error(
|| {
let maybe_path = updater::next_boot_patch()?.map(|p| p.path);
path_to_c_string(maybe_path)
},
"fetching next_boot_patch_path",
std::ptr::null_mut(),
)
}
/// Free a string returned by the updater library.
/// # Safety /// # Safety
/// ///
/// If this function is called with a non-null pointer, it must be a pointer /// `c_string` must be null or a pointer previously returned by
/// returned by the updater library. /// `allocate_c_string` and not yet freed.
#[no_mangle] pub(super) unsafe fn free_c_string(c_string: *const c_char) {
pub unsafe extern "C" fn shorebird_free_string(c_string: *const c_char) {
if c_string.is_null() { if c_string.is_null() {
return; return;
} }
@@ -278,124 +66,15 @@ pub unsafe extern "C" fn shorebird_free_string(c_string: *const c_char) {
} }
} }
/// Frees an `UpdateResult` previously returned by `shorebird_check_for_update`. /// Helper function to log errors instead of panicking or returning a result.
/// pub(super) fn log_on_error<F, R>(f: F, context: &str, error_result: R) -> R
/// # Safety where
/// F: FnOnce() -> Result<R, anyhow::Error>,
/// `result` must be a valid pointer returned by `shorebird_check_for_update`, {
/// or null (in which case this is a no-op). f().unwrap_or_else(|e| {
#[no_mangle] shorebird_error!("Error {}: {:?}", context, e);
pub unsafe extern "C" fn shorebird_free_update_result(result: *mut UpdateResult) { error_result
if result.is_null() { })
return;
}
let message = (*result).message;
if !message.is_null() {
shorebird_free_string(message);
}
unsafe {
drop(Box::from_raw(result));
}
}
/// Check for an update. Returns true if an update is available.
#[no_mangle]
pub extern "C" fn shorebird_check_for_update() -> bool {
log_on_error(
|| updater::check_for_downloadable_update(None),
"checking for update",
false,
)
}
/// Check for an update on the first non-null channel of:
/// 1. `c_channel`
/// 2. The channel specified in shorebird.yaml
/// 3. The default "stable" channel
///
/// Returns true if an update exists that has not yet been downloaded.
#[no_mangle]
pub extern "C" fn shorebird_check_for_downloadable_update(c_channel: *const c_char) -> bool {
log_on_error(
|| {
let channel = to_rust_option(c_channel)?;
updater::check_for_downloadable_update(channel.as_deref())
},
"checking for update",
false,
)
}
/// Synchronously download an update if one is available.
#[no_mangle]
pub extern "C" fn shorebird_update() {
log_on_error(
|| updater::update(None).map(|result| shorebird_info!("Update result: {}", result)),
"downloading update",
(),
);
}
/// Synchronously download an update on the first non-null channel of:
/// 1. `c_channel`
/// 2. The channel specified in shorebird.yaml
/// 3. The default "stable" channel
///
/// Returns an [UpdateResult] indicating whether the update was successful.
#[no_mangle]
pub extern "C" fn shorebird_update_with_result(c_channel: *const c_char) -> *const UpdateResult {
let channel = to_rust_option(c_channel);
let result = match channel {
Ok(channel) => to_update_result(updater::update(channel.as_deref())),
Err(err) => to_update_result(Err(err)),
};
Box::into_raw(Box::new(result))
}
/// Start a thread to download an update if one is available.
#[no_mangle]
pub extern "C" fn shorebird_start_update_thread() {
updater::start_update_thread();
}
/// Tell the updater that we're launching from what it told us was the
/// next patch to boot from. This will copy the next boot patch to be the
/// `current_boot` patch.
///
/// It is required to call this function before calling
/// `shorebird_report_launch_success` or `shorebird_report_launch_failure`.
#[no_mangle]
pub extern "C" fn shorebird_report_launch_start() {
log_on_error(updater::report_launch_start, "reporting launch start", ());
}
/// Report that the app failed to launch. This will cause the updater to
/// attempt to roll back to the previous version if this version has not
/// been launched successfully before.
#[no_mangle]
pub extern "C" fn shorebird_report_launch_failure() {
log_on_error(
updater::report_launch_failure,
"reporting launch failure",
(),
);
}
/// Report that the app launched successfully. This will mark the current
/// as having been launched successfully. We don't currently do anything
/// with this information, but it could be used to record a point at which
/// we will not roll back from.
///
/// This is not currently wired up to be called from the Engine. It's unclear
/// where best to connect it. Expo waits 5 seconds after the app launches
/// and then marks the launch as successful. We could do something similar.
#[no_mangle]
pub extern "C" fn shorebird_report_launch_success() {
log_on_error(
updater::report_launch_success,
"reporting launch success",
(),
);
} }
#[cfg(test)] #[cfg(test)]
@@ -407,6 +86,7 @@ mod test {
UNEXPECTED_REPORT, UNEXPECTED_REPORT,
}, },
test_utils::write_fake_apk, test_utils::write_fake_apk,
updater,
}; };
use anyhow::Ok; use anyhow::Ok;
use serial_test::serial; use serial_test::serial;
@@ -451,13 +131,13 @@ mod test {
// libapp_path is currently Android-style with a virtual path // libapp_path is currently Android-style with a virtual path
// of at least 3 directories in depth ending in libapp.so. // of at least 3 directories in depth ending in libapp.so.
fn parameters(tmp_dir: &TempDir, libapp_path: &str) -> super::AppParameters { fn parameters(tmp_dir: &TempDir, libapp_path: &str) -> AppParameters {
let cache_dir = tmp_dir.path().to_str().unwrap().to_string(); let cache_dir = tmp_dir.path().to_str().unwrap().to_string();
let app_paths_vec = vec![libapp_path.to_owned()]; let app_paths_vec = vec![libapp_path.to_owned()];
let app_paths_size = app_paths_vec.len() as i32; let app_paths_size = app_paths_vec.len() as i32;
let app_paths = c_array(app_paths_vec); let app_paths = c_array(app_paths_vec);
super::AppParameters { AppParameters {
app_storage_dir: c_string(&cache_dir), app_storage_dir: c_string(&cache_dir),
code_cache_dir: c_string(&cache_dir), code_cache_dir: c_string(&cache_dir),
release_version: c_string("1.0.0"), release_version: c_string("1.0.0"),
@@ -466,7 +146,7 @@ mod test {
} }
} }
fn free_parameters(params: super::AppParameters) { fn free_parameters(params: AppParameters) {
free_c_string(params.app_storage_dir as *mut libc::c_char); free_c_string(params.app_storage_dir as *mut libc::c_char);
free_c_string(params.code_cache_dir as *mut libc::c_char); free_c_string(params.code_cache_dir as *mut libc::c_char);
free_c_string(params.release_version as *mut libc::c_char); free_c_string(params.release_version as *mut libc::c_char);
@@ -476,6 +156,18 @@ mod test {
) )
} }
/// Run `shorebird_update_with_result` with the given channel, assert the
/// status equals `expected`, then free the result. Replaces uses of the
/// retired `shorebird_update()` helper inside tests that don't otherwise
/// inspect the result.
fn run_update_expecting(channel: *const c_char, expected: i32) {
let result = shorebird_update_with_result(channel);
unsafe {
assert_eq!(result.read().status, expected);
shorebird_free_update_result(result as *mut UpdateResult);
}
}
/// A precomputed bidiff patch artifact along with the inputs that /// A precomputed bidiff patch artifact along with the inputs that
/// produced it. Generate one with: /// produced it. Generate one with:
/// cargo run --bin string_patch -- "<base>" "<new>" /// cargo run --bin string_patch -- "<base>" "<new>"
@@ -572,6 +264,68 @@ mod test {
)); ));
} }
/// Exercises the `to_rust_vector` failure path in
/// `app_config_from_c` (engine.rs). All scalar fields are valid, but
/// the libapp_paths array contains a null entry — `to_rust` rejects
/// null and the `?` propagates. Without this test the array-conversion
/// branch is never reached because `init_with_null_app_parameters`
/// fails earlier on the scalar fields.
#[serial]
#[test]
fn init_with_null_libapp_path_in_array() {
testing_reset_config();
let tmp_dir = TempDir::new().unwrap();
let cache_dir = tmp_dir.path().to_str().unwrap().to_string();
let null_path: *const libc::c_char = std::ptr::null();
let paths_array = [null_path];
let c_params = AppParameters {
app_storage_dir: c_string(&cache_dir),
code_cache_dir: c_string(&cache_dir),
release_version: c_string("1.0.0"),
original_libapp_paths: paths_array.as_ptr(),
original_libapp_paths_size: 1,
};
let c_yaml = c_string("app_id: foo");
assert!(!shorebird_init(&c_params, FileCallbacks::new(), c_yaml));
free_c_string(c_yaml);
free_c_string(c_params.app_storage_dir as *mut libc::c_char);
free_c_string(c_params.code_cache_dir as *mut libc::c_char);
free_c_string(c_params.release_version as *mut libc::c_char);
}
/// Exercises the `Err` arm of the channel decode in
/// `shorebird_update_with_result` (dart.rs). The channel pointer is
/// non-null (so `to_rust_option` doesn't short-circuit to `Ok(None)`),
/// but contains invalid UTF-8, so `CStr::to_str()` returns an error
/// that propagates into `to_update_result(Err(_))`.
#[serial]
#[test]
fn update_with_result_with_invalid_utf8_channel() {
testing_reset_config();
let tmp_dir = TempDir::new().unwrap();
let fake_libapp_path = tmp_dir.path().join("lib/arch/libapp.so");
let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap());
let c_yaml = c_string("app_id: foo");
assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml));
free_c_string(c_yaml);
free_parameters(c_params);
// 0xFF is an invalid UTF-8 start byte. CStr::from_ptr accepts it
// (C strings are null-terminated bytes, no encoding) but the
// to_str() conversion fails — which is the path we're covering.
let bad_bytes: [u8; 4] = [0xFF, 0xFE, 0xFD, 0];
let result = shorebird_update_with_result(bad_bytes.as_ptr() as *const c_char);
unsafe {
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
shorebird_free_update_result(result as *mut UpdateResult);
}
}
#[serial] #[serial]
#[test] #[test]
fn init_with_bad_yaml() { fn init_with_bad_yaml() {
@@ -683,7 +437,7 @@ mod test {
assert!(shorebird_check_for_downloadable_update(std::ptr::null())); assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
// Go ahead and do the update. // Go ahead and do the update.
shorebird_update(); run_update_expecting(std::ptr::null(), SHOREBIRD_UPDATE_INSTALLED);
assert_eq!(shorebird_current_boot_patch_number(), 0); assert_eq!(shorebird_current_boot_patch_number(), 0);
assert_eq!(shorebird_next_boot_patch_number(), 1); assert_eq!(shorebird_next_boot_patch_number(), 1);
@@ -931,7 +685,7 @@ mod test {
// There is an update available. // There is an update available.
assert!(shorebird_check_for_downloadable_update(std::ptr::null())); assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
// Go ahead and do the update. // Go ahead and do the update.
shorebird_update(); run_update_expecting(std::ptr::null(), SHOREBIRD_UPDATE_INSTALLED);
// Ensure we have not yet updated the current patch. // Ensure we have not yet updated the current patch.
assert_eq!(shorebird_current_boot_patch_number(), 0); assert_eq!(shorebird_current_boot_patch_number(), 0);
@@ -992,7 +746,7 @@ mod test {
); );
assert!(shorebird_check_for_downloadable_update(std::ptr::null())); assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
shorebird_update(); run_update_expecting(std::ptr::null(), SHOREBIRD_UPDATE_INSTALLED);
shorebird_report_launch_start(); shorebird_report_launch_start();
shorebird_report_launch_success(); shorebird_report_launch_success();
@@ -1076,7 +830,7 @@ mod test {
|_url, _event| Ok(()), |_url, _event| Ok(()),
); );
assert!(shorebird_check_for_downloadable_update(std::ptr::null())); assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
shorebird_update(); run_update_expecting(std::ptr::null(), SHOREBIRD_UPDATE_INSTALLED);
shorebird_report_launch_start(); shorebird_report_launch_start();
shorebird_report_launch_success(); shorebird_report_launch_success();
assert_eq!(shorebird_current_boot_patch_number(), 1); assert_eq!(shorebird_current_boot_patch_number(), 1);
@@ -1125,8 +879,9 @@ mod test {
/// Patch-to-patch rollback: device on patch 2, server rolls back to /// Patch-to-patch rollback: device on patch 2, server rolls back to
/// patch 1 (sends rollback signal AND a downloadable replacement). /// patch 1 (sends rollback signal AND a downloadable replacement).
/// `check_for_downloadable_update` returns true (replacement available), /// `check_for_downloadable_update` returns true (replacement available),
/// and after `update()` installs patch 1, the running session sees /// and after `update_with_result` installs patch 1, the running session
/// `current=2, next=1` — the signal Dart needs for `restartRequired`. /// sees `current=2, next=1` — the signal Dart needs for
/// `restartRequired`.
#[serial] #[serial]
#[test] #[test]
fn rollback_patch_to_patch_reports_current_and_next_distinctly() { fn rollback_patch_to_patch_reports_current_and_next_distinctly() {
@@ -1163,7 +918,7 @@ mod test {
|_url, _event| Ok(()), |_url, _event| Ok(()),
); );
assert!(shorebird_check_for_downloadable_update(std::ptr::null())); assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
shorebird_update(); run_update_expecting(std::ptr::null(), SHOREBIRD_UPDATE_INSTALLED);
shorebird_report_launch_start(); shorebird_report_launch_start();
shorebird_report_launch_success(); shorebird_report_launch_success();
assert_eq!(shorebird_current_boot_patch_number(), 2); assert_eq!(shorebird_current_boot_patch_number(), 2);
@@ -1187,7 +942,7 @@ mod test {
|_url, _event| Ok(()), |_url, _event| Ok(()),
); );
assert!(shorebird_check_for_downloadable_update(std::ptr::null())); assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
shorebird_update(); run_update_expecting(std::ptr::null(), SHOREBIRD_UPDATE_INSTALLED);
// Running process is still on patch 2; next boot will be patch 1. // Running process is still on patch 2; next boot will be patch 1.
assert_eq!(shorebird_current_boot_patch_number(), 2); assert_eq!(shorebird_current_boot_patch_number(), 2);
+4
View File
@@ -1,3 +1,7 @@
# 2.0.7
- chore: internal cleanup; no public API changes.
# 2.0.6 # 2.0.6
- fix: `checkForUpdate` now reports `restartRequired` when the current patch - fix: `checkForUpdate` now reports `restartRequired` when the current patch
+11 -6
View File
@@ -11,11 +11,16 @@ Flutter engine) via FFI.
For an Updater function to be visible to the Dart code, it must: For an Updater function to be visible to the Dart code, it must:
1. Be declared in c_api.rs as `pub extern "C"`. 1. Be declared in `library/src/c_api/dart.rs` as `pub extern "C"` (the
1. This will add the function to the `library/include/updater.h` header Dart-stable surface). Functions only meant for the Flutter engine go in
file, which is generated by [cbindgen](https://github.com/mozilla/cbindgen) `library/src/c_api/engine.rs` instead and will not appear in the Dart
when the Updater is built. bindings.
1. The two buckets emit `library/include/updater_dart.h` and
`library/include/updater_engine.h` respectively, generated by
[cbindgen](https://github.com/mozilla/cbindgen) when the Updater is
built.
1. Be included in the generated ffi bindings. These can be regenerated using 1. Be included in the generated ffi bindings. These can be regenerated using
`dart run ffigen`. `dart run ffigen`. ffigen reads only `updater_dart.h`, so engine-only
symbols are not bound.
1. Android specific: be listed in 1. Android specific: be listed in
https://github.com/shorebirdtech/engine/blob/main/shell/platform/android/android_exports.lst https://github.com/shorebirdtech/flutter/blob/shorebird/dev/engine/src/flutter/shell/platform/android/android_exports.lst
File diff suppressed because it is too large Load Diff
@@ -95,15 +95,7 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
Future<void> update({UpdateTrack? track}) async { Future<void> update({UpdateTrack? track}) async {
if (!_isAvailable) return; if (!_isAvailable) return;
Pointer<UpdateResult> result = nullptr; final result = await _run(() => _updater.update(track: track));
try {
result = await _run(() => _updater.update(track: track));
// Explicitly catch all errors/exceptions to ensure we gracefully fallback.
// ignore: avoid_catches_without_on_clauses
} catch (_) {
return _legacyFallback();
}
const unknownErrorMessage = 'An unknown error occurred.'; const unknownErrorMessage = 'An unknown error occurred.';
@@ -138,22 +130,6 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
_updater.freeUpdateResult(result); _updater.freeUpdateResult(result);
} }
} }
// Fallback to downloadUpdate if update is not available.
Future<void> _legacyFallback() async {
await _run(_updater.downloadUpdate);
final (current, next) = await (readCurrentPatch(), readNextPatch()).wait;
final status = next != null && current?.number != next.number
? UpdateStatus.restartRequired
: UpdateStatus.upToDate;
if (status == UpdateStatus.restartRequired) return;
throw const UpdateException(
message: '''
Downloading update failed but reason is unknown due to legacy updater.
Please upgrade the Shorebird Engine for improved error messages.''',
reason: UpdateFailureReason.unknown,
);
}
} }
extension on int { extension on int {
-5
View File
@@ -26,11 +26,6 @@ class Updater {
/// currentPatchNumber if no new patch is available. /// currentPatchNumber if no new patch is available.
int nextPatchNumber() => bindings.shorebird_next_boot_patch_number(); int nextPatchNumber() => bindings.shorebird_next_boot_patch_number();
/// Downloads the latest patch, if available.
void downloadUpdate() => bindings.shorebird_update();
// New Methods added to support v2.0.0 of the Dart APIs //
/// Whether a new patch is available for download. /// Whether a new patch is available for download.
bool checkForDownloadableUpdate({UpdateTrack? track}) => bool checkForDownloadableUpdate({UpdateTrack? track}) =>
bindings.shorebird_check_for_downloadable_update( bindings.shorebird_check_for_downloadable_update(
+6 -2
View File
@@ -1,6 +1,6 @@
name: shorebird_code_push name: shorebird_code_push
description: Check for and download Shorebird code push updates from your app. description: Check for and download Shorebird code push updates from your app.
version: 2.0.6 version: 2.0.7
homepage: https://shorebird.dev homepage: https://shorebird.dev
repository: https://github.com/shorebirdtech/updater/tree/main/shorebird_code_push repository: https://github.com/shorebirdtech/updater/tree/main/shorebird_code_push
@@ -22,7 +22,11 @@ ffigen:
output: "lib/src/generated/updater_bindings.g.dart" output: "lib/src/generated/updater_bindings.g.dart"
name: "UpdaterBindings" name: "UpdaterBindings"
headers: headers:
# Only the Dart-stable surface drives ffigen. Engine-only symbols live in
# ../library/include/updater_engine.h and must NOT be reachable from the
# generated bindings — the package's public ABI is exactly what this file
# exposes.
entry-points: entry-points:
- "../library/include/updater.h" - "../library/include/updater_dart.h"
preamble: | preamble: |
// ignore_for_file: unused_element, unused_field // ignore_for_file: unused_element, unused_field
@@ -354,7 +354,7 @@ void main() {
overridePrint((_) async { overridePrint((_) async {
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run); shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
await expectLater(shorebirdUpdater.update(), completes); await expectLater(shorebirdUpdater.update(), completes);
verifyNever(updater.downloadUpdate); verifyNever(() => updater.update());
}), }),
); );
}); });
@@ -387,6 +387,31 @@ void main() {
}); });
}); });
group('when the FFI call throws', () {
// Pre-2.0.7 the package wrapped this call in a try/catch and routed
// throws into a legacy fallback that called the now-removed
// shorebird_update symbol. The fallback was unreachable under the
// package's flutter: >=3.24.5 constraint, so it was deleted along
// with the symbol. The replacement contract: any unexpected throw
// from the FFI propagates to the caller, and freeUpdateResult is
// not invoked (we never received a pointer to free).
setUp(() {
when(() => updater.currentPatchNumber()).thenReturn(0);
when(() => updater.update()).thenThrow(Exception('boom'));
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
});
test('propagates the exception and does not call freeUpdateResult',
() async {
await expectLater(
shorebirdUpdater.update(),
throwsA(isA<Exception>()),
);
verify(() => updater.update()).called(1);
verifyNever(() => updater.freeUpdateResult(any()));
});
});
group('when no update is available', () { group('when no update is available', () {
setUp(() { setUp(() {
when(() => updater.currentPatchNumber()).thenReturn(0); when(() => updater.currentPatchNumber()).thenReturn(0);
@@ -540,52 +565,6 @@ void main() {
}); });
}); });
group('when an outdated version of the engine is used', () {
setUp(() {
when(updater.currentPatchNumber).thenReturn(0);
when(updater.nextPatchNumber).thenReturn(1);
when(() => updater.update()).thenThrow(Exception('oops'));
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
});
test('falls back to downloadUpdate', () async {
await expectLater(shorebirdUpdater.update(), completes);
verify(updater.update).called(1);
verify(updater.downloadUpdate).called(1);
verifyNever(() => updater.freeUpdateResult(any()));
});
group('when update fails', () {
setUp(() {
when(updater.currentPatchNumber).thenReturn(0);
when(updater.nextPatchNumber).thenReturn(0);
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
});
test('throws if legacy update fails', () async {
await expectLater(
shorebirdUpdater.update,
throwsA(
isA<UpdateException>().having(
(e) => e.message,
'message',
'''
Downloading update failed but reason is unknown due to legacy updater.
Please upgrade the Shorebird Engine for improved error messages.''',
).having(
(e) => e.reason,
'reason',
UpdateFailureReason.unknown,
),
),
);
verify(updater.update).called(1);
verify(updater.downloadUpdate).called(1);
verifyNever(() => updater.freeUpdateResult(any()));
});
});
});
group('when an unsupported status code is returned', () { group('when an unsupported status code is returned', () {
setUp(() { setUp(() {
when(() => updater.currentPatchNumber()).thenReturn(0); when(() => updater.currentPatchNumber()).thenReturn(0);
+5 -11
View File
@@ -30,7 +30,7 @@ void main() {
}); });
group('currentPatchNumber', () { group('currentPatchNumber', () {
test('forwards the result of shorebird_next_boot_patch_number', () { test('forwards the result of shorebird_current_boot_patch_number', () {
when( when(
() => updaterBindings.shorebird_current_boot_patch_number(), () => updaterBindings.shorebird_current_boot_patch_number(),
).thenReturn(123); ).thenReturn(123);
@@ -40,7 +40,8 @@ void main() {
}); });
group('checkForDownloadableUpdate', () { group('checkForDownloadableUpdate', () {
test('forwards the result of shorebird_check_for_update', () { test('forwards the result of shorebird_check_for_downloadable_update',
() {
when( when(
() => updaterBindings.shorebird_check_for_downloadable_update( () => updaterBindings.shorebird_check_for_downloadable_update(
nullptr, nullptr,
@@ -65,7 +66,8 @@ void main() {
).thenReturn(true); ).thenReturn(true);
}); });
test('forwards the result of shorebird_check_for_update', () { test('forwards the result of shorebird_check_for_downloadable_update',
() {
expect( expect(
updater.checkForDownloadableUpdate(track: UpdateTrack.beta), updater.checkForDownloadableUpdate(track: UpdateTrack.beta),
isTrue, isTrue,
@@ -101,14 +103,6 @@ void main() {
}); });
}); });
group('downloadUpdate', () {
test('calls bindings.shorebird_update', () {
when(() => updaterBindings.shorebird_update()).thenReturn(null);
updater.downloadUpdate();
verify(() => updaterBindings.shorebird_update()).called(1);
});
});
group('update', () { group('update', () {
test('calls bindings.shorebird_update_with_result', () { test('calls bindings.shorebird_update_with_result', () {
when( when(