|  | // Copyright 2017 The Chromium 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 'dart:async'; | 
|  |  | 
|  | import 'package:meta/meta.dart'; | 
|  | import 'package:pub_semver/pub_semver.dart'; | 
|  | import 'package:yaml/yaml.dart'; | 
|  |  | 
|  | import 'base/file_system.dart'; | 
|  | import 'base/user_messages.dart'; | 
|  | import 'base/utils.dart'; | 
|  | import 'cache.dart'; | 
|  | import 'globals.dart'; | 
|  |  | 
|  | /// A wrapper around the `flutter` section in the `pubspec.yaml` file. | 
|  | class FlutterManifest { | 
|  | FlutterManifest._(); | 
|  |  | 
|  | /// Returns an empty manifest. | 
|  | static FlutterManifest empty() { | 
|  | final FlutterManifest manifest = FlutterManifest._(); | 
|  | manifest._descriptor = const <String, dynamic>{}; | 
|  | manifest._flutterDescriptor = const <String, dynamic>{}; | 
|  | return manifest; | 
|  | } | 
|  |  | 
|  | /// Returns null on invalid manifest. Returns empty manifest on missing file. | 
|  | static Future<FlutterManifest> createFromPath(String path) async { | 
|  | if (path == null || !fs.isFileSync(path)) | 
|  | return _createFromYaml(null); | 
|  | final String manifest = await fs.file(path).readAsString(); | 
|  | return createFromString(manifest); | 
|  | } | 
|  |  | 
|  | /// Returns null on missing or invalid manifest | 
|  | @visibleForTesting | 
|  | static Future<FlutterManifest> createFromString(String manifest) async { | 
|  | return _createFromYaml(loadYaml(manifest)); | 
|  | } | 
|  |  | 
|  | static Future<FlutterManifest> _createFromYaml(dynamic yamlDocument) async { | 
|  | final FlutterManifest pubspec = FlutterManifest._(); | 
|  | if (yamlDocument != null && !await _validate(yamlDocument)) | 
|  | return null; | 
|  |  | 
|  | final Map<dynamic, dynamic> yamlMap = yamlDocument; | 
|  | if (yamlMap != null) { | 
|  | pubspec._descriptor = yamlMap.cast<String, dynamic>(); | 
|  | } else { | 
|  | pubspec._descriptor = <String, dynamic>{}; | 
|  | } | 
|  |  | 
|  | final Map<dynamic, dynamic> flutterMap = pubspec._descriptor['flutter']; | 
|  | if (flutterMap != null) { | 
|  | pubspec._flutterDescriptor = flutterMap.cast<String, dynamic>(); | 
|  | } else { | 
|  | pubspec._flutterDescriptor = <String, dynamic>{}; | 
|  | } | 
|  |  | 
|  | return pubspec; | 
|  | } | 
|  |  | 
|  | /// A map representation of the entire `pubspec.yaml` file. | 
|  | Map<String, dynamic> _descriptor; | 
|  |  | 
|  | /// A map representation of the `flutter` section in the `pubspec.yaml` file. | 
|  | Map<String, dynamic> _flutterDescriptor; | 
|  |  | 
|  | /// True if the `pubspec.yaml` file does not exist. | 
|  | bool get isEmpty => _descriptor.isEmpty; | 
|  |  | 
|  | /// The string value of the top-level `name` property in the `pubspec.yaml` file. | 
|  | String get appName => _descriptor['name'] ?? ''; | 
|  |  | 
|  | // Flag to avoid printing multiple invalid version messages. | 
|  | bool _hasShowInvalidVersionMsg = false; | 
|  |  | 
|  | /// The version String from the `pubspec.yaml` file. | 
|  | /// Can be null if it isn't set or has a wrong format. | 
|  | String get appVersion { | 
|  | final String verStr = _descriptor['version']?.toString(); | 
|  | if (verStr == null) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | Version version; | 
|  | try { | 
|  | version = Version.parse(verStr); | 
|  | } on Exception { | 
|  | if (!_hasShowInvalidVersionMsg) { | 
|  | printStatus(userMessages.invalidVersionSettingHintMessage(verStr), emphasis: true); | 
|  | _hasShowInvalidVersionMsg = true; | 
|  | } | 
|  | } | 
|  | return version?.toString(); | 
|  | } | 
|  |  | 
|  | /// The build version name from the `pubspec.yaml` file. | 
|  | /// Can be null if version isn't set or has a wrong format. | 
|  | String get buildName { | 
|  | if (appVersion != null && appVersion.contains('+')) | 
|  | return appVersion.split('+')?.elementAt(0); | 
|  | else | 
|  | return appVersion; | 
|  | } | 
|  |  | 
|  | /// The build version number from the `pubspec.yaml` file. | 
|  | /// Can be null if version isn't set or has a wrong format. | 
|  | String get buildNumber { | 
|  | if (appVersion != null && appVersion.contains('+')) { | 
|  | final String value = appVersion.split('+')?.elementAt(1); | 
|  | return value; | 
|  | } else { | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | bool get usesMaterialDesign { | 
|  | return _flutterDescriptor['uses-material-design'] ?? false; | 
|  | } | 
|  |  | 
|  | /// True if this manifest declares a Flutter module project. | 
|  | /// | 
|  | /// A Flutter project is considered a module when it has a `module:` | 
|  | /// descriptor. A Flutter module project supports integration into an | 
|  | /// existing host app, and has managed platform host code. | 
|  | /// | 
|  | /// Such a project can be created using `flutter create -t module`. | 
|  | bool get isModule => _flutterDescriptor.containsKey('module'); | 
|  |  | 
|  | /// True if this manifest declares a Flutter plugin project. | 
|  | /// | 
|  | /// A Flutter project is considered a plugin when it has a `plugin:` | 
|  | /// descriptor. A Flutter plugin project wraps custom Android and/or | 
|  | /// iOS code in a Dart interface for consumption by other Flutter app | 
|  | /// projects. | 
|  | /// | 
|  | /// Such a project can be created using `flutter create -t plugin`. | 
|  | bool get isPlugin => _flutterDescriptor.containsKey('plugin'); | 
|  |  | 
|  | /// Returns the Android package declared by this manifest in its | 
|  | /// module or plugin descriptor. Returns null, if there is no | 
|  | /// such declaration. | 
|  | String get androidPackage { | 
|  | if (isModule) | 
|  | return _flutterDescriptor['module']['androidPackage']; | 
|  | if (isPlugin) | 
|  | return _flutterDescriptor['plugin']['androidPackage']; | 
|  | return null; | 
|  | } | 
|  |  | 
|  | /// Returns the iOS bundle identifier declared by this manifest in its | 
|  | /// module descriptor. Returns null if there is no such declaration. | 
|  | String get iosBundleIdentifier { | 
|  | if (isModule) | 
|  | return _flutterDescriptor['module']['iosBundleIdentifier']; | 
|  | return null; | 
|  | } | 
|  |  | 
|  | List<Map<String, dynamic>> get fontsDescriptor { | 
|  | return fonts.map((Font font) => font.descriptor).toList(); | 
|  | } | 
|  |  | 
|  | List<Map<String, dynamic>> get _rawFontsDescriptor { | 
|  | final List<dynamic> fontList = _flutterDescriptor['fonts']; | 
|  | return fontList == null | 
|  | ? const <Map<String, dynamic>>[] | 
|  | : fontList.map<Map<String, dynamic>>(castStringKeyedMap).toList(); | 
|  | } | 
|  |  | 
|  | List<Uri> get assets { | 
|  | final List<dynamic> assets = _flutterDescriptor['assets']; | 
|  | if (assets == null) { | 
|  | return const <Uri>[]; | 
|  | } | 
|  | return assets | 
|  | .cast<String>() | 
|  | .map<String>(Uri.encodeFull) | 
|  | ?.map<Uri>(Uri.parse) | 
|  | ?.toList(); | 
|  | } | 
|  |  | 
|  | List<Font> _fonts; | 
|  |  | 
|  | List<Font> get fonts { | 
|  | _fonts ??= _extractFonts(); | 
|  | return _fonts; | 
|  | } | 
|  |  | 
|  | List<Font> _extractFonts() { | 
|  | if (!_flutterDescriptor.containsKey('fonts')) | 
|  | return <Font>[]; | 
|  |  | 
|  | final List<Font> fonts = <Font>[]; | 
|  | for (Map<String, dynamic> fontFamily in _rawFontsDescriptor) { | 
|  | final List<dynamic> fontFiles = fontFamily['fonts']; | 
|  | final String familyName = fontFamily['family']; | 
|  | if (familyName == null) { | 
|  | printError('Warning: Missing family name for font.', emphasis: true); | 
|  | continue; | 
|  | } | 
|  | if (fontFiles == null) { | 
|  | printError('Warning: No fonts specified for font $familyName', emphasis: true); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | final List<FontAsset> fontAssets = <FontAsset>[]; | 
|  | for (Map<dynamic, dynamic> fontFile in fontFiles) { | 
|  | final String asset = fontFile['asset']; | 
|  | if (asset == null) { | 
|  | printError('Warning: Missing asset in fonts for $familyName', emphasis: true); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | fontAssets.add(FontAsset( | 
|  | Uri.parse(asset), | 
|  | weight: fontFile['weight'], | 
|  | style: fontFile['style'], | 
|  | )); | 
|  | } | 
|  | if (fontAssets.isNotEmpty) | 
|  | fonts.add(Font(fontFamily['family'], fontAssets)); | 
|  | } | 
|  | return fonts; | 
|  | } | 
|  | } | 
|  |  | 
|  | class Font { | 
|  | Font(this.familyName, this.fontAssets) | 
|  | : assert(familyName != null), | 
|  | assert(fontAssets != null), | 
|  | assert(fontAssets.isNotEmpty); | 
|  |  | 
|  | final String familyName; | 
|  | final List<FontAsset> fontAssets; | 
|  |  | 
|  | Map<String, dynamic> get descriptor { | 
|  | return <String, dynamic>{ | 
|  | 'family': familyName, | 
|  | 'fonts': fontAssets.map<Map<String, dynamic>>((FontAsset a) => a.descriptor).toList(), | 
|  | }; | 
|  | } | 
|  |  | 
|  | @override | 
|  | String toString() => '$runtimeType(family: $familyName, assets: $fontAssets)'; | 
|  | } | 
|  |  | 
|  | class FontAsset { | 
|  | FontAsset(this.assetUri, {this.weight, this.style}) | 
|  | : assert(assetUri != null); | 
|  |  | 
|  | final Uri assetUri; | 
|  | final int weight; | 
|  | final String style; | 
|  |  | 
|  | Map<String, dynamic> get descriptor { | 
|  | final Map<String, dynamic> descriptor = <String, dynamic>{}; | 
|  | if (weight != null) | 
|  | descriptor['weight'] = weight; | 
|  |  | 
|  | if (style != null) | 
|  | descriptor['style'] = style; | 
|  |  | 
|  | descriptor['asset'] = assetUri.path; | 
|  | return descriptor; | 
|  | } | 
|  |  | 
|  | @override | 
|  | String toString() => '$runtimeType(asset: ${assetUri.path}, weight; $weight, style: $style)'; | 
|  | } | 
|  |  | 
|  | @visibleForTesting | 
|  | String buildSchemaDir(FileSystem fs) { | 
|  | return fs.path.join( | 
|  | fs.path.absolute(Cache.flutterRoot), 'packages', 'flutter_tools', 'schema', | 
|  | ); | 
|  | } | 
|  |  | 
|  | @visibleForTesting | 
|  | String buildSchemaPath(FileSystem fs) { | 
|  | return fs.path.join( | 
|  | buildSchemaDir(fs), | 
|  | 'pubspec_yaml.json', | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// This method should be kept in sync with the schema in | 
|  | /// `$FLUTTER_ROOT/packages/flutter_tools/schema/pubspec_yaml.json`, | 
|  | /// but avoid introducing depdendencies on packages for simple validation. | 
|  | Future<bool> _validate(YamlMap manifest) async { | 
|  | final List<String> errors = <String>[]; | 
|  | for (final MapEntry<dynamic, dynamic> kvp in manifest.entries) { | 
|  | if (kvp.key is! String) { | 
|  | errors.add('Expected YAML key to be a a string, but got ${kvp.key}.'); | 
|  | continue; | 
|  | } | 
|  | switch (kvp.key) { | 
|  | case 'name': | 
|  | if (kvp.value is! String) { | 
|  | errors.add('Expected "${kvp.key}" to be a string, but got ${kvp.value}.'); | 
|  | } | 
|  | break; | 
|  | case 'flutter': | 
|  | if (kvp.value == null) { | 
|  | continue; | 
|  | } | 
|  | if (kvp.value is! YamlMap) { | 
|  | errors.add('Expected "${kvp.key}" section to be an object or null, but got ${kvp.value}.'); | 
|  | } | 
|  | _validateFlutter(kvp.value, errors); | 
|  | break; | 
|  | default: | 
|  | // additionalProperties are allowed. | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (errors.isNotEmpty) { | 
|  | printStatus('Error detected in pubspec.yaml:', emphasis: true); | 
|  | printError(errors.join('\n')); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void _validateFlutter(YamlMap yaml, List<String> errors) { | 
|  | if (yaml == null || yaml.entries == null) { | 
|  | return; | 
|  | } | 
|  | for (final MapEntry<dynamic, dynamic> kvp in yaml.entries) { | 
|  | if (kvp.key is! String) { | 
|  | errors.add('Expected YAML key to be a a string, but got ${kvp.key} (${kvp.value.runtimeType}).'); | 
|  | continue; | 
|  | } | 
|  | switch (kvp.key) { | 
|  | case 'uses-material-design': | 
|  | if (kvp.value is! bool) { | 
|  | errors.add('Expected "${kvp.key}" to be a bool, but got ${kvp.value} (${kvp.value.runtimeType}).'); | 
|  | } | 
|  | break; | 
|  | case 'assets': | 
|  | case 'services': | 
|  | if (kvp.value is! YamlList || kvp.value[0] is! String) { | 
|  | errors.add('Expected "${kvp.key}" to be a list, but got ${kvp.value} (${kvp.value.runtimeType}).'); | 
|  | } | 
|  | break; | 
|  | case 'fonts': | 
|  | if (kvp.value is! YamlList || kvp.value[0] is! YamlMap) { | 
|  | errors.add('Expected "${kvp.key}" to be a list, but got ${kvp.value} (${kvp.value.runtimeType}).'); | 
|  | } | 
|  | _validateFonts(kvp.value, errors); | 
|  | break; | 
|  | case 'module': | 
|  | if (kvp.value is! YamlMap) { | 
|  | errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).'); | 
|  | } | 
|  |  | 
|  | if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) { | 
|  | errors.add('The "androidPackage" value must be a string if set.'); | 
|  | } | 
|  | if (kvp.value['iosBundleIdentifier'] != null && kvp.value['iosBundleIdentifier'] is! String) { | 
|  | errors.add('The "iosBundleIdentifier" section must be a string if set.'); | 
|  | } | 
|  | break; | 
|  | case 'plugin': | 
|  | if (kvp.value is! YamlMap) { | 
|  | errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).'); | 
|  | } | 
|  | if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) { | 
|  | errors.add('The "androidPackage" must either be null or a string.'); | 
|  | } | 
|  | if (kvp.value['iosPrefix'] != null && kvp.value['iosPrefix'] is! String) { | 
|  | errors.add('The "iosPrefix" must eithe rbe null or a string.'); | 
|  | } | 
|  | if (kvp.value['pluginClass'] != null && kvp.value['pluginClass'] is! String) { | 
|  | errors.add('The "pluginClass" must either be null or a string..'); | 
|  | } | 
|  | break; | 
|  | default: | 
|  | errors.add('Unexpected child "${kvp.key}" found under "flutter".'); | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void _validateFonts(YamlList fonts, List<String> errors) { | 
|  | if (fonts == null) { | 
|  | return; | 
|  | } | 
|  | const Set<int> fontWeights = <int>{ | 
|  | 100, 200, 300, 400, 500, 600, 700, 800, 900, | 
|  | }; | 
|  | for (final YamlMap fontMap in fonts) { | 
|  | for (dynamic key in fontMap.keys.where((dynamic key) => key != 'family' && key != 'fonts')) { | 
|  | errors.add('Unexpected child "$key" found under "fonts".'); | 
|  | } | 
|  | if (fontMap['family'] != null && fontMap['family'] is! String) { | 
|  | errors.add('Font family must either be null or a String.'); | 
|  | } | 
|  | if (fontMap['fonts'] != null && fontMap['fonts'] is! YamlList) { | 
|  | errors.add('Expected "fonts" to either be null or a list.'); | 
|  | } | 
|  | if (fontMap['fonts'] == null) { | 
|  | continue; | 
|  | } | 
|  | for (final YamlMap fontListItem in fontMap['fonts']) { | 
|  | for (final MapEntry<dynamic, dynamic> kvp in fontListItem.entries) { | 
|  | if (kvp.key is! String) { | 
|  | errors.add('Expected "${kvp.key}" under "fonts" to be a string.'); | 
|  | } | 
|  | switch(kvp.key) { | 
|  | case 'asset': | 
|  | if (kvp.value is! String) { | 
|  | errors.add('Expected font asset ${kvp.value} ((${kvp.value.runtimeType})) to be a string.'); | 
|  | } | 
|  | break; | 
|  | case 'weight': | 
|  | if (!fontWeights.contains(kvp.value)) { | 
|  | errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> weight.'); | 
|  | } | 
|  | break; | 
|  | case 'style': | 
|  | if (kvp.value != 'normal' && kvp.value != 'italic') { | 
|  | errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> style.'); | 
|  | } | 
|  | break; | 
|  | default: | 
|  | errors.add('Unexpected key ${kvp.key} ((${kvp.value.runtimeType})) under font.'); | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | } |