From 30ebcf38c90bc9a28a0e524d7e17ae81bc5068ee Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 22 May 2023 13:10:38 -0700 Subject: [PATCH] [ci] Add a legacy Android build-all test (#4005) Adds the ability to replace portions of the `flutter create`d app with saved copies, and adds a second build-all phase for Android that uses a Flutter 2.0.6-created `android/` directory (AGP 4.1, Gradle 6.7) to catch issues like https://github.com/flutter/flutter/issues/125621 and https://github.com/flutter/flutter/issues/125482 prior to release. Includes some incidental cleanup: - Extracts a helper method for adjusting files, so that this doesn't add even more copies of basically identical code. - (This was motivated by an earlier version of the PR that added modifications to several more files, which I ended up undoing, but the cleanup seemed worth keeping.) - Adds missing unit test coverage. - Reworks the unit tests to use a mock process manager and dummy files, instead of each test actually calling `flutter create`, which made each new test add several seconds to the unit test suite. - While this reduces the integration-style coverage, in practice the integration tests of the repo tooling is the CI itself, so the unit tests should be true unit tests. - Changes the non-legacy test to Kotlin; we were still testing with Java even though Kotlin has been the default for quite a while, so we weren't testing what most new users would actually be running. Since we now have a legacy test, I used Java there to cover both. - Removes some dead code for modifying the AndroidManifest.xml; when trying to set up unit tests for it I discovered that it no longer matches anything in an actual project. It dates back to the original command, and seems to have been a camera-related hack, which we clearly no longer need since it wasn't working and camera still works in build-all. This is captured somewhat in the README in the legacy project directory, but to document the approach here: originally I was going to add flags to change individual items (AGP version, Gradle version), but quickly ran into the fact that selective downgrading is extremely fragile. E.g.,: - The Kotlin version set in current projects doesn't work when downgrading AGP and Gradle. - The app template can unconditionally use anything (e.g., `namespace`) that the AGP version it uses supports, so arbitrary future breakage is possible. It's also less useful as a real test of what a plugin client's project likely looks like. Starting with a complete platform directory, and doing whatever the minimal changes are to keep it working, will likely reflect a common real-world scenario. On the flip side, the reason this doesn't use a complete 2.0.6 project, but instead is based on specific platform directories, is that we don't want to waste time manually maintaining, e.g., old Dart code that is irrelevant to the goal of the test. For now this only uses Android because that's where we've seen problems in practice, but we can alway add other legacy platform tests later if we find a need. Fixes https://github.com/flutter/flutter/issues/125689 --- .ci/legacy_project/README.md | 37 ++ .ci/legacy_project/all_packages/.gitignore | 46 +++ .ci/legacy_project/all_packages/.metadata | 10 + .../all_packages/android/.gitignore | 11 + .../all_packages/android/app/build.gradle | 47 +++ .../android/app/src/debug/AndroidManifest.xml | 4 + .../android/app/src/main/AndroidManifest.xml | 41 ++ .../example/all_packages/MainActivity.java | 10 + .../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 | 7 + .../all_packages/android/build.gradle | 29 ++ .../all_packages/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../all_packages/android/settings.gradle | 11 + .cirrus.yml | 5 + script/tool/lib/src/common/file_utils.dart | 15 +- .../src/create_all_packages_app_command.dart | 275 ++++++++----- script/tool/test/common/file_utils_test.dart | 54 ++- .../create_all_packages_app_command_test.dart | 382 +++++++++++++++--- 27 files changed, 888 insertions(+), 165 deletions(-) create mode 100644 .ci/legacy_project/README.md create mode 100644 .ci/legacy_project/all_packages/.gitignore create mode 100644 .ci/legacy_project/all_packages/.metadata create mode 100644 .ci/legacy_project/all_packages/android/.gitignore create mode 100644 .ci/legacy_project/all_packages/android/app/build.gradle create mode 100644 .ci/legacy_project/all_packages/android/app/src/debug/AndroidManifest.xml create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/AndroidManifest.xml create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/java/com/example/all_packages/MainActivity.java create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/drawable/launch_background.xml create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/values-night/styles.xml create mode 100644 .ci/legacy_project/all_packages/android/app/src/main/res/values/styles.xml create mode 100644 .ci/legacy_project/all_packages/android/app/src/profile/AndroidManifest.xml create mode 100644 .ci/legacy_project/all_packages/android/build.gradle create mode 100644 .ci/legacy_project/all_packages/android/gradle.properties create mode 100644 .ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 .ci/legacy_project/all_packages/android/settings.gradle diff --git a/.ci/legacy_project/README.md b/.ci/legacy_project/README.md new file mode 100644 index 0000000000..c10ace0b2c --- /dev/null +++ b/.ci/legacy_project/README.md @@ -0,0 +1,37 @@ +This directory contains a partial snapshot of an old Flutter project; it is +intended to replace the corresponding parts of a newly Flutter-created project +to allow testing plugin builds with a legacy project. + +It was originally created with Flutter 2.0.6. In general the guidelines are: +- Pieces here should be largely self-contained rather than portions of + major project components; for instance, it currently contains the entire + `android/` directory from a legacy project, rather than a subset of it + which would be combined with a subset of a new project's `android/` + directory. This is to avoid random breakage in the future due to + conflicts between those subsets. For instance, we could probably get + away with not including android/app/src/main/res for a while, and + instead layer in the versions from a new project, but then someday + if the resources were renamed, there would be dangling references to + the old resources in files that are included here. +- Updates over time should be minimal. We don't expect that an unchanged + project will keep working forever, but this directory should simulate + a developer who has done the bare minimum to keep their project working + as they have updated Flutter. +- Updates should be logged below. + +The reason for the hybrid model, rather than checking in a full legacy +project, is to minimize unnecessary maintenance work. E.g., there's no +need to manually keep Dart code updated for Flutter changes just to +test legacy native Android build behaviors. + +## Manual changes to files + +The following are the changes relative to running: + +```bash +flutter create -a java all_packages +``` + +and then deleting everything but `android/` from it: + +- Added license boilerplate. diff --git a/.ci/legacy_project/all_packages/.gitignore b/.ci/legacy_project/all_packages/.gitignore new file mode 100644 index 0000000000..0fa6b675c0 --- /dev/null +++ b/.ci/legacy_project/all_packages/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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/ + +# Web related +lib/generated_plugin_registrant.dart + +# 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/.ci/legacy_project/all_packages/.metadata b/.ci/legacy_project/all_packages/.metadata new file mode 100644 index 0000000000..d7e64d0b32 --- /dev/null +++ b/.ci/legacy_project/all_packages/.metadata @@ -0,0 +1,10 @@ +# 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 and should not be manually edited. + +version: + revision: 1d9032c7e1d867f071f2277eb1673e8f9b0274e3 + channel: unknown + +project_type: app diff --git a/.ci/legacy_project/all_packages/android/.gitignore b/.ci/legacy_project/all_packages/android/.gitignore new file mode 100644 index 0000000000..0a741cb43d --- /dev/null +++ b/.ci/legacy_project/all_packages/android/.gitignore @@ -0,0 +1,11 @@ +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 diff --git a/.ci/legacy_project/all_packages/android/app/build.gradle b/.ci/legacy_project/all_packages/android/app/build.gradle new file mode 100644 index 0000000000..b75c7b0561 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/build.gradle @@ -0,0 +1,47 @@ +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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 30 + + defaultConfig { + applicationId "com.example.all_packages" + minSdkVersion 16 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} diff --git a/.ci/legacy_project/all_packages/android/app/src/debug/AndroidManifest.xml b/.ci/legacy_project/all_packages/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..3a38eba348 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/AndroidManifest.xml b/.ci/legacy_project/all_packages/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..70c010f286 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/java/com/example/all_packages/MainActivity.java b/.ci/legacy_project/all_packages/android/app/src/main/java/com/example/all_packages/MainActivity.java new file mode 100644 index 0000000000..f494afad85 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/java/com/example/all_packages/MainActivity.java @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package com.example.all_packages; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/drawable-v21/launch_background.xml b/.ci/legacy_project/all_packages/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000..f74085f3f6 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/drawable/launch_background.xml b/.ci/legacy_project/all_packages/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000..304732f884 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/.ci/legacy_project/all_packages/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/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/.ci/legacy_project/all_packages/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/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/.ci/legacy_project/all_packages/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/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/.ci/legacy_project/all_packages/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/.ci/legacy_project/all_packages/android/app/src/main/res/values-night/styles.xml b/.ci/legacy_project/all_packages/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..449a9f9308 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/values/styles.xml b/.ci/legacy_project/all_packages/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..d74aa35c28 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/profile/AndroidManifest.xml b/.ci/legacy_project/all_packages/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000..02ba522d3d --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/.ci/legacy_project/all_packages/android/build.gradle b/.ci/legacy_project/all_packages/android/build.gradle new file mode 100644 index 0000000000..c9e3db0a0f --- /dev/null +++ b/.ci/legacy_project/all_packages/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/.ci/legacy_project/all_packages/android/gradle.properties b/.ci/legacy_project/all_packages/android/gradle.properties new file mode 100644 index 0000000000..94adc3a3f9 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/.ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties b/.ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..bc6a58afdd --- /dev/null +++ b/.ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/.ci/legacy_project/all_packages/android/settings.gradle b/.ci/legacy_project/all_packages/android/settings.gradle new file mode 100644 index 0000000000..44e62bcf06 --- /dev/null +++ b/.ci/legacy_project/all_packages/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/.cirrus.yml b/.cirrus.yml index e6645cb6cf..c8a7af48cc 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -302,6 +302,11 @@ task: CHANNEL: "master" CHANNEL: "stable" << : *BUILD_ALL_PACKAGES_APP_TEMPLATE + create_all_packages_app_legacy_script: + - $PLUGIN_TOOL_COMMAND create-all-packages-app --legacy-source=.ci/legacy_project/all_packages --output-dir=legacy/ --exclude script/configs/exclude_all_packages_app.yaml + build_all_packages_legacy_script: + - cd legacy/all_packages + - flutter build $BUILD_ALL_ARGS --debug ### Web tasks ### - name: web-platform_tests env: diff --git a/script/tool/lib/src/common/file_utils.dart b/script/tool/lib/src/common/file_utils.dart index 3c2f2f18f9..968de011a2 100644 --- a/script/tool/lib/src/common/file_utils.dart +++ b/script/tool/lib/src/common/file_utils.dart @@ -11,10 +11,21 @@ import 'package:file/file.dart'; /// childFileWithSubcomponents(rootDir, ['foo', 'bar', 'baz.txt']) /// creates a File representing /rootDir/foo/bar/baz.txt. File childFileWithSubcomponents(Directory base, List components) { - Directory dir = base; final String basename = components.removeLast(); + return childDirectoryWithSubcomponents(base, components).childFile(basename); +} + +/// Returns a [Directory] created by appending everything in [components] +/// to [base] as subdirectories. +/// +/// Example: +/// childFileWithSubcomponents(rootDir, ['foo', 'bar']) +/// creates a File representing /rootDir/foo/bar/. +Directory childDirectoryWithSubcomponents( + Directory base, List components) { + Directory dir = base; for (final String directoryName in components) { dir = dir.childDirectory(directoryName); } - return dir.childFile(basename); + return dir; } diff --git a/script/tool/lib/src/create_all_packages_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart index fb07e335e0..01f37f3a31 100644 --- a/script/tool/lib/src/create_all_packages_app_command.dart +++ b/script/tool/lib/src/create_all_packages_app_command.dart @@ -2,26 +2,27 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io' as io; - import 'package:file/file.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; +import 'common/file_utils.dart'; import 'common/package_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -const String _outputDirectoryFlag = 'output-dir'; +/// The name of the build-all-packages project, as passed to `flutter create`. +@visibleForTesting +const String allPackagesProjectName = 'all_packages'; -const String _projectName = 'all_packages'; - -const int _exitUpdateMacosPodfileFailed = 3; -const int _exitUpdateMacosPbxprojFailed = 4; -const int _exitGenNativeBuildFilesFailed = 5; +const int _exitFlutterCreateFailed = 3; +const int _exitGenNativeBuildFilesFailed = 4; +const int _exitMissingFile = 5; +const int _exitMissingLegacySource = 6; /// A command to create an application that builds all in a single application. class CreateAllPackagesAppCommand extends PackageCommand { @@ -29,22 +30,29 @@ class CreateAllPackagesAppCommand extends PackageCommand { CreateAllPackagesAppCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - Directory? pluginsRoot, Platform platform = const LocalPlatform(), }) : super(packagesDir, processRunner: processRunner, platform: platform) { - final Directory defaultDir = - pluginsRoot ?? packagesDir.fileSystem.currentDirectory; argParser.addOption(_outputDirectoryFlag, - defaultsTo: defaultDir.path, - help: - 'The path the directory to create the "$_projectName" project in.\n' + defaultsTo: packagesDir.parent.path, + help: 'The path the directory to create the "$allPackagesProjectName" ' + 'project in.\n' 'Defaults to the repository root.'); + argParser.addOption(_legacySourceFlag, + help: 'A partial project directory to use as a source for replacing ' + 'portions of the created app. All top-level directories in the ' + 'source will replace the corresponding directories in the output ' + 'directory post-create.\n\n' + 'The replacement will be done before any tool-driven ' + 'modifications.'); } + static const String _legacySourceFlag = 'legacy-source'; + static const String _outputDirectoryFlag = 'output-dir'; + /// The location to create the synthesized app project. Directory get _appDirectory => packagesDir.fileSystem .directory(getStringArg(_outputDirectoryFlag)) - .childDirectory(_projectName); + .childDirectory(allPackagesProjectName); /// The synthesized app project. RepositoryPackage get app => RepositoryPackage(_appDirectory); @@ -60,7 +68,15 @@ class CreateAllPackagesAppCommand extends PackageCommand { Future run() async { final int exitCode = await _createApp(); if (exitCode != 0) { - throw ToolExit(exitCode); + printError('Failed to `flutter create`: $exitCode'); + throw ToolExit(_exitFlutterCreateFailed); + } + + final String? legacySource = getNullableStringArg(_legacySourceFlag); + if (legacySource != null) { + final Directory legacyDir = + packagesDir.fileSystem.directory(legacySource); + await _replaceWithLegacy(target: _appDirectory, source: legacyDir); } final Set excluded = getExcludedPackageNames(); @@ -89,7 +105,6 @@ class CreateAllPackagesAppCommand extends PackageCommand { await Future.wait(>[ _updateAppGradle(), - _updateManifest(), _updateMacosPbxproj(), // This step requires the native file generation triggered by // flutter pub get above, so can't currently be run on Windows. @@ -98,20 +113,101 @@ class CreateAllPackagesAppCommand extends PackageCommand { } Future _createApp() async { - final io.ProcessResult result = io.Process.runSync( + return processRunner.runAndStream( flutterCommand, [ 'create', '--template=app', - '--project-name=$_projectName', - '--android-language=java', + '--project-name=$allPackagesProjectName', _appDirectory.path, ], ); + } - print(result.stdout); - print(result.stderr); - return result.exitCode; + Future _replaceWithLegacy( + {required Directory target, required Directory source}) async { + if (!source.existsSync()) { + printError('No such legacy source directory: ${source.path}'); + throw ToolExit(_exitMissingLegacySource); + } + for (final FileSystemEntity entity in source.listSync()) { + final String basename = entity.basename; + print('Replacing $basename with legacy version...'); + if (entity is Directory) { + target.childDirectory(basename).deleteSync(recursive: true); + } else { + target.childFile(basename).deleteSync(); + } + _copyDirectory(source: source, target: target); + } + } + + void _copyDirectory({required Directory target, required Directory source}) { + target.createSync(recursive: true); + for (final FileSystemEntity entity in source.listSync(recursive: true)) { + final List subcomponents = + p.split(p.relative(entity.path, from: source.path)); + if (entity is Directory) { + childDirectoryWithSubcomponents(target, subcomponents) + .createSync(recursive: true); + } else if (entity is File) { + final File targetFile = + childFileWithSubcomponents(target, subcomponents); + targetFile.parent.createSync(recursive: true); + entity.copySync(targetFile.path); + } else { + throw UnimplementedError('Unsupported entity: $entity'); + } + } + } + + /// Rewrites [file], replacing any lines contain a key in [replacements] with + /// the lines in the corresponding value, and adding any lines in [additions]' + /// values after lines containing the key. + void _adjustFile( + File file, { + Map> replacements = const >{}, + Map> additions = const >{}, + Map> regexReplacements = + const >{}, + }) { + if (replacements.isEmpty && additions.isEmpty) { + return; + } + if (!file.existsSync()) { + printError('Unable to find ${file.path} for updating.'); + throw ToolExit(_exitMissingFile); + } + + final StringBuffer output = StringBuffer(); + for (final String line in file.readAsLinesSync()) { + List? replacementLines; + for (final MapEntry> replacement + in replacements.entries) { + if (line.contains(replacement.key)) { + replacementLines = replacement.value; + break; + } + } + if (replacementLines == null) { + for (final MapEntry> replacement + in regexReplacements.entries) { + final RegExpMatch? match = replacement.key.firstMatch(line); + if (match != null) { + replacementLines = replacement.value; + break; + } + } + } + (replacementLines ?? [line]).forEach(output.writeln); + + for (final String targetString in additions.keys) { + if (line.contains(targetString)) { + additions[targetString]!.forEach(output.writeln); + } + } + } + file.writeAsStringSync(output.toString()); } Future _updateAppGradle() async { @@ -119,59 +215,50 @@ class CreateAllPackagesAppCommand extends PackageCommand { .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childFile('build.gradle'); - if (!gradleFile.existsSync()) { - throw ToolExit(64); + + // Ensure that there is a dependencies section, so the dependencies addition + // below will work. + final String content = gradleFile.readAsStringSync(); + if (!content.contains('\ndependencies {')) { + gradleFile.writeAsStringSync(''' +$content +dependencies {} +'''); } - final StringBuffer newGradle = StringBuffer(); - for (final String line in gradleFile.readAsLinesSync()) { - if (line.contains('minSdkVersion')) { + const String lifecycleDependency = + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'"; + + _adjustFile( + gradleFile, + replacements: >{ // minSdkVersion 21 is required by camera_android. - newGradle.writeln('minSdkVersion 21'); - } else if (line.contains('compileSdkVersion')) { + 'minSdkVersion': ['minSdkVersion 21'], // compileSdkVersion 33 is required by local_auth. - newGradle.writeln('compileSdkVersion 33'); - } else { - newGradle.writeln(line); - } - if (line.contains('defaultConfig {')) { - newGradle.writeln(' multiDexEnabled true'); - } else if (line.contains('dependencies {')) { + 'compileSdkVersion': ['compileSdkVersion 33'], + }, + additions: >{ + 'defaultConfig {': [' multiDexEnabled true'], + }, + regexReplacements: >{ // Tests for https://github.com/flutter/flutter/issues/43383 - newGradle.writeln( - " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", - ); - } - } - gradleFile.writeAsStringSync(newGradle.toString()); - } - - Future _updateManifest() async { - final File manifestFile = app - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childDirectory('src') - .childDirectory('main') - .childFile('AndroidManifest.xml'); - if (!manifestFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newManifest = StringBuffer(); - for (final String line in manifestFile.readAsLinesSync()) { - if (line.contains('package="com.example.$_projectName"')) { - newManifest - ..writeln('package="com.example.$_projectName"') - ..writeln('xmlns:tools="http://schemas.android.com/tools">') - ..writeln() - ..writeln( - '', - ); - } else { - newManifest.writeln(line); - } - } - manifestFile.writeAsStringSync(newManifest.toString()); + // Handling of 'dependencies' is more complex since it hasn't been very + // stable across template versions. + // - Handle an empty, collapsed dependencies section. + RegExp(r'^dependencies\s+{\s*}$'): [ + 'dependencies {', + lifecycleDependency, + '}', + ], + // - Handle a normal dependencies section. + RegExp(r'^dependencies\s+{$'): [ + 'dependencies {', + lifecycleDependency, + ], + // - See below for handling of the case where there is no dependencies + // section. + }, + ); } Future _genPubspecWithAllPlugins() async { @@ -190,7 +277,7 @@ class CreateAllPackagesAppCommand extends PackageCommand { final Map pluginDeps = await _getValidPathDependencies(); final Pubspec pubspec = Pubspec( - _projectName, + allPackagesProjectName, description: 'Flutter app containing all 1st party plugins.', version: Version.parse('1.0.0+1'), environment: { @@ -300,23 +387,15 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} return; } - final File podfileFile = + final File podfile = app.platformDirectory(FlutterPlatform.macos).childFile('Podfile'); - if (!podfileFile.existsSync()) { - printError("Can't find Podfile for macOS"); - throw ToolExit(_exitUpdateMacosPodfileFailed); - } - - final StringBuffer newPodfile = StringBuffer(); - for (final String line in podfileFile.readAsLinesSync()) { - if (line.contains('platform :osx')) { + _adjustFile( + podfile, + replacements: >{ // macOS 10.15 is required by in_app_purchase. - newPodfile.writeln("platform :osx, '10.15'"); - } else { - newPodfile.writeln(line); - } - } - podfileFile.writeAsStringSync(newPodfile.toString()); + 'platform :osx': ["platform :osx, '10.15'"], + }, + ); } Future _updateMacosPbxproj() async { @@ -324,20 +403,14 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} .platformDirectory(FlutterPlatform.macos) .childDirectory('Runner.xcodeproj') .childFile('project.pbxproj'); - if (!pbxprojFile.existsSync()) { - printError("Can't find project.pbxproj for macOS"); - throw ToolExit(_exitUpdateMacosPbxprojFailed); - } - - final StringBuffer newPbxproj = StringBuffer(); - for (final String line in pbxprojFile.readAsLinesSync()) { - if (line.contains('MACOSX_DEPLOYMENT_TARGET')) { + _adjustFile( + pbxprojFile, + replacements: >{ // macOS 10.15 is required by in_app_purchase. - newPbxproj.writeln(' MACOSX_DEPLOYMENT_TARGET = 10.15;'); - } else { - newPbxproj.writeln(line); - } - } - pbxprojFile.writeAsStringSync(newPbxproj.toString()); + 'MACOSX_DEPLOYMENT_TARGET': [ + ' MACOSX_DEPLOYMENT_TARGET = 10.15;' + ], + }, + ); } } diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart index 79b804e31e..4640aa8f69 100644 --- a/script/tool/test/common/file_utils_test.dart +++ b/script/tool/test/common/file_utils_test.dart @@ -8,25 +8,51 @@ import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:test/test.dart'; void main() { - test('works on Posix', () async { - final FileSystem fileSystem = - MemoryFileSystem(); + group('childFileWithSubcomponents', () { + test('works on Posix', () async { + final FileSystem fileSystem = MemoryFileSystem(); - final Directory base = fileSystem.directory('/').childDirectory('base'); - final File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + final Directory base = fileSystem.directory('/').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); - expect(file.absolute.path, '/base/foo/bar/baz.txt'); + expect(file.absolute.path, '/base/foo/bar/baz.txt'); + }); + + test('works on Windows', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.windows); + + final Directory base = + fileSystem.directory(r'C:\').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + + expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); + }); }); - test('works on Windows', () async { - final FileSystem fileSystem = - MemoryFileSystem(style: FileSystemStyle.windows); + group('childDirectoryWithSubcomponents', () { + test('works on Posix', () async { + final FileSystem fileSystem = MemoryFileSystem(); - final Directory base = fileSystem.directory(r'C:\').childDirectory('base'); - final File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + final Directory base = fileSystem.directory('/').childDirectory('base'); + final Directory dir = + childDirectoryWithSubcomponents(base, ['foo', 'bar', 'baz']); - expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); + expect(dir.absolute.path, '/base/foo/bar/baz'); + }); + + test('works on Windows', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.windows); + + final Directory base = + fileSystem.directory(r'C:\').childDirectory('base'); + final Directory dir = + childDirectoryWithSubcomponents(base, ['foo', 'bar', 'baz']); + + expect(dir.absolute.path, r'C:\base\foo\bar\baz'); + }); }); } diff --git a/script/tool/test/create_all_packages_app_command_test.dart b/script/tool/test/create_all_packages_app_command_test.dart index c545c1f3f5..6f7ba8ead2 100644 --- a/script/tool/test/create_all_packages_app_command_test.dart +++ b/script/tool/test/create_all_packages_app_command_test.dart @@ -6,7 +6,7 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; -import 'package:file/local.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/create_all_packages_app_command.dart'; import 'package:platform/platform.dart'; @@ -18,16 +18,15 @@ import 'util.dart'; void main() { late CommandRunner runner; late CreateAllPackagesAppCommand command; + late Platform mockPlatform; late FileSystem fileSystem; late Directory testRoot; late Directory packagesDir; late RecordingProcessRunner processRunner; setUp(() { - // Since the core of this command is a call to 'flutter create', the test - // has to use the real filesystem. Put everything possible in a unique - // temporary to minimize effect on the host system. - fileSystem = const LocalFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + fileSystem = MemoryFileSystem(); testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); processRunner = RecordingProcessRunner(); @@ -35,34 +34,142 @@ void main() { command = CreateAllPackagesAppCommand( packagesDir, processRunner: processRunner, - pluginsRoot: testRoot, + platform: mockPlatform, ); runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); runner.addCommand(command); }); - tearDown(() { - testRoot.deleteSync(recursive: true); - }); + /// Simulates enough of `flutter create`s output to allow the modifications + /// made by the command to work. + void writeFakeFlutterCreateOutput( + Directory outputDirectory, { + String dartSdkConstraint = '>=3.0.0 <4.0.0', + String? appBuildGradleDependencies, + bool androidOnly = false, + }) { + final RepositoryPackage package = RepositoryPackage( + outputDirectory.childDirectory(allPackagesProjectName)); + + // Android + final String dependencies = appBuildGradleDependencies ?? + r''' +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +'''; + package + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync(''' +android { + namespace 'dev.flutter.packages.foo.example' + compileSdkVersion flutter.compileSdkVersion + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + applicationId "dev.flutter.packages.foo.example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion 32 + } +} + +$dependencies +'''); + + if (androidOnly) { + return; + } + + // Non-platform-specific + package.pubspecFile + ..createSync(recursive: true) + ..writeAsStringSync(''' +name: $allPackagesProjectName +description: Flutter app containing all 1st party plugins. +publish_to: none +version: 1.0.0 + +environment: + sdk: '$dartSdkConstraint' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter +### +'''); + + // macOS + final Directory macOS = package.platformDirectory(FlutterPlatform.macos); + macOS.childDirectory('Runner.xcodeproj').childFile('project.pbxproj') + ..createSync(recursive: true) + ..writeAsStringSync(''' + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + }; + name = Release; + }; +'''); + macOS.childFile('Podfile') + ..createSync(recursive: true) + ..writeAsStringSync(''' +# platform :osx, '10.14' + +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} +'''); + } group('non-macOS host', () { setUp(() { + mockPlatform = MockPlatform(isLinux: true); command = CreateAllPackagesAppCommand( packagesDir, processRunner: processRunner, - // Set isWindows or not based on the actual host, so that - // `flutterCommand` works, since these tests actually call 'flutter'. - // The important thing is that isMacOS always returns false. - platform: MockPlatform(isWindows: const LocalPlatform().isWindows), - pluginsRoot: testRoot, + platform: mockPlatform, ); runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); runner.addCommand(command); }); + test('calls "flutter create"', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['create-all-packages-app']); + + expect( + processRunner.recordedCalls, + contains(ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'create', + '--template=app', + '--project-name=$allPackagesProjectName', + testRoot.childDirectory(allPackagesProjectName).path, + ], + null))); + }); + test('pubspec includes all plugins', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); @@ -80,6 +187,7 @@ void main() { }); test('pubspec has overrides for all plugins', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); @@ -97,33 +205,186 @@ void main() { ])); }); - test('pubspec preserves existing Dart SDK version', () async { - const String baselineProjectName = 'baseline'; - final Directory baselineProjectDirectory = - testRoot.childDirectory(baselineProjectName); - io.Process.runSync( - getFlutterCommand(const LocalPlatform()), - [ - 'create', - '--template=app', - '--project-name=$baselineProjectName', - baselineProjectDirectory.path, - ], - ); - final Pubspec baselinePubspec = - RepositoryPackage(baselineProjectDirectory).parsePubspec(); + test('legacy files are copied when requested', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + // Make a fake legacy source with all the necessary files, replacing one + // of them. + final Directory legacyDir = testRoot.childDirectory('legacy'); + final RepositoryPackage legacySource = + RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName)); + writeFakeFlutterCreateOutput(legacyDir, androidOnly: true); + const String legacyAppBuildGradleContents = 'Fake legacy content'; + final File legacyGradleFile = legacySource + .platformDirectory(FlutterPlatform.android) + .childFile('build.gradle'); + legacyGradleFile.writeAsStringSync(legacyAppBuildGradleContents); + await runCapturingPrint(runner, [ + 'create-all-packages-app', + '--legacy-source=${legacySource.path}', + ]); + + final File buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childFile('build.gradle'); + + expect(buildGradle.readAsStringSync(), legacyAppBuildGradleContents); + }); + + test('legacy directory replaces, rather than overlaying', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + final File extraFile = + RepositoryPackage(testRoot.childDirectory(allPackagesProjectName)) + .platformDirectory(FlutterPlatform.android) + .childFile('extra_file'); + extraFile.createSync(recursive: true); + // Make a fake legacy source with all the necessary files, but not + // including the extra file. + final Directory legacyDir = testRoot.childDirectory('legacy'); + final RepositoryPackage legacySource = + RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName)); + writeFakeFlutterCreateOutput(legacyDir, androidOnly: true); + + await runCapturingPrint(runner, [ + 'create-all-packages-app', + '--legacy-source=${legacySource.path}', + ]); + + expect(extraFile.existsSync(), false); + }); + + test('legacy files are modified as needed by the tool', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + // Make a fake legacy source with all the necessary files, replacing one + // of them. + final Directory legacyDir = testRoot.childDirectory('legacy'); + final RepositoryPackage legacySource = + RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName)); + writeFakeFlutterCreateOutput(legacyDir, androidOnly: true); + const String legacyAppBuildGradleContents = ''' +# This is the legacy file +android { + compileSdkVersion flutter.compileSdkVersion + defaultConfig { + minSdkVersion flutter.minSdkVersion + } +} +'''; + final File legacyGradleFile = legacySource + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle'); + legacyGradleFile.writeAsStringSync(legacyAppBuildGradleContents); + + await runCapturingPrint(runner, [ + 'create-all-packages-app', + '--legacy-source=${legacySource.path}', + ]); + + final List buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + .readAsLinesSync(); + + expect( + buildGradle, + containsAll([ + contains('This is the legacy file'), + contains('minSdkVersion 21'), + contains('compileSdkVersion 33'), + ])); + }); + + test('pubspec preserves existing Dart SDK version', () async { + const String existingSdkConstraint = '>=1.0.0 <99.0.0'; + writeFakeFlutterCreateOutput(testRoot, + dartSdkConstraint: existingSdkConstraint); createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['create-all-packages-app']); final Pubspec generatedPubspec = command.app.parsePubspec(); const String dartSdkKey = 'sdk'; - expect(generatedPubspec.environment?[dartSdkKey], - baselinePubspec.environment?[dartSdkKey]); + expect(generatedPubspec.environment?[dartSdkKey].toString(), + existingSdkConstraint); + }); + + test('Android app gradle is modified as expected', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['create-all-packages-app']); + + final List buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + .readAsLinesSync(); + + expect( + buildGradle, + containsAll([ + contains('minSdkVersion 21'), + contains('compileSdkVersion 33'), + contains('multiDexEnabled true'), + contains('androidx.lifecycle:lifecycle-runtime'), + ])); + }); + + // The template's app/build.gradle does not always have a dependencies + // section; ensure that the dependency is added if there is not one. + test('Android lifecyle dependency is added with no dependencies', () async { + writeFakeFlutterCreateOutput(testRoot, appBuildGradleDependencies: ''); + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['create-all-packages-app']); + + final List buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + .readAsLinesSync(); + + expect( + buildGradle, + containsAllInOrder([ + equals('dependencies {'), + contains('androidx.lifecycle:lifecycle-runtime'), + equals('}'), + ])); + }); + + // Some versions of the template's app/build.gradle has an empty + // dependencies section; ensure that the dependency is added in that case. + test('Android lifecyle dependency is added with empty dependencies', + () async { + writeFakeFlutterCreateOutput(testRoot, + appBuildGradleDependencies: 'dependencies {}'); + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['create-all-packages-app']); + + final List buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + .readAsLinesSync(); + + expect( + buildGradle, + containsAllInOrder([ + equals('dependencies {'), + contains('androidx.lifecycle:lifecycle-runtime'), + equals('}'), + ])); }); test('macOS deployment target is modified in pbxproj', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['create-all-packages-app']); @@ -141,27 +402,50 @@ void main() { }); test('calls flutter pub get', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['create-all-packages-app']); expect( processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(const LocalPlatform()), - const ['pub', 'get'], - testRoot.childDirectory('all_packages').path), - ])); - }, - // See comment about Windows in create_all_packages_app_command.dart - skip: io.Platform.isWindows); + contains(ProcessCall( + getFlutterCommand(mockPlatform), + const ['pub', 'get'], + testRoot.childDirectory(allPackagesProjectName).path))); + }); - test('fails if flutter pub get fails', () async { + test('fails if flutter create fails', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); - processRunner.mockProcessesForExecutable[ - getFlutterCommand(const LocalPlatform())] = [ + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + FakeProcessInfo(MockProcess(exitCode: 1), ['create']) + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['create-all-packages-app'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Failed to `flutter create`'), + ])); + }); + + test('fails if flutter pub get fails', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + FakeProcessInfo(MockProcess(), ['create']), FakeProcessInfo(MockProcess(exitCode: 1), ['pub', 'get']) ]; Error? commandError; @@ -182,20 +466,22 @@ void main() { skip: io.Platform.isWindows); test('handles --output-dir', () async { - createFakePlugin('plugina', packagesDir); - final Directory customOutputDir = fileSystem.systemTempDirectory.createTempSync(); + writeFakeFlutterCreateOutput(customOutputDir); + createFakePlugin('plugina', packagesDir); + await runCapturingPrint(runner, [ 'create-all-packages-app', '--output-dir=${customOutputDir.path}' ]); expect(command.app.path, - customOutputDir.childDirectory('all_packages').path); + customOutputDir.childDirectory(allPackagesProjectName).path); }); test('logs exclusions', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); @@ -219,7 +505,6 @@ void main() { packagesDir, processRunner: processRunner, platform: MockPlatform(isMacOS: true), - pluginsRoot: testRoot, ); runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); @@ -227,10 +512,11 @@ void main() { }); test('macOS deployment target is modified in Podfile', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); final File podfileFile = RepositoryPackage( - command.packagesDir.parent.childDirectory('all_packages')) + command.packagesDir.parent.childDirectory(allPackagesProjectName)) .platformDirectory(FlutterPlatform.macos) .childFile('Podfile'); podfileFile.createSync(recursive: true);