updater/ has moved to its own repository
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Cache directory from updater cli.
|
||||
updater_cache
|
||||
@@ -1,122 +0,0 @@
|
||||
# Building the Shorebird Flutter Engine
|
||||
|
||||
Shorebird uses a modified version of the Flutter engine. Normally
|
||||
when you use Shorebird, you would use the pre-built engine binaries
|
||||
that we provide. However, if you want to build the engine yourself,
|
||||
this document describes how to do that.
|
||||
|
||||
The primary modification Shorebird makes to the stock Flutter engine
|
||||
is adding support for the updater library. The updater library is
|
||||
written in Rust and is used to update the code running in the Flutter
|
||||
app. The updater library is built as a static library and is linked
|
||||
into the Flutter engine during build time.
|
||||
|
||||
## Building the Updater Library
|
||||
|
||||
### Installing Rust
|
||||
|
||||
The updater library is written in Rust. You can install Rust using
|
||||
rustup. See https://rustup.rs/ for details.
|
||||
|
||||
## Building for Android
|
||||
|
||||
Rust Android tooling *mostly* works out of the box, but needs a bunch
|
||||
of configuration to get it to work.
|
||||
|
||||
The best way I found was to install:
|
||||
https://github.com/bbqsrc/cargo-ndk
|
||||
|
||||
```
|
||||
rustup install beta
|
||||
cargo +beta install cargo-ndk
|
||||
rustup +beta target add \
|
||||
aarch64-linux-android \
|
||||
armv7-linux-androideabi \
|
||||
x86_64-linux-android \
|
||||
i686-linux-android
|
||||
```
|
||||
|
||||
If others know of better instructions, please send us a PR!
|
||||
|
||||
Once you have cargo-ndk installed, you can build the updater library with the
|
||||
beta toolchain you installed and the ndk command:
|
||||
|
||||
```
|
||||
cargo +beta ndk --target aarch64-linux-android build --release
|
||||
```
|
||||
|
||||
### Setting up to build the Flutter Engine:
|
||||
|
||||
https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment
|
||||
https://github.com/flutter/flutter/wiki/Compiling-the-engine
|
||||
|
||||
The .gclient file I recommend is:
|
||||
```
|
||||
solutions = [
|
||||
{
|
||||
"managed": False,
|
||||
"name": "src/flutter",
|
||||
"url": "git@github.com:shorebirdtech/engine.git",
|
||||
"custom_deps": {},
|
||||
"deps_file": "DEPS",
|
||||
"safesync_url": "",
|
||||
},
|
||||
]
|
||||
```
|
||||
(We should probably just check that in somewhere.)
|
||||
|
||||
Once you have that set up and `gclient sync` has run, you will need
|
||||
to switch your flutter checkout to the `codepush` branch:
|
||||
|
||||
```
|
||||
cd src/flutter
|
||||
git checkout codepush
|
||||
```
|
||||
|
||||
And then `gclient sync` again.
|
||||
|
||||
### Symlink in the Rust binaries
|
||||
|
||||
Currently you need to symlink in the results of the rust build into the engine/src directory:
|
||||
|
||||
```
|
||||
cd flutter
|
||||
mkdir updater
|
||||
cd updater
|
||||
ln -s $SRC/shorebird/updater/library/include/updater.h
|
||||
mkdir android_aarch64
|
||||
cd android_aarch64
|
||||
ln -s $SRC/shorebird/updater/target/aarch64-linux-android/release/libupdater.a
|
||||
```
|
||||
|
||||
## Building Flutter Engine
|
||||
|
||||
```
|
||||
./flutter/tools/gn --android --android-cpu arm64 --runtime-mode=release
|
||||
ninja -C out/android_release_arm64
|
||||
```
|
||||
|
||||
The linking step for android_release_arm64 is _much_ longer than other platforms
|
||||
we may need to use unopt or debug builds for faster iteration.
|
||||
|
||||
I also add `&& say "done"` to the end of the ninja command so I know when it's
|
||||
done (because it takes minutes).
|
||||
|
||||
|
||||
## Running with your local engine
|
||||
|
||||
The `shorebird` tools don't yet support local engines, so you need to use
|
||||
`flutter run` directly.
|
||||
https://github.com/shorebirdtech/shorebird/issues/42
|
||||
|
||||
Here is a script:
|
||||
```
|
||||
#! /bin/sh -x
|
||||
|
||||
LOCAL_ENGINE_SRC_PATH=/path/to/local/flutter/engine
|
||||
LOCAL_ENGINE=android_release_arm64
|
||||
flutter build apk --release --no-tree-shake-icons --local-engine-src-path $LOCAL_ENGINE_SRC_PATH --local-engine=$LOCAL_ENGINE
|
||||
```
|
||||
|
||||
Only need to build with your custom engine once. Once the app is installed on
|
||||
the phone then you can `shorebird publish` to it as normal.
|
||||
@@ -1,2 +0,0 @@
|
||||
[workspace]
|
||||
members = ["cli", "library", "patch"]
|
||||
@@ -1,16 +0,0 @@
|
||||
# Updater library
|
||||
|
||||
This is the C/Rust side of the Shorebird code push system. This is built
|
||||
in Rust with a C API for easy calling from other languages, most notably
|
||||
for linking into libflutter.so.
|
||||
|
||||
See cli/README.md for more documentation on the library.
|
||||
|
||||
## Parts
|
||||
* cli: Test the updater library via the Rust API (for development).
|
||||
* dart_cli: Test ffi wrapping of updater library.
|
||||
* library: The rust library that does the actual update work.
|
||||
* dart_bindings: The Dart bindings for the updater library.
|
||||
|
||||
All of the interesting code is in the `library` directory. There is also
|
||||
a README.md in that directory explaining the design.
|
||||
@@ -1,30 +0,0 @@
|
||||
Shorebird currently maintains patches to the Flutter Engine to integrate the Shorebird updater library.
|
||||
|
||||
We have changes to both flutter/engine and flutter/buildroot. We need to make sure those changes are applied to the correct version of each, to be compatible with a given `flutter/flutter` version.
|
||||
|
||||
* flutter/flutter version is whatever is the latest `stable` in flutter/flutter.
|
||||
* From that you can get the `flutter/engine` version in `bin/internal/engine.version` in flutter/flutter.
|
||||
* From that you can get the `flutter/buildroot` version in `DEPS` in `flutter/engine`
|
||||
|
||||
Working from a `flutter/engine` checkout can be confusing because the layout is:
|
||||
src/ <- `flutter/buildroot`
|
||||
src/flutter <- `flutter/engine`
|
||||
|
||||
`gclient` uses the file in `src/flutter/DEPS` to control the whole checkout, as directed by your `.gclient` file in the parent directory of `src`.
|
||||
|
||||
Here are the steps to update those forks every time Flutter releases:
|
||||
|
||||
1. You need the release name / git id for the Flutter release. e.g. 3.7.7
|
||||
1. Fetch flutter at that tag
|
||||
1. Look at engine version https://github.com/flutter/flutter/blob/3.7.7/bin/internal/engine.version
|
||||
See that is 1837b5be5f0f1376a1ccf383950e83a80177fb4e.
|
||||
1. Rebase our engine patches onto that engine version. https://github.com/shorebirdtech/engine/tree/stable_codepush
|
||||
If the engine is tagged correctly (it isn't always) it's as simple as:
|
||||
`git rebase --onto 3.7.7 3.7.6
|
||||
1. Should then also look at DEPS file in engine: https://github.com/flutter/engine/blob/3.7.7/DEPS
|
||||
Where we are looking for the buildroot hash: https://github.com/flutter/engine/blob/3.7.7/DEPS#L239
|
||||
1. We now need to rebase the buildroot (src/) changes onto the buildroot version in that DEPS. https://github.com/shorebirdtech/buildroot/tree/stable_codepush
|
||||
e.g. `git rebase --onto 8747bce41d0dc6d9dc45c4d1b46d2100bb9ee688 93f7f85422a8604bdc44ef76c3f105ead65e8c1c`
|
||||
Where as that should be `--onto new_base_revision previous_base_revision`. The base revision is the git id *before* we started making changes.
|
||||
1. Once you've successfully rebased the buildroot, you then need to change the buildroot id in our fork of the engine (so that when others use `gclient sync` it pulls our modified buildroot rather than an unmodified one.
|
||||
1. Finally, update the [Shorebird CLI flutterEngineRevision](https://github.com/shorebirdtech/shorebird/blob/main/packages/shorebird_cli/lib/src/flutter_engine_revision.dart#L3) to the new Flutter engine revision
|
||||
@@ -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"
|
||||
@@ -1,10 +0,0 @@
|
||||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.1.6", features = ["derive"] }
|
||||
updater = { path = "../library" }
|
||||
@@ -1,66 +0,0 @@
|
||||
extern crate updater;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None, arg_required_else_help=true)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
Check {},
|
||||
Current {},
|
||||
Update {},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let config = updater::AppConfig {
|
||||
cache_dir: "updater_cache".to_owned(),
|
||||
release_version: "0.1.0".to_owned(),
|
||||
original_libapp_paths: vec!["libapp.so".to_owned()],
|
||||
vm_path: "libflutter.so".to_owned(),
|
||||
};
|
||||
let yaml_str = "
|
||||
app_id: demo
|
||||
channel: stable
|
||||
base_url: http://localhost:8000
|
||||
";
|
||||
updater::init(config, yaml_str).expect("init failed");
|
||||
|
||||
// You can check for the existence of subcommands, and if found use their
|
||||
// matches just as you would the top level cmd
|
||||
match &cli.command {
|
||||
Some(Commands::Check {}) => {
|
||||
let needs_update = updater::check_for_update();
|
||||
println!("Checking for update...");
|
||||
if needs_update {
|
||||
println!("Update needed.");
|
||||
} else {
|
||||
println!("No update needed.");
|
||||
}
|
||||
}
|
||||
Some(Commands::Current {}) => {
|
||||
let version = updater::active_patch();
|
||||
println!("Current version info:");
|
||||
match version {
|
||||
Some(v) => {
|
||||
println!("path: {:?}", v.path);
|
||||
println!("number: {:?}", v.number);
|
||||
}
|
||||
None => {
|
||||
println!("None");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Update {}) => {
|
||||
let status = updater::update();
|
||||
println!("Update: {}", status);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
|
||||
# Avoid committing pubspec.lock for library packages; see
|
||||
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
pubspec.lock
|
||||
@@ -1,6 +0,0 @@
|
||||
# dart_bindings
|
||||
|
||||
Dart bindings for the updater library.
|
||||
|
||||
This presumably eventually either gets published to pub.dev, or more likely
|
||||
ends up as dart:shorebird or something included with the Shorebird SDK.
|
||||
@@ -1,30 +0,0 @@
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -1,116 +0,0 @@
|
||||
// This entire file could be easily autogenerated.
|
||||
// Probably https://pub.dev/packages/ffigen would work.
|
||||
|
||||
import 'dart:ffi' as ffi;
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
// This must be kept in sync with the C struct in updater.h.
|
||||
// Including *in the same order* as the C struct.
|
||||
class AppParameters extends ffi.Struct {
|
||||
external ffi.Pointer<Utf8> channel;
|
||||
// ignore: non_constant_identifier_names
|
||||
external ffi.Pointer<Utf8> app_id;
|
||||
// ignore: non_constant_identifier_names
|
||||
external ffi.Pointer<Utf8> base_version;
|
||||
// ignore: non_constant_identifier_names
|
||||
external ffi.Pointer<Utf8> update_url;
|
||||
// ignore: non_constant_identifier_names
|
||||
external ffi.Pointer<ffi.Pointer<Utf8>> original_libapp_paths;
|
||||
@ffi.Int8()
|
||||
// ignore: non_constant_identifier_names
|
||||
external int original_libapp_paths_size;
|
||||
// ignore: non_constant_identifier_names
|
||||
external ffi.Pointer<Utf8> vm_path;
|
||||
// ignore: non_constant_identifier_names
|
||||
external ffi.Pointer<Utf8> cache_dir;
|
||||
|
||||
static ffi.Pointer<AppParameters> allocate({
|
||||
required String appId,
|
||||
required String version,
|
||||
required String channel,
|
||||
required String? updateUrl,
|
||||
required List<String> libappPaths,
|
||||
required String libflutterPath,
|
||||
required String cacheDir,
|
||||
}) {
|
||||
var config = calloc<AppParameters>();
|
||||
config.ref.app_id = appId.toNativeUtf8();
|
||||
config.ref.base_version = version.toNativeUtf8();
|
||||
config.ref.channel = channel.toNativeUtf8();
|
||||
if (updateUrl != null) {
|
||||
config.ref.update_url = updateUrl.toNativeUtf8();
|
||||
}
|
||||
config.ref.original_libapp_paths = calloc<ffi.Pointer<Utf8>>(
|
||||
libappPaths.length,
|
||||
);
|
||||
|
||||
for (var i = 0; i < libappPaths.length; i++) {
|
||||
config.ref.original_libapp_paths[i] = libappPaths[i].toNativeUtf8();
|
||||
}
|
||||
config.ref.vm_path = libflutterPath.toNativeUtf8();
|
||||
config.ref.cache_dir = cacheDir.toNativeUtf8();
|
||||
return config;
|
||||
}
|
||||
|
||||
static void free(ffi.Pointer<AppParameters> config) {
|
||||
calloc.free(config.ref.app_id);
|
||||
calloc.free(config.ref.base_version);
|
||||
calloc.free(config.ref.channel);
|
||||
calloc.free(config.ref.update_url);
|
||||
// Free all paths in original_libapp_path.
|
||||
for (var i = 0; i < config.ref.original_libapp_paths_size; i++) {
|
||||
calloc.free(config.ref.original_libapp_paths[i]);
|
||||
}
|
||||
calloc.free(config.ref.original_libapp_paths);
|
||||
calloc.free(config.ref.vm_path);
|
||||
calloc.free(config.ref.cache_dir);
|
||||
calloc.free(config);
|
||||
}
|
||||
}
|
||||
|
||||
typedef _GetBoolFunc = ffi.Bool Function();
|
||||
typedef GetBool = bool Function();
|
||||
|
||||
typedef _GetStringFunc = ffi.Pointer<Utf8> Function();
|
||||
typedef GetString = ffi.Pointer<Utf8> Function();
|
||||
|
||||
typedef _GetVoidFunc = ffi.Void Function();
|
||||
typedef GetVoid = void Function();
|
||||
|
||||
typedef _SBInitFunc = ffi.Void Function(ffi.Pointer<AppParameters> config);
|
||||
typedef SBInit = void Function(ffi.Pointer<AppParameters> config);
|
||||
|
||||
typedef _FreeStringFunc = ffi.Void Function(ffi.Pointer<Utf8> str);
|
||||
typedef FreeString = void Function(ffi.Pointer<Utf8> str);
|
||||
|
||||
class UpdaterBindings {
|
||||
final ffi.DynamicLibrary library;
|
||||
|
||||
late SBInit init;
|
||||
late GetBool checkForUpdate;
|
||||
late GetString activeVersion;
|
||||
late GetString activePath;
|
||||
late FreeString freeString;
|
||||
late GetVoid update;
|
||||
|
||||
UpdaterBindings(this.library) {
|
||||
// None of these call back into Dart, so they're all safely "isLeaf: true".
|
||||
init = library.lookupFunction<_SBInitFunc, SBInit>('shorebird_init',
|
||||
isLeaf: true);
|
||||
activeVersion = library.lookupFunction<_GetStringFunc, GetString>(
|
||||
'shorebird_active_version',
|
||||
isLeaf: true);
|
||||
activePath = library.lookupFunction<_GetStringFunc, GetString>(
|
||||
'shorebird_active_path',
|
||||
isLeaf: true);
|
||||
freeString = library.lookupFunction<_FreeStringFunc, FreeString>(
|
||||
'shorebird_free_string',
|
||||
isLeaf: true);
|
||||
checkForUpdate = library.lookupFunction<_GetBoolFunc, GetBool>(
|
||||
'shorebird_check_for_update',
|
||||
isLeaf: true);
|
||||
update = library.lookupFunction<_GetVoidFunc, GetVoid>('shorebird_update',
|
||||
isLeaf: true);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import 'dart:ffi' as ffi;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'src/bindings.dart';
|
||||
|
||||
class Updater {
|
||||
Updater();
|
||||
|
||||
static UpdaterBindings? _bindings;
|
||||
|
||||
static ffi.DynamicLibrary _loadLibraryInDirectory(
|
||||
{required String directory, required String name}) {
|
||||
if (Platform.isMacOS) {
|
||||
return ffi.DynamicLibrary.open(path.join(directory, 'lib$name.dylib'));
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
return ffi.DynamicLibrary.open(path.join(directory, '$name.dll'));
|
||||
}
|
||||
// Assume everything else follows the Linux pattern.
|
||||
return ffi.DynamicLibrary.open(path.join(directory, 'lib$name.so'));
|
||||
}
|
||||
|
||||
static loadLibrary({required String name, required String directory}) {
|
||||
if (_bindings != null) {
|
||||
throw Exception('Library already loaded.');
|
||||
}
|
||||
final updater = _loadLibraryInDirectory(directory: directory, name: name);
|
||||
_bindings = UpdaterBindings(updater);
|
||||
}
|
||||
|
||||
static loadFlutterLibrary() {
|
||||
if (_bindings != null) {
|
||||
throw Exception('Library already loaded.');
|
||||
}
|
||||
final updater = ffi.DynamicLibrary.process();
|
||||
_bindings = UpdaterBindings(updater);
|
||||
}
|
||||
|
||||
static UpdaterBindings get bindings {
|
||||
if (_bindings == null) {
|
||||
throw Exception('Must call loadLibrary() first.');
|
||||
}
|
||||
return _bindings!;
|
||||
}
|
||||
|
||||
// This is only used when called from a Dart command line.
|
||||
// Shorebird will have initialized the library already for you when
|
||||
// inside a Flutter app.
|
||||
static void initUpdaterLibrary({
|
||||
required String appId,
|
||||
required String version,
|
||||
required String channel,
|
||||
required String? updateUrl,
|
||||
required List<String> baseLibraryPaths,
|
||||
required String vmPath,
|
||||
required String cacheDir,
|
||||
}) {
|
||||
var config = AppParameters.allocate(
|
||||
appId: appId,
|
||||
version: version,
|
||||
channel: channel,
|
||||
updateUrl: updateUrl,
|
||||
libappPaths: baseLibraryPaths,
|
||||
libflutterPath: vmPath,
|
||||
cacheDir: cacheDir,
|
||||
);
|
||||
try {
|
||||
bindings.init(config);
|
||||
} finally {
|
||||
AppParameters.free(config);
|
||||
}
|
||||
}
|
||||
|
||||
bool checkForUpdate() {
|
||||
return bindings.checkForUpdate();
|
||||
}
|
||||
|
||||
void update() {
|
||||
return bindings.update();
|
||||
}
|
||||
|
||||
String? _returnsMaybeString(ffi.Pointer<Utf8> Function() f) {
|
||||
ffi.Pointer<Utf8> cString = ffi.Pointer<Utf8>.fromAddress(0);
|
||||
cString = f();
|
||||
if (cString.address == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return cString.toDartString();
|
||||
} finally {
|
||||
// Using finally for two reasons:
|
||||
// 1. it runs after the return (saving us a local)
|
||||
// 2. it runs even if toDartString throws (which it shouldn't)
|
||||
bindings.freeString(cString);
|
||||
}
|
||||
}
|
||||
|
||||
String? activeVersion() {
|
||||
return _returnsMaybeString(bindings.activeVersion);
|
||||
}
|
||||
|
||||
String? activePath() {
|
||||
return _returnsMaybeString(bindings.activePath);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
name: dart_bindings
|
||||
description: dart bindings for Shorebird updater library
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=2.19.0 <4.0.0'
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
ffi: ^2.0.1
|
||||
# path version must match that depended on by Flutter SDK.
|
||||
path: ^1.8.2
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
test: ^1.21.0
|
||||
@@ -1,4 +0,0 @@
|
||||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
pubspec.lock
|
||||
@@ -1 +0,0 @@
|
||||
Command line application to test ffi wrapping of updater library.
|
||||
@@ -1,30 +0,0 @@
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -1,137 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:dart_bindings/updater.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
void main(List<String> args) async {
|
||||
var directory = path.join(Directory.current.path, 'target', 'debug');
|
||||
Updater.loadLibrary(directory: directory, name: "updater");
|
||||
|
||||
Updater.initUpdaterLibrary(
|
||||
appId: 'demo',
|
||||
version: '1.0.0',
|
||||
channel: 'stable',
|
||||
updateUrl: null,
|
||||
baseLibraryPaths: ['libapp.so'],
|
||||
vmPath: Platform.executable,
|
||||
cacheDir: 'updater_cache',
|
||||
);
|
||||
|
||||
var updater = Updater();
|
||||
final runner = CommandRunner<void>('updater', 'Updater CLI')
|
||||
..addCommand(CheckForUpdate(updater))
|
||||
..addCommand(PrintVersion(updater))
|
||||
..addCommand(PrintPath(updater))
|
||||
..addCommand(Update(updater))
|
||||
..addCommand(Run(updater));
|
||||
await runner.run(args);
|
||||
}
|
||||
|
||||
class CheckForUpdate extends Command<void> {
|
||||
final Updater updater;
|
||||
CheckForUpdate(this.updater);
|
||||
|
||||
@override
|
||||
final name = 'check';
|
||||
|
||||
@override
|
||||
final description = 'Check for an update.';
|
||||
|
||||
@override
|
||||
void run() {
|
||||
var result = updater.checkForUpdate();
|
||||
if (result) {
|
||||
print('Update available');
|
||||
} else {
|
||||
print('No update available');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrintVersion extends Command<void> {
|
||||
final Updater updater;
|
||||
PrintVersion(this.updater);
|
||||
|
||||
@override
|
||||
final name = 'version';
|
||||
|
||||
@override
|
||||
final description = 'Print current installed version.';
|
||||
|
||||
@override
|
||||
void run() {
|
||||
print(updater.activeVersion());
|
||||
}
|
||||
}
|
||||
|
||||
class PrintPath extends Command<void> {
|
||||
final Updater updater;
|
||||
PrintPath(this.updater);
|
||||
|
||||
@override
|
||||
final name = 'path';
|
||||
|
||||
@override
|
||||
final description = 'Print current installed path.';
|
||||
|
||||
@override
|
||||
void run() {
|
||||
print(updater.activePath());
|
||||
}
|
||||
}
|
||||
|
||||
class Update extends Command<void> {
|
||||
final Updater updater;
|
||||
Update(this.updater);
|
||||
|
||||
@override
|
||||
final name = 'update';
|
||||
|
||||
@override
|
||||
final description = 'Update to the latest version.';
|
||||
|
||||
@override
|
||||
void run() {
|
||||
updater.update();
|
||||
}
|
||||
}
|
||||
|
||||
class Run extends Command<void> {
|
||||
final Updater updater;
|
||||
Run(this.updater) {
|
||||
argParser.addFlag('update', abbr: 'u', help: 'Update before running.');
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'run';
|
||||
|
||||
@override
|
||||
final description = 'Run the active version.';
|
||||
|
||||
@override
|
||||
void run() async {
|
||||
// This is a basic demo of what this might look like.
|
||||
// Real callers wouldn't likely do this from Dart as there is no need
|
||||
// to have two copies of the Dart VM running.
|
||||
|
||||
if (argResults!['update']) {
|
||||
updater.update();
|
||||
}
|
||||
|
||||
var path = updater.activePath();
|
||||
if (path == null) {
|
||||
print('No active version (should run the bundled version)');
|
||||
return;
|
||||
}
|
||||
// Should this run update first?
|
||||
print('Running $path');
|
||||
// Is there a portable way to just "exec" and replace the current process?
|
||||
var process = await Process.start(Platform.executable, ['run', path]);
|
||||
process.stdout.transform(utf8.decoder).forEach(stdout.write);
|
||||
process.stderr.transform(utf8.decoder).forEach(stderr.write);
|
||||
// TODO: Handle stdin.
|
||||
exit(await process.exitCode);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
name: dart_cli
|
||||
description: A sample command-line application.
|
||||
version: 1.0.0
|
||||
# repository: https://github.com/my_org/my_repo
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
args: ^2.4.0
|
||||
dart_bindings:
|
||||
path: ../dart_bindings
|
||||
path: ^1.8.3
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
test: ^1.21.0
|
||||
@@ -1,14 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
@@ -1,50 +0,0 @@
|
||||
[package]
|
||||
name = "updater"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# "lib" is used by the "cli" target for testing from Rust
|
||||
# "cdylib" is used by the "dart_cli" target for testing from Dart
|
||||
# "staticlib" is used by the engine build for linking into libflutter.so
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
# Used for exposing C API
|
||||
libc = "0.2.98"
|
||||
# Used for networking.
|
||||
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
# Json serialization/de-serialization.
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.93"
|
||||
# Used for error handling for now.
|
||||
anyhow = {version = "1.0.69", features = ["backtrace"]}
|
||||
# For error!(), info!(), etc macros. `print` will not show up on Android.
|
||||
log = "0.4.14"
|
||||
# For implementing thread-local-storage of ResolvedConfig object.
|
||||
once_cell = "1.17.1"
|
||||
# For reading shorebird.yaml
|
||||
serde_yaml = "0.9.19"
|
||||
# For inflating compressed patch files.
|
||||
bipatch = "1.0.0"
|
||||
# comde is a wrapper around several compression libraries.
|
||||
# We only use zstd and could depend on it directly instead.
|
||||
comde = {version = "0.2.3", default-features = false, features = ["zstandard"]}
|
||||
# Pipe is a simple in-memory pipe implementation, there might be a std way too?
|
||||
pipe = "0.4.0"
|
||||
# For computing hashes of patch files for validation.
|
||||
sha2 = "0.10.6"
|
||||
# For decoding the hex-encoded hashes in Patch network responses.
|
||||
hex = "0.4.3"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
# For logging to Android logcat.
|
||||
android_logger = "0.13.0"
|
||||
# Send panics to log (instead of stderr), thus logcat on Android.
|
||||
log-panics = { version = "2", features = ["with-backtrace"]}
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3.7"
|
||||
@@ -1,175 +0,0 @@
|
||||
# Shorebird CodePush Updater
|
||||
|
||||
The rust library that does the actual update work.
|
||||
|
||||
## Design
|
||||
|
||||
The updater library is built in Rust for safety (and modernity). It's built
|
||||
as a C-compatible library, so it can be used from any language.
|
||||
|
||||
The library is thread-safe, as it needs to be called both from the flutter_main
|
||||
thread (during initialization) and then later from the Dart/UI thread
|
||||
(from application Dart code) in Flutter.
|
||||
|
||||
The overarching principle with the Updater is "first, do no harm". The updater
|
||||
should "fail open", terms of continuing to work with the currently installed
|
||||
or active version of the application even when the network is unavailable.
|
||||
|
||||
The updater also needs to handle error cases conservatively, such as partial
|
||||
downloads from a server, or malformed responses (e.g. a proxy interfering)
|
||||
and not crash the application or leave the application in a broken state.
|
||||
|
||||
Every time the updater runs it needs to verify that the currently installed
|
||||
patch is compatible with the currently installed base version. If it is not,
|
||||
it should refuse to return paths to incompatible patches.
|
||||
|
||||
The updater also needs to regularly verify that the current state directory
|
||||
is in a consistent state. If it is not, it should invalidate any installed
|
||||
patches and return to a clean state.
|
||||
|
||||
Not all of the above is implemented yet, but such is the intent.
|
||||
|
||||
## Architecture
|
||||
|
||||
The updater is split into separate layers. The top layer is the C-compatible
|
||||
API, which is used by all consumers of the updater. The C-compatible API
|
||||
is a thin wrapper around the Rust API, which is the main implementation but
|
||||
only used directly for testing (see the `cli` directory).
|
||||
|
||||
Thread safety is handled by a global configuration object that is locked
|
||||
when accessed. It's possible I've missed cases where this is not sufficient,
|
||||
and there could be thread safety issues in the library.
|
||||
|
||||
* src/c_api.rs - C-compatible API
|
||||
* src/lib.rs - Rust API (and crate root)
|
||||
* src/update.rs - Core updater logic
|
||||
* src/config.rs - In memory configuration and thread locking
|
||||
* src/cache.rs - On-disk state management
|
||||
* src/logging.rs - Logging configuration (for platforms that need it)
|
||||
* src/network.rs - Logic dealing with network requests and updater server
|
||||
|
||||
## Rust
|
||||
We use normal rust idioms (e.g. Result) inside the library and then bridge those
|
||||
to C via an explicit stable C API (explicit enums, null pointers for optional
|
||||
arguments, etc). The reason for this is that it lets the Rust code feel natural
|
||||
and also gives us maximum flexibility in the future for exposing more in the C
|
||||
API without having to refactor the internals of the library.
|
||||
|
||||
https://docs.rust-embedded.org/book/interoperability/rust-with-c.html
|
||||
are docs on how to use Rust from C (what we're doing).
|
||||
|
||||
https://github.com/RubberDuckEng/safe_wren has an example of building in Rust
|
||||
and exposing it with a C api.
|
||||
|
||||
## Integration
|
||||
|
||||
The updater library is built as a static library, and is linked into the
|
||||
libflutter.so as part of a custom build of Flutter. We also link libflutter.so
|
||||
with the correct flags such that updater symbols are exposed to Dart.
|
||||
|
||||
The `dart_bindings` directory contains the Dart bindings for the updater
|
||||
library.
|
||||
|
||||
## Building for Android
|
||||
|
||||
The best way I found was to install:
|
||||
https://github.com/bbqsrc/cargo-ndk
|
||||
|
||||
```
|
||||
cargo install cargo-ndk
|
||||
rustup target add \
|
||||
aarch64-linux-android \
|
||||
armv7-linux-androideabi \
|
||||
x86_64-linux-android \
|
||||
i686-linux-android
|
||||
cargo ndk -t armeabi-v7a -t arm64-v8a build --release
|
||||
```
|
||||
|
||||
When building to include with libflutter.so, you need to build with the same
|
||||
version of the ndk as Flutter is using:
|
||||
|
||||
You'll need to have a Flutter engine checkout already setup and synced.
|
||||
As part of `gclient sync` the Flutter engine repo will pull down a copy of the
|
||||
ndk into `src/third_party/android_tools/ndk`.
|
||||
|
||||
Then you can set the NDK_HOME environment variable to point to that directory.
|
||||
e.g.:
|
||||
```
|
||||
NDK_HOME=$HOME/Documents/GitHub/engine/src/third_party/android_tools/ndk
|
||||
```
|
||||
|
||||
Then you can build the updater library as above. If you don't want to change
|
||||
your NDK_HOME, you can also set the environment variable for just the one call:
|
||||
```
|
||||
NDK_HOME=$HOME/Documents/GitHub/engine/src/third_party/android_tools/ndk cargo ndk -t armeabi-v7a -t arm64-v8a build --release
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Uses cbindgen to generate the header file.
|
||||
|
||||
It isn't currently wired into the build process, so you'll need to run it
|
||||
manually if you change the API.
|
||||
https://github.com/shorebirdtech/shorebird/issues/121
|
||||
|
||||
```
|
||||
cargo install cbindgen
|
||||
cbindgen --config cbindgen.toml --crate updater --output library/include/updater.h
|
||||
```
|
||||
|
||||
## Imagined Architecture (not all implemented)
|
||||
|
||||
### Assumptions (not all enforced yet)
|
||||
* Updater library is never allowed to crash, except on bad parameters from C.
|
||||
* Network and Disk are untrusted.
|
||||
* Running code is trusted.
|
||||
* Store-installed bundle is trusted (e.g. APK).
|
||||
* Updates are signed by a trusted key.
|
||||
* Updates must be applied in order.
|
||||
* Updates are applied in a single transaction.
|
||||
|
||||
### Update State Machine
|
||||
* Server is authoritative, regarding current update/patch state. Client can
|
||||
cache state in memory. Not written to disk.
|
||||
* Patches are downloaded to a temporary location on disk.
|
||||
* Update State Machine:
|
||||
* `ready`: Just woke up, ready to check for updates.
|
||||
* `checking`: Checking for updates.
|
||||
* `update_available`: Update or rollback is available.
|
||||
* `no_update_available`: No update is available.
|
||||
* `downloading`: Downloading an update.
|
||||
* `downloaded`: Downloaded an update.
|
||||
* Client keeps on disk:
|
||||
* cache of patches in "slots"
|
||||
* cache of in-progress download state.
|
||||
* Last booted patch (may not have been successful).
|
||||
* Last successful patch (never rolled back from unless becomes invalid).
|
||||
* Boot State Machine:
|
||||
* `ready`: Just woke up, ready to boot.
|
||||
* `booting`: Booting a patch.
|
||||
* `booted`: Patch is booted, we will not go back from here.
|
||||
|
||||
### Slot State Machine
|
||||
* Patches are cached on disk in "slots".
|
||||
* There is a currently active slot (the one that is booted).
|
||||
* Patches are identified by base revision + patch number.
|
||||
* A given slot is:
|
||||
* `empty`: No update is installed.
|
||||
* `pending`: An update is installed but has not been validated.
|
||||
* `valid`: An update is installed and has been validated.
|
||||
* Validation is a temporary state. Patches/slots are revalidated on boot.
|
||||
|
||||
### Trust model
|
||||
* Network and Disk are untrusted.
|
||||
* Running software (including apk service) is trusted.
|
||||
* Patch contents are signed, public key is included in the APK.
|
||||
|
||||
## TODO:
|
||||
* Add an async API.
|
||||
* Write tests for state management.
|
||||
* Make state management/filesystem management atomic (and tested).
|
||||
* Support validating patches/slots (hashes, signatures, etc).
|
||||
|
||||
## Later-stage update system design docs
|
||||
* https://theupdateframework.io/
|
||||
* https://fuchsia.dev/fuchsia-src/concepts/packages/software_update_system
|
||||
@@ -1,101 +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
|
||||
|
||||
|
||||
/**
|
||||
* 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 the app's libflutter.so, required. May be used for ensuring
|
||||
* downloaded artifacts are compatible with the Flutter/Dart versions
|
||||
* used by the app. For Flutter apps this should be the path to the
|
||||
* bundled libflutter.so. For Dart apps this should be the path to the
|
||||
* dart executable.
|
||||
*/
|
||||
const char *vm_path;
|
||||
/**
|
||||
* Path to cache_dir where the updater will store downloaded artifacts.
|
||||
*/
|
||||
const char *cache_dir;
|
||||
} AppParameters;
|
||||
|
||||
#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.
|
||||
*/
|
||||
SHOREBIRD_EXPORT
|
||||
void shorebird_init(const struct AppParameters *c_params,
|
||||
const char *c_yaml);
|
||||
|
||||
/**
|
||||
* Return the active patch number, or NULL if there is no active patch.
|
||||
*/
|
||||
SHOREBIRD_EXPORT char *shorebird_active_patch_number(void);
|
||||
|
||||
/**
|
||||
* Return the path to the active patch for the app, or NULL if there is no
|
||||
* active patch.
|
||||
*/
|
||||
SHOREBIRD_EXPORT char *shorebird_active_path(void);
|
||||
|
||||
/**
|
||||
* Free a string returned by the updater library.
|
||||
*/
|
||||
SHOREBIRD_EXPORT void shorebird_free_string(char *c_string);
|
||||
|
||||
/**
|
||||
* Check for an update. Returns true if an update is available.
|
||||
*/
|
||||
SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
|
||||
|
||||
/**
|
||||
* Synchronously download an update if one is available.
|
||||
*/
|
||||
SHOREBIRD_EXPORT void shorebird_update(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_failed_launch(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif /* updater_h */
|
||||
@@ -1,146 +0,0 @@
|
||||
// This file handles translating the updater library's types into C types.
|
||||
|
||||
// Currently manually prefixing all functions with "shorebird_" to avoid
|
||||
// name collisions with other libraries.
|
||||
// cbindgen:prefix-with-name could do this for us.
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use crate::updater;
|
||||
|
||||
/// Struct containing configuration parameters for the updater.
|
||||
/// Passed to all updater functions.
|
||||
/// 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 the app's libflutter.so, required. May be used for ensuring
|
||||
/// downloaded artifacts are compatible with the Flutter/Dart versions
|
||||
/// used by the app. For Flutter apps this should be the path to the
|
||||
/// bundled libflutter.so. For Dart apps this should be the path to the
|
||||
/// dart executable.
|
||||
pub vm_path: *const libc::c_char,
|
||||
|
||||
/// Path to cache_dir where the updater will store downloaded artifacts.
|
||||
pub cache_dir: *const libc::c_char,
|
||||
}
|
||||
|
||||
fn to_rust(c_string: *const libc::c_char) -> String {
|
||||
unsafe { CStr::from_ptr(c_string).to_str().unwrap() }.to_string()
|
||||
}
|
||||
|
||||
fn to_rust_vector(c_array: *const *const libc::c_char, size: libc::c_int) -> 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));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn app_config_from_c(c_params: *const AppParameters) -> updater::AppConfig {
|
||||
let c_params_ref = unsafe { &*c_params };
|
||||
|
||||
updater::AppConfig {
|
||||
cache_dir: to_rust(c_params_ref.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,
|
||||
),
|
||||
vm_path: to_rust(c_params_ref.vm_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_init(c_params: *const AppParameters, c_yaml: *const libc::c_char) {
|
||||
let config = app_config_from_c(c_params);
|
||||
|
||||
let yaml_string = to_rust(c_yaml);
|
||||
let result = updater::init(config, &yaml_string);
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("Error initializing updater: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the active patch number, or NULL if there is no active patch.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_active_patch_number() -> *mut c_char {
|
||||
let patch = updater::active_patch();
|
||||
match patch {
|
||||
Some(v) => {
|
||||
let c_patch = CString::new(v.number.to_string()).unwrap();
|
||||
c_patch.into_raw()
|
||||
}
|
||||
None => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the path to the active patch for the app, or NULL if there is no
|
||||
/// active patch.
|
||||
#[no_mangle]
|
||||
// rename to shorebird_patch_path
|
||||
pub extern "C" fn shorebird_active_path() -> *mut c_char {
|
||||
let version = updater::active_patch();
|
||||
match version {
|
||||
Some(v) => {
|
||||
let c_version = CString::new(v.path).unwrap();
|
||||
c_version.into_raw()
|
||||
}
|
||||
None => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Free a string returned by the updater library.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_free_string(c_string: *mut c_char) {
|
||||
unsafe {
|
||||
if c_string.is_null() {
|
||||
return;
|
||||
}
|
||||
drop(CString::from_raw(c_string));
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for an update. Returns true if an update is available.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_check_for_update() -> bool {
|
||||
return updater::check_for_update();
|
||||
}
|
||||
|
||||
/// Synchronously download an update if one is available.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_update() {
|
||||
updater::update();
|
||||
}
|
||||
|
||||
/// 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_failed_launch() {
|
||||
let result = updater::report_failed_launch();
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("Error recording launch failure: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
// This file deals with the cache / state management for the updater.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Ok;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::updater::UpdateError;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct PatchInfo {
|
||||
pub path: String,
|
||||
pub number: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Default, Clone, Debug)]
|
||||
struct Slot {
|
||||
/// Path to the slot directory.
|
||||
path: String,
|
||||
/// Patch number for the patch in this slot.
|
||||
patch_number: usize,
|
||||
}
|
||||
|
||||
impl Slot {
|
||||
fn to_patch_info(&self) -> PatchInfo {
|
||||
PatchInfo {
|
||||
path: self.path.clone(),
|
||||
number: self.patch_number,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This struct is public, as callers can have a handle to it, but modifying
|
||||
// anything inside should be done via the functions below.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct UpdaterState {
|
||||
/// Where this writes to disk.
|
||||
cache_dir: String,
|
||||
/// The release version this cache corresponds to.
|
||||
/// If this does not match the release version we're booting from we will
|
||||
/// clear the cache.
|
||||
release_version: String,
|
||||
/// The patch number of the patch that was last downloaded.
|
||||
latest_downloaded_patch: Option<usize>,
|
||||
/// List of patches that failed to boot. We will never attempt these again.
|
||||
failed_patches: Vec<usize>,
|
||||
/// List of patches that successfully booted. We will never rollback past
|
||||
/// one of these for this device.
|
||||
successful_patches: Vec<usize>,
|
||||
/// Currently selected slot.
|
||||
current_slot_index: Option<usize>,
|
||||
/// List of slots.
|
||||
slots: Vec<Slot>,
|
||||
// Add file path or FD so modifying functions can save it to disk?
|
||||
}
|
||||
|
||||
impl UpdaterState {
|
||||
fn new(cache_dir: String, release_version: String) -> Self {
|
||||
Self {
|
||||
cache_dir,
|
||||
release_version,
|
||||
current_slot_index: None,
|
||||
latest_downloaded_patch: None,
|
||||
failed_patches: Vec::new(),
|
||||
successful_patches: Vec::new(),
|
||||
slots: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdaterState {
|
||||
pub fn is_known_good_patch(&self, patch: &PatchInfo) -> bool {
|
||||
self.successful_patches.iter().any(|v| v == &patch.number)
|
||||
}
|
||||
|
||||
pub fn is_known_bad_patch(&self, patch: &PatchInfo) -> bool {
|
||||
self.failed_patches.iter().any(|v| v == &patch.number)
|
||||
}
|
||||
|
||||
pub fn mark_patch_as_bad(&mut self, patch: &PatchInfo) {
|
||||
if self.is_known_good_patch(patch) {
|
||||
warn!("Tried to report failed launch for a known good patch. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_known_bad_patch(patch) {
|
||||
return;
|
||||
}
|
||||
info!("Marking patch {} as bad", patch.number);
|
||||
self.failed_patches.push(patch.number.clone());
|
||||
}
|
||||
|
||||
pub fn mark_patch_as_good(&mut self, patch: &PatchInfo) {
|
||||
if self.is_known_bad_patch(patch) {
|
||||
warn!("Tried to report successful launch for a known bad patch. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_known_good_patch(patch) {
|
||||
return;
|
||||
}
|
||||
self.successful_patches.push(patch.number.clone());
|
||||
}
|
||||
|
||||
fn load(cache_dir: &str) -> anyhow::Result<Self> {
|
||||
// Load UpdaterState from disk
|
||||
let path = Path::new(cache_dir).join("state.json");
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
// TODO: Now that we depend on serde_yaml for shorebird.yaml
|
||||
// we could use yaml here instead of json.
|
||||
let state = serde_json::from_reader(reader)?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub fn load_or_new_on_error(cache_dir: &str, release_version: &str) -> Self {
|
||||
let loaded = Self::load(cache_dir).unwrap_or_else(|e| {
|
||||
warn!("Failed to load updater state: {}", e);
|
||||
Self::new(cache_dir.to_owned(), release_version.to_owned())
|
||||
});
|
||||
if loaded.release_version != release_version {
|
||||
warn!("Release version changed, clearing updater state");
|
||||
Self::new(cache_dir.to_owned(), release_version.to_owned())
|
||||
} else {
|
||||
loaded
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
// Save UpdaterState to disk
|
||||
std::fs::create_dir_all(&self.cache_dir)?;
|
||||
let path = Path::new(&self.cache_dir).join("state.json");
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
serde_json::to_writer_pretty(writer, self)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is NOT the current booted path (we don't keep that in memory yet).
|
||||
/// This is the patch that is selected in the state.json, which may or may
|
||||
/// not be the one that is booted, but will be the one used next boot.
|
||||
pub fn current_patch(&self) -> Option<PatchInfo> {
|
||||
if self.slots.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(slot_index) = self.current_slot_index {
|
||||
if slot_index >= self.slots.len() {
|
||||
return None;
|
||||
}
|
||||
let slot = &self.slots[slot_index];
|
||||
// Otherwise return the version info from the current slot.
|
||||
return Some(slot.to_patch_info());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn validate_slot(&self, slot: &Slot) -> bool {
|
||||
// Check if the patch is known bad.
|
||||
if self.is_known_bad_patch(&slot.to_patch_info()) {
|
||||
return false;
|
||||
}
|
||||
if PathBuf::from(&slot.path).exists() {
|
||||
return true;
|
||||
}
|
||||
// TODO: This should also check if the hash matches?
|
||||
// let hash = compute_hash(&PathBuf::from(&slot.path));
|
||||
// if let Ok(hash) = hash {
|
||||
// if hash == slot.hash {
|
||||
// return true;
|
||||
// }
|
||||
// error!("Hash mismatch for slot: {:?}", slot);
|
||||
// }
|
||||
false
|
||||
}
|
||||
|
||||
fn latest_bootable_slot(&self) -> Option<usize> {
|
||||
// Find the latest slot that has a patch that is not bad.
|
||||
// Sort the slots by patch number, then return the highest
|
||||
// patch number that is not bad.
|
||||
let mut slots = self.slots.clone();
|
||||
slots.sort_by(|a, b| a.patch_number.cmp(&b.patch_number));
|
||||
slots.reverse();
|
||||
for slot in slots {
|
||||
if self.validate_slot(&slot) {
|
||||
return Some(slot.patch_number);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn activate_latest_bootable_patch(&mut self) -> Result<(), UpdateError> {
|
||||
self.set_current_slot(self.latest_bootable_slot());
|
||||
self.save().map_err(|_| UpdateError::FailedToSaveState)
|
||||
}
|
||||
|
||||
fn available_slot(&self) -> usize {
|
||||
// Assume we only use two slots and pick the one that's not current.
|
||||
if self.slots.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
if let Some(slot_index) = self.current_slot_index {
|
||||
if slot_index == 0 {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn clear_slot(&mut self, index: usize) {
|
||||
if self.slots.len() < index + 1 {
|
||||
return;
|
||||
}
|
||||
self.slots[index] = Slot::default();
|
||||
}
|
||||
|
||||
fn set_slot(&mut self, index: usize, slot: Slot) {
|
||||
info!("Setting slot {} to {:?}", index, slot);
|
||||
if self.slots.len() < index + 1 {
|
||||
// Make sure we're not filling with empty slots.
|
||||
assert!(self.slots.len() == index);
|
||||
self.slots.resize(index + 1, Slot::default());
|
||||
}
|
||||
// Set the given slot to the given version.
|
||||
self.slots[index] = slot
|
||||
}
|
||||
|
||||
fn slot_dir(&self, index: usize) -> String {
|
||||
Path::new(&self.cache_dir)
|
||||
.join(format!("slot_{}", index))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn install_patch(&mut self, patch: PatchInfo) -> anyhow::Result<()> {
|
||||
let slot_index = self.available_slot();
|
||||
let slot_dir_string = self.slot_dir(slot_index);
|
||||
let slot_dir = PathBuf::from(&slot_dir_string);
|
||||
|
||||
// Clear the slot.
|
||||
self.clear_slot(slot_index); // Invalidate the slot.
|
||||
self.save()?;
|
||||
if slot_dir.exists() {
|
||||
std::fs::remove_dir_all(&slot_dir)?;
|
||||
}
|
||||
std::fs::create_dir_all(&slot_dir)?;
|
||||
|
||||
if self.is_known_bad_patch(&patch) {
|
||||
return Err(UpdateError::InvalidArgument(
|
||||
"patch".to_owned(),
|
||||
format!("Refusing to install known bad patch: {:?}", patch),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Move the artifact into the slot.
|
||||
let artifact_path = slot_dir.join("dlc.vmcode");
|
||||
std::fs::rename(&patch.path, &artifact_path)?;
|
||||
|
||||
// Update the state to include the new slot.
|
||||
self.set_slot(
|
||||
slot_index,
|
||||
Slot {
|
||||
path: artifact_path.to_str().unwrap().to_owned(),
|
||||
patch_number: patch.number,
|
||||
},
|
||||
);
|
||||
self.set_current_slot(Some(slot_index));
|
||||
|
||||
if (self.latest_downloaded_patch.is_none())
|
||||
|| (self.latest_downloaded_patch.unwrap() < patch.number)
|
||||
{
|
||||
self.latest_downloaded_patch = Some(patch.number);
|
||||
} else {
|
||||
warn!(
|
||||
"Installed patch {} but latest downloaded patch is {:?}",
|
||||
patch.number, self.latest_downloaded_patch
|
||||
);
|
||||
}
|
||||
self.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_current_slot(&mut self, maybe_index: Option<usize>) {
|
||||
self.current_slot_index = maybe_index;
|
||||
}
|
||||
pub fn latest_patch_number(&self) -> Option<usize> {
|
||||
self.latest_downloaded_patch
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::cache::{PatchInfo, UpdaterState};
|
||||
|
||||
fn test_state(tmp_dir: &TempDir) -> UpdaterState {
|
||||
let cache_dir = tmp_dir.path().to_str().unwrap().to_string();
|
||||
UpdaterState::new(cache_dir, "1.0.0".to_string())
|
||||
}
|
||||
|
||||
fn fake_patch(tmp_dir: &TempDir, number: usize) -> super::PatchInfo {
|
||||
let path = PathBuf::from(tmp_dir.path()).join(format!("patch_{}", number));
|
||||
std::fs::write(&path, "fake patch").unwrap();
|
||||
PatchInfo {
|
||||
number,
|
||||
path: path.to_str().unwrap().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_patch_does_not_crash() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
let mut state = test_state(&tmp_dir);
|
||||
assert_eq!(state.current_patch(), None);
|
||||
state.current_slot_index = Some(3);
|
||||
assert_eq!(state.current_patch(), None);
|
||||
state.slots.push(super::Slot::default());
|
||||
// This used to crash, where index was bad, but slots were not empty.
|
||||
assert_eq!(state.current_patch(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_version_changed() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
let mut state = test_state(&tmp_dir);
|
||||
state.latest_downloaded_patch = Some(1);
|
||||
state.save().unwrap();
|
||||
let loaded = UpdaterState::load_or_new_on_error(&state.cache_dir, &state.release_version);
|
||||
assert_eq!(loaded.latest_downloaded_patch, Some(1));
|
||||
|
||||
let loaded_after_version_change =
|
||||
UpdaterState::load_or_new_on_error(&state.cache_dir, "1.0.1");
|
||||
assert_eq!(loaded_after_version_change.latest_downloaded_patch, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_downloaded_patch() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
let mut state = test_state(&tmp_dir);
|
||||
assert_eq!(state.latest_downloaded_patch, None);
|
||||
state.install_patch(fake_patch(&tmp_dir, 1)).unwrap();
|
||||
assert_eq!(state.latest_downloaded_patch, Some(1));
|
||||
state.install_patch(fake_patch(&tmp_dir, 2)).unwrap();
|
||||
assert_eq!(state.latest_downloaded_patch, Some(2));
|
||||
state.install_patch(fake_patch(&tmp_dir, 1)).unwrap();
|
||||
assert_eq!(state.latest_downloaded_patch, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn do_not_install_known_bad_patch() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
let mut state = test_state(&tmp_dir);
|
||||
let bad_patch = fake_patch(&tmp_dir, 1);
|
||||
state.mark_patch_as_bad(&bad_patch);
|
||||
assert!(state.install_patch(bad_patch).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// This file handles the global config for the updater library.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::updater::AppConfig;
|
||||
use crate::yaml::YamlConfig;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
// cbindgen looks for const, ignore these so it doesn't warn about them.
|
||||
|
||||
/// cbindgen:ignore
|
||||
const DEFAULT_BASE_URL: &'static str = "https://api.shorebird.dev";
|
||||
/// cbindgen:ignore
|
||||
const DEFAULT_CHANNEL: &'static str = "stable";
|
||||
|
||||
fn global_config() -> &'static Mutex<ResolvedConfig> {
|
||||
static INSTANCE: OnceCell<Mutex<ResolvedConfig>> = OnceCell::new();
|
||||
INSTANCE.get_or_init(|| Mutex::new(ResolvedConfig::empty()))
|
||||
}
|
||||
|
||||
pub fn with_config<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&ResolvedConfig) -> R,
|
||||
{
|
||||
let lock = global_config()
|
||||
.lock()
|
||||
.expect("Failed to acquire updater lock.");
|
||||
|
||||
if !lock.is_initialized {
|
||||
panic!("Must call shorebird_init() before using the updater.");
|
||||
}
|
||||
return f(&lock);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ResolvedConfig {
|
||||
is_initialized: bool,
|
||||
pub cache_dir: String,
|
||||
pub download_dir: String,
|
||||
pub channel: String,
|
||||
pub app_id: String,
|
||||
pub release_version: String,
|
||||
pub original_libapp_paths: Vec<String>,
|
||||
pub vm_path: String,
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
impl ResolvedConfig {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
is_initialized: false,
|
||||
cache_dir: String::new(),
|
||||
download_dir: String::new(),
|
||||
channel: String::new(),
|
||||
app_id: String::new(),
|
||||
release_version: String::new(),
|
||||
original_libapp_paths: Vec::new(),
|
||||
vm_path: String::new(),
|
||||
base_url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_config(config: AppConfig, yaml: YamlConfig) {
|
||||
// If there is no base_url, use the default.
|
||||
// If there is no channel, use the default.
|
||||
let mut lock = global_config()
|
||||
.lock()
|
||||
.expect("Failed to acquire updater lock.");
|
||||
lock.base_url = yaml
|
||||
.base_url
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_BASE_URL)
|
||||
.to_owned();
|
||||
lock.channel = yaml
|
||||
.channel
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_CHANNEL)
|
||||
.to_owned();
|
||||
lock.cache_dir = config.cache_dir.to_string();
|
||||
let mut cache_path = std::path::PathBuf::from(config.cache_dir);
|
||||
cache_path.push("downloads");
|
||||
lock.download_dir = cache_path.to_str().unwrap().to_string();
|
||||
lock.app_id = yaml.app_id.to_string();
|
||||
lock.release_version = config.release_version.to_string();
|
||||
lock.original_libapp_paths = config.original_libapp_paths;
|
||||
lock.vm_path = config.vm_path.to_string();
|
||||
lock.is_initialized = true;
|
||||
info!("Updater configured with: {:?}", lock);
|
||||
}
|
||||
|
||||
pub fn current_arch() -> &'static str {
|
||||
#[cfg(target_arch = "x86")]
|
||||
static ARCH: &str = "x86";
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
static ARCH: &str = "x86_64";
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
static ARCH: &str = "aarch64";
|
||||
#[cfg(target_arch = "arm")]
|
||||
static ARCH: &str = "arm";
|
||||
return ARCH;
|
||||
}
|
||||
|
||||
pub fn current_platform() -> &'static str {
|
||||
#[cfg(target_os = "macos")]
|
||||
static PLATFORM: &str = "macos";
|
||||
#[cfg(target_os = "linux")]
|
||||
static PLATFORM: &str = "linux";
|
||||
#[cfg(target_os = "windows")]
|
||||
static PLATFORM: &str = "windows";
|
||||
#[cfg(target_os = "android")]
|
||||
static PLATFORM: &str = "android";
|
||||
return PLATFORM;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// This is a required file for rust libraries which declares what files are
|
||||
// part of the library and what interfaces are public from the library.
|
||||
|
||||
// Declare that the c_api.rs file exists and is a public sub-namespace.
|
||||
// C doesn't care about the namespaces, but Rust does.
|
||||
pub mod c_api;
|
||||
|
||||
// Declare other .rs file/module exists, but make them public.
|
||||
mod cache;
|
||||
mod config;
|
||||
mod logging;
|
||||
mod network;
|
||||
mod updater;
|
||||
mod yaml;
|
||||
|
||||
// Take all public items from the updater namespace and make them public.
|
||||
pub use self::updater::*;
|
||||
|
||||
// Exposes error!(), info!(), etc macros.
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate tempdir;
|
||||
@@ -1,18 +0,0 @@
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn init_logging() {
|
||||
log_panics::init();
|
||||
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default()
|
||||
// `flutter` tool ignores non-flutter tagged logs.
|
||||
.with_tag("flutter")
|
||||
.with_max_level(log::LevelFilter::Debug),
|
||||
);
|
||||
debug!("Logging initialized");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn init_logging() {
|
||||
// Nothing to do on non-Android platforms.
|
||||
// Eventually iOS/MacOS may need something here.
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// This file's job is to deal with the update_server and network side
|
||||
// of the updater library.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::cache::UpdaterState;
|
||||
use crate::config::{current_arch, current_platform, ResolvedConfig};
|
||||
|
||||
fn patches_check_url(base_url: &str) -> String {
|
||||
return format!("{}/api/v1/patches/check", base_url);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Patch {
|
||||
/// The patch number. Starts at 1 for each new release and increases
|
||||
/// monotonically.
|
||||
pub number: usize,
|
||||
/// The hex-encoded sha256 hash of the final uncompressed patch file.
|
||||
/// Legacy: originally "#" before we implemented hash checks (remove).
|
||||
pub hash: String,
|
||||
/// The URL to download the patch file from.
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PatchCheckRequest {
|
||||
/// The Shorebird app_id built into the shorebird.yaml in the app.
|
||||
pub app_id: String,
|
||||
/// The Shorebird channel built into the shorebird.yaml in the app.
|
||||
pub channel: String,
|
||||
/// The release version from AndroidManifest.xml, Info.plist in the app.
|
||||
pub release_version: String,
|
||||
/// The latest patch number that the client has downloaded.
|
||||
/// Not necessarily the one it's running (if some have been marked bad).
|
||||
/// We could rename this to be more clear.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub patch_number: Option<usize>,
|
||||
/// Platform (e.g. "android", "ios", "windows", "macos", "linux").
|
||||
pub platform: String,
|
||||
/// Architecture we're running (e.g. "aarch64", "x86", "x86_64").
|
||||
pub arch: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PatchCheckResponse {
|
||||
pub patch_available: bool,
|
||||
#[serde(default)]
|
||||
pub patch: Option<Patch>,
|
||||
}
|
||||
|
||||
pub fn send_patch_check_request(
|
||||
config: &ResolvedConfig,
|
||||
state: &UpdaterState,
|
||||
) -> anyhow::Result<PatchCheckResponse> {
|
||||
let latest_patch_number = state.latest_patch_number();
|
||||
|
||||
// Send the request to the server.
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let req = PatchCheckRequest {
|
||||
app_id: config.app_id.clone(),
|
||||
channel: config.channel.clone(),
|
||||
release_version: config.release_version.clone(),
|
||||
patch_number: latest_patch_number,
|
||||
platform: current_platform().to_string(),
|
||||
arch: current_arch().to_string(),
|
||||
};
|
||||
info!("Sending patch check request: {:?}", req);
|
||||
let response = client
|
||||
.post(&patches_check_url(&config.base_url))
|
||||
.json(&req)
|
||||
.send()?
|
||||
.json()?;
|
||||
|
||||
info!("Patch check response: {:?}", response);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
pub fn download_to_path(url: &str, path: &Path) -> anyhow::Result<()> {
|
||||
// Download the file at the given url to the given path.
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.get(url).send()?;
|
||||
let mut bytes = response.bytes()?;
|
||||
|
||||
// Ensure the download directory exists.
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(&mut bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::network::PatchCheckResponse;
|
||||
|
||||
#[test]
|
||||
fn check_patch_request_response_deserialization() {
|
||||
let data = r###"
|
||||
{
|
||||
"patch_available": true,
|
||||
"patch": {
|
||||
"number": 1,
|
||||
"download_url": "https://storage.googleapis.com/patch_artifacts/17a28ec1-00cf-452d-bdf9-dbb9acb78600/dlc.vmcode",
|
||||
"hash": "#"
|
||||
}
|
||||
}"###;
|
||||
|
||||
let response: PatchCheckResponse = serde_json::from_str(data).unwrap();
|
||||
|
||||
assert!(response.patch_available == true);
|
||||
assert!(response.patch.is_some());
|
||||
|
||||
let patch = response.patch.unwrap();
|
||||
assert_eq!(patch.number, 1);
|
||||
assert_eq!(patch.download_url, "https://storage.googleapis.com/patch_artifacts/17a28ec1-00cf-452d-bdf9-dbb9acb78600/dlc.vmcode");
|
||||
assert_eq!(patch.hash, "#");
|
||||
}
|
||||
}
|
||||
@@ -1,464 +0,0 @@
|
||||
// This file's job is to be the Rust API for the updater.
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::cache::{PatchInfo, UpdaterState};
|
||||
use crate::config::{set_config, with_config, ResolvedConfig};
|
||||
use crate::logging::init_logging;
|
||||
use crate::network::{download_to_path, send_patch_check_request};
|
||||
use crate::yaml::YamlConfig;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub enum UpdateStatus {
|
||||
NoUpdate,
|
||||
UpdateAvailable,
|
||||
UpdateDownloaded,
|
||||
UpdateInstalled,
|
||||
UpdateHadError,
|
||||
}
|
||||
|
||||
impl Display for UpdateStatus {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UpdateStatus::NoUpdate => write!(f, "No update"),
|
||||
UpdateStatus::UpdateAvailable => write!(f, "Update available"),
|
||||
UpdateStatus::UpdateDownloaded => write!(f, "Update downloaded"),
|
||||
UpdateStatus::UpdateInstalled => write!(f, "Update installed"),
|
||||
UpdateStatus::UpdateHadError => write!(f, "Update had error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum UpdateError {
|
||||
InvalidArgument(String, String),
|
||||
InvalidState(String),
|
||||
BadServerResponse,
|
||||
FailedToSaveState,
|
||||
}
|
||||
|
||||
impl std::error::Error for UpdateError {}
|
||||
|
||||
impl Display for UpdateError {
|
||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
UpdateError::InvalidArgument(name, value) => {
|
||||
write!(f, "Invalid Argument: {} -> {}", name, value)
|
||||
}
|
||||
UpdateError::InvalidState(msg) => write!(f, "Invalid State: {}", msg),
|
||||
UpdateError::FailedToSaveState => write!(f, "Failed to save state"),
|
||||
UpdateError::BadServerResponse => write!(f, "Bad server response"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AppConfig is the rust API. ResolvedConfig is the internal storage.
|
||||
// However rusty api would probably used &str instead of String,
|
||||
// but making &str from CStr* is a bit of a pain.
|
||||
pub struct AppConfig {
|
||||
pub cache_dir: String,
|
||||
pub release_version: String,
|
||||
pub original_libapp_paths: Vec<String>,
|
||||
pub vm_path: String,
|
||||
}
|
||||
|
||||
/// Initialize the updater library.
|
||||
/// Takes a AppConfig struct and a yaml string.
|
||||
/// The yaml string is the contents of the shorebird.yaml file.
|
||||
/// The AppConfig struct is information about the running app and where
|
||||
/// the updater should keep its cache.
|
||||
pub fn init(app_config: AppConfig, yaml: &str) -> Result<(), UpdateError> {
|
||||
init_logging();
|
||||
let config = YamlConfig::from_yaml(&yaml)
|
||||
.map_err(|err| UpdateError::InvalidArgument("yaml".to_string(), err.to_string()))?;
|
||||
set_config(app_config, config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_for_update_internal(config: &ResolvedConfig) -> bool {
|
||||
// Load UpdaterState from disk
|
||||
// If there is no state, make an empty state.
|
||||
let state = UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version);
|
||||
// Send info from app + current slot to server.
|
||||
let response_result = send_patch_check_request(&config, &state);
|
||||
match response_result {
|
||||
Err(err) => {
|
||||
error!("Failed update check: {err}");
|
||||
return false;
|
||||
}
|
||||
Ok(response) => {
|
||||
return response.patch_available;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronously checks for an update and returns true if an update is available.
|
||||
pub fn check_for_update() -> bool {
|
||||
return with_config(check_for_update_internal);
|
||||
}
|
||||
|
||||
fn check_hash(path: &Path, expected_string: &str) -> anyhow::Result<bool> {
|
||||
let result = hex::decode(expected_string);
|
||||
// Remove this legacy behavior.
|
||||
if result.is_err() {
|
||||
warn!("Failed to decode hash from server, allowing: {expected_string}");
|
||||
return Ok(true);
|
||||
}
|
||||
let expected = result.unwrap();
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fs, io};
|
||||
// Based on guidance from:
|
||||
// https://github.com/RustCrypto/hashes#hashing-readable-objects
|
||||
|
||||
let mut file = fs::File::open(&path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
io::copy(&mut file, &mut hasher)?;
|
||||
// Check that the length from copy is the same as the file size?
|
||||
let hash = hasher.finalize();
|
||||
let hash_matches = hash.as_slice() == expected;
|
||||
if !hash_matches {
|
||||
warn!(
|
||||
"Hash mismatch: {:?}, expected: {}, got: {:?}",
|
||||
path,
|
||||
expected_string,
|
||||
hex::encode(hash)
|
||||
);
|
||||
} else {
|
||||
info!("Hash match: {:?}", path);
|
||||
}
|
||||
return Ok(hash_matches);
|
||||
}
|
||||
|
||||
fn update_internal(config: &ResolvedConfig) -> anyhow::Result<UpdateStatus> {
|
||||
// Load the state from disk.
|
||||
let mut state = UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version);
|
||||
// Check for update.
|
||||
let response = send_patch_check_request(&config, &state)?;
|
||||
if !response.patch_available {
|
||||
return Ok(UpdateStatus::NoUpdate);
|
||||
}
|
||||
|
||||
let patch = response.patch.ok_or(UpdateError::BadServerResponse)?;
|
||||
|
||||
let download_dir = PathBuf::from(&config.cache_dir);
|
||||
let download_path = download_dir.join(patch.number.to_string());
|
||||
download_to_path(&patch.download_url, &download_path)?;
|
||||
|
||||
let base_path = get_base_path(&config.original_libapp_paths)?;
|
||||
let output_path = download_dir.join(format!("{}.full", patch.number.to_string()));
|
||||
inflate(&download_path, &base_path, &output_path)?;
|
||||
|
||||
// Check the hash before moving into place.
|
||||
let hash_ok = check_hash(&output_path, &patch.hash)?;
|
||||
if !hash_ok {
|
||||
return Err(UpdateError::InvalidState("Hash mismatch".to_string()).into());
|
||||
}
|
||||
// Move/state update should be "atomic".
|
||||
// Consider supporting allowing the system to download for us (e.g. iOS).
|
||||
|
||||
let patch_info = PatchInfo {
|
||||
path: output_path.to_str().unwrap().to_string(),
|
||||
number: patch.number,
|
||||
};
|
||||
state.install_patch(patch_info)?;
|
||||
info!("Patch {} successfully installed.", patch.number);
|
||||
|
||||
// Set the state to "restart required".
|
||||
return Ok(UpdateStatus::UpdateInstalled);
|
||||
}
|
||||
|
||||
fn get_base_path(original_lib_app_paths: &Vec<String>) -> anyhow::Result<PathBuf> {
|
||||
// Iterate through the paths and find the first one that exists.
|
||||
for path in original_lib_app_paths {
|
||||
let path = PathBuf::from(path);
|
||||
match path.try_exists() {
|
||||
Ok(true) => {
|
||||
return Ok(path);
|
||||
}
|
||||
Ok(false) => {
|
||||
info!("File does not exist: {:?}", path);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Failed to check for file: {:?}, err: {err}", path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(UpdateError::InvalidState("No base file found".to_string()).into());
|
||||
}
|
||||
|
||||
fn inflate(patch_path: &Path, base_path: &Path, output_path: &Path) -> anyhow::Result<()> {
|
||||
info!("Patch is compressed, inflating...");
|
||||
use anyhow::Context;
|
||||
use comde::de::Decompressor;
|
||||
use comde::zstd::ZstdDecompressor;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
|
||||
// Open all our files first for error clarity. Otherwise we might see
|
||||
// PipeReader/Writer errors instead of file open errors.
|
||||
info!("Reading base file: {:?}", base_path);
|
||||
let base_r =
|
||||
File::open(base_path).context(format!("Failed to open base file: {:?}", base_path))?;
|
||||
|
||||
info!("Reading patch file: {:?}", patch_path);
|
||||
let compressed_patch_r = BufReader::new(
|
||||
File::open(patch_path).context(format!("Failed to open patch file: {:?}", patch_path))?,
|
||||
);
|
||||
let output_file_w = File::create(&output_path)?;
|
||||
|
||||
// Set up a pipe to connect the writing from the decompression thread
|
||||
// to the reading of the decompressed patch data on this thread.
|
||||
let (patch_r, patch_w) = pipe::pipe();
|
||||
|
||||
let decompress = ZstdDecompressor::new();
|
||||
// Spawn a thread to run the decompression in parallel to the patching.
|
||||
// decompress.copy will block on the pipe being full (I think) and then
|
||||
// when it returns the thread will exit.
|
||||
std::thread::spawn(move || {
|
||||
let result = decompress.copy(compressed_patch_r, patch_w);
|
||||
// If this thread fails, undoubtedly the main thread will fail too.
|
||||
// Most important is to not crash.
|
||||
if let Err(err) = result {
|
||||
error!("Decompression thread failed: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
// Do the patch, using the uncompressed patch data from the pipe.
|
||||
let mut fresh_r = bipatch::Reader::new(patch_r, base_r)?;
|
||||
|
||||
// Write out the resulting patched file to the new location.
|
||||
let mut output_w = BufWriter::new(output_file_w);
|
||||
std::io::copy(&mut fresh_r, &mut output_w)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads the current patch from the cache and returns it.
|
||||
pub fn active_patch() -> Option<PatchInfo> {
|
||||
return with_config(|config| {
|
||||
let state = UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version);
|
||||
return state.current_patch();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn report_failed_launch() -> Result<(), UpdateError> {
|
||||
info!("Reporting failed launch.");
|
||||
with_config(|config| {
|
||||
let mut state =
|
||||
UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version);
|
||||
|
||||
let patch = state
|
||||
.current_patch()
|
||||
.ok_or(UpdateError::InvalidState("No current patch".to_string()))?;
|
||||
state.mark_patch_as_bad(&patch);
|
||||
state.activate_latest_bootable_patch()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn report_successful_launch() -> Result<(), UpdateError> {
|
||||
with_config(|config| {
|
||||
let mut state =
|
||||
UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version);
|
||||
|
||||
let patch = state
|
||||
.current_patch()
|
||||
.ok_or(UpdateError::InvalidState("No current patch".to_string()))?;
|
||||
state.mark_patch_as_good(&patch);
|
||||
state.save().map_err(|_| UpdateError::FailedToSaveState)
|
||||
})
|
||||
}
|
||||
|
||||
/// Synchronously checks for an update and downloads and installs it if available.
|
||||
pub fn update() -> UpdateStatus {
|
||||
return with_config(|config| {
|
||||
let result = update_internal(&config);
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("Problem updating: {err}");
|
||||
error!("{}", err.backtrace());
|
||||
return UpdateStatus::UpdateHadError;
|
||||
}
|
||||
Ok(status) => status,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempdir::TempDir;
|
||||
|
||||
fn init_for_testing(tmp_dir: &TempDir) {
|
||||
let cache_dir = tmp_dir.path().to_str().unwrap().to_string();
|
||||
crate::init(
|
||||
crate::AppConfig {
|
||||
cache_dir: cache_dir.clone(),
|
||||
release_version: "1.0.0".to_string(),
|
||||
original_libapp_paths: vec!["original_libapp_path".to_string()],
|
||||
vm_path: "vm_path".to_string(),
|
||||
},
|
||||
"app_id: 1234",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_missing_yaml() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
let cache_dir = tmp_dir.path().to_str().unwrap().to_string();
|
||||
assert_eq!(
|
||||
crate::init(
|
||||
crate::AppConfig {
|
||||
cache_dir: cache_dir.clone(),
|
||||
release_version: "1.0.0".to_string(),
|
||||
original_libapp_paths: vec!["original_libapp_path".to_string()],
|
||||
vm_path: "vm_path".to_string(),
|
||||
},
|
||||
"",
|
||||
),
|
||||
Err(crate::UpdateError::InvalidArgument(
|
||||
"yaml".to_string(),
|
||||
"missing field `app_id`".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_launch_result_with_no_current_patch() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
init_for_testing(&tmp_dir);
|
||||
assert_eq!(
|
||||
crate::report_failed_launch(),
|
||||
Err(crate::UpdateError::InvalidState(
|
||||
"No current patch".to_string()
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
crate::report_successful_launch(),
|
||||
Err(crate::UpdateError::InvalidState(
|
||||
"No current patch".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_version_after_marked_bad() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
init_for_testing(&tmp_dir);
|
||||
|
||||
use crate::cache::{PatchInfo, UpdaterState};
|
||||
use crate::config::with_config;
|
||||
|
||||
// Install a fake patch.
|
||||
with_config(|config| {
|
||||
let download_dir = std::path::PathBuf::from(&config.download_dir);
|
||||
let artifact_path = download_dir.join("1");
|
||||
println!("artifact_path: {:?}", artifact_path);
|
||||
std::fs::create_dir_all(&download_dir).unwrap();
|
||||
std::fs::write(&artifact_path, "hello").unwrap();
|
||||
|
||||
let mut state =
|
||||
UpdaterState::load_or_new_on_error(&config.cache_dir, &config.release_version);
|
||||
state
|
||||
.install_patch(PatchInfo {
|
||||
path: artifact_path.to_str().unwrap().to_string(),
|
||||
number: 1,
|
||||
})
|
||||
.expect("move failed");
|
||||
state.save().expect("save failed");
|
||||
});
|
||||
assert!(crate::active_patch().is_some());
|
||||
// pretend we booted from it
|
||||
crate::report_successful_launch().unwrap();
|
||||
assert!(crate::active_patch().is_some());
|
||||
// mark it bad.
|
||||
crate::report_failed_launch().unwrap();
|
||||
// Technically might need to "reload"
|
||||
// ask for current patch (should get none).
|
||||
assert!(crate::active_patch().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_matches() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
|
||||
let input_path = tmp_dir.path().join("input");
|
||||
std::fs::write(&input_path, "hello world").unwrap();
|
||||
|
||||
let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
|
||||
assert!(super::check_hash(&input_path, expected).unwrap());
|
||||
|
||||
// modify hash to not match
|
||||
let expected = "a94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
|
||||
assert_eq!(super::check_hash(&input_path, expected).unwrap(), false);
|
||||
|
||||
// invalid hashes should not match either
|
||||
// Except for now they do (legacy behavior).
|
||||
let expected = "foo";
|
||||
assert_eq!(super::check_hash(&input_path, expected).unwrap(), true);
|
||||
|
||||
// Remove this case when legacy clients are gone.
|
||||
let expected = "#";
|
||||
assert!(super::check_hash(&input_path, expected).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inflate_missing_files() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
let missing_file = tmp_dir.path().join("missing_file");
|
||||
let existing_file = tmp_dir.path().join("existing_file");
|
||||
std::fs::write(&existing_file, "hello world").unwrap();
|
||||
|
||||
let mut result = super::inflate(&missing_file, &missing_file, &missing_file);
|
||||
let mut error = result.unwrap_err();
|
||||
assert!(format!("{}", error).starts_with("Failed to open base file:"));
|
||||
|
||||
result = super::inflate(&missing_file, &existing_file, &missing_file);
|
||||
error = result.unwrap_err();
|
||||
assert!(format!("{}", error).starts_with("Failed to open patch file:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_base_path_uses_correct_path() {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
let missing_file = tmp_dir.path().join("missing_file");
|
||||
let existing_file = tmp_dir.path().join("existing_file");
|
||||
std::fs::write(&existing_file, "hello world").unwrap();
|
||||
|
||||
// Should use first file since it exists.
|
||||
let mut base_paths = vec![
|
||||
existing_file.as_path().to_str().unwrap().to_string(),
|
||||
missing_file.as_path().to_str().unwrap().to_string(),
|
||||
];
|
||||
|
||||
let mut result = super::get_base_path(&base_paths);
|
||||
|
||||
assert_eq!(result.unwrap(), existing_file.as_path());
|
||||
|
||||
// Should skip first file since it is missing.
|
||||
base_paths = vec![
|
||||
missing_file.as_path().to_str().unwrap().to_string(),
|
||||
existing_file.as_path().to_str().unwrap().to_string(),
|
||||
];
|
||||
|
||||
result = super::get_base_path(&base_paths);
|
||||
|
||||
assert_eq!(result.unwrap(), existing_file.as_path());
|
||||
|
||||
// Should error since all files are missing.
|
||||
base_paths = vec![
|
||||
missing_file.as_path().to_str().unwrap().to_string(),
|
||||
missing_file.as_path().to_str().unwrap().to_string(),
|
||||
];
|
||||
|
||||
result = super::get_base_path(&base_paths);
|
||||
|
||||
let error = result.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
"Invalid State: No base file found".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Struct for parsing shorebird.yaml.
|
||||
#[derive(Deserialize)]
|
||||
pub struct YamlConfig {
|
||||
/// App ID. Required. Generated by Shorebird and included
|
||||
/// in your app to identify which app/channel/version triple to update.
|
||||
pub app_id: String,
|
||||
/// Update channel name. Defaults to "stable" if not set.
|
||||
pub channel: Option<String>,
|
||||
/// Update URL. Defaults to the default update URL if not set.
|
||||
pub base_url: Option<String>,
|
||||
}
|
||||
|
||||
impl YamlConfig {
|
||||
/// Read in shorebird.yaml from a string.
|
||||
pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
|
||||
serde_yaml::from_str(yaml)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "patch"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# Compression and decompression of patch files.
|
||||
bidiff = "1.0.0"
|
||||
# Pipe is a simple in-memory pipe implementation, there might be a std way too?
|
||||
pipe = "0.4.0"
|
||||
# comde is a wrapper around several compression libraries.
|
||||
# We only use zstd and could depend on it directly instead.
|
||||
comde = {version = "0.2.3", default-features = false, features = ["zstandard"]}
|
||||
@@ -1,13 +0,0 @@
|
||||
# patch command line tool
|
||||
|
||||
This is the tool used by the `shorebird` command line to compute the patch
|
||||
file for uploading to the server.
|
||||
|
||||
This currently uses the rust `bidiff` crate to compute the patch file.
|
||||
and could just use the `bic` command line tool included in that crate. However
|
||||
we're explicitly writing our own command line to allow us to change the
|
||||
underlying compression without affecting the `shorebird` command line callers.
|
||||
|
||||
## Usage
|
||||
|
||||
patch <old> <new> <patch>
|
||||
@@ -1,52 +0,0 @@
|
||||
use bidiff::DiffParams;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufWriter, Write},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use comde::com::Compressor;
|
||||
use comde::zstd::ZstdCompressor;
|
||||
|
||||
// Originally inspired from example in:
|
||||
// https://github.com/divvun/bidiff/blob/main/crates/bic/src/main.rs
|
||||
// and then hacked down to just service our needs.
|
||||
|
||||
// comde is just a wrapper around various compression/decompression libraries.
|
||||
// and we could just depend on the zstd crate directly if we end up using
|
||||
// zstd long term.
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args();
|
||||
args.next(); // skip program name
|
||||
let older = args.next().expect("path to base file");
|
||||
let newer = args.next().expect("path to new file");
|
||||
let patch = args.next().expect("path to output file");
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let older_contents = fs::read(older).expect("read base file");
|
||||
let newer_contents = fs::read(newer).expect("read new file");
|
||||
|
||||
let (mut patch_r, mut patch_w) = pipe::pipe();
|
||||
let diff_params = DiffParams::new(1, None).unwrap();
|
||||
std::thread::spawn(move || {
|
||||
bidiff::simple_diff_with_params(
|
||||
&older_contents[..],
|
||||
&newer_contents[..],
|
||||
&mut patch_w,
|
||||
&diff_params,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let compressor = ZstdCompressor::new();
|
||||
|
||||
let mut compatch_w = BufWriter::new(File::create(patch).expect("create patch file"));
|
||||
compressor
|
||||
.compress(&mut compatch_w, &mut patch_r)
|
||||
.expect("compress patch");
|
||||
compatch_w.flush().expect("flush patch");
|
||||
|
||||
println!("Completed in {:?}", start.elapsed());
|
||||
}
|
||||
Reference in New Issue
Block a user