From dcf5f080ad923150a78e25241f5389cc5e186a13 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Wed, 8 Mar 2023 09:11:10 -0800 Subject: [PATCH] refactor: split updater library into layers (#47) --- .vscode/settings.json | 18 + bootstrap.sh | 28 ++ example/counter/.gitignore | 44 +++ example/counter/.metadata | 45 +++ example/counter/README.md | 16 + example/counter/analysis_options.yaml | 29 ++ example/counter/android/.gitignore | 13 + example/counter/android/app/build.gradle | 71 ++++ .../android/app/src/debug/AndroidManifest.xml | 8 + .../android/app/src/main/AndroidManifest.xml | 34 ++ .../com/example/counter/MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 8 + example/counter/android/build.gradle | 31 ++ example/counter/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + example/counter/android/settings.gradle | 11 + example/counter/lib/main.dart | 130 +++++++ example/counter/pubspec.lock | 203 +++++++++++ example/counter/pubspec.yaml | 91 +++++ updater/BUILDING_ENGINE.md | 122 +++++++ updater/UPDATER_DEMO.md | 128 ------- updater/cbindgen.toml | 7 + updater/cli/src/main.rs | 17 +- updater/dart_bindings/.gitignore | 7 + updater/dart_bindings/README.md | 6 + updater/dart_bindings/analysis_options.yaml | 30 ++ updater/dart_bindings/lib/src/bindings.dart | 102 ++++++ updater/dart_bindings/lib/updater.dart | 110 ++++++ updater/dart_bindings/pubspec.yaml | 15 + updater/dart_cli/bin/dart_cli.dart | 21 +- updater/dart_cli/lib/bindings.dart | 60 ---- updater/dart_cli/lib/updater.dart | 86 ----- updater/dart_cli/pubspec.yaml | 5 +- updater/library/Cargo.toml | 8 +- updater/library/README.md | 10 + updater/library/include/updater.h | 94 +++++- updater/library/src/c_api.rs | 110 ++++-- updater/library/src/cache.rs | 162 +++++++++ updater/library/src/config.rs | 68 ++++ updater/library/src/lib.rs | 10 +- updater/library/src/logging.rs | 19 ++ updater/library/src/network.rs | 69 ++++ updater/library/src/updater.rs | 316 +++--------------- 52 files changed, 1836 insertions(+), 600 deletions(-) create mode 100644 .vscode/settings.json create mode 100755 bootstrap.sh create mode 100644 example/counter/.gitignore create mode 100644 example/counter/.metadata create mode 100644 example/counter/README.md create mode 100644 example/counter/analysis_options.yaml create mode 100644 example/counter/android/.gitignore create mode 100644 example/counter/android/app/build.gradle create mode 100644 example/counter/android/app/src/debug/AndroidManifest.xml create mode 100644 example/counter/android/app/src/main/AndroidManifest.xml create mode 100644 example/counter/android/app/src/main/kotlin/com/example/counter/MainActivity.kt create mode 100644 example/counter/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 example/counter/android/app/src/main/res/drawable/launch_background.xml create mode 100644 example/counter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/counter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/counter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/counter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/counter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 example/counter/android/app/src/main/res/values-night/styles.xml create mode 100644 example/counter/android/app/src/main/res/values/styles.xml create mode 100644 example/counter/android/app/src/profile/AndroidManifest.xml create mode 100644 example/counter/android/build.gradle create mode 100644 example/counter/android/gradle.properties create mode 100644 example/counter/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 example/counter/android/settings.gradle create mode 100644 example/counter/lib/main.dart create mode 100644 example/counter/pubspec.lock create mode 100644 example/counter/pubspec.yaml create mode 100644 updater/BUILDING_ENGINE.md delete mode 100644 updater/UPDATER_DEMO.md create mode 100644 updater/cbindgen.toml create mode 100644 updater/dart_bindings/.gitignore create mode 100644 updater/dart_bindings/README.md create mode 100644 updater/dart_bindings/analysis_options.yaml create mode 100644 updater/dart_bindings/lib/src/bindings.dart create mode 100644 updater/dart_bindings/lib/updater.dart create mode 100644 updater/dart_bindings/pubspec.yaml delete mode 100644 updater/dart_cli/lib/bindings.dart delete mode 100644 updater/dart_cli/lib/updater.dart create mode 100644 updater/library/src/cache.rs create mode 100644 updater/library/src/config.rs create mode 100644 updater/library/src/logging.rs create mode 100644 updater/library/src/network.rs 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 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 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> = RefCell::new(None)); + +pub fn with_config(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(), + }; +} diff --git a/updater/library/src/lib.rs b/updater/library/src/lib.rs index 70c1cad0..4f44cf3f 100644 --- a/updater/library/src/lib.rs +++ b/updater/library/src/lib.rs @@ -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; diff --git a/updater/library/src/logging.rs b/updater/library/src/logging.rs new file mode 100644 index 00000000..1616a2c7 --- /dev/null +++ b/updater/library/src/logging.rs @@ -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. +} diff --git a/updater/library/src/network.rs b/updater/library/src/network.rs new file mode 100644 index 00000000..06bb2d15 --- /dev/null +++ b/updater/library/src/network.rs @@ -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, +} + +pub fn send_update_request( + config: &ResolvedConfig, + patch: Option, +) -> anyhow::Result { + #[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); +} diff --git a/updater/library/src/updater.rs b/updater/library/src/updater.rs index 31c92ebd..da88d21e 100644 --- a/updater/library/src/updater.rs +++ b/updater/library/src/updater.rs @@ -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, // If None, use the 'stable'. pub client_id: String, - pub cache_dir: Option, - // 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, // 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, -} - -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 { - // 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, -} - -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, -) -> anyhow::Result { - #[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 { - // 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 { - 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 { // 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 { + 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, - } + }); }