refactor: split updater library into layers (#47)
This commit is contained in:
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"aarch",
|
||||
"androideabi",
|
||||
"armv",
|
||||
"calloc",
|
||||
"cbindgen",
|
||||
"dylib",
|
||||
"libapp",
|
||||
"libc",
|
||||
"libflutter",
|
||||
"repr",
|
||||
"reqwest",
|
||||
"rollouts",
|
||||
"rustup",
|
||||
"struct"
|
||||
]
|
||||
}
|
||||
Executable
+28
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
@@ -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 => {}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user