mirror of
https://github.com/flutter/packages.git
synced 2025-06-29 06:06:59 +08:00
[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
This commit is contained in:
37
.ci/legacy_project/README.md
Normal file
37
.ci/legacy_project/README.md
Normal file
@ -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.
|
46
.ci/legacy_project/all_packages/.gitignore
vendored
Normal file
46
.ci/legacy_project/all_packages/.gitignore
vendored
Normal file
@ -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
|
10
.ci/legacy_project/all_packages/.metadata
Normal file
10
.ci/legacy_project/all_packages/.metadata
Normal file
@ -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
|
11
.ci/legacy_project/all_packages/android/.gitignore
vendored
Normal file
11
.ci/legacy_project/all_packages/android/.gitignore
vendored
Normal file
@ -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
|
47
.ci/legacy_project/all_packages/android/app/build.gradle
Normal file
47
.ci/legacy_project/all_packages/android/app/build.gradle
Normal file
@ -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 '../..'
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.all_packages">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
@ -0,0 +1,41 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.all_packages">
|
||||
<application
|
||||
android:label="all_packages"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<!-- Displays an Android View that continues showing the launch screen
|
||||
Drawable until Flutter paints its first frame, then this splash
|
||||
screen fades out. A splash screen is useful to avoid any visual
|
||||
gap between the end of Android's launch screen and the painting of
|
||||
Flutter's first frame. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,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 {
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
Binary file not shown.
After Width: | Height: | Size: 544 B |
Binary file not shown.
After Width: | Height: | Size: 442 B |
Binary file not shown.
After Width: | Height: | Size: 721 B |
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.all_packages">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
29
.ci/legacy_project/all_packages/android/build.gradle
Normal file
29
.ci/legacy_project/all_packages/android/build.gradle
Normal file
@ -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
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
6
.ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
.ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -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
|
11
.ci/legacy_project/all_packages/android/settings.gradle
Normal file
11
.ci/legacy_project/all_packages/android/settings.gradle
Normal file
@ -0,0 +1,11 @@
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
@ -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:
|
||||
|
@ -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<String> 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<String> components) {
|
||||
Directory dir = base;
|
||||
for (final String directoryName in components) {
|
||||
dir = dir.childDirectory(directoryName);
|
||||
}
|
||||
return dir.childFile(basename);
|
||||
return dir;
|
||||
}
|
||||
|
@ -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<void> 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<String> excluded = getExcludedPackageNames();
|
||||
@ -89,7 +105,6 @@ class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
|
||||
await Future.wait(<Future<void>>[
|
||||
_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<int> _createApp() async {
|
||||
final io.ProcessResult result = io.Process.runSync(
|
||||
return processRunner.runAndStream(
|
||||
flutterCommand,
|
||||
<String>[
|
||||
'create',
|
||||
'--template=app',
|
||||
'--project-name=$_projectName',
|
||||
'--android-language=java',
|
||||
'--project-name=$allPackagesProjectName',
|
||||
_appDirectory.path,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
print(result.stdout);
|
||||
print(result.stderr);
|
||||
return result.exitCode;
|
||||
Future<void> _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<String> 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<String, List<String>> replacements = const <String, List<String>>{},
|
||||
Map<String, List<String>> additions = const <String, List<String>>{},
|
||||
Map<RegExp, List<String>> regexReplacements =
|
||||
const <RegExp, List<String>>{},
|
||||
}) {
|
||||
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<String>? replacementLines;
|
||||
for (final MapEntry<String, List<String>> replacement
|
||||
in replacements.entries) {
|
||||
if (line.contains(replacement.key)) {
|
||||
replacementLines = replacement.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (replacementLines == null) {
|
||||
for (final MapEntry<RegExp, List<String>> replacement
|
||||
in regexReplacements.entries) {
|
||||
final RegExpMatch? match = replacement.key.firstMatch(line);
|
||||
if (match != null) {
|
||||
replacementLines = replacement.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
(replacementLines ?? <String>[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<void> _updateAppGradle() async {
|
||||
@ -119,60 +215,51 @@ 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: <String, List<String>>{
|
||||
// minSdkVersion 21 is required by camera_android.
|
||||
newGradle.writeln('minSdkVersion 21');
|
||||
} else if (line.contains('compileSdkVersion')) {
|
||||
'minSdkVersion': <String>['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': <String>['compileSdkVersion 33'],
|
||||
},
|
||||
additions: <String, List<String>>{
|
||||
'defaultConfig {': <String>[' multiDexEnabled true'],
|
||||
},
|
||||
regexReplacements: <RegExp, List<String>>{
|
||||
// Tests for https://github.com/flutter/flutter/issues/43383
|
||||
newGradle.writeln(
|
||||
" implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n",
|
||||
// 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*}$'): <String>[
|
||||
'dependencies {',
|
||||
lifecycleDependency,
|
||||
'}',
|
||||
],
|
||||
// - Handle a normal dependencies section.
|
||||
RegExp(r'^dependencies\s+{$'): <String>[
|
||||
'dependencies {',
|
||||
lifecycleDependency,
|
||||
],
|
||||
// - See below for handling of the case where there is no dependencies
|
||||
// section.
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
gradleFile.writeAsStringSync(newGradle.toString());
|
||||
}
|
||||
|
||||
Future<void> _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(
|
||||
'<uses-sdk tools:overrideLibrary="io.flutter.plugins.camera"/>',
|
||||
);
|
||||
} else {
|
||||
newManifest.writeln(line);
|
||||
}
|
||||
}
|
||||
manifestFile.writeAsStringSync(newManifest.toString());
|
||||
}
|
||||
|
||||
Future<void> _genPubspecWithAllPlugins() async {
|
||||
// Read the old pubspec file's Dart SDK version, in order to preserve it
|
||||
@ -190,7 +277,7 @@ class CreateAllPackagesAppCommand extends PackageCommand {
|
||||
final Map<String, PathDependency> 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: <String, VersionConstraint>{
|
||||
@ -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: <String, List<String>>{
|
||||
// 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': <String>["platform :osx, '10.15'"],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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: <String, List<String>>{
|
||||
// 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': <String>[
|
||||
' MACOSX_DEPLOYMENT_TARGET = 10.15;'
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ import 'package:flutter_plugin_tools/src/common/file_utils.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('childFileWithSubcomponents', () {
|
||||
test('works on Posix', () async {
|
||||
final FileSystem fileSystem =
|
||||
MemoryFileSystem();
|
||||
final FileSystem fileSystem = MemoryFileSystem();
|
||||
|
||||
final Directory base = fileSystem.directory('/').childDirectory('base');
|
||||
final File file =
|
||||
@ -23,10 +23,36 @@ void main() {
|
||||
final FileSystem fileSystem =
|
||||
MemoryFileSystem(style: FileSystemStyle.windows);
|
||||
|
||||
final Directory base = fileSystem.directory(r'C:\').childDirectory('base');
|
||||
final Directory base =
|
||||
fileSystem.directory(r'C:\').childDirectory('base');
|
||||
final File file =
|
||||
childFileWithSubcomponents(base, <String>['foo', 'bar', 'baz.txt']);
|
||||
|
||||
expect(file.absolute.path, r'C:\base\foo\bar\baz.txt');
|
||||
});
|
||||
});
|
||||
|
||||
group('childDirectoryWithSubcomponents', () {
|
||||
test('works on Posix', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem();
|
||||
|
||||
final Directory base = fileSystem.directory('/').childDirectory('base');
|
||||
final Directory dir =
|
||||
childDirectoryWithSubcomponents(base, <String>['foo', 'bar', 'baz']);
|
||||
|
||||
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, <String>['foo', 'bar', 'baz']);
|
||||
|
||||
expect(dir.absolute.path, r'C:\base\foo\bar\baz');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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<void> 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<void>(
|
||||
'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<void>(
|
||||
'create_all_test', 'Test for $CreateAllPackagesAppCommand');
|
||||
runner.addCommand(command);
|
||||
});
|
||||
|
||||
test('calls "flutter create"', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
|
||||
expect(
|
||||
processRunner.recordedCalls,
|
||||
contains(ProcessCall(
|
||||
getFlutterCommand(mockPlatform),
|
||||
<String>[
|
||||
'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()),
|
||||
<String>[
|
||||
'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, <String>[
|
||||
'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, <String>[
|
||||
'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, <String>[
|
||||
'create-all-packages-app',
|
||||
'--legacy-source=${legacySource.path}',
|
||||
]);
|
||||
|
||||
final List<String> buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
.readAsLinesSync();
|
||||
|
||||
expect(
|
||||
buildGradle,
|
||||
containsAll(<Matcher>[
|
||||
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, <String>['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, <String>['create-all-packages-app']);
|
||||
|
||||
final List<String> buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
.readAsLinesSync();
|
||||
|
||||
expect(
|
||||
buildGradle,
|
||||
containsAll(<Matcher>[
|
||||
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, <String>['create-all-packages-app']);
|
||||
|
||||
final List<String> buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
.readAsLinesSync();
|
||||
|
||||
expect(
|
||||
buildGradle,
|
||||
containsAllInOrder(<Matcher>[
|
||||
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, <String>['create-all-packages-app']);
|
||||
|
||||
final List<String> buildGradle = command.app
|
||||
.platformDirectory(FlutterPlatform.android)
|
||||
.childDirectory('app')
|
||||
.childFile('build.gradle')
|
||||
.readAsLinesSync();
|
||||
|
||||
expect(
|
||||
buildGradle,
|
||||
containsAllInOrder(<Matcher>[
|
||||
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, <String>['create-all-packages-app']);
|
||||
@ -141,27 +402,50 @@ void main() {
|
||||
});
|
||||
|
||||
test('calls flutter pub get', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
await runCapturingPrint(runner, <String>['create-all-packages-app']);
|
||||
|
||||
expect(
|
||||
processRunner.recordedCalls,
|
||||
orderedEquals(<ProcessCall>[
|
||||
ProcessCall(
|
||||
getFlutterCommand(const LocalPlatform()),
|
||||
contains(ProcessCall(
|
||||
getFlutterCommand(mockPlatform),
|
||||
const <String>['pub', 'get'],
|
||||
testRoot.childDirectory('all_packages').path),
|
||||
]));
|
||||
},
|
||||
// See comment about Windows in create_all_packages_app_command.dart
|
||||
skip: io.Platform.isWindows);
|
||||
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())] = <FakeProcessInfo>[
|
||||
processRunner
|
||||
.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
|
||||
<FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(exitCode: 1), <String>['create'])
|
||||
];
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['create-all-packages-app'], errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Failed to `flutter create`'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('fails if flutter pub get fails', () async {
|
||||
writeFakeFlutterCreateOutput(testRoot);
|
||||
createFakePlugin('plugina', packagesDir);
|
||||
|
||||
processRunner
|
||||
.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
|
||||
<FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(), <String>['create']),
|
||||
FakeProcessInfo(MockProcess(exitCode: 1), <String>['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, <String>[
|
||||
'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<void>(
|
||||
'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);
|
||||
|
Reference in New Issue
Block a user