Unverified Commit 21a32fdd authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Fixes the project detection logic when creating new projects over existing directories. (#22744)

This all happened because I was trying to be a little too helpful...

Part of the job of the "create" command is to recreate missing pieces of existing projects, and now that the default has changed, I wanted to make it so that if someone had created a default flutter create project before, that they could run a default flutter create there again, and not have it trashed by using the new default template (application) over the old one (app).

This meant I had to detect what type of project it was. Unfortunately, in the past we didn't write anything in the .metadata file to identify the type of project, and since the goal was regenerating missing files, I can't count on anything existing, so it's just a heuristic match.

This simplifies the heuristics down to just detecting the difference between "app" and "application" projects, and only detect the other types if they're explicitly listed in the .metadata file (I changed the code in my original PR to add the project type to the .metadata file). People used to have to specify the type for those anyhow, so it shouldn't be a surprise to users.

So, the main difference in the new heuristics from my last attempt is that if you have a directory that has some other stuff it (like maybe a "plugin" project), then we'll recreate (pronounced "mess up") the project using the "application" template, but that was true before (except it would use the "app" template).

Fixes #22726
parent 3184b7cb
...@@ -24,28 +24,36 @@ import '../runner/flutter_command.dart'; ...@@ -24,28 +24,36 @@ import '../runner/flutter_command.dart';
import '../template.dart'; import '../template.dart';
import '../version.dart'; import '../version.dart';
enum ProjectType { enum _ProjectType {
/// This is the legacy "app" module type that was created before the default
/// was "application". It is kept around to allow users to recreate files that
/// have been removed in old projects.
app, app,
/// The is the default type of project created. It is an application with
/// ephemeral .ios and .android directories that can be updated automatically.
application, application,
/// This is the old name for the [application] style project.
module, // TODO(gspencer): deprecated -- should be removed once IntelliJ no longer uses it. module, // TODO(gspencer): deprecated -- should be removed once IntelliJ no longer uses it.
/// This is a Flutter Dart package project. It doesn't have any native
/// components, only Dart.
package, package,
/// This is a native plugin project.
plugin, plugin,
} }
ProjectType _stringToProjectType(String value) { _ProjectType _stringToProjectType(String value) {
ProjectType result; _ProjectType result;
// TODO(gspencer): remove module when it is no longer used by IntelliJ plugin. // TODO(gspencer): remove module when it is no longer used by IntelliJ plugin.
// Module is just an alias for application. // Module is just an alias for application.
if (value == 'module') { if (value == 'module') {
value = 'application'; value = 'application';
} }
for (ProjectType type in ProjectType.values) { for (_ProjectType type in _ProjectType.values) {
if (value == getEnumName(type)) { if (value == getEnumName(type)) {
result = type; result = type;
break; break;
} }
} }
assert(result != null, 'Unsupported template type $value requested.');
return result; return result;
} }
...@@ -70,23 +78,23 @@ class CreateCommand extends FlutterCommand { ...@@ -70,23 +78,23 @@ class CreateCommand extends FlutterCommand {
argParser.addOption( argParser.addOption(
'template', 'template',
abbr: 't', abbr: 't',
allowed: ProjectType.values.map<String>((ProjectType type) => getEnumName(type)), allowed: _ProjectType.values.map<String>((_ProjectType type) => getEnumName(type)),
help: 'Specify the type of project to create.', help: 'Specify the type of project to create.',
valueHelp: 'type', valueHelp: 'type',
allowedHelp: <String, String>{ allowedHelp: <String, String>{
getEnumName(ProjectType.application): '(default) Generate a Flutter application.', getEnumName(_ProjectType.application): '(default) Generate a Flutter application.',
getEnumName(ProjectType.package): 'Generate a shareable Flutter project containing modular ' getEnumName(_ProjectType.package): 'Generate a shareable Flutter project containing modular '
'Dart code.', 'Dart code.',
getEnumName(ProjectType.plugin): 'Generate a shareable Flutter project containing an API ' getEnumName(_ProjectType.plugin): 'Generate a shareable Flutter project containing an API '
'in Dart code with a platform-specific implementation for Android, for iOS code, or ' 'in Dart code with a platform-specific implementation for Android, for iOS code, or '
'for both.', 'for both.',
}..addAll(verboseHelp }..addAll(verboseHelp
? <String, String>{ ? <String, String>{
getEnumName(ProjectType.app): 'Generate the legacy form of an application project. Use ' getEnumName(_ProjectType.app): 'Generate the legacy form of an application project. Use '
'"application" instead, unless you are working with an existing legacy app project. ' '"application" instead, unless you are working with an existing legacy app project. '
'This is not just an alias for the "application" template, it produces different ' 'This is not just an alias for the "application" template, it produces different '
'output.', 'output.',
getEnumName(ProjectType.module): 'Legacy, deprecated form of an application project. Use ' getEnumName(_ProjectType.module): 'Legacy, deprecated form of an application project. Use '
'"application" instead. This is just an alias for the "application" template, it ' '"application" instead. This is just an alias for the "application" template, it '
'produces the same output. It will be removed in a future release.', 'produces the same output. It will be removed in a future release.',
} }
...@@ -130,20 +138,23 @@ class CreateCommand extends FlutterCommand { ...@@ -130,20 +138,23 @@ class CreateCommand extends FlutterCommand {
// If it has a .metadata file with the project_type in it, use that. // If it has a .metadata file with the project_type in it, use that.
// If it has an android dir and an android/app dir, it's a legacy app // If it has an android dir and an android/app dir, it's a legacy app
// If it has an android dir and an android/src dir, it's a plugin // If it has an ios dir and an ios/Flutter dir, it's a legacy app
// If it has .ios and/or .android dirs, it's an application (nee module) // Otherwise, we don't presume to know what type of project it could be, since
// If it has an ios dir and an ios/Classes dir, it's a plugin // many of the files could be missing, and we can't really tell definitively.
// If it has no ios dir, no android dir, and no .ios or .android, then it's a package. _ProjectType _determineTemplateType(Directory projectDir) {
ProjectType _determineTemplateType(Directory projectDir) {
yaml.YamlMap loadMetadata(Directory projectDir) { yaml.YamlMap loadMetadata(Directory projectDir) {
if (!projectDir.existsSync()) if (!projectDir.existsSync())
return null; return null;
final File metadataFile =fs.file(fs.path.join(projectDir.absolute.path, '.metadata')); final File metadataFile = fs.file(fs.path.join(projectDir.absolute.path, '.metadata'));
if (!metadataFile.existsSync()) if (!metadataFile.existsSync())
return null; return null;
return yaml.loadYaml(metadataFile.readAsStringSync()); return yaml.loadYaml(metadataFile.readAsStringSync());
} }
bool exists(List<String> path) {
return fs.directory(fs.path.joinAll(<String>[projectDir.absolute.path] + path)).existsSync();
}
// If it exists, the project type in the metadata is definitive. // If it exists, the project type in the metadata is definitive.
final yaml.YamlMap metadata = loadMetadata(projectDir); final yaml.YamlMap metadata = loadMetadata(projectDir);
if (metadata != null && metadata['project_type'] != null) { if (metadata != null && metadata['project_type'] != null) {
...@@ -153,20 +164,13 @@ class CreateCommand extends FlutterCommand { ...@@ -153,20 +164,13 @@ class CreateCommand extends FlutterCommand {
// There either wasn't any metadata, or it didn't contain the project type, // There either wasn't any metadata, or it didn't contain the project type,
// so try and figure out what type of project it is from the existing // so try and figure out what type of project it is from the existing
// directory structure. // directory structure.
if (fs.directory(fs.path.join(projectDir.absolute.path, 'android', 'app')).existsSync()) if (exists(<String>['android', 'app'])
return ProjectType.app; || exists(<String>['ios', 'Runner'])
final bool dotPlatformDirExists = fs.directory(fs.path.join(projectDir.absolute.path, '.ios')).existsSync() || || exists(<String>['ios', 'Flutter'])) {
fs.directory(fs.path.join(projectDir.absolute.path, '.android')).existsSync(); return _ProjectType.app;
final bool platformDirExists = fs.directory(fs.path.join(projectDir.absolute.path, 'ios')).existsSync() || }
fs.directory(fs.path.join(projectDir.absolute.path, 'android')).existsSync(); // Since we can't really be definitive on nearly-empty directories, err on
if (dotPlatformDirExists) // the side of prudence and just say we don't know.
return ProjectType.application;
if (!platformDirExists && !dotPlatformDirExists)
return ProjectType.package;
if (platformDirExists &&
(fs.directory(fs.path.join(projectDir.absolute.path, 'ios', 'Classes')).existsSync() ||
fs.directory(fs.path.join(projectDir.absolute.path, 'android', 'src')).existsSync()))
return ProjectType.plugin;
return null; return null;
} }
...@@ -204,31 +208,36 @@ class CreateCommand extends FlutterCommand { ...@@ -204,31 +208,36 @@ class CreateCommand extends FlutterCommand {
throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2); throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
final Directory projectDir = fs.directory(argResults.rest.first); final Directory projectDir = fs.directory(argResults.rest.first);
final String dirPath = fs.path.normalize(projectDir.absolute.path); final String projectDirPath = fs.path.normalize(projectDir.absolute.path);
ProjectType detectedProjectType;
if (projectDir.existsSync()) {
detectedProjectType = _determineTemplateType(projectDir);
if (detectedProjectType == null) {
throwToolExit('Sorry, unable to detect the type of project to recreate. '
'Try creating a fresh project and migrating your existing code to '
'the new project manually.');
}
}
ProjectType template; _ProjectType template;
_ProjectType detectedProjectType;
final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
if (argResults['template'] != null) { if (argResults['template'] != null) {
template = _stringToProjectType(argResults['template']); template = _stringToProjectType(argResults['template']);
} else {
if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) {
detectedProjectType = _determineTemplateType(projectDir);
if (detectedProjectType == null && metadataExists) {
// We can only be definitive that this is the wrong type if the .metadata file
// exists and contains a type that we don't understand, or doesn't contain a type.
throwToolExit('Sorry, unable to detect the type of project to recreate. '
'Try creating a fresh project and migrating your existing code to '
'the new project manually.');
}
}
} }
template ??= detectedProjectType ?? ProjectType.application; template ??= detectedProjectType ?? _ProjectType.application;
if (detectedProjectType != null && template != detectedProjectType) { if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
// We can only be definitive that this is the wrong type if the .metadata file
// exists and contains a type that doesn't match.
throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the " throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the "
"existing template type of '${getEnumName(detectedProjectType)}'."); "existing template type of '${getEnumName(detectedProjectType)}'.");
} }
final bool generateApplication = template == ProjectType.application; final bool generateApplication = template == _ProjectType.application;
final bool generatePlugin = template == ProjectType.plugin; final bool generatePlugin = template == _ProjectType.plugin;
final bool generatePackage = template == ProjectType.package; final bool generatePackage = template == _ProjectType.package;
String organization = argResults['org']; String organization = argResults['org'];
if (!argResults.wasParsed('org')) { if (!argResults.wasParsed('org')) {
...@@ -243,9 +252,9 @@ class CreateCommand extends FlutterCommand { ...@@ -243,9 +252,9 @@ class CreateCommand extends FlutterCommand {
); );
} }
} }
final String projectName = fs.path.basename(dirPath); final String projectName = fs.path.basename(projectDirPath);
String error =_validateProjectDir(dirPath, flutterRoot: flutterRoot); String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot);
if (error != null) if (error != null)
throwToolExit(error); throwToolExit(error);
...@@ -264,40 +273,51 @@ class CreateCommand extends FlutterCommand { ...@@ -264,40 +273,51 @@ class CreateCommand extends FlutterCommand {
iosLanguage: argResults['ios-language'], iosLanguage: argResults['ios-language'],
); );
final String relativeDirPath = fs.path.relative(dirPath); final String relativeDirPath = fs.path.relative(projectDirPath);
printStatus('Creating project $relativeDirPath...'); if (!projectDir.existsSync()) {
final Directory directory = fs.directory(dirPath); printStatus('Creating project $relativeDirPath...');
} else {
printStatus('Recreating project $relativeDirPath...');
}
int generatedFileCount = 0; int generatedFileCount = 0;
switch (template) { switch (template) {
case ProjectType.app: case _ProjectType.app:
generatedFileCount += await _generateLegacyApp(directory, templateContext); generatedFileCount += await _generateLegacyApp(projectDir, templateContext);
break; break;
case ProjectType.module: case _ProjectType.module:
case ProjectType.application: case _ProjectType.application:
generatedFileCount += await _generateApplication(directory, templateContext); generatedFileCount += await _generateApplication(projectDir, templateContext);
break; break;
case ProjectType.package: case _ProjectType.package:
generatedFileCount += await _generatePackage(directory, templateContext); generatedFileCount += await _generatePackage(projectDir, templateContext);
break; break;
case ProjectType.plugin: case _ProjectType.plugin:
generatedFileCount += await _generatePlugin(directory, templateContext); generatedFileCount += await _generatePlugin(projectDir, templateContext);
break; break;
} }
printStatus('Wrote $generatedFileCount files.'); printStatus('Wrote $generatedFileCount files.');
printStatus('\nAll done!'); printStatus('\nAll done!');
if (generatePackage) { if (generatePackage) {
final String relativeMainPath = fs.path.normalize(fs.path.join(relativeDirPath, 'lib', '${templateContext['projectName']}.dart')); final String relativeMainPath = fs.path.normalize(fs.path.join(
relativeDirPath,
'lib',
'${templateContext['projectName']}.dart',
));
printStatus('Your package code is in $relativeMainPath'); printStatus('Your package code is in $relativeMainPath');
} else if (generateApplication) { } else if (generateApplication) {
final String relativeMainPath = fs.path.normalize(fs.path.join(relativeDirPath, 'lib', 'main.dart')); final String relativeMainPath = fs.path.normalize(fs.path.join(
relativeDirPath,
'lib',
'main.dart',
));
printStatus('Your application code is in $relativeMainPath.'); printStatus('Your application code is in $relativeMainPath.');
} else { } else {
// Run doctor; tell the user the next steps. // Run doctor; tell the user the next steps.
final FlutterProject project = await FlutterProject.fromPath(dirPath); final FlutterProject project = await FlutterProject.fromPath(projectDirPath);
final FlutterProject app = project.hasExampleApp ? project.example : project; final FlutterProject app = project.hasExampleApp ? project.example : project;
final String relativeAppPath = fs.path.normalize(fs.path.relative(app.directory.path)); final String relativeAppPath = fs.path.normalize(fs.path.relative(app.directory.path));
final String relativeAppMain = fs.path.join(relativeAppPath, 'lib', 'main.dart'); final String relativeAppMain = fs.path.join(relativeAppPath, 'lib', 'main.dart');
final String relativePluginPath = fs.path.normalize(fs.path.relative(dirPath)); final String relativePluginPath = fs.path.normalize(fs.path.relative(projectDirPath));
final String relativePluginMain = fs.path.join(relativePluginPath, 'lib', '$projectName.dart'); final String relativePluginMain = fs.path.join(relativePluginPath, 'lib', '$projectName.dart');
if (doctor.canLaunchAnything) { if (doctor.canLaunchAnything) {
// Let them know a summary of the state of their tooling. // Let them know a summary of the state of their tooling.
...@@ -360,8 +380,8 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit ...@@ -360,8 +380,8 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit
Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext) async { Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext) async {
int generatedCount = 0; int generatedCount = 0;
final String description = argResults.wasParsed('description') final String description = argResults.wasParsed('description')
? argResults['description'] ? argResults['description']
: 'A new flutter package project.'; : 'A new flutter package project.';
templateContext['description'] = description; templateContext['description'] = description;
generatedCount += _renderTemplate('package', directory, templateContext); generatedCount += _renderTemplate('package', directory, templateContext);
if (argResults['pub']) { if (argResults['pub']) {
......
...@@ -62,8 +62,156 @@ void main() { ...@@ -62,8 +62,156 @@ void main() {
return _runFlutterTest(projectDir); return _runFlutterTest(projectDir);
}, timeout: allowForRemotePubInvocation); }, timeout: allowForRemotePubInvocation);
testUsingContext('can create a default project if empty directory exists', () async {
await projectDir.create(recursive: true);
return _createAndAnalyzeProject(projectDir, <String>[], <String>[
'.android/app/',
'.gitignore',
'.ios/Flutter',
'.metadata',
'lib/main.dart',
'pubspec.yaml',
'README.md',
'test/widget_test.dart',
], unexpectedPaths: <String>[
'android/',
'ios/',
]);
}, timeout: allowForRemotePubInvocation);
testUsingContext('creates a legacy app project correctly', () async {
await _createAndAnalyzeProject(
projectDir,
<String>['--template=app'],
<String>[
'android/app/src/main/java/com/example/flutterproject/MainActivity.java',
'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
'flutter_project.iml',
'ios/Flutter/AppFrameworkInfo.plist',
'ios/Runner/AppDelegate.m',
'ios/Runner/GeneratedPluginRegistrant.h',
],
);
return _runFlutterTest(projectDir);
}, timeout: allowForRemotePubInvocation);
testUsingContext('cannot create a project if non-empty non-project directory exists with .metadata', () async {
await projectDir.absolute.childDirectory('blag').create(recursive: true);
await projectDir.absolute.childFile('.metadata').writeAsString('project_type: blag\n');
expect(() async => await _createAndAnalyzeProject(projectDir, <String>[], <String>[
], unexpectedPaths: <String>[
'android/',
'ios/',
'.android/',
'.ios/',
]), throwsToolExit(message: 'Sorry, unable to detect the type of project to recreate'));
}, timeout: allowForRemotePubInvocation);
testUsingContext('Will create an application project if non-empty non-project directory exists without .metadata', () async {
await projectDir.absolute.childDirectory('blag').create(recursive: true);
await projectDir.absolute.childDirectory('.idea').create(recursive: true);
return _createAndAnalyzeProject(projectDir, <String>[], <String>[
'.android/app/',
'.gitignore',
'.ios/Flutter',
'.metadata',
'lib/main.dart',
'pubspec.yaml',
'README.md',
'test/widget_test.dart',
], unexpectedPaths: <String>[
'android/',
'ios/',
]);
}, timeout: allowForRemotePubInvocation);
testUsingContext('detects and recreates an application project correctly', () async {
await projectDir.absolute.childDirectory('lib').create(recursive: true);
await projectDir.absolute.childDirectory('.ios').create(recursive: true);
return _createAndAnalyzeProject(projectDir, <String>[], <String>[
'.android/app/',
'.gitignore',
'.ios/Flutter',
'.metadata',
'lib/main.dart',
'pubspec.yaml',
'README.md',
'test/widget_test.dart',
], unexpectedPaths: <String>[
'android/',
'ios/',
]);
}, timeout: allowForRemotePubInvocation);
testUsingContext('detects and recreates a plugin project correctly', () async {
await projectDir.create(recursive: true);
await projectDir.absolute.childFile('.metadata').writeAsString('project_type: plugin\n');
return _createAndAnalyzeProject(
projectDir,
<String>[],
<String>[
'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java',
'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java',
'example/ios/Runner/AppDelegate.h',
'example/ios/Runner/AppDelegate.m',
'example/ios/Runner/main.m',
'example/lib/main.dart',
'flutter_project.iml',
'ios/Classes/FlutterProjectPlugin.h',
'ios/Classes/FlutterProjectPlugin.m',
'lib/flutter_project.dart',
],
);
}, timeout: allowForRemotePubInvocation);
testUsingContext('detects and recreates a package project correctly', () async {
await projectDir.create(recursive: true);
await projectDir.absolute.childFile('.metadata').writeAsString('project_type: package\n');
return _createAndAnalyzeProject(
projectDir,
<String>[],
<String>[
'lib/flutter_project.dart',
'test/flutter_project_test.dart',
],
unexpectedPaths: <String>[
'android/app/src/main/java/com/example/flutterproject/MainActivity.java',
'android/src/main/java/com/example/flutterproject/FlutterProjectPlugin.java',
'example/android/app/src/main/java/com/example/flutterprojectexample/MainActivity.java',
'example/ios/Runner/AppDelegate.h',
'example/ios/Runner/AppDelegate.m',
'example/ios/Runner/main.m',
'example/lib/main.dart',
'ios/Classes/FlutterProjectPlugin.h',
'ios/Classes/FlutterProjectPlugin.m',
'ios/Runner/AppDelegate.h',
'ios/Runner/AppDelegate.m',
'ios/Runner/main.m',
'lib/main.dart',
'test/widget_test.dart',
],
);
}, timeout: allowForRemotePubInvocation);
testUsingContext('detects and recreates a legacy app project correctly', () async {
await projectDir.absolute.childDirectory('lib').create(recursive: true);
await projectDir.absolute.childDirectory('ios').childDirectory('Flutter').create(recursive: true);
return _createAndAnalyzeProject(
projectDir,
<String>[],
<String>[
'android/app/src/main/java/com/example/flutterproject/MainActivity.java',
'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
'flutter_project.iml',
'ios/Flutter/AppFrameworkInfo.plist',
'ios/Runner/AppDelegate.m',
'ios/Runner/GeneratedPluginRegistrant.h',
],
);
}, timeout: allowForRemotePubInvocation);
testUsingContext('can create a legacy module project', () async { testUsingContext('can create a legacy module project', () async {
await _createAndAnalyzeProject(projectDir, <String>[ return _createAndAnalyzeProject(projectDir, <String>[
'--template=module', '--template=module',
], <String>[ ], <String>[
'.android/app/', '.android/app/',
...@@ -78,7 +226,6 @@ void main() { ...@@ -78,7 +226,6 @@ void main() {
'android/', 'android/',
'ios/', 'ios/',
]); ]);
return _runFlutterTest(projectDir);
}, timeout: allowForRemotePubInvocation); }, timeout: allowForRemotePubInvocation);
testUsingContext('kotlin/swift legacy app project', () async { testUsingContext('kotlin/swift legacy app project', () async {
...@@ -100,7 +247,7 @@ void main() { ...@@ -100,7 +247,7 @@ void main() {
); );
}, timeout: allowForCreateFlutterProject); }, timeout: allowForCreateFlutterProject);
testUsingContext('package project', () async { testUsingContext('can create a package project', () async {
await _createAndAnalyzeProject( await _createAndAnalyzeProject(
projectDir, projectDir,
<String>['--template=package'], <String>['--template=package'],
...@@ -128,7 +275,7 @@ void main() { ...@@ -128,7 +275,7 @@ void main() {
return _runFlutterTest(projectDir); return _runFlutterTest(projectDir);
}, timeout: allowForRemotePubInvocation); }, timeout: allowForRemotePubInvocation);
testUsingContext('plugin project', () async { testUsingContext('can create a plugin project', () async {
await _createAndAnalyzeProject( await _createAndAnalyzeProject(
projectDir, projectDir,
<String>['--template=plugin'], <String>['--template=plugin'],
......
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