Unverified Commit 3c83c526 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

FlutterProject refactoring and test coverage (#20296)

parent e60087a1
......@@ -200,22 +200,26 @@ distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersio
}
}
/// Overwrite android/local.properties in the specified Flutter project, if needed.
/// Overwrite local.properties in the specified Flutter project's Android
/// sub-project, if needed.
///
/// Throws, if `pubspec.yaml` or Android SDK cannot be located.
/// Throws tool exit, if `pubspec.yaml` is invalid.
///
/// If [requireSdk] is `true` this will fail with a tool-exit if no Android Sdk
/// If [requireSdk] is `true` this will fail with a tool exit if no Android Sdk
/// is found.
Future<void> updateLocalProperties({
@required FlutterProject project,
BuildInfo buildInfo,
bool requireAndroidSdk = true,
}) async {
if (project.manifest == null) {
throwToolExit('Invalid `pubspec.yaml`');
}
if (requireAndroidSdk && androidSdk == null) {
throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
}
final File localProperties = project.androidLocalPropertiesFile;
final File localProperties = project.android.localPropertiesFile;
bool changed = false;
SettingsFile settings;
......
......@@ -15,7 +15,6 @@ import '../base/process.dart';
import '../base/process_manager.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../bundle.dart' as bundle;
import '../cache.dart';
import '../flutter_manifest.dart';
import '../globals.dart';
......@@ -28,25 +27,6 @@ String flutterFrameworkDir(BuildMode mode) {
return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, mode)));
}
/// Writes default Xcode properties files in the Flutter project at [projectPath],
/// if project is an iOS project and such files are out of date or do not
/// already exist.
Future<void> generateXcodeProperties({FlutterProject project}) async {
if (project.manifest.isModule ||
project.ios.directory.existsSync()) {
if (!Cache.instance.fileOlderThanToolsStamp(project.generatedXcodePropertiesFile)) {
return;
}
await updateGeneratedXcodeProperties(
project: project,
buildInfo: BuildInfo.debug,
targetOverride: bundle.defaultMainPath,
previewDart2: true,
);
}
}
/// Writes or rewrites Xcode property files with the specified information.
///
/// targetOverride: Optional parameter, if null or unspecified the default value
......@@ -119,7 +99,7 @@ Future<void> updateGeneratedXcodeProperties({
localsBuffer.writeln('TRACK_WIDGET_CREATION=true');
}
final File generatedXcodePropertiesFile = project.generatedXcodePropertiesFile;
final File generatedXcodePropertiesFile = project.ios.generatedXcodePropertiesFile;
generatedXcodePropertiesFile.createSync(recursive: true);
generatedXcodePropertiesFile.writeAsStringSync(localsBuffer.toString());
}
......
......@@ -158,7 +158,7 @@ Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin>
};
final String javaSourcePath = fs.path.join(
project.androidPluginRegistrantHost.path,
project.android.pluginRegistrantHost.path,
'src',
'main',
'java',
......@@ -247,8 +247,8 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
'plugins': iosPlugins,
};
final String registryDirectory = project.iosPluginRegistrantHost.path;
if (project.manifest.isModule) {
final String registryDirectory = project.ios.pluginRegistrantHost.path;
if (project.isModule) {
final String registryClassesDirectory = fs.path.join(registryDirectory, 'Classes');
_renderTemplateToFile(
_iosPluginRegistrantPodspecTemplate,
......
......@@ -5,8 +5,11 @@
import 'dart:async';
import 'dart:convert';
import 'package:meta/meta.dart';
import 'android/gradle.dart' as gradle;
import 'base/file_system.dart';
import 'build_info.dart';
import 'bundle.dart' as bundle;
import 'cache.dart';
import 'flutter_manifest.dart';
......@@ -16,21 +19,27 @@ import 'template.dart';
/// Represents the contents of a Flutter project at the specified [directory].
///
/// Instances should be treated as immutable snapshots, to be replaced by new
/// instances on changes to `pubspec.yaml` files.
/// [FlutterManifest] information is read from `pubspec.yaml` and
/// `example/pubspec.yaml` files on construction of a [FlutterProject] instance.
/// The constructed instance carries an immutable snapshot representation of the
/// presence and content of those files. Accordingly, [FlutterProject] instances
/// should be discarded upon changes to the `pubspec.yaml` files, but can be
/// used across changes to other files, as no other file-level information is
/// cached.
class FlutterProject {
FlutterProject._(this.directory, this.manifest, this._exampleManifest);
@visibleForTesting
FlutterProject(this.directory, this.manifest, this._exampleManifest);
/// Returns a future that completes with a FlutterProject view of the given directory.
static Future<FlutterProject> fromDirectory(Directory directory) async {
final FlutterManifest manifest = await FlutterManifest.createFromPath(
directory.childFile(bundle.defaultManifestPath).path,
);
final Directory exampleDirectory = directory.childDirectory('example');
final Directory exampleDirectory = _exampleDirectory(directory);
final FlutterManifest exampleManifest = await FlutterManifest.createFromPath(
exampleDirectory.childFile(bundle.defaultManifestPath).path,
);
return new FlutterProject._(directory, manifest, exampleManifest);
return new FlutterProject(directory, manifest, exampleManifest);
}
/// Returns a future that completes with a FlutterProject view of the current directory.
......@@ -73,83 +82,56 @@ class FlutterProject {
}
/// The iOS sub project of this project.
IosProject get ios => new IosProject(directory.childDirectory('ios'));
IosProject get ios => new IosProject._(this);
/// The Android sub project of this project.
AndroidProject get android {
if (manifest.isModule) {
return new AndroidProject(directory.childDirectory('.android'));
}
return new AndroidProject(directory.childDirectory('android'));
}
/// The generated AndroidModule sub project of this module project.
AndroidModuleProject get androidModule => new AndroidModuleProject(directory.childDirectory('.android'));
/// The generated IosModule sub project of this module project.
IosModuleProject get iosModule => new IosModuleProject(directory.childDirectory('.ios'));
File get androidLocalPropertiesFile {
return directory
.childDirectory(manifest.isModule ? '.android' : 'android')
.childFile('local.properties');
}
File get generatedXcodePropertiesFile {
return directory
.childDirectory(manifest.isModule ? '.ios' : 'ios')
.childDirectory('Flutter')
.childFile('Generated.xcconfig');
}
AndroidProject get android => new AndroidProject._(this);
File get flutterPluginsFile => directory.childFile('.flutter-plugins');
Directory get androidPluginRegistrantHost {
return manifest.isModule
? directory.childDirectory('.android').childDirectory('Flutter')
: directory.childDirectory('android').childDirectory('app');
}
Directory get iosPluginRegistrantHost {
// In a module create the GeneratedPluginRegistrant as a pod to be included
// from a hosting app.
// For a non-module create the GeneratedPluginRegistrant as source files
// directly in the iOS project.
return manifest.isModule
? directory.childDirectory('.ios').childDirectory('Flutter').childDirectory('FlutterPluginRegistrant')
: directory.childDirectory('ios').childDirectory('Runner');
}
/// The example sub-project of this project.
FlutterProject get example => new FlutterProject._(_exampleDirectory, _exampleManifest, FlutterManifest.empty());
FlutterProject get example => new FlutterProject(
_exampleDirectory(directory),
_exampleManifest,
FlutterManifest.empty(),
);
bool get isModule => manifest != null && manifest.isModule;
/// True, if this project has an example application
bool get hasExampleApp => _exampleDirectory.childFile('pubspec.yaml').existsSync();
/// True, if this project has an example application.
bool get hasExampleApp => _exampleDirectory(directory).existsSync();
/// The directory that will contain the example if an example exists.
Directory get _exampleDirectory => directory.childDirectory('example');
static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example');
/// Generates project files necessary to make Gradle builds work on Android
/// and CocoaPods+Xcode work on iOS, for app and module projects only.
Future<void> ensureReadyForPlatformSpecificTooling() async {
if (!directory.existsSync() || hasExampleApp) {
if (!directory.existsSync() || hasExampleApp)
return;
}
if (manifest.isModule) {
await androidModule.ensureReadyForPlatformSpecificTooling(this);
await iosModule.ensureReadyForPlatformSpecificTooling();
}
await xcode.generateXcodeProperties(project: this);
await android.ensureReadyForPlatformSpecificTooling();
await ios.ensureReadyForPlatformSpecificTooling();
await injectPlugins(this);
}
}
/// Represents the contents of the ios/ folder of a Flutter project.
/// Represents the iOS sub-project of a Flutter project.
///
/// Instances will reflect the contents of the `ios/` sub-folder of
/// Flutter applications and the `.ios/` sub-folder of Flutter modules.
class IosProject {
static final RegExp _productBundleIdPattern = new RegExp(r'^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(.*);\s*$');
IosProject(this.directory);
final Directory directory;
IosProject._(this.parent);
/// The parent of this project.
final FlutterProject parent;
/// The directory of this project.
Directory get directory => parent.directory.childDirectory(isModule ? '.ios' : 'ios');
/// True, if the parent Flutter project is a module.
bool get isModule => parent.isModule;
/// The xcode config file for [mode].
File xcodeConfigFor(String mode) => directory.childDirectory('Flutter').childFile('$mode.xcconfig');
......@@ -167,33 +149,55 @@ class IosProject {
final File projectFile = directory.childDirectory('Runner.xcodeproj').childFile('project.pbxproj');
return _firstMatchInFile(projectFile, _productBundleIdPattern).then((Match match) => match?.group(1));
}
}
/// Represents the contents of the .ios/ folder of a Flutter module
/// project.
class IosModuleProject {
IosModuleProject(this.directory);
final Directory directory;
Future<void> ensureReadyForPlatformSpecificTooling() async {
if (_shouldRegenerate()) {
if (isModule && _shouldRegenerateFromTemplate()) {
final Template template = new Template.fromName(fs.path.join('module', 'ios'));
template.render(directory, <String, dynamic>{}, printStatusWhenWriting: false);
}
if (!directory.existsSync())
return;
if (Cache.instance.fileOlderThanToolsStamp(generatedXcodePropertiesFile)) {
await xcode.updateGeneratedXcodeProperties(
project: parent,
buildInfo: BuildInfo.debug,
targetOverride: bundle.defaultMainPath,
previewDart2: true,
);
}
}
bool _shouldRegenerate() {
bool _shouldRegenerateFromTemplate() {
return Cache.instance.fileOlderThanToolsStamp(directory.childFile('podhelper.rb'));
}
File get generatedXcodePropertiesFile => directory.childDirectory('Flutter').childFile('Generated.xcconfig');
Directory get pluginRegistrantHost {
return isModule
? directory.childDirectory('Flutter').childDirectory('FlutterPluginRegistrant')
: directory.childDirectory('Runner');
}
}
/// Represents the contents of the android/ folder of a Flutter project.
/// Represents the Android sub-project of a Flutter project.
///
/// Instances will reflect the contents of the `android/` sub-folder of
/// Flutter applications and the `.android/` sub-folder of Flutter modules.
class AndroidProject {
static final RegExp _applicationIdPattern = new RegExp('^\\s*applicationId\\s+[\'\"](.*)[\'\"]\\s*\$');
static final RegExp _groupPattern = new RegExp('^\\s*group\\s+[\'\"](.*)[\'\"]\\s*\$');
AndroidProject(this.directory);
AndroidProject._(this.parent);
/// The parent of this project.
final FlutterProject parent;
/// The directory of this project.
Directory get directory => parent.directory.childDirectory(isModule ? '.android' : 'android');
/// True, if the parent Flutter project is a module.
bool get isModule => parent.isModule;
File get gradleManifestFile {
return isUsingGradle()
......@@ -211,8 +215,6 @@ class AndroidProject {
return directory.childFile('build.gradle').existsSync();
}
final Directory directory;
Future<String> applicationId() {
final File gradleFile = directory.childDirectory('app').childFile('build.gradle');
return _firstMatchInFile(gradleFile, _applicationIdPattern).then((Match match) => match?.group(1));
......@@ -222,32 +224,31 @@ class AndroidProject {
final File gradleFile = directory.childFile('build.gradle');
return _firstMatchInFile(gradleFile, _groupPattern).then((Match match) => match?.group(1));
}
}
/// Represents the contents of the .android/ folder of a Flutter module project.
class AndroidModuleProject {
AndroidModuleProject(this.directory);
final Directory directory;
Future<void> ensureReadyForPlatformSpecificTooling(FlutterProject project) async {
if (_shouldRegenerate()) {
Future<void> ensureReadyForPlatformSpecificTooling() async {
if (isModule && _shouldRegenerateFromTemplate()) {
final Template template = new Template.fromName(fs.path.join('module', 'android'));
template.render(
directory,
<String, dynamic>{
'androidIdentifier': project.manifest.androidPackage,
'androidIdentifier': parent.manifest.androidPackage,
},
printStatusWhenWriting: false,
);
gradle.injectGradleWrapper(directory);
}
await gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
if (!directory.existsSync())
return;
await gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
}
bool _shouldRegenerate() {
bool _shouldRegenerateFromTemplate() {
return Cache.instance.fileOlderThanToolsStamp(directory.childFile('build.gradle'));
}
File get localPropertiesFile => directory.childFile('local.properties');
Directory get pluginRegistrantHost => directory.childDirectory(isModule ? 'Flutter' : 'app');
}
/// Asynchronously returns the first line-based match for [regExp] in [file].
......
......@@ -3,12 +3,14 @@
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:test/test.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'src/context.dart';
void main() {
......@@ -23,73 +25,151 @@ void main() {
(await FlutterProject.fromPath(directory.path)).directory.absolute.path,
directory.absolute.path,
);
expect(
(await FlutterProject.current()).directory.absolute.path,
fs.currentDirectory.absolute.path,
);
});
group('ensure ready for platform-specific tooling', () {
testInMemory('does nothing, if project is not created', () async {
final FlutterProject project = await someProject();
final FlutterProject project = new FlutterProject(
fs.directory('not_created'),
FlutterManifest.empty(),
FlutterManifest.empty(),
);
await project.ensureReadyForPlatformSpecificTooling();
expect(project.directory.existsSync(), isFalse);
});
testInMemory('does nothing in plugin or package root project', () async {
final FlutterProject project = await aPluginProject();
final FlutterProject project = aPluginProject();
await project.ensureReadyForPlatformSpecificTooling();
expect(project.ios.directory.childFile('Runner/GeneratedPluginRegistrant.h').existsSync(), isFalse);
expect(project.android.directory.childFile(
'app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
).existsSync(), isFalse);
expect(project.ios.directory.childFile('Flutter/Generated.xcconfig').existsSync(), isFalse);
expect(project.android.directory.childFile('local.properties').existsSync(), isFalse);
});
testInMemory('injects plugins', () async {
final FlutterProject project = await aProjectWithIos();
testInMemory('injects plugins for iOS', () async {
final FlutterProject project = someProject();
await project.ensureReadyForPlatformSpecificTooling();
expect(project.ios.directory.childFile('Runner/GeneratedPluginRegistrant.h').existsSync(), isTrue);
});
testInMemory('generates Xcode configuration', () async {
final FlutterProject project = await aProjectWithIos();
testInMemory('generates Xcode configuration for iOS', () async {
final FlutterProject project = someProject();
await project.ensureReadyForPlatformSpecificTooling();
expect(project.ios.directory.childFile('Flutter/Generated.xcconfig').existsSync(), isTrue);
});
testInMemory('injects plugins for Android', () async {
final FlutterProject project = someProject();
await project.ensureReadyForPlatformSpecificTooling();
expect(project.android.directory.childFile(
'app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
).existsSync(), isTrue);
});
testInMemory('updates local properties for Android', () async {
final FlutterProject project = someProject();
await project.ensureReadyForPlatformSpecificTooling();
expect(project.android.directory.childFile('local.properties').existsSync(), isTrue);
});
testInMemory('creates Android library in module', () async {
final FlutterProject project = aModuleProject();
await project.ensureReadyForPlatformSpecificTooling();
expect(project.android.directory.childFile('template_content').existsSync(), isTrue);
expect(project.android.directory.childFile('local.properties').existsSync(), isTrue);
expect(project.android.directory.childFile(
'Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
).existsSync(), isTrue);
});
testInMemory('creates iOS pod in module', () async {
final FlutterProject project = aModuleProject();
await project.ensureReadyForPlatformSpecificTooling();
final Directory flutter = project.ios.directory.childDirectory('Flutter');
expect(flutter.childFile('template_content').existsSync(), isTrue);
expect(flutter.childFile('Generated.xcconfig').existsSync(), isTrue);
expect(flutter.childFile(
'FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.h',
).existsSync(), isTrue);
expect(flutter.childFile(
'FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.m',
).existsSync(), isTrue);
});
});
group('module status', () {
testInMemory('is known for module', () async {
final FlutterProject project = aModuleProject();
expect(project.isModule, isTrue);
expect(project.android.isModule, isTrue);
expect(project.ios.isModule, isTrue);
expect(project.android.directory.path, startsWith('module_project/.android'));
expect(project.ios.directory.path, startsWith('module_project/.ios'));
});
testInMemory('is known for non-module', () async {
final FlutterProject project = someProject();
expect(project.isModule, isFalse);
expect(project.android.isModule, isFalse);
expect(project.ios.isModule, isFalse);
expect(project.android.directory.path, startsWith('some_project/android'));
expect(project.ios.directory.path, startsWith('some_project/ios'));
});
});
group('example', () {
testInMemory('exists for plugin', () async {
final FlutterProject project = aPluginProject();
expect(project.hasExampleApp, isTrue);
});
testInMemory('does not exist for non-plugin', () async {
final FlutterProject project = someProject();
expect(project.hasExampleApp, isFalse);
});
});
group('organization names set', () {
testInMemory('is empty, if project not created', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
expect(await project.organizationNames(), isEmpty);
});
testInMemory('is empty, if no platform folders exist', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
project.directory.createSync();
expect(await project.organizationNames(), isEmpty);
});
testInMemory('is populated from iOS bundle identifier', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
addIosWithBundleId(project.directory, 'io.flutter.someProject');
expect(await project.organizationNames(), <String>['io.flutter']);
});
testInMemory('is populated from Android application ID', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
addAndroidWithApplicationId(project.directory, 'io.flutter.someproject');
expect(await project.organizationNames(), <String>['io.flutter']);
});
testInMemory('is populated from iOS bundle identifier in plugin example', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
addIosWithBundleId(project.example.directory, 'io.flutter.someProject');
expect(await project.organizationNames(), <String>['io.flutter']);
});
testInMemory('is populated from Android application ID in plugin example', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
addAndroidWithApplicationId(project.example.directory, 'io.flutter.someproject');
expect(await project.organizationNames(), <String>['io.flutter']);
});
testInMemory('is populated from Android group in plugin', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
addAndroidWithGroup(project.directory, 'io.flutter.someproject');
expect(await project.organizationNames(), <String>['io.flutter']);
});
testInMemory('is singleton, if sources agree', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
addIosWithBundleId(project.directory, 'io.flutter.someProject');
addAndroidWithApplicationId(project.directory, 'io.flutter.someproject');
expect(await project.organizationNames(), <String>['io.flutter']);
});
testInMemory('is non-singleton, if sources disagree', () async {
final FlutterProject project = await someProject();
final FlutterProject project = someProject();
addIosWithBundleId(project.directory, 'io.flutter.someProject');
addAndroidWithApplicationId(project.directory, 'io.clutter.someproject');
expect(
......@@ -101,30 +181,88 @@ void main() {
});
}
Future<FlutterProject> someProject() => FlutterProject.fromPath('some_project');
Future<FlutterProject> aProjectWithIos() {
final Directory directory = fs.directory('ios_project');
directory.childFile('pubspec.yaml').createSync(recursive: true);
FlutterProject someProject() {
final Directory directory = fs.directory('some_project');
directory.childFile('.packages').createSync(recursive: true);
directory.childDirectory('ios').createSync(recursive: true);
return FlutterProject.fromDirectory(directory);
directory.childDirectory('android').createSync(recursive: true);
return new FlutterProject(
directory,
FlutterManifest.empty(),
FlutterManifest.empty(),
);
}
Future<FlutterProject> aPluginProject() {
final Directory directory = fs.directory('plugin_project/example');
directory.childFile('pubspec.yaml').createSync(recursive: true);
directory.childFile('.packages').createSync(recursive: true);
FlutterProject aPluginProject() {
final Directory directory = fs.directory('plugin_project');
directory.childDirectory('ios').createSync(recursive: true);
return FlutterProject.fromDirectory(directory.parent);
directory.childDirectory('android').createSync(recursive: true);
directory.childDirectory('example').createSync(recursive: true);
return new FlutterProject(
directory,
FlutterManifest.mock(const <String, dynamic>{
'flutter': <String, dynamic>{
'plugin': <String, dynamic>{}
}
}),
FlutterManifest.empty(),
);
}
FlutterProject aModuleProject() {
final Directory directory = fs.directory('module_project');
directory.childFile('.packages').createSync(recursive: true);
return new FlutterProject(
directory,
FlutterManifest.mock(const <String, dynamic>{
'flutter': <String, dynamic>{
'module': <String, dynamic>{
'androidPackage': 'com.example'
}
}
}),
FlutterManifest.empty(),
);
}
void testInMemory(String description, Future<Null> testMethod()) {
Cache.flutterRoot = 'flutter';
final FileSystem fs = new MemoryFileSystem();
// Pretend we have a Flutter module project template.
fs.directory(Cache.flutterRoot)
.childDirectory('packages')
.childDirectory('flutter_tools')
.childDirectory('templates')
.childDirectory('module')
.childDirectory('android')
.childFile('template_content.copy.tmpl')
.createSync(recursive: true);
fs.directory(Cache.flutterRoot)
.childDirectory('packages')
.childDirectory('flutter_tools')
.childDirectory('templates')
.childDirectory('module')
.childDirectory('ios')
.childDirectory('Flutter.tmpl')
.childFile('template_content.copy.tmpl')
.createSync(recursive: true);
// Sets up cache in a test execution context where `fs` is the file system.
Cache cacheCreator() {
final Cache cache = new Cache(rootOverride: fs.directory('flutter'));
cache.getArtifactDirectory('gradle_wrapper')
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.createSync(recursive: true);
return cache;
}
testUsingContext(
description,
testMethod,
overrides: <Type, Generator>{
FileSystem: () => new MemoryFileSystem(),
FileSystem: () => fs,
Cache: cacheCreator,
},
);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment