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:
@@ -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
@@ -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.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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 */
|
|
||||||
@@ -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 */
|
||||||
@@ -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 */
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) };
|
||||||
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user