diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..887ac650
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,18 @@
+{
+ "cSpell.words": [
+ "aarch",
+ "androideabi",
+ "armv",
+ "calloc",
+ "cbindgen",
+ "dylib",
+ "libapp",
+ "libc",
+ "libflutter",
+ "repr",
+ "reqwest",
+ "rollouts",
+ "rustup",
+ "struct"
+ ]
+}
\ No newline at end of file
diff --git a/bootstrap.sh b/bootstrap.sh
new file mode 100755
index 00000000..e63d303a
--- /dev/null
+++ b/bootstrap.sh
@@ -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
\ No newline at end of file
diff --git a/example/counter/.gitignore b/example/counter/.gitignore
new file mode 100644
index 00000000..24476c5d
--- /dev/null
+++ b/example/counter/.gitignore
@@ -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
diff --git a/example/counter/.metadata b/example/counter/.metadata
new file mode 100644
index 00000000..66993e16
--- /dev/null
+++ b/example/counter/.metadata
@@ -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'
diff --git a/example/counter/README.md b/example/counter/README.md
new file mode 100644
index 00000000..9498e65d
--- /dev/null
+++ b/example/counter/README.md
@@ -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.
diff --git a/example/counter/analysis_options.yaml b/example/counter/analysis_options.yaml
new file mode 100644
index 00000000..61b6c4de
--- /dev/null
+++ b/example/counter/analysis_options.yaml
@@ -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
diff --git a/example/counter/android/.gitignore b/example/counter/android/.gitignore
new file mode 100644
index 00000000..6f568019
--- /dev/null
+++ b/example/counter/android/.gitignore
@@ -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
diff --git a/example/counter/android/app/build.gradle b/example/counter/android/app/build.gradle
new file mode 100644
index 00000000..afc4500b
--- /dev/null
+++ b/example/counter/android/app/build.gradle
@@ -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"
+}
diff --git a/example/counter/android/app/src/debug/AndroidManifest.xml b/example/counter/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..6bdce5f9
--- /dev/null
+++ b/example/counter/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/example/counter/android/app/src/main/AndroidManifest.xml b/example/counter/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..855cea65
--- /dev/null
+++ b/example/counter/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/counter/android/app/src/main/kotlin/com/example/counter/MainActivity.kt b/example/counter/android/app/src/main/kotlin/com/example/counter/MainActivity.kt
new file mode 100644
index 00000000..64a37ba8
--- /dev/null
+++ b/example/counter/android/app/src/main/kotlin/com/example/counter/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.counter
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/example/counter/android/app/src/main/res/drawable-v21/launch_background.xml b/example/counter/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 00000000..f74085f3
--- /dev/null
+++ b/example/counter/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/counter/android/app/src/main/res/drawable/launch_background.xml b/example/counter/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 00000000..304732f8
--- /dev/null
+++ b/example/counter/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/counter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/counter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..db77bb4b
Binary files /dev/null and b/example/counter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/counter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/counter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..17987b79
Binary files /dev/null and b/example/counter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/counter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/counter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..09d43914
Binary files /dev/null and b/example/counter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/counter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/counter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..d5f1c8d3
Binary files /dev/null and b/example/counter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/counter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/counter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..4d6372ee
Binary files /dev/null and b/example/counter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/counter/android/app/src/main/res/values-night/styles.xml b/example/counter/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..06952be7
--- /dev/null
+++ b/example/counter/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/counter/android/app/src/main/res/values/styles.xml b/example/counter/android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..cb1ef880
--- /dev/null
+++ b/example/counter/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/counter/android/app/src/profile/AndroidManifest.xml b/example/counter/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 00000000..6bdce5f9
--- /dev/null
+++ b/example/counter/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/example/counter/android/build.gradle b/example/counter/android/build.gradle
new file mode 100644
index 00000000..e50c3a02
--- /dev/null
+++ b/example/counter/android/build.gradle
@@ -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
+}
diff --git a/example/counter/android/gradle.properties b/example/counter/android/gradle.properties
new file mode 100644
index 00000000..94adc3a3
--- /dev/null
+++ b/example/counter/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/example/counter/android/gradle/wrapper/gradle-wrapper.properties b/example/counter/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3c472b99
--- /dev/null
+++ b/example/counter/android/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/example/counter/android/settings.gradle b/example/counter/android/settings.gradle
new file mode 100644
index 00000000..44e62bcf
--- /dev/null
+++ b/example/counter/android/settings.gradle
@@ -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"
diff --git a/example/counter/lib/main.dart b/example/counter/lib/main.dart
new file mode 100644
index 00000000..fe5cbc18
--- /dev/null
+++ b/example/counter/lib/main.dart
@@ -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 createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State {
+ 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: [
+ 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.
+ );
+ }
+}
diff --git a/example/counter/pubspec.lock b/example/counter/pubspec.lock
new file mode 100644
index 00000000..32dd9632
--- /dev/null
+++ b/example/counter/pubspec.lock
@@ -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"
diff --git a/example/counter/pubspec.yaml b/example/counter/pubspec.yaml
new file mode 100644
index 00000000..5549234f
--- /dev/null
+++ b/example/counter/pubspec.yaml
@@ -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
diff --git a/updater/BUILDING_ENGINE.md b/updater/BUILDING_ENGINE.md
new file mode 100644
index 00000000..73bbe829
--- /dev/null
+++ b/updater/BUILDING_ENGINE.md
@@ -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.
diff --git a/updater/UPDATER_DEMO.md b/updater/UPDATER_DEMO.md
deleted file mode 100644
index 7ea9c919..00000000
--- a/updater/UPDATER_DEMO.md
+++ /dev/null
@@ -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
-```
diff --git a/updater/cbindgen.toml b/updater/cbindgen.toml
new file mode 100644
index 00000000..d7624ad1
--- /dev/null
+++ b/updater/cbindgen.toml
@@ -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
diff --git a/updater/cli/src/main.rs b/updater/cli/src/main.rs
index 3d992ac8..d6928984 100644
--- a/updater/cli/src/main.rs
+++ b/updater/cli/src/main.rs
@@ -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 => {}
diff --git a/updater/dart_bindings/.gitignore b/updater/dart_bindings/.gitignore
new file mode 100644
index 00000000..3cceda55
--- /dev/null
+++ b/updater/dart_bindings/.gitignore
@@ -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
diff --git a/updater/dart_bindings/README.md b/updater/dart_bindings/README.md
new file mode 100644
index 00000000..c0e9f834
--- /dev/null
+++ b/updater/dart_bindings/README.md
@@ -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.
\ No newline at end of file
diff --git a/updater/dart_bindings/analysis_options.yaml b/updater/dart_bindings/analysis_options.yaml
new file mode 100644
index 00000000..dee8927a
--- /dev/null
+++ b/updater/dart_bindings/analysis_options.yaml
@@ -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
diff --git a/updater/dart_bindings/lib/src/bindings.dart b/updater/dart_bindings/lib/src/bindings.dart
new file mode 100644
index 00000000..2b74b457
--- /dev/null
+++ b/updater/dart_bindings/lib/src/bindings.dart
@@ -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 channel;
+ // ignore: non_constant_identifier_names
+ external ffi.Pointer client_id;
+ // ignore: non_constant_identifier_names
+ external ffi.Pointer product_id;
+ // ignore: non_constant_identifier_names
+ external ffi.Pointer base_version;
+ // ignore: non_constant_identifier_names
+ external ffi.Pointer update_url;
+ // ignore: non_constant_identifier_names
+ external ffi.Pointer original_libapp_path;
+ // ignore: non_constant_identifier_names
+ external ffi.Pointer vm_path;
+ // ignore: non_constant_identifier_names
+ external ffi.Pointer cache_dir;
+
+ static ffi.Pointer 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();
+ 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 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 Function();
+typedef GetString = ffi.Pointer Function();
+
+typedef _GetVoidFunc = ffi.Void Function();
+typedef GetVoid = void Function();
+
+typedef _SBInitFunc = ffi.Void Function(ffi.Pointer config);
+typedef SBInit = void Function(ffi.Pointer config);
+
+typedef _FreeStringFunc = ffi.Void Function(ffi.Pointer str);
+typedef FreeString = void Function(ffi.Pointer 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');
+ }
+}
diff --git a/updater/dart_bindings/lib/updater.dart b/updater/dart_bindings/lib/updater.dart
new file mode 100644
index 00000000..d2af3118
--- /dev/null
+++ b/updater/dart_bindings/lib/updater.dart
@@ -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 Function() f) {
+ ffi.Pointer cString = ffi.Pointer.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);
+ }
+}
diff --git a/updater/dart_bindings/pubspec.yaml b/updater/dart_bindings/pubspec.yaml
new file mode 100644
index 00000000..b458c493
--- /dev/null
+++ b/updater/dart_bindings/pubspec.yaml
@@ -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
diff --git a/updater/dart_cli/bin/dart_cli.dart b/updater/dart_cli/bin/dart_cli.dart
index 533a780b..7500a480 100644
--- a/updater/dart_cli/bin/dart_cli.dart
+++ b/updater/dart_cli/bin/dart_cli.dart
@@ -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 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('updater', 'Updater CLI')
..addCommand(CheckForUpdate(updater))
..addCommand(PrintVersion(updater))
diff --git a/updater/dart_cli/lib/bindings.dart b/updater/dart_cli/lib/bindings.dart
deleted file mode 100644
index 69ae67c1..00000000
--- a/updater/dart_cli/lib/bindings.dart
+++ /dev/null
@@ -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 clientId, ffi.Pointer cacheDir);
-typedef GetBool = bool Function(
- ffi.Pointer clientId, ffi.Pointer cacheDir);
-
-typedef _GetStringFunc = ffi.Pointer Function(
- ffi.Pointer clientId, ffi.Pointer cacheDir);
-typedef GetString = ffi.Pointer Function(
- ffi.Pointer clientId, ffi.Pointer cacheDir);
-
-typedef _GetVoidFunc = ffi.Void Function(
- ffi.Pointer clientId, ffi.Pointer cacheDir);
-typedef GetVoid = void Function(
- ffi.Pointer clientId, ffi.Pointer cacheDir);
-
-typedef _FreeStringFunc = ffi.Void Function(ffi.Pointer str);
-typedef FreeString = void Function(ffi.Pointer 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');
- }
-}
diff --git a/updater/dart_cli/lib/updater.dart b/updater/dart_cli/lib/updater.dart
deleted file mode 100644
index dfc7938c..00000000
--- a/updater/dart_cli/lib/updater.dart
+++ /dev/null
@@ -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 Function(ffi.Pointer clientId, ffi.Pointer 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 cVersion = ffi.Pointer.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 cVersion = ffi.Pointer.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);
- }
- });
- }
-}
diff --git a/updater/dart_cli/pubspec.yaml b/updater/dart_cli/pubspec.yaml
index 548ee662..1446965a 100644
--- a/updater/dart_cli/pubspec.yaml
+++ b/updater/dart_cli/pubspec.yaml
@@ -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
diff --git a/updater/library/Cargo.toml b/updater/library/Cargo.toml
index cf9707b9..c73f7f54 100644
--- a/updater/library/Cargo.toml
+++ b/updater/library/Cargo.toml
@@ -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"
diff --git a/updater/library/README.md b/updater/library/README.md
index c468976e..1ca6af4c 100644
--- a/updater/library/README.md
+++ b/updater/library/README.md
@@ -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
+```
diff --git a/updater/library/include/updater.h b/updater/library/include/updater.h
index e378512c..3f9089a5 100644
--- a/updater/library/include/updater.h
+++ b/updater/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
+#include
+#include
+#include
+
+/**
+ * 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 */
diff --git a/updater/library/src/c_api.rs b/updater/library/src/c_api.rs
index 4ca2943d..5ce47e27 100644
--- a/updater/library/src/c_api.rs
+++ b/updater/library/src/c_api.rs
@@ -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 {
+ 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();
}
diff --git a/updater/library/src/cache.rs b/updater/library/src/cache.rs
new file mode 100644
index 00000000..c8be9501
--- /dev/null
+++ b/updater/library/src/cache.rs
@@ -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,
+ // 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 {
+ // 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 {
+ // 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 {
+ // 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(());
+}
diff --git a/updater/library/src/config.rs b/updater/library/src/config.rs
new file mode 100644
index 00000000..c5f763d0
--- /dev/null
+++ b/updater/library/src/config.rs
@@ -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