refactor: split updater library into layers (#47)

This commit is contained in:
Eric Seidel
2023-03-08 09:11:10 -08:00
committed by GitHub
parent c45cd3a69d
commit dcf5f080ad
52 changed files with 1836 additions and 600 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"cSpell.words": [
"aarch",
"androideabi",
"armv",
"calloc",
"cbindgen",
"dylib",
"libapp",
"libc",
"libflutter",
"repr",
"reqwest",
"rollouts",
"rustup",
"struct"
]
}
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# We could do something much fancier here.
# Fetch dart dependencies
cd packages/shorebird_cli
dart pub get
cd ../..
cd packages/shorebird_code_push_api
dart pub get
cd ../..
cd packages/shorebird_code_push_api_client
dart pub get
cd ../..
cd updater/dart_bindings
dart pub get
cd ../..
cd updater/dart_cli
dart pub get
cd ../..
# And the rust side
cd updater
cargo check
+44
View File
@@ -0,0 +1,44 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+45
View File
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
channel: master
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
base_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
- platform: android
create_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
base_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
- platform: ios
create_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
base_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
- platform: linux
create_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
base_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
- platform: macos
create_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
base_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
- platform: web
create_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
base_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
- platform: windows
create_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
base_revision: c4bc9d5f37b8a0e3fe5402f30659cb87c3dce3cb
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+16
View File
@@ -0,0 +1,16 @@
# counter
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+29
View File
@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+13
View File
@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
+71
View File
@@ -0,0 +1,71 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.counter"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.counter">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,34 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.counter">
<application
android:label="counter"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
@@ -0,0 +1,6 @@
package com.example.counter
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.counter">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+31
View File
@@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
+11
View File
@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+130
View File
@@ -0,0 +1,130 @@
import 'package:dart_bindings/updater.dart';
import 'package:flutter/material.dart';
void main() {
Updater.loadFlutterLibrary();
var updater = Updater();
// Just to prove the bindings work at all:
print(updater.activeVersion());
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a blue toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
+203
View File
@@ -0,0 +1,203 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
url: "https://pub.dev"
source: hosted
version: "2.10.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev"
source: hosted
version: "1.17.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
url: "https://pub.dev"
source: hosted
version: "1.0.5"
dart_bindings:
dependency: "direct main"
description:
path: "../../updater/dart_bindings"
relative: true
source: path
version: "1.0.0"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
lints:
dependency: transitive
description:
name: lints
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8
url: "https://pub.dev"
source: hosted
version: "0.12.14"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.dev"
source: hosted
version: "0.2.0"
meta:
dependency: transitive
description:
name: meta
sha256: "12307e7f0605ce3da64cf0db90e5fcab0869f3ca03f76be6bb2991ce0a55e82b"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
url: "https://pub.dev"
source: hosted
version: "1.9.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
url: "https://pub.dev"
source: hosted
version: "1.11.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "6182294da5abf431177fccc1ee02401f6df30f766bc6130a0852c6b6d7ee6b2d"
url: "https://pub.dev"
source: hosted
version: "0.4.18"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
sdks:
dart: ">=3.0.0-266.0.dev <4.0.0"
+91
View File
@@ -0,0 +1,91 @@
name: counter
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=2.19.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
dart_bindings:
path: ../../updater/dart_bindings
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
+122
View File
@@ -0,0 +1,122 @@
# 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.
-128
View File
@@ -1,128 +0,0 @@
To replicate the updater demo, you'll need a copy of the Flutter Engine.
These are _not_ how Shorebird will work, but this is what I hacked together
for the demo video. Writing these down so others can replicate if desired.
# Building the updater library for Android
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
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
I would consider building a clean engine first and testing that you have
that working before trying Shorebird's modified engine.
The hacked up version of the engine used for my demo can be found here:
https://github.com/shorebirdtech/engine/tree/codepush
# Symlink in the Rust binaries
I symlinked the results of the rust build into the engine/src directory:
```
cd flutter
mkdir updater
cd updater
ln -s $HOME/Documents/GitHub/shorebird_private/shorebird/updater/library/include/updater.h
mkdir android_aarch64
cd android_aarch64
ln -s $HOME/Documents/GitHub/shorebird_private/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 the updater
From updater_demo:
```
flutter run --local-engine-src-path $HOME/Documents/GitHub/engine/src --local-engine=android_release_arm64 --release
```
Only need to do that once, once it's installed on the phone then you don't
need `flutter run` anymore.
# Building the replacement libraries
For the demo I used "android.a" and "android.b" which were just copies of
libapp.so files which Flutter had built for me.
Once you've built the Flutter app in the way you want it:
```
cp build/app/intermediates/stripped_native_libs/release/out/lib/arm64-v8a/libapp.so android.a
```
You could dig them out of the apk, but that intermediate directory should be
the correct file and is much easier.
`flutter build apk -t lib/main_b.dart` should build the app in the way I used
in my demo (I built it with `flutter run` and modifying main.dart directly, but
that command should work too).
# shorebird command line
I hadn't yet modified `shorebird` to include the `publisher` functionality,
so I had this in my path:
```
#!/bin/bash
dart run $HOME/Documents/Github/shorebird_private/shorebird/updater/publisher/bin/publisher.dart publish $2
```
The right solution is to remove the old shorebird functionality and integrate publisher.
# Ports
Because updater_server as running locally I also had to forward ports from my
host into the emulator:
```
adb reverse tcp:8080 tcp:8080
```
# Running the updater_server
In a separate terminal:
```
cd shorebird/updater/updater_server
dart run
```
# The demo
The demo was then just launching the app on the emulator (manually)
and then using the `shorebird` command line to publish the new libraries
to change what code the app ran:
```
shorebird publish android.a
shorebird publish android.b
```
+7
View File
@@ -0,0 +1,7 @@
# 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
+11 -6
View File
@@ -21,16 +21,21 @@ fn main() {
let config = updater::AppConfig {
client_id: "demo".to_string(),
cache_dir: None,
// base_url: "http://localhost:8080",
// channel: "stable",
cache_dir: "updater_cache".to_owned(),
base_url: Some("http://localhost:8000".to_owned()),
channel: Some("stable".to_owned()),
product_id: "demo".to_owned(),
base_version: "0.1.0".to_owned(),
original_libapp_path: "libapp.so".to_owned(),
vm_path: "libflutter.so".to_owned(),
};
updater::init(config);
// 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(&config);
let needs_update = updater::check_for_update();
println!("Checking for update...");
if needs_update {
println!("Update needed.");
@@ -39,7 +44,7 @@ fn main() {
}
}
Some(Commands::Current {}) => {
let version = updater::active_version(&config);
let version = updater::active_patch();
println!("Current version info:");
match version {
Some(v) => {
@@ -53,7 +58,7 @@ fn main() {
}
}
Some(Commands::Update {}) => {
let status = updater::update(&config);
let status = updater::update();
println!("Update: {}", status);
}
None => {}
+7
View File
@@ -0,0 +1,7 @@
# 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
+6
View File
@@ -0,0 +1,6 @@
# 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.
@@ -0,0 +1,30 @@
# 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
+102
View File
@@ -0,0 +1,102 @@
// This entire file could be easily autogenerated.
// Probably https://pub.dev/packages/ffigen would work.
import 'dart:ffi' as ffi;
import 'dart:io' show Directory, Platform;
import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as path;
// 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> client_id;
// ignore: non_constant_identifier_names
external ffi.Pointer<Utf8> product_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<Utf8> original_libapp_path;
// 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 clientId,
required String productId,
required String version,
required String channel,
required String? updateUrl,
required String libappPath,
required String libflutterPath,
required String cacheDir}) {
var config = calloc<AppParameters>();
config.ref.client_id = clientId.toNativeUtf8();
config.ref.product_id = productId.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_path = libappPath.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.client_id);
calloc.free(config.ref.product_id);
calloc.free(config.ref.base_version);
calloc.free(config.ref.channel);
calloc.free(config.ref.update_url);
calloc.free(config.ref.original_libapp_path);
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) {
init = library.lookupFunction<_SBInitFunc, SBInit>('shorebird_init');
activeVersion = library
.lookupFunction<_GetStringFunc, GetString>('shorebird_active_version');
activePath = library
.lookupFunction<_GetStringFunc, GetString>('shorebird_active_path');
freeString = library
.lookupFunction<_FreeStringFunc, FreeString>('shorebird_free_string');
checkForUpdate = library
.lookupFunction<_GetBoolFunc, GetBool>('shorebird_check_for_update');
update = library.lookupFunction<_GetVoidFunc, GetVoid>('shorebird_update');
}
}
+110
View File
@@ -0,0 +1,110 @@
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 clientId,
required String productId,
required String version,
required String channel,
required String? updateUrl,
required String baseLibraryPath,
required String vmPath,
required String cacheDir,
}) {
var config = AppParameters.allocate(
productId: productId,
version: version,
channel: channel,
updateUrl: updateUrl,
libappPath: baseLibraryPath,
libflutterPath: vmPath,
clientId: clientId,
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);
}
}
+15
View File
@@ -0,0 +1,15 @@
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: ^1.8.3
dev_dependencies:
lints: ^2.0.0
test: ^1.21.0
+15 -6
View File
@@ -2,16 +2,25 @@ import 'dart:convert';
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:dart_cli/updater.dart';
import 'package:dart_bindings/updater.dart';
import 'package:path/path.dart' as path;
void main(List<String> args) async {
// This might pass a path or flavor (debug/release) later.
Updater.loadLibrary();
var directory = path.join(Directory.current.path, 'target', 'debug');
Updater.loadLibrary(directory: directory, name: "updater");
var clientId = 'my-client-id';
var cacheDir = 'updater_cache';
var updater = Updater(clientId, cacheDir);
Updater.initUpdaterLibrary(
clientId: 'my-client-id',
productId: 'product',
version: '1.0.0',
channel: 'stable',
updateUrl: null,
baseLibraryPath: 'libapp.so',
vmPath: Platform.executable,
cacheDir: 'updater_cache',
);
var updater = Updater();
final runner = CommandRunner<void>('updater', 'Updater CLI')
..addCommand(CheckForUpdate(updater))
..addCommand(PrintVersion(updater))
-60
View File
@@ -1,60 +0,0 @@
// This entire file could be easily autogenerated.
import 'dart:ffi' as ffi;
import 'dart:io' show Directory, Platform;
import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as path;
typedef _GetBoolFunc = ffi.Bool Function(
ffi.Pointer<Utf8> clientId, ffi.Pointer<Utf8> cacheDir);
typedef GetBool = bool Function(
ffi.Pointer<Utf8> clientId, ffi.Pointer<Utf8> cacheDir);
typedef _GetStringFunc = ffi.Pointer<Utf8> Function(
ffi.Pointer<Utf8> clientId, ffi.Pointer<Utf8> cacheDir);
typedef GetString = ffi.Pointer<Utf8> Function(
ffi.Pointer<Utf8> clientId, ffi.Pointer<Utf8> cacheDir);
typedef _GetVoidFunc = ffi.Void Function(
ffi.Pointer<Utf8> clientId, ffi.Pointer<Utf8> cacheDir);
typedef GetVoid = void Function(
ffi.Pointer<Utf8> clientId, ffi.Pointer<Utf8> cacheDir);
typedef _FreeStringFunc = ffi.Void Function(ffi.Pointer<Utf8> str);
typedef FreeString = void Function(ffi.Pointer<Utf8> str);
class UpdaterBindings {
final ffi.DynamicLibrary _updater;
late GetBool checkForUpdate;
late GetString activeVersion;
late GetString activePath;
late FreeString freeString;
late GetVoid update;
static ffi.DynamicLibrary loadLibrary(String directory, 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'));
}
UpdaterBindings()
: _updater = loadLibrary(
path.join(Directory.current.path, 'target', 'debug'), "updater") {
checkForUpdate =
_updater.lookupFunction<_GetBoolFunc, GetBool>('check_for_update');
activeVersion =
_updater.lookupFunction<_GetStringFunc, GetString>('active_version');
activePath =
_updater.lookupFunction<_GetStringFunc, GetString>('active_path');
freeString =
_updater.lookupFunction<_FreeStringFunc, FreeString>('free_string');
update = _updater.lookupFunction<_GetVoidFunc, GetVoid>('update');
}
}
-86
View File
@@ -1,86 +0,0 @@
// This eventually moves to its own package.
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';
import 'bindings.dart';
class Updater {
final String clientId;
final String cacheDir;
Updater(this.clientId, this.cacheDir);
static UpdaterBindings? _bindings;
static loadLibrary() {
if (_bindings != null) {
throw Exception('Library already loaded.');
}
_bindings = UpdaterBindings();
}
static UpdaterBindings get bindings {
if (_bindings == null) {
throw Exception('Must call loadLibrary() first.');
}
return _bindings!;
}
// Currently bindings passes context as two separate char*, if it ever
// uses a struct instead at least we only have one place to change.
T _callWithContext<T>(
T Function(ffi.Pointer<Utf8> clientId, ffi.Pointer<Utf8> cacheDir) f) {
// Will this leak if the second toNativeUtf8 throws an exception?
var clientId = this.clientId.toNativeUtf8();
var cacheDir = this.cacheDir.toNativeUtf8();
try {
return f(clientId, cacheDir);
} finally {
calloc.free(clientId);
calloc.free(cacheDir);
}
}
bool checkForUpdate() {
return _callWithContext(bindings.checkForUpdate);
}
void update() {
return _callWithContext(bindings.update);
}
String? activeVersion() {
return _callWithContext((clientId, cacheDir) {
ffi.Pointer<Utf8> cVersion = ffi.Pointer<Utf8>.fromAddress(0);
try {
cVersion = bindings.activeVersion(clientId, cacheDir);
if (cVersion.address == 0) {
return null;
}
return cVersion.toDartString();
} finally {
// Can toDartString ever throw an exception, such that this finally
// block is necessary?
bindings.freeString(cVersion);
}
});
}
String? activePath() {
return _callWithContext((clientId, cacheDir) {
ffi.Pointer<Utf8> cVersion = ffi.Pointer<Utf8>.fromAddress(0);
try {
cVersion = bindings.activePath(clientId, cacheDir);
if (cVersion.address == 0) {
return null;
}
return cVersion.toDartString();
} finally {
// Can toDartString ever throw an exception, such that this finally
// block is necessary?
bindings.freeString(cVersion);
}
});
}
}
+3 -2
View File
@@ -2,14 +2,15 @@ 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
ffi: ^2.0.1
path: ^1.8.3
dart_bindings:
path: ../dart_bindings
dev_dependencies:
lints: ^2.0.0
+4 -4
View File
@@ -19,10 +19,10 @@ reqwest = { version = "0.11", default-features = false, features = ["blocking",
# Json serialization/de-serialization.
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.93"
# Used for creating custom errors.
thiserror = "1.0"
# Used for error handling.
anyhow = {version = "1.0.69", features = ["backtrace"]}
# Used for logging.
android_logger = "0.13.0"
# For error!(), info!(), etc macros.
log = "0.4.14"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.0"
+10
View File
@@ -17,3 +17,13 @@ rustup +beta target add \
i686-linux-android
cargo +beta ndk --target aarch64-linux-android build --release
```
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.
```
cargo install cbindgen
cbindgen --config cbindgen.toml --crate updater --output library/include/updater.h
```
+85 -9
View File
@@ -1,20 +1,96 @@
#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>
/**
* 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 {
/**
* Update channel name. Set to NULL or "eng" to disable updates.
*/
const char *channel;
/**
* Client ID, required. Typically a UUID, used for handling
* percentage rollouts.
*/
const char *client_id;
/**
* Product ID, required. Typically generated by Shorebird and included
* in your app to identify which app/channel/version triple to update.
*/
const char *product_id;
/**
* base_version, required. Named version of the app, off of which updates
* are based. Can be either a version number or a hash.
*/
const char *base_version;
/**
* Update URL. Set to NULL to use the default update URL.
*/
const char *update_url;
/**
* Path to the original aot library, required. For Flutter apps this
* is the path to the bundled libapp.so. May be used for compression
* downloaded artifacts.
*/
const char *original_libapp_path;
/**
* 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
extern "C" {
#endif // __cplusplus
char *active_version(const char *client_id, const char *cache_dir);
char *active_path(const char *client_id, const char *cache_dir);
bool check_for_update(const char *client_id, const char *cache_dir);
void update(const char *client_id, const char *cache_dir);
void shorebird_init(const struct AppParameters *c_params);
void free_string(char *str);
/**
* Return the active version of the app, or NULL if there is no active version.
*/
char *shorebird_active_version(void);
/**
* Return the path to the active version of the app, or NULL if there is no
* active version.
*/
char *shorebird_active_path(void);
/**
* Free a string returned by the updater library.
*/
void shorebird_free_string(char *c_string);
/**
* Check for an update. Returns true if an update is available.
*/
bool shorebird_check_for_update(void);
/**
* Synchronously download an update if one is available.
*/
void shorebird_update(void);
#ifdef __cplusplus
} // extern "C"
#endif
#endif // __cplusplus
#endif /* updater_h */
+82 -28
View File
@@ -1,29 +1,83 @@
// 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;
fn app_config_from_c(c_client_id: *const c_char, c_cache_dir: *const c_char) -> updater::AppConfig {
let client_id = unsafe { CStr::from_ptr(c_client_id) }.to_str().unwrap();
let cache_dir = if c_cache_dir == std::ptr::null() {
None
} else {
Some(unsafe { CStr::from_ptr(c_cache_dir).to_str().unwrap() }.to_string())
};
/// 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 {
/// Update channel name. Set to NULL or "eng" to disable updates.
pub channel: *const libc::c_char,
/// Client ID, required. Typically a UUID, used for handling
/// percentage rollouts.
pub client_id: *const libc::c_char,
/// Product ID, required. Typically generated by Shorebird and included
/// in your app to identify which app/channel/version triple to update.
pub product_id: *const libc::c_char,
/// base_version, required. Named version of the app, off of which updates
/// are based. Can be either a version number or a hash.
pub base_version: *const libc::c_char,
/// Update URL. Set to NULL to use the default update URL.
pub update_url: *const libc::c_char,
/// Path to the original aot library, required. For Flutter apps this
/// is the path to the bundled libapp.so. May be used for compression
/// downloaded artifacts.
pub original_libapp_path: *const libc::c_char,
/// 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 app_config_from_c(c_params: *const AppParameters) -> updater::AppConfig {
let c_params_ref = unsafe { &*c_params };
fn required(c_string: *const libc::c_char) -> String {
unsafe { CStr::from_ptr(c_string).to_str().unwrap() }.to_string()
}
fn optional(c_string: *const libc::c_char) -> Option<String> {
if c_string == std::ptr::null() {
None
} else {
Some(required(c_string))
}
}
updater::AppConfig {
client_id: client_id.to_string(),
cache_dir: cache_dir,
client_id: required(c_params_ref.client_id),
cache_dir: required(c_params_ref.cache_dir),
channel: optional(c_params_ref.channel),
product_id: required(c_params_ref.product_id),
base_url: optional(c_params_ref.update_url),
base_version: required(c_params_ref.base_version),
original_libapp_path: required(c_params_ref.original_libapp_path),
vm_path: required(c_params_ref.vm_path),
}
}
#[no_mangle]
pub extern "C" fn active_version(
c_client_id: *const c_char,
c_cache_dir: *const c_char,
) -> *mut c_char {
let config = app_config_from_c(c_client_id, c_cache_dir);
let version = updater::active_version(&config);
pub extern "C" fn shorebird_init(c_params: *const AppParameters) {
let config = app_config_from_c(c_params);
updater::init(config);
}
/// Return the active version of the app, or NULL if there is no active version.
#[no_mangle]
pub extern "C" fn shorebird_active_version() -> *mut c_char {
let version = updater::active_patch();
match version {
Some(v) => {
let c_version = CString::new(v.version).unwrap();
@@ -33,13 +87,12 @@ pub extern "C" fn active_version(
}
}
/// Return the path to the active version of the app, or NULL if there is no
/// active version.
#[no_mangle]
pub extern "C" fn active_path(
c_client_id: *const c_char,
c_cache_dir: *const c_char,
) -> *mut c_char {
let config = app_config_from_c(c_client_id, c_cache_dir);
let version = updater::active_version(&config);
// 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();
@@ -49,8 +102,9 @@ pub extern "C" fn active_path(
}
}
/// Free a string returned by the updater library.
#[no_mangle]
pub extern "C" fn free_string(c_string: *mut c_char) {
pub extern "C" fn shorebird_free_string(c_string: *mut c_char) {
unsafe {
if c_string.is_null() {
return;
@@ -59,14 +113,14 @@ pub extern "C" fn free_string(c_string: *mut c_char) {
}
}
/// Check for an update. Returns true if an update is available.
#[no_mangle]
pub extern "C" fn check_for_update(c_client_id: *const c_char, c_cache_dir: *const c_char) -> bool {
let config = app_config_from_c(c_client_id, c_cache_dir);
return updater::check_for_update(&config);
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 update(c_client_id: *const c_char, c_cache_dir: *const c_char) {
let config = app_config_from_c(c_client_id, c_cache_dir);
updater::update(&config);
pub extern "C" fn shorebird_update() {
updater::update();
}
+162
View File
@@ -0,0 +1,162 @@
// This file deals with the cache / state management for the updater.
use std::fs::File;
use std::io::{BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::network::UpdateResponse;
pub struct PatchInfo {
pub path: String,
pub version: String,
pub hash: String,
}
#[derive(Deserialize, Serialize, Default, Clone)]
struct Slot {
path: String,
version: String,
hash: String,
}
// 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 {
current_slot_index: usize,
slots: Vec<Slot>,
// Add file path or FD so modifying functions can save it to disk?
}
impl Default for UpdaterState {
fn default() -> Self {
Self {
current_slot_index: 0,
slots: Vec::new(),
}
}
}
pub fn load_state(cache_dir: &str) -> anyhow::Result<UpdaterState> {
// Load UpdaterState from disk
let path = Path::new(cache_dir).join("state.json");
let file = File::open(path)?;
let reader = BufReader::new(file);
let state = serde_json::from_reader(reader)?;
Ok(state)
}
pub fn save_state(state: &UpdaterState, cache_dir: &str) -> anyhow::Result<()> {
// Save UpdaterState to disk
std::fs::create_dir_all(cache_dir)?;
let path = Path::new(cache_dir).join("state.json");
let file = File::create(path)?;
let writer = BufWriter::new(file);
serde_json::to_writer_pretty(writer, &state)?;
Ok(())
}
pub fn current_patch_internal(state: &UpdaterState) -> Option<PatchInfo> {
// If there is no state, return None.
if state.slots.is_empty() {
return None;
}
let slot = &state.slots[state.current_slot_index];
// Otherwise return the version info from the current slot.
return Some(PatchInfo {
path: slot.path.clone(),
version: slot.version.clone(),
hash: slot.hash.clone(),
});
}
fn unused_slot(state: &UpdaterState) -> usize {
// Assume we only use two slots and pick the one that's not current.
if state.slots.is_empty() {
return 0;
}
if state.current_slot_index == 0 {
return 1;
}
return 0;
}
fn set_slot(state: &mut UpdaterState, index: usize, slot: Slot) {
if state.slots.len() < index + 1 {
// Make sure we're not filling with empty slots.
assert!(state.slots.len() == index);
state.slots.resize(index + 1, Slot::default());
}
// Set the given slot to the given version.
state.slots[index] = slot
}
pub fn set_current_slot(state: &mut UpdaterState, index: usize) {
state.current_slot_index = index;
// This does not implicitly save the state, but maybe should?
}
pub fn download_file_to_path(url: &str, path: &PathBuf) -> 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.
std::fs::create_dir_all(path.parent().unwrap())?;
let mut file = File::create(path)?;
file.write_all(&mut bytes)?;
Ok(())
}
pub fn download_into_unused_slot(
cache_dir: &str,
update_response: &UpdateResponse,
state: &mut UpdaterState,
) -> anyhow::Result<usize> {
// Download the new version into the unused slot.
let slot_index = unused_slot(state);
download_into_slot(cache_dir, update_response, state, slot_index)?;
Ok(slot_index)
}
fn download_into_slot(
cache_dir: &str,
update_response: &UpdateResponse,
state: &mut UpdaterState,
slot_index: usize,
) -> anyhow::Result<()> {
// Download the new version into the given slot.
// TODO: Give it a name other than libapp.txt.
let path = Path::new(cache_dir)
.join(format!("slot_{}", slot_index))
.join("libapp.txt");
// TODO: Shouldn't crash on malformed response.
let update = update_response.update.as_ref().unwrap();
// We should download into a separate place and move into place.
// That would allow us to check the hash before moving into place.
// Would also allow the move/state update to be "atomic" or at least allow
// us to carefully guard against state corruption.
// Would also let us support when we need to allow the system to download for us (e.g. iOS).
download_file_to_path(&update.download_url, &path)?;
// Check the hash against the download?
// Update the state to include the new version.
set_slot(
state,
slot_index,
Slot {
path: path.to_str().unwrap().to_string(),
version: update.version.clone(),
hash: update.hash.clone(),
},
);
save_state(&state, cache_dir)?;
return Ok(());
}
+68
View File
@@ -0,0 +1,68 @@
// This file handles the global config for the updater library.
use std::cell::RefCell;
use crate::updater::AppConfig;
const DEFAULT_BASE_URL: &'static str = "https://shorebird-code-push-api-cypqazu4da-uc.a.run.app";
const DEFAULT_CHANNEL: &'static str = "stable";
thread_local!(static CONFIG: RefCell<Option<ResolvedConfig>> = RefCell::new(None));
pub fn with_config<F, R>(f: F) -> R
where
F: FnOnce(&ResolvedConfig) -> R,
{
CONFIG
.try_with(|config| {
let config = config.borrow();
let config = config
.as_ref()
.expect("Must call updater_init before using the updater library.");
return f(config);
})
.expect("Must call updater_init before using the updater library.")
}
pub fn set_config(config: AppConfig) {
let config = resolve_config(config);
CONFIG.with(|c| {
let mut c = c.borrow_mut();
*c = Some(config);
});
}
pub struct ResolvedConfig {
pub cache_dir: String,
pub channel: String,
pub client_id: String,
pub product_id: String,
pub base_version: String,
pub original_libapp_path: String,
pub vm_path: String,
pub base_url: String,
}
fn resolve_config(config: AppConfig) -> ResolvedConfig {
// Resolve the config
// If there is no base_url, use the default.
// If there is no channel, use the default.
return ResolvedConfig {
client_id: config.client_id.to_string(),
base_url: config
.base_url
.as_deref()
.unwrap_or(DEFAULT_BASE_URL)
.to_owned(),
cache_dir: config.cache_dir.to_string(),
channel: config
.channel
.as_deref()
.unwrap_or(DEFAULT_CHANNEL)
.to_owned(),
product_id: config.product_id.to_string(),
base_version: config.base_version.to_string(),
original_libapp_path: config.original_libapp_path.to_string(),
vm_path: config.vm_path.to_string(),
};
}
+9 -1
View File
@@ -1,12 +1,20 @@
// 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 that the updater.rs file/module exists, but don't make it public.
// Declare other .rs file/module exists, but make them public.
mod cache;
mod config;
mod logging;
mod network;
mod updater;
// 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;
+19
View File
@@ -0,0 +1,19 @@
#[cfg(target_os = "android")]
pub fn init_logging() {
use android_logger::Config;
use log::LevelFilter;
android_logger::init_once(
Config::default()
// `flutter` tool ignores non-flutter tagged logs.
.with_tag("flutter")
.with_max_level(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.
}
+69
View File
@@ -0,0 +1,69 @@
// This file's job is to deal with the update_server and network side
// of the updater library.
use std::collections::HashMap;
use std::string::ToString;
use serde::Deserialize;
use crate::cache::PatchInfo;
use crate::config::ResolvedConfig;
fn updates_url(base_url: &str) -> String {
return format!("{}/api/v1/updates", base_url);
}
#[derive(Deserialize)]
pub struct Update {
pub version: String,
pub hash: String,
pub download_url: String,
}
#[derive(Deserialize)]
pub struct UpdateResponse {
pub update_available: bool,
#[serde(default)]
pub update: Option<Update>,
}
pub fn send_update_request(
config: &ResolvedConfig,
patch: Option<PatchInfo>,
) -> anyhow::Result<UpdateResponse> {
#[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";
#[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";
// Send the request to the server.
let client = reqwest::blocking::Client::new();
let mut body = HashMap::new();
body.insert("client_id", config.client_id.clone());
body.insert("product_id", config.product_id.clone());
body.insert("channel", config.channel.clone());
body.insert("base_version", config.base_version.clone());
if let Some(patch) = patch {
body.insert("patch_version", patch.version);
body.insert("patch_hash", patch.hash);
}
body.insert("platform", PLATFORM.to_string());
body.insert("arch", ARCH.to_string());
let response = client
.post(&updates_url(&config.base_url))
.json(&body)
.send()?
.json()?;
return Ok(response);
}
+46 -270
View File
@@ -1,23 +1,14 @@
use std::collections::HashMap;
// This file's job is to be the Rust API for the updater.
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::{BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::string::ToString;
use android_logger::Config;
use log::LevelFilter;
use serde::{Deserialize, Serialize};
// use thiserror::Error;
// #[derive(Error, Debug)]
// pub enum UpdateError {
// #[error("update server disconnected")]
// NetworkFailure(#[from] std::io::Error),
// #[error("unknown error")]
// Unknown,
// }
use crate::cache::{
current_patch_internal, download_into_unused_slot, load_state, save_state, set_current_slot,
PatchInfo,
};
use crate::config::{set_config, with_config, ResolvedConfig};
use crate::logging::init_logging;
use crate::network::send_update_request;
pub enum UpdateStatus {
NoUpdate,
@@ -39,116 +30,33 @@ impl Display for UpdateStatus {
}
}
// 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 {
// provided from the application
pub cache_dir: String,
pub channel: Option<String>, // If None, use the 'stable'.
pub client_id: String,
pub cache_dir: Option<String>,
// typically default=shorebird, but provided by the app as override?
// pub base_url: Option<&'a str>,
// typically default=stable, but provided by the app as override.
// pub channel: Option<&'a str>,
// Other needs:
// Architecture? Or engine can get that itself?
// fallback path? Or engine just returns null and caller figures that out?
pub product_id: String,
pub base_version: String,
pub original_libapp_path: String,
pub vm_path: String,
pub base_url: Option<String>, // If None, use the default.
}
pub struct VersionInfo {
pub path: String,
pub version: String,
pub hash: String,
pub fn init(app_config: AppConfig) {
init_logging();
set_config(app_config);
}
#[derive(Deserialize, Serialize, Default, Clone)]
struct Slot {
path: String,
version: String,
hash: String,
}
#[derive(Deserialize, Serialize)]
struct UpdaterState {
current_slot_index: usize,
slots: Vec<Slot>,
}
impl Default for UpdaterState {
fn default() -> Self {
Self {
current_slot_index: 0,
slots: Vec::new(),
}
}
}
struct ResolvedConfig {
client_id: String,
base_url: String,
channel: String,
cache_dir: String,
}
fn load_state(cache_dir: &str) -> anyhow::Result<UpdaterState> {
// Load UpdaterState from disk
let path = Path::new(cache_dir).join("state.json");
let file = File::open(path)?;
let reader = BufReader::new(file);
let state = serde_json::from_reader(reader)?;
Ok(state)
}
fn save_state(state: &UpdaterState, cache_dir: &str) -> anyhow::Result<()> {
// Save UpdaterState to disk
std::fs::create_dir_all(cache_dir)?;
let path = Path::new(cache_dir).join("state.json");
let file = File::create(path)?;
let writer = BufWriter::new(file);
serde_json::to_writer_pretty(writer, &state)?;
Ok(())
}
fn resolve_config(config: &AppConfig) -> ResolvedConfig {
// Resolve the config
// If there is no base_url, use the default.
// If there is no channel, use the default.
return ResolvedConfig {
client_id: config.client_id.to_string(),
base_url: "https://shorebird-code-push-api-cypqazu4da-uc.a.run.app".to_string(),
cache_dir: config
.cache_dir
.as_deref()
.unwrap_or("updater_cache")
.to_owned(),
channel: "stable".to_string(),
};
}
fn updates_url(config: &ResolvedConfig) -> String {
return format!("{}/api/v1/updates", config.base_url);
}
#[derive(Deserialize)]
struct Update {
version: String,
hash: String,
download_url: String,
}
#[derive(Deserialize)]
struct UpdateResponse {
update_available: bool,
#[serde(default)]
update: Option<Update>,
}
pub fn check_for_update(app_config: &AppConfig) -> bool {
let config = resolve_config(app_config);
pub fn check_for_update_internal(config: &ResolvedConfig) -> bool {
// Load UpdaterState from disk
// If there is no state, make an empty state.
let state = load_state(&config.cache_dir).unwrap_or_default();
// Check the current slot.
let version = current_version_internal(&state);
let patch = current_patch_internal(&state);
// Send info from app + current slot to server.
let response_result = send_update_request(&config, version);
let response_result = send_update_request(&config, patch);
match response_result {
Err(err) => {
error!("Failed update check: {err}");
@@ -160,177 +68,45 @@ pub fn check_for_update(app_config: &AppConfig) -> bool {
}
}
fn send_update_request(
config: &ResolvedConfig,
version: Option<VersionInfo>,
) -> anyhow::Result<UpdateResponse> {
#[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";
#[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";
// Send the request to the server.
let client = reqwest::blocking::Client::new();
let mut body = HashMap::new();
body.insert("client_id", config.client_id.clone());
body.insert("channel", config.channel.clone());
if let Some(version) = version {
body.insert("version", version.version);
body.insert("hash", version.hash);
}
body.insert("platform", PLATFORM.to_string());
body.insert("arch", ARCH.to_string());
let response = client
.post(&updates_url(config))
.json(&body)
.send()?
.json()?;
return Ok(response);
}
fn current_version_internal(state: &UpdaterState) -> Option<VersionInfo> {
// If there is no state, return None.
if state.slots.is_empty() {
return None;
}
let slot = &state.slots[state.current_slot_index];
// Otherwise return the version info from the current slot.
return Some(VersionInfo {
path: slot.path.clone(),
version: slot.version.clone(),
hash: slot.hash.clone(),
});
}
pub fn active_version(config: &AppConfig) -> Option<VersionInfo> {
let config = resolve_config(config);
let state = load_state(&config.cache_dir).unwrap_or_default();
return current_version_internal(&state);
}
fn unused_slot(state: &UpdaterState) -> usize {
// Assume we only use two slots and pick the one that's not current.
if state.slots.is_empty() {
return 0;
}
if state.current_slot_index == 0 {
return 1;
}
return 0;
}
fn set_slot(state: &mut UpdaterState, index: usize, slot: Slot) {
if state.slots.len() < index + 1 {
// Make sure we're not filling with empty slots.
assert!(state.slots.len() == index);
state.slots.resize(index + 1, Slot::default());
}
// Set the given slot to the given version.
state.slots[index] = slot
}
fn download_file_to_path(url: &str, path: &PathBuf) -> 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.
std::fs::create_dir_all(path.parent().unwrap())?;
let mut file = File::create(path)?;
file.write_all(&mut bytes)?;
Ok(())
}
fn download_into_slot(
config: &ResolvedConfig,
update_response: &UpdateResponse,
state: &mut UpdaterState,
slot_index: usize,
) -> anyhow::Result<()> {
// Download the new version into the given slot.
let path = Path::new(&config.cache_dir)
.join(format!("slot_{}", slot_index))
.join("libapp.txt");
// TODO: Shouldn't crash on malformed response.
let update = update_response.update.as_ref().unwrap();
// We should download into a separate place and move into place.
// That would allow us to check the hash before moving into place.
// Would also allow the move/state update to be "atomic" or at least allow
// us to carefully guard against state corruption.
// Would also let us support when we need to allow the system to download for us (e.g. iOS).
download_file_to_path(&update.download_url, &path)?;
// Check the hash against the download?
// Update the state to include the new version.
set_slot(
state,
slot_index,
Slot {
path: path.to_str().unwrap().to_string(),
version: update.version.clone(),
hash: update.hash.clone(),
},
);
save_state(&state, &config.cache_dir)?;
return Ok(());
pub fn check_for_update() -> bool {
return with_config(check_for_update_internal);
}
fn update_internal(config: &ResolvedConfig) -> anyhow::Result<UpdateStatus> {
// Load the state from disk.
let mut state = load_state(&config.cache_dir).unwrap_or_default();
let version = current_version_internal(&state);
let version = current_patch_internal(&state);
// Check for update.
let response = send_update_request(&config, version)?;
if !response.update_available {
return Ok(UpdateStatus::NoUpdate);
}
// If needed, download the new version.
let slot = unused_slot(&mut state);
download_into_slot(&config, &response, &mut state, slot)?;
let slot = download_into_unused_slot(&config.cache_dir, &response, &mut state)?;
// Install the new version.
state.current_slot_index = slot;
set_current_slot(&mut state, slot);
save_state(&state, &config.cache_dir)?;
// Set the state to "restart required".
return Ok(UpdateStatus::UpdateInstalled);
}
fn init_logging() {
android_logger::init_once(
Config::default()
// `flutter` tool ignores non-flutter tagged logs.
.with_tag("flutter")
.with_max_level(LevelFilter::Debug),
);
debug!("Logging initialized");
pub fn active_patch() -> Option<PatchInfo> {
return with_config(|config| {
let state = load_state(&config.cache_dir).unwrap_or_default();
return current_patch_internal(&state);
});
}
pub fn update(app_config: &AppConfig) -> UpdateStatus {
init_logging();
let config = resolve_config(&app_config);
let result = update_internal(&config);
match result {
Err(err) => {
error!("Problem updating: {err}");
error!("{}", err.backtrace());
return UpdateStatus::UpdateHadError;
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,
}
Ok(status) => status,
}
});
}