diff --git a/.ci/legacy_project/README.md b/.ci/legacy_project/README.md new file mode 100644 index 00000000000..c10ace0b2c2 --- /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 00000000000..0fa6b675c0a --- /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 00000000000..d7e64d0b322 --- /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 00000000000..0a741cb43d6 --- /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 00000000000..b75c7b0561b --- /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 00000000000..3a38eba348d --- /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 00000000000..70c010f2867 --- /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 00000000000..f494afad857 --- /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 00000000000..f74085f3f6a --- /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 00000000000..304732f8842 --- /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 00000000000..db77bb4b7b0 Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ 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 00000000000..17987b79bb8 Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000000..09d4391482b Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 00000000000..d5f1c8d34e7 Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 00000000000..4d6372eebdb Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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 00000000000..449a9f93082 --- /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 00000000000..d74aa35c282 --- /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 00000000000..02ba522d3d9 --- /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 00000000000..c9e3db0a0f3 --- /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 00000000000..94adc3a3f97 --- /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 00000000000..bc6a58afdda --- /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 00000000000..44e62bcf06a --- /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 e239560c968..41534365ab4 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -301,6 +301,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 3c2f2f18f95..968de011a21 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 fb07e335e0f..01f37f3a310 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'; - -const String _projectName = 'all_packages'; +/// The name of the build-all-packages project, as passed to `flutter create`. +@visibleForTesting +const String allPackagesProjectName = '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,80 +113,152 @@ 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 _updateAppGradle() async { - final File gradleFile = app - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childFile('build.gradle'); - if (!gradleFile.existsSync()) { - throw ToolExit(64); + 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); } + } - final StringBuffer newGradle = StringBuffer(); - for (final String line in gradleFile.readAsLinesSync()) { - if (line.contains('minSdkVersion')) { - // minSdkVersion 21 is required by camera_android. - newGradle.writeln('minSdkVersion 21'); - } else if (line.contains('compileSdkVersion')) { - // compileSdkVersion 33 is required by local_auth. - newGradle.writeln('compileSdkVersion 33'); + 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 { - newGradle.writeln(line); + throw UnimplementedError('Unsupported entity: $entity'); } - if (line.contains('defaultConfig {')) { - newGradle.writeln(' multiDexEnabled true'); - } else if (line.contains('dependencies {')) { - // Tests for https://github.com/flutter/flutter/issues/43383 - newGradle.writeln( - " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", - ); + } + } + + /// 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); + } } } - gradleFile.writeAsStringSync(newGradle.toString()); + file.writeAsStringSync(output.toString()); } - Future _updateManifest() async { - final File manifestFile = app + Future _updateAppGradle() async { + final File gradleFile = app .platformDirectory(FlutterPlatform.android) .childDirectory('app') - .childDirectory('src') - .childDirectory('main') - .childFile('AndroidManifest.xml'); - if (!manifestFile.existsSync()) { - throw ToolExit(64); - } + .childFile('build.gradle'); - 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); - } + // 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 {} +'''); } - manifestFile.writeAsStringSync(newManifest.toString()); + + const String lifecycleDependency = + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'"; + + _adjustFile( + gradleFile, + replacements: >{ + // minSdkVersion 21 is required by camera_android. + 'minSdkVersion': ['minSdkVersion 21'], + // compileSdkVersion 33 is required by local_auth. + 'compileSdkVersion': ['compileSdkVersion 33'], + }, + additions: >{ + 'defaultConfig {': [' multiDexEnabled true'], + }, + regexReplacements: >{ + // Tests for https://github.com/flutter/flutter/issues/43383 + // 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 79b804e31ea..4640aa8f691 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('/').childDirectory('base'); + final Directory dir = + childDirectoryWithSubcomponents(base, ['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 File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + final Directory base = + fileSystem.directory(r'C:\').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, 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 c545c1f3f5a..6f7ba8ead2e 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), + contains(ProcessCall( + getFlutterCommand(mockPlatform), + const ['pub', 'get'], + testRoot.childDirectory(allPackagesProjectName).path))); + }); + + test('fails if flutter create fails', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + + 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`'), ])); - }, - // See comment about Windows in create_all_packages_app_command.dart - skip: io.Platform.isWindows); + }); test('fails if flutter pub get fails', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); - processRunner.mockProcessesForExecutable[ - getFlutterCommand(const LocalPlatform())] = [ + 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);