Include metadata in GitHub crash template (#53118)
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 81e117a..2c73de4 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -5,7 +5,6 @@
import 'dart:async';
import 'package:meta/meta.dart';
-import 'package:yaml/yaml.dart' as yaml;
import '../android/android.dart' as android;
import '../android/android_sdk.dart' as android_sdk;
@@ -21,38 +20,13 @@
import '../dart/pub.dart';
import '../doctor.dart';
import '../features.dart';
+import '../flutter_project_metadata.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../reporting/reporting.dart';
import '../runner/flutter_command.dart';
import '../template.dart';
-enum _ProjectType {
- /// This is the default project with the user-managed host code.
- /// It is different than the "module" template in that it exposes and doesn't
- /// manage the platform code.
- app,
- /// The is a project that has managed platform host code. It is an application with
- /// ephemeral .ios and .android directories that can be updated automatically.
- module,
- /// This is a Flutter Dart package project. It doesn't have any native
- /// components, only Dart.
- package,
- /// This is a native plugin project.
- plugin,
-}
-
-_ProjectType _stringToProjectType(String value) {
- _ProjectType result;
- for (final _ProjectType type in _ProjectType.values) {
- if (value == getEnumName(type)) {
- result = type;
- break;
- }
- }
- return result;
-}
-
class CreateCommand extends FlutterCommand {
CreateCommand() {
argParser.addFlag('pub',
@@ -74,17 +48,17 @@
argParser.addOption(
'template',
abbr: 't',
- allowed: _ProjectType.values.map<String>((_ProjectType type) => getEnumName(type)),
+ allowed: FlutterProjectType.values.map<String>((FlutterProjectType type) => type.name),
help: 'Specify the type of project to create.',
valueHelp: 'type',
allowedHelp: <String, String>{
- getEnumName(_ProjectType.app): '(default) Generate a Flutter application.',
- getEnumName(_ProjectType.package): 'Generate a shareable Flutter project containing modular '
+ FlutterProjectType.app.name: '(default) Generate a Flutter application.',
+ FlutterProjectType.package.name: 'Generate a shareable Flutter project containing modular '
'Dart code.',
- getEnumName(_ProjectType.plugin): 'Generate a shareable Flutter project containing an API '
+ FlutterProjectType.plugin.name: 'Generate a shareable Flutter project containing an API '
'in Dart code with a platform-specific implementation for Android, for iOS code, or '
'for both.',
- getEnumName(_ProjectType.module): 'Generate a project to add a Flutter module to an '
+ FlutterProjectType.module.name: 'Generate a project to add a Flutter module to an '
'existing Android or iOS application.',
},
defaultsTo: null,
@@ -180,47 +154,24 @@
// If it has an ios dir and an ios/Flutter dir, it's a legacy app
// Otherwise, we don't presume to know what type of project it could be, since
// many of the files could be missing, and we can't really tell definitively.
- _ProjectType _determineTemplateType(Directory projectDir) {
- yaml.YamlMap loadMetadata(Directory projectDir) {
- if (!projectDir.existsSync()) {
- return null;
- }
- final File metadataFile = globals.fs.file(globals.fs.path.join(projectDir.absolute.path, '.metadata'));
- if (!metadataFile.existsSync()) {
- return null;
- }
- final dynamic metadataYaml = yaml.loadYaml(metadataFile.readAsStringSync());
- if (metadataYaml is yaml.YamlMap) {
- return metadataYaml;
- } else {
- throwToolExit('pubspec.yaml is malformed.');
- return null;
- }
+ FlutterProjectType _determineTemplateType(Directory projectDir) {
+ final File metadataFile = globals.fs.file(globals.fs.path.join(projectDir.absolute.path, '.metadata'));
+ final FlutterProjectMetadata projectMetadata = FlutterProjectMetadata(metadataFile, globals.logger);
+ if (projectMetadata.projectType != null) {
+ return projectMetadata.projectType;
}
bool exists(List<String> path) {
return globals.fs.directory(globals.fs.path.joinAll(<String>[projectDir.absolute.path, ...path])).existsSync();
}
- // If it exists, the project type in the metadata is definitive.
- final yaml.YamlMap metadata = loadMetadata(projectDir);
- if (metadata != null && metadata['project_type'] != null) {
- final dynamic projectType = metadata['project_type'];
- if (projectType is String) {
- return _stringToProjectType(projectType);
- } else {
- throwToolExit('.metadata is malformed.');
- return null;
- }
- }
-
// 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
// directory structure.
if (exists(<String>['android', 'app'])
|| exists(<String>['ios', 'Runner'])
|| exists(<String>['ios', 'Flutter'])) {
- return _ProjectType.app;
+ return FlutterProjectType.app;
}
// Since we can't really be definitive on nearly-empty directories, err on
// the side of prudence and just say we don't know.
@@ -277,12 +228,12 @@
}
}
- _ProjectType _getProjectType(Directory projectDir) {
- _ProjectType template;
- _ProjectType detectedProjectType;
+ FlutterProjectType _getProjectType(Directory projectDir) {
+ FlutterProjectType template;
+ FlutterProjectType detectedProjectType;
final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
if (argResults['template'] != null) {
- template = _stringToProjectType(stringArg('template'));
+ template = stringToProjectType(stringArg('template'));
} else {
// If the project directory exists and isn't empty, then try to determine the template
// type from the project directory.
@@ -297,12 +248,12 @@
}
}
}
- template ??= detectedProjectType ?? _ProjectType.app;
+ template ??= detectedProjectType ?? FlutterProjectType.app;
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 "
- "existing template type of '${getEnumName(detectedProjectType)}'.");
+ throwToolExit("The requested template type '${template.name}' doesn't match the "
+ "existing template type of '${detectedProjectType.name}'.");
}
return template;
}
@@ -356,18 +307,18 @@
String sampleCode;
if (argResults['sample'] != null) {
if (argResults['template'] != null &&
- _stringToProjectType(stringArg('template') ?? 'app') != _ProjectType.app) {
+ stringToProjectType(stringArg('template') ?? 'app') != FlutterProjectType.app) {
throwToolExit('Cannot specify --sample with a project type other than '
- '"${getEnumName(_ProjectType.app)}"');
+ '"${FlutterProjectType.app.name}"');
}
// Fetch the sample from the server.
sampleCode = await _fetchSampleFromServer(stringArg('sample'));
}
- final _ProjectType template = _getProjectType(projectDir);
- final bool generateModule = template == _ProjectType.module;
- final bool generatePlugin = template == _ProjectType.plugin;
- final bool generatePackage = template == _ProjectType.package;
+ final FlutterProjectType template = _getProjectType(projectDir);
+ final bool generateModule = template == FlutterProjectType.module;
+ final bool generatePlugin = template == FlutterProjectType.plugin;
+ final bool generatePackage = template == FlutterProjectType.package;
String organization = stringArg('org');
if (!argResults.wasParsed('org')) {
@@ -424,16 +375,16 @@
final Directory relativeDir = globals.fs.directory(projectDirPath);
int generatedFileCount = 0;
switch (template) {
- case _ProjectType.app:
+ case FlutterProjectType.app:
generatedFileCount += await _generateApp(relativeDir, templateContext, overwrite: overwrite);
break;
- case _ProjectType.module:
+ case FlutterProjectType.module:
generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: overwrite);
break;
- case _ProjectType.package:
+ case FlutterProjectType.package:
generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: overwrite);
break;
- case _ProjectType.plugin:
+ case FlutterProjectType.plugin:
generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: overwrite);
break;
}
diff --git a/packages/flutter_tools/lib/src/flutter_project_metadata.dart b/packages/flutter_tools/lib/src/flutter_project_metadata.dart
new file mode 100644
index 0000000..a6d9414
--- /dev/null
+++ b/packages/flutter_tools/lib/src/flutter_project_metadata.dart
@@ -0,0 +1,99 @@
+// Copyright 2014 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.
+
+import 'package:yaml/yaml.dart';
+
+import 'base/file_system.dart';
+import 'base/logger.dart';
+import 'base/utils.dart';
+
+enum FlutterProjectType {
+ /// This is the default project with the user-managed host code.
+ /// It is different than the "module" template in that it exposes and doesn't
+ /// manage the platform code.
+ app,
+ /// The is a project that has managed platform host code. It is an application with
+ /// ephemeral .ios and .android directories that can be updated automatically.
+ module,
+ /// This is a Flutter Dart package project. It doesn't have any native
+ /// components, only Dart.
+ package,
+ /// This is a native plugin project.
+ plugin,
+}
+
+extension FlutterProjectTypeExtension on FlutterProjectType {
+ String get name => getEnumName(this);
+}
+
+FlutterProjectType stringToProjectType(String value) {
+ FlutterProjectType result;
+ for (final FlutterProjectType type in FlutterProjectType.values) {
+ if (value == type.name) {
+ result = type;
+ break;
+ }
+ }
+ return result;
+}
+
+/// A wrapper around the `.metadata` file.
+class FlutterProjectMetadata {
+ FlutterProjectMetadata(
+ File metadataFile,
+ Logger logger,
+ ) : _metadataFile = metadataFile,
+ _logger = logger;
+
+ final File _metadataFile;
+ final Logger _logger;
+
+ String get versionChannel => _versionValue('channel');
+ String get versionRevision => _versionValue('revision');
+
+ FlutterProjectType get projectType {
+ final dynamic projectTypeYaml = _metadataValue('project_type');
+ if (projectTypeYaml is String) {
+ return stringToProjectType(projectTypeYaml);
+ } else {
+ _logger.printTrace('.metadata project_type version is malformed.');
+ return null;
+ }
+ }
+
+ YamlMap _versionYaml;
+ String _versionValue(String key) {
+ if (_versionYaml == null) {
+ final dynamic versionYaml = _metadataValue('version');
+ if (versionYaml is YamlMap) {
+ _versionYaml = versionYaml;
+ } else {
+ _logger.printTrace('.metadata version is malformed.');
+ return null;
+ }
+ }
+ if (_versionYaml != null && _versionYaml.containsKey(key) && _versionYaml[key] is String) {
+ return _versionYaml[key] as String;
+ }
+ return null;
+ }
+
+ YamlMap _metadataYaml;
+ dynamic _metadataValue(String key) {
+ if (_metadataYaml == null) {
+ if (!_metadataFile.existsSync()) {
+ return null;
+ }
+ final dynamic metadataYaml = loadYaml(_metadataFile.readAsStringSync());
+ if (metadataYaml is YamlMap) {
+ _metadataYaml = metadataYaml;
+ } else {
+ _logger.printTrace('.metadata is malformed.');
+ return null;
+ }
+ }
+
+ return _metadataYaml[key];
+ }
+}
diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart
index 4d77793..69c8311 100644
--- a/packages/flutter_tools/lib/src/globals.dart
+++ b/packages/flutter_tools/lib/src/globals.dart
@@ -30,6 +30,7 @@
import 'macos/cocoapods.dart';
import 'macos/xcode.dart';
import 'persistent_tool_state.dart';
+import 'project.dart';
import 'reporting/reporting.dart';
import 'version.dart';
import 'web/chrome.dart';
@@ -42,6 +43,7 @@
OperatingSystemUtils get os => context.get<OperatingSystemUtils>();
PersistentToolState get persistentToolState => PersistentToolState.instance;
Usage get flutterUsage => context.get<Usage>();
+FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? FlutterProjectFactory();
const FileSystem _kLocalFs = LocalFileSystem();
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 2926a18..4efd7c5 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -11,7 +11,6 @@
import 'android/gradle_utils.dart' as gradle;
import 'artifacts.dart';
import 'base/common.dart';
-import 'base/context.dart';
import 'base/file_system.dart';
import 'build_info.dart';
import 'bundle.dart' as bundle;
@@ -24,8 +23,6 @@
import 'plugins.dart';
import 'template.dart';
-FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? FlutterProjectFactory();
-
class FlutterProjectFactory {
FlutterProjectFactory();
@@ -69,7 +66,7 @@
/// Returns a [FlutterProject] view of the given directory or a ToolExit error,
/// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
- static FlutterProject fromDirectory(Directory directory) => projectFactory.fromDirectory(directory);
+ static FlutterProject fromDirectory(Directory directory) => globals.projectFactory.fromDirectory(directory);
/// Returns a [FlutterProject] view of the current directory or a ToolExit error,
/// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
@@ -145,6 +142,9 @@
/// The `.packages` file of this project.
File get packagesFile => directory.childFile('.packages');
+ /// The `.metadata` file of this project.
+ File get metadataFile => directory.childFile('.metadata');
+
/// The `.flutter-plugins` file of this project.
File get flutterPluginsFile => directory.childFile('.flutter-plugins');
diff --git a/packages/flutter_tools/lib/src/reporting/github_template.dart b/packages/flutter_tools/lib/src/reporting/github_template.dart
index 5d958b7..76a1bd3 100644
--- a/packages/flutter_tools/lib/src/reporting/github_template.dart
+++ b/packages/flutter_tools/lib/src/reporting/github_template.dart
@@ -5,23 +5,31 @@
import 'dart:async';
import 'package:file/file.dart';
+import 'package:meta/meta.dart';
-import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
-import '../base/net.dart';
+import '../base/logger.dart';
import '../convert.dart';
import '../flutter_manifest.dart';
-import '../globals.dart' as globals;
+import '../flutter_project_metadata.dart';
import '../project.dart';
/// Provide suggested GitHub issue templates to user when Flutter encounters an error.
class GitHubTemplateCreator {
- GitHubTemplateCreator() :
- _client = (context.get<HttpClientFactory>() == null)
- ? HttpClient()
- : context.get<HttpClientFactory>()();
+ GitHubTemplateCreator({
+ @required FileSystem fileSystem,
+ @required Logger logger,
+ @required FlutterProjectFactory flutterProjectFactory,
+ @required HttpClient client,
+ }) : _fileSystem = fileSystem,
+ _logger = logger,
+ _flutterProjectFactory = flutterProjectFactory,
+ _client = client;
+ final FileSystem _fileSystem;
+ final Logger _logger;
+ final FlutterProjectFactory _flutterProjectFactory;
final HttpClient _client;
Future<String> toolCrashSimilarIssuesGitHubURL(String errorString) async {
@@ -76,7 +84,7 @@
String _projectMetadataInformation() {
FlutterProject project;
try {
- project = FlutterProject.current();
+ project = _flutterProjectFactory.fromDirectory(_fileSystem.currentDirectory);
} on Exception catch (exception) {
// pubspec may be malformed.
return exception.toString();
@@ -86,14 +94,18 @@
if (project == null || manifest == null || manifest.isEmpty) {
return 'No pubspec in working directory.';
}
+ final FlutterProjectMetadata metadata = FlutterProjectMetadata(project.metadataFile, _logger);
final StringBuffer description = StringBuffer()
+ ..writeln('**Type**: ${metadata.projectType?.name}')
..writeln('**Version**: ${manifest.appVersion}')
..writeln('**Material**: ${manifest.usesMaterialDesign}')
..writeln('**Android X**: ${manifest.usesAndroidX}')
..writeln('**Module**: ${manifest.isModule}')
..writeln('**Plugin**: ${manifest.isPlugin}')
..writeln('**Android package**: ${manifest.androidPackage}')
- ..writeln('**iOS bundle identifier**: ${manifest.iosBundleIdentifier}');
+ ..writeln('**iOS bundle identifier**: ${manifest.iosBundleIdentifier}')
+ ..writeln('**Creation channel**: ${metadata.versionChannel}')
+ ..writeln('**Creation framework version**: ${metadata.versionRevision}');
final File file = project.flutterPluginsFile;
if (file.existsSync()) {
@@ -107,7 +119,7 @@
}
// Write the last part of the path, which includes the plugin name and version.
// Example: camera-0.5.7+2
- final List<String> pathParts = globals.fs.path.split(pluginParts[1]);
+ final List<String> pathParts = _fileSystem.path.split(pluginParts[1]);
description.writeln(pathParts.isEmpty ? pluginParts.first : pathParts.last);
}
}
@@ -124,7 +136,7 @@
Future<String> _shortURL(String fullURL) async {
String url;
try {
- globals.printTrace('Attempting git.io shortener: $fullURL');
+ _logger.printTrace('Attempting git.io shortener: $fullURL');
final List<int> bodyBytes = utf8.encode('url=${Uri.encodeQueryComponent(fullURL)}');
final HttpClientRequest request = await _client.postUrl(Uri.parse('https://git.io'));
request.headers.set(HttpHeaders.contentLengthHeader, bodyBytes.length.toString());
@@ -134,10 +146,10 @@
if (response.statusCode == 201) {
url = response.headers[HttpHeaders.locationHeader]?.first;
} else {
- globals.printTrace('Failed to shorten GitHub template URL. Server responded with HTTP status code ${response.statusCode}');
+ _logger.printTrace('Failed to shorten GitHub template URL. Server responded with HTTP status code ${response.statusCode}');
}
} on Exception catch (sendError) {
- globals.printTrace('Failed to shorten GitHub template URL: $sendError');
+ _logger.printTrace('Failed to shorten GitHub template URL: $sendError');
}
return url ?? fullURL;